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

41
.air.toml Normal file
View File

@@ -0,0 +1,41 @@
# Config file for [Air](https://github.com/cosmtrek/air) in TOML format
# Working directory
root = "."
tmp_dir = "tmp"
[build]
# Just plain old shell command. You could use `make` as well.
args_bin = []
bin = "./bin/database-render"
cmd = "make build"
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "node_modules", "web/static", "dist"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html", "yaml", "yml", "json"]
kill_delay = "0s"
log = "build-errors.log"
send_interrupt = false
stop_on_root = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
time = false
[misc]
clean_on_exit = false
[screen]
clear_on_rebuild = false
keep_scroll = true

55
.dockerignore Normal file
View File

@@ -0,0 +1,55 @@
# Build artifacts
bin/
dist/
tmp/
# Dependencies
vendor/
node_modules/
# Logs
*.log
logs/
# Environment files
.env
.env.local
.env.*.local
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
# OS files
.DS_Store
Thumbs.db
# Git
.git/
.gitignore
# Documentation
README.md
docs/
# Test files
*_test.go
.coverage/
coverage.out
coverage.html
# Temporary files
*.tmp
*.temp
# Database files (for development)
data/*.db
data/*.sqlite
data/*.sqlite3
# Configuration (keep example configs)
config/config.yaml
config/local.yaml

1
.gitignore vendored
View File

@@ -25,3 +25,4 @@ go.work.sum
# env file
.env
bin/

57
Dockerfile Normal file
View File

@@ -0,0 +1,57 @@
# Multi-stage build for Database Render Application
FROM golang:1.21-alpine AS builder
# Set working directory
WORKDIR /app
# Install build dependencies
RUN apk add --no-cache git ca-certificates tzdata
# Copy go mod files
COPY go.mod go.sum ./
# Download dependencies
RUN go mod download
# Copy source code
COPY . .
# Build the application
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o database-render ./cmd/server
# Final stage
FROM alpine:latest
# Install runtime dependencies
RUN apk --no-cache add ca-certificates tzdata
# Create non-root user
RUN addgroup -g 1000 -S appgroup && \
adduser -u 1000 -S appuser -G appgroup
# Set working directory
WORKDIR /app
# Copy binary from builder
COPY --from=builder /app/database-render .
# Copy configuration files
COPY --from=builder /app/config ./config
COPY --from=builder /app/web ./web
# Create data directory for SQLite database
RUN mkdir -p /app/data && \
chown -R appuser:appgroup /app
# Switch to non-root user
USER appuser
# Expose port
EXPOSE 8080
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
# Run the application
CMD ["./database-render"]

240
Makefile Normal file
View File

@@ -0,0 +1,240 @@
# Database Render Application Makefile
# Variables
BINARY_NAME=database-render
APP_NAME=database-render
VERSION?=latest
BUILD_TIME=$(shell date -u '+%Y-%m-%d_%H:%M:%S')
GIT_COMMIT=$(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
LDFLAGS=-ldflags "-X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME) -X main.GitCommit=$(GIT_COMMIT)"
# Go parameters
GOCMD=go
GOBUILD=$(GOCMD) build
GOCLEAN=$(GOCMD) clean
GOTEST=$(GOCMD) test
GOGET=$(GOCMD) get
GOMOD=$(GOCMD) mod
GOFMT=gofmt
GOFMT_CHECK=$(GOFMT) -l
GOFMT_FMT=$(GOFMT) -w
# Directories
BIN_DIR=bin
DIST_DIR=dist
CONFIG_DIR=config
WEB_DIR=web
TEMPLATES_DIR=web/templates
STATIC_DIR=web/static
# Default target
.PHONY: all
all: clean build
# Build the application
.PHONY: build
build: deps
@echo "Building $(BINARY_NAME)..."
@mkdir -p $(BIN_DIR)
CGO_ENABLED=1 $(GOBUILD) $(LDFLAGS) -o $(BIN_DIR)/$(BINARY_NAME) ./cmd/server
@echo "Build completed: $(BIN_DIR)/$(BINARY_NAME)"
# Build for multiple platforms
.PHONY: build-all
build-all: clean deps
@echo "Building for multiple platforms..."
@mkdir -p $(DIST_DIR)
# Linux AMD64 with CGO
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME)-linux-amd64 ./cmd/server
# Linux ARM64 with CGO
CGO_ENABLED=1 GOOS=linux GOARCH=arm64 $(GOBUILD) $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME)-linux-arm64 ./cmd/server
# macOS AMD64 with CGO
CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME)-darwin-amd64 ./cmd/server
# macOS ARM64 with CGO
CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 $(GOBUILD) $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME)-darwin-arm64 ./cmd/server
# Windows AMD64 with CGO (requires mingw-w64)
CGO_ENABLED=1 GOOS=windows GOARCH=amd64 CC=x86_64-w64-mingw32-gcc $(GOBUILD) $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME)-windows-amd64.exe ./cmd/server
@echo "Multi-platform build completed in $(DIST_DIR)/"
# Install dependencies
.PHONY: deps
deps:
@echo "Installing dependencies..."
$(GOMOD) tidy
$(GOMOD) download
# Clean build artifacts
.PHONY: clean
clean:
@echo "Cleaning..."
$(GOCLEAN)
rm -rf $(BIN_DIR)
rm -rf $(DIST_DIR)
@echo "Clean completed"
# Run tests
.PHONY: test
test:
@echo "Running tests..."
$(GOTEST) -v ./...
# Run tests with coverage
.PHONY: test-coverage
test-coverage:
@echo "Running tests with coverage..."
$(GOTEST) -v -coverprofile=coverage.out ./...
$(GOCMD) tool cover -html=coverage.out -o coverage.html
@echo "Coverage report generated: coverage.html"
# Format code
.PHONY: fmt
fmt:
@echo "Formatting code..."
$(GOFMT_FMT) .
# Check code formatting
.PHONY: fmt-check
fmt-check:
@echo "Checking code formatting..."
@if [ -n "$$($(GOFMT_CHECK) .)" ]; then \
echo "Code is not formatted. Please run 'make fmt' to format the code."; \
exit 1; \
fi
@echo "Code formatting is correct"
# Lint code
.PHONY: lint
lint:
@echo "Running linter..."
@if command -v golangci-lint >/dev/null 2>&1; then \
golangci-lint run; \
else \
echo "golangci-lint not found. Please install it: https://golangci-lint.run/"; \
exit 1; \
fi
# Security scan
.PHONY: security
security:
@echo "Running security scan..."
@if command -v gosec >/dev/null 2>&1; then \
gosec ./...; \
else \
echo "gosec not found. Please install it: go install github.com/securecodewarrior/gosec/v2/cmd/gosec@latest"; \
exit 1; \
fi
# Run the application
.PHONY: run
run: build
@echo "Starting $(BINARY_NAME)..."
./$(BIN_DIR)/$(BINARY_NAME)
# Run with development configuration
.PHONY: dev
dev: build
@echo "Starting $(BINARY_NAME) in development mode..."
CONFIG_FILE=config/config.yaml ./$(BIN_DIR)/$(BINARY_NAME)
# Create sample configuration
.PHONY: init-config
init-config:
@echo "Creating sample configuration..."
@mkdir -p $(CONFIG_DIR)
@if [ ! -f $(CONFIG_DIR)/config.yaml ]; then \
cp config/config.example.yaml $(CONFIG_DIR)/config.yaml; \
echo "Sample configuration created at $(CONFIG_DIR)/config.yaml"; \
else \
echo "Configuration file already exists at $(CONFIG_DIR)/config.yaml"; \
fi
# Generate database schema example
.PHONY: init-db
init-db:
@echo "Creating sample database schema..."
@mkdir -p $(CONFIG_DIR)
@if [ ! -f $(CONFIG_DIR)/schema.sql ]; then \
cp config/schema.example.sql $(CONFIG_DIR)/schema.sql; \
echo "Sample database schema created at $(CONFIG_DIR)/schema.sql"; \
else \
echo "Database schema file already exists at $(CONFIG_DIR)/schema.sql"; \
fi
# Initialize project
.PHONY: init
init: init-config init-db deps
@echo "Project initialized successfully!"
@echo "Please edit $(CONFIG_DIR)/config.yaml to configure your database connection"
@echo "Then run: make run"
# Docker commands
.PHONY: docker-build
docker-build:
@echo "Building Docker image..."
docker build -t $(APP_NAME):$(VERSION) .
.PHONY: docker-run
docker-run:
@echo "Running Docker container..."
docker run -p 8080:8080 -v $(PWD)/config:/app/config $(APP_NAME):$(VERSION)
.PHONY: docker-compose-up
docker-compose-up:
@echo "Starting with Docker Compose..."
docker-compose up --build
.PHONY: docker-compose-down
docker-compose-down:
@echo "Stopping Docker Compose..."
docker-compose down
# Development helpers
.PHONY: watch
watch:
@echo "Watching for changes..."
@if command -v air >/dev/null 2>&1; then \
air; \
else \
echo "air not found. Please install it: go install github.com/cosmtrek/air@latest"; \
exit 1; \
fi
# Check system health
.PHONY: health
health:
@echo "Checking system health..."
@curl -f http://localhost:8080/health || echo "Server not running"
# Show help
.PHONY: help
help:
@echo "Database Render Application - Available Commands:"
@echo ""
@echo "Development:"
@echo " make init - Initialize project with sample config and database"
@echo " make run - Build and run the application"
@echo " make dev - Run in development mode"
@echo " make watch - Run with hot reload (requires air)"
@echo ""
@echo "Building:"
@echo " make build - Build the application"
@echo " make build-all - Build for multiple platforms"
@echo " make clean - Clean build artifacts"
@echo ""
@echo "Quality:"
@echo " make test - Run tests"
@echo " make test-coverage - Run tests with coverage"
@echo " make fmt - Format code"
@echo " make fmt-check - Check code formatting"
@echo " make lint - Run linter"
@echo " make security - Run security scan"
@echo ""
@echo "Docker:"
@echo " make docker-build - Build Docker image"
@echo " make docker-run - Run Docker container"
@echo " make docker-compose-up - Start with Docker Compose"
@echo " make docker-compose-down - Stop Docker Compose"
@echo ""
@echo "Utilities:"
@echo " make health - Check application health"
@echo " make help - Show this help message"

155
TODO.md Normal file
View File

@@ -0,0 +1,155 @@
# Database Render Application - 后续任务清单
## 已完成 ✅
### 核心系统
- [x] 项目基础结构和 go.mod 配置
- [x] 配置管理系统 (Viper + YAML)
- [x] 数据库连接和 ORM 层 (GORM + 多数据库支持)
- [x] HTTP 处理器层 (Fiber 框架)
- [x] 模板渲染系统 (Go templates)
- [x] 前端模板和静态资源 (TailwindCSS + 响应式设计)
- [x] 主应用入口点
- [x] Makefile 和构建脚本
- [x] Docker 配置
- [x] 开发工具脚本
### 文档和配置
- [x] Makefile (包含所有常用命令)
- [x] Docker 配置 (Dockerfile + docker-compose.yml)
- [x] 示例配置文件
- [x] 示例数据库 schema
- [x] 构建脚本 (scripts/build.sh)
- [x] 开发脚本 (scripts/dev.sh)
- [x] Air 热重载配置
## 待完成 📋
### 测试相关
- [ ] 单元测试 (repository 层)
- [ ] 单元测试 (service 层)
- [ ] 单元测试 (handler 层)
- [ ] 集成测试 (API 端点)
- [ ] 端到端测试 (UI 测试)
- [ ] 测试数据工厂
- [ ] Mock 数据生成器
### 功能增强
- [ ] 导出功能 (CSV, JSON, Excel)
- [ ] 批量操作 (删除, 状态更新)
- [ ] 高级搜索 (过滤器, 排序)
- [ ] 数据可视化图表
- [ ] 用户认证系统
- [ ] 权限管理
- [ ] 审计日志
- [ ] 数据备份/恢复
### 性能优化
- [ ] 数据库连接池配置优化
- [ ] 缓存层 (Redis)
- [ ] 分页性能优化
- [ ] 静态资源缓存
- [ ] 数据库索引优化
- [ ] 查询性能分析
### 前端增强
- [ ] 暗黑模式切换
- [ ] 移动端优化
- [ ] 表格视图 (可选)
- [ ] 数据编辑界面
- [ ] 图片上传和预览
- [ ] 富文本编辑器
### 部署和运维
- [ ] CI/CD 配置 (GitHub Actions)
- [ ] 生产环境配置模板
- [ ] 监控和告警
- [ ] 日志轮转配置
- [ ] 健康检查增强
- [ ] 容器编排 (K8s manifests)
### 文档完善
- [ ] 项目 README
- [ ] API 文档 (OpenAPI/Swagger)
- [ ] 部署指南
- [ ] 配置说明文档
- [ ] 开发指南
- [ ] 故障排除指南
### 安全增强
- [ ] 输入验证和清理
- [ ] SQL 注入防护检查
- [ ] XSS 防护
- [ ] CSRF 保护
- [ ] 安全头部配置
- [ ] 依赖安全扫描
## 立即行动项 🚀
### 启动应用测试
```bash
# 初始化项目
./scripts/dev.sh init-db
./scripts/build.sh build
./scripts/dev.sh start
# 或者使用Makefile
make init
make run
```
### 测试验证
1. 访问 http://localhost:8080
2. 验证数据库连接
3. 测试数据展示
4. 检查搜索功能
5. 验证分页功能
### 下一步开发建议
1. **优先完成测试**:为现有功能添加测试覆盖
2. **功能增强**:从导出功能开始,逐步增加实用性功能
3. **性能优化**:添加 Redis 缓存提升查询性能
4. **安全加固**:实施基本的输入验证和安全防护
### 快速开始命令
```bash
# 完整开发环境启动
./scripts/dev.sh start
# 热重载开发
./scripts/dev.sh watch
# 多平台构建
./scripts/build.sh build-all
# Docker环境
make docker-compose-up
```
## 技术债务
- [ ] 代码注释完善
- [ ] 错误处理统一
- [ ] 日志格式标准化
- [ ] 配置验证增强
- [ ] 代码重构优化
## 版本规划
- **v1.0.0**: 基础功能完成,稳定运行
- **v1.1.0**: 增加导出和批量操作
- **v1.2.0**: 添加用户认证和权限
- **v2.0.0**: 架构优化,性能提升

51
cmd/root.go Normal file
View File

@@ -0,0 +1,51 @@
/*
Copyright © 2025 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
"os"
"github.com/spf13/cobra"
)
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "database_render",
Short: "A brief description of your application",
Long: `A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
// Uncomment the following line if your bare application
// has an action associated with it:
// Run: func(cmd *cobra.Command, args []string) { },
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
// Here you will define your flags and configuration settings.
// Cobra supports persistent flags, which, if defined here,
// will be global for your application.
// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.database_render.yaml)")
// Cobra also supports local flags, which will only run
// when this action is called directly.
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

137
cmd/server/main.go Normal file
View File

@@ -0,0 +1,137 @@
package main
import (
"fmt"
"log/slog"
"os"
"os/signal"
"syscall"
"time"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/cors"
"github.com/gofiber/fiber/v3/middleware/recover"
"github.com/rogeecn/database_render/internal/config"
"github.com/rogeecn/database_render/internal/database"
"github.com/rogeecn/database_render/internal/handler"
"github.com/rogeecn/database_render/internal/repository"
"github.com/rogeecn/database_render/internal/service"
"github.com/rogeecn/database_render/internal/template"
)
func main() {
// Initialize structured logging
opts := &slog.HandlerOptions{
Level: slog.LevelInfo,
}
logger := slog.New(slog.NewJSONHandler(os.Stdout, opts))
slog.SetDefault(logger)
// Load configuration
cfg, err := config.LoadConfig("")
if err != nil {
slog.Error("failed to load configuration", "error", err)
os.Exit(1)
}
// Initialize database
db, err := database.NewConnectionManager(cfg)
if err != nil {
slog.Error("failed to initialize database", "error", err)
os.Exit(1)
}
defer func() {
if err := db.Close(); err != nil {
slog.Error("failed to close database", "error", err)
}
}()
// Initialize repository and services
repo := repository.NewDataRepository(db, cfg)
svc := service.NewDataService(repo, cfg)
if err := svc.ValidateConfiguration(); err != nil {
slog.Error("configuration validation failed", "error", err)
os.Exit(1)
}
// Initialize renderer
renderer, err := template.NewRenderer(svc, cfg)
if err != nil {
slog.Error("failed to initialize renderer", "error", err)
os.Exit(1)
}
// Initialize handlers
dataHandler := handler.NewDataHandler(svc)
// Initialize Fiber app
app := fiber.New(fiber.Config{
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
ErrorHandler: renderer.ErrorHandler,
Views: nil, // Using custom renderer
})
// Setup middleware
app.Use(recover.New())
app.Use(cors.New(cors.Config{
AllowOrigins: []string{"*"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Accept"},
}))
// Setup routes
// API routes
dataHandler.SetupRoutes(app)
// Template routes
app.Get("/", func(c fiber.Ctx) error {
tableName := c.Query("table")
if tableName == "" {
return renderer.RenderIndex(c)
}
return renderer.RenderList(c, tableName)
})
// Setup static file serving
renderer.ServeStatic(app)
// Health check endpoint
app.Get("/health", func(c fiber.Ctx) error {
return c.JSON(map[string]string{
"status": "ok",
"timestamp": time.Now().Format(time.RFC3339),
})
})
// Start server
port := cfg.App.Port
if port == 0 {
port = 8080
}
// Start server in a goroutine
go func() {
slog.Info("starting server", "port", port)
if err := app.Listen(fmt.Sprintf(":%d", port)); err != nil {
slog.Error("server error", "error", err)
os.Exit(1)
}
}()
// Wait for interrupt signal to gracefully shutdown the server
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
slog.Info("shutting down server...")
// Shutdown server
if err := app.Shutdown(); err != nil {
slog.Error("server shutdown error", "error", err)
os.Exit(1)
}
slog.Info("server gracefully stopped")
}

147
config/config.example.yaml Normal file
View File

@@ -0,0 +1,147 @@
# Database Render Application Configuration
# Copy this file to config.yaml and modify as needed
app:
name: "Database Render"
port: 8080
debug: false
database:
# Database type: sqlite, mysql, postgres
type: "sqlite"
# SQLite configuration (used when type is sqlite)
path: "data/app.db"
# MySQL configuration (used when type is mysql)
# host: "localhost"
# port: 3306
# user: "root"
# password: "password"
# db_name: "database_render"
# PostgreSQL configuration (used when type is postgres)
# host: "localhost"
# port: 5432
# user: "postgres"
# password: "password"
# db_name: "database_render"
# dsn: "host=localhost port=5432 user=postgres password=password dbname=database_render sslmode=disable"
# Table configurations
tables:
- name: "posts"
alias: "文章管理"
description: "博客文章管理"
default: true
layout: "cards" # cards or list
columns:
- name: "id"
alias: "ID"
type: "int"
primary: true
hidden: true
- name: "title"
alias: "标题"
type: "string"
searchable: true
- name: "content"
alias: "内容"
type: "text"
render_type: "markdown"
searchable: true
- name: "category"
alias: "分类"
type: "string"
searchable: true
render_type: "tag"
values:
technology:
label: "技术"
color: "#3b82f6"
life:
label: "生活"
color: "#10b981"
work:
label: "工作"
color: "#f59e0b"
- name: "tags"
alias: "标签"
type: "string"
searchable: true
- name: "created_at"
alias: "创建时间"
type: "datetime"
render_type: "time"
- name: "updated_at"
alias: "更新时间"
type: "datetime"
render_type: "time"
- name: "users"
alias: "用户管理"
description: "系统用户管理"
layout: "list"
columns:
- name: "id"
alias: "ID"
type: "int"
primary: true
hidden: true
- name: "username"
alias: "用户名"
type: "string"
searchable: true
- name: "email"
alias: "邮箱"
type: "string"
searchable: true
- name: "full_name"
alias: "姓名"
type: "string"
searchable: true
- name: "status"
alias: "状态"
type: "string"
render_type: "tag"
values:
active:
label: "激活"
color: "#10b981"
inactive:
label: "未激活"
color: "#ef4444"
pending:
label: "待审核"
color: "#f59e0b"
- name: "created_at"
alias: "创建时间"
type: "datetime"
render_type: "time"
# Logging configuration
logging:
level: "info" # debug, info, warn, error
format: "json" # json, text
output: "stdout" # stdout, stderr, file
file: "logs/app.log" # Only used when output is file
# Server configuration
server:
read_timeout: 30s
write_timeout: 30s
idle_timeout: 60s
# Security configuration
security:
cors:
enabled: true
allow_origins: ["*"]
allow_methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
allow_headers: ["Origin", "Content-Type", "Accept", "Authorization"]
# Cache configuration
cache:
enabled: false
ttl: 5m
max_size: 100

230
config/config.yaml Normal file
View File

@@ -0,0 +1,230 @@
# Database Render Application Configuration
app:
name: "Database Render"
port: 9080
debug: false
database:
type: "sqlite"
path: "data/app.db"
# Table configurations
tables:
posts:
alias: "文章管理"
description: "博客文章管理"
page_size: 10
fields:
id:
type: "int"
alias: "ID"
hidden: true
title:
type: "string"
alias: "标题"
searchable: true
size: "large"
content:
type: "text"
alias: "内容"
hidden: true # Hide from list view, show in detail
markdown: true
category:
type: "string"
alias: "分类"
searchable: true
colors:
Technology: "#3b82f6"
Programming: "#8b5cf6"
"Web Development": "#10b981"
DevOps: "#f59e0b"
"Data Science": "#ef4444"
Lifestyle: "#ec4899"
tags:
type: "string"
alias: "标签"
searchable: true
status:
type: "string"
alias: "状态"
searchable: true
colors:
published: "#10b981"
draft: "#6b7280"
pending: "#f59e0b"
view_count:
type: "int"
alias: "浏览量"
created_at:
type: "time"
alias: "创建时间"
format: "2006-01-02 15:04:05"
updated_at:
type: "time"
alias: "更新时间"
format: "2006-01-02 15:04:05"
users:
alias: "用户管理"
description: "系统用户管理"
page_size: 10
fields:
id:
type: "int"
alias: "ID"
hidden: true
username:
type: "string"
alias: "用户名"
searchable: true
email:
type: "string"
alias: "邮箱"
searchable: true
full_name:
type: "string"
alias: "姓名"
searchable: true
bio:
type: "text"
alias: "简介"
max_length: 200
status:
type: "string"
alias: "状态"
searchable: true
colors:
active: "#10b981"
inactive: "#ef4444"
pending: "#f59e0b"
role:
type: "string"
alias: "角色"
searchable: true
colors:
admin: "#ef4444"
user: "#3b82f6"
last_login:
type: "time"
alias: "最后登录"
format: "2006-01-02 15:04:05"
created_at:
type: "time"
alias: "创建时间"
format: "2006-01-02 15:04:05"
updated_at:
type: "time"
alias: "更新时间"
format: "2006-01-02 15:04:05"
categories:
alias: "分类管理"
description: "文章分类管理"
page_size: 10
fields:
id:
type: "int"
alias: "ID"
hidden: true
name:
type: "string"
alias: "名称"
searchable: true
slug:
type: "string"
alias: "别名"
searchable: true
description:
type: "text"
alias: "描述"
max_length: 200
color:
type: "string"
alias: "颜色"
render_type: "color"
sort_order:
type: "int"
alias: "排序"
is_active:
type: "bool"
alias: "激活"
render_type: "bool"
created_at:
type: "time"
alias: "创建时间"
format: "2006-01-02 15:04:05"
updated_at:
type: "time"
alias: "更新时间"
format: "2006-01-02 15:04:05"
comments:
alias: "评论管理"
description: "文章评论管理"
page_size: 15
fields:
id:
type: "int"
alias: "ID"
hidden: true
post_id:
type: "int"
alias: "文章ID"
user_id:
type: "int"
alias: "用户ID"
author_name:
type: "string"
alias: "作者"
searchable: true
author_email:
type: "string"
alias: "邮箱"
searchable: true
content:
type: "text"
alias: "内容"
max_length: 500
status:
type: "string"
alias: "状态"
searchable: true
colors:
approved: "#10b981"
pending: "#f59e0b"
rejected: "#ef4444"
created_at:
type: "time"
alias: "创建时间"
format: "2006-01-02 15:04:05"
updated_at:
type: "time"
alias: "更新时间"
format: "2006-01-02 15:04:05"
# Logging configuration
logging:
level: "info"
format: "json"
output: "stdout"
# Server configuration
server:
read_timeout: 30s
write_timeout: 30s
idle_timeout: 60s
# Security configuration
security:
cors:
enabled: true
allow_origins: ["*"]
allow_methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
allow_headers: ["Origin", "Content-Type", "Accept"]
# Cache configuration
cache:
enabled: false
ttl: 5m
max_size: 100

346
config/schema.example.sql Normal file
View File

@@ -0,0 +1,346 @@
-- Database Render Application - Sample Database Schema
-- This file contains sample database tables for testing the application
-- Posts table (blog posts/articles)
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title VARCHAR(255) NOT NULL,
content TEXT,
category VARCHAR(50),
tags VARCHAR(255),
author_id INTEGER,
status VARCHAR(20) DEFAULT 'draft',
view_count INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Users table
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
full_name VARCHAR(100),
password_hash VARCHAR(255),
avatar_url VARCHAR(255),
bio TEXT,
status VARCHAR(20) DEFAULT 'active',
role VARCHAR(20) DEFAULT 'user',
last_login DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Categories table
CREATE TABLE IF NOT EXISTS categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(100) UNIQUE NOT NULL,
slug VARCHAR(100) UNIQUE NOT NULL,
description TEXT,
color VARCHAR(7),
sort_order INTEGER DEFAULT 0,
is_active BOOLEAN DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Tags table
CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(50) UNIQUE NOT NULL,
slug VARCHAR(50) UNIQUE NOT NULL,
color VARCHAR(7),
description TEXT,
usage_count INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Post-Tag relationship table
CREATE TABLE IF NOT EXISTS post_tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
post_id INTEGER NOT NULL,
tag_id INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE,
UNIQUE(post_id, tag_id)
);
-- Comments table
CREATE TABLE IF NOT EXISTS comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
post_id INTEGER NOT NULL,
user_id INTEGER,
author_name VARCHAR(100),
author_email VARCHAR(255),
content TEXT NOT NULL,
parent_id INTEGER,
status VARCHAR(20) DEFAULT 'pending',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
FOREIGN KEY (parent_id) REFERENCES comments(id) ON DELETE CASCADE
);
-- Media/Attachments table
CREATE TABLE IF NOT EXISTS media (
id INTEGER PRIMARY KEY AUTOINCREMENT,
filename VARCHAR(255) NOT NULL,
original_filename VARCHAR(255),
file_path VARCHAR(500),
file_size INTEGER,
mime_type VARCHAR(100),
alt_text VARCHAR(255),
title VARCHAR(255),
description TEXT,
uploaded_by INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (uploaded_by) REFERENCES users(id) ON DELETE SET NULL
);
-- Sample data insertion
-- Insert sample users
INSERT OR IGNORE INTO users (username, email, full_name, bio, role) VALUES
('admin', 'admin@example.com', 'System Administrator', 'System administrator account', 'admin'),
('john_doe', 'john@example.com', 'John Doe', 'Technology enthusiast and blogger', 'user'),
('jane_smith', 'jane@example.com', 'Jane Smith', 'Content creator and writer', 'user'),
('bob_wilson', 'bob@example.com', 'Bob Wilson', 'Software developer and tech reviewer', 'user');
-- Insert sample categories
INSERT OR IGNORE INTO categories (name, slug, description, color) VALUES
('Technology', 'technology', 'Latest technology trends and developments', '#3b82f6'),
('Programming', 'programming', 'Programming tutorials and best practices', '#8b5cf6'),
('Web Development', 'web-development', 'Web development articles and tutorials', '#10b981'),
('DevOps', 'devops', 'DevOps practices and tools', '#f59e0b'),
('Data Science', 'data-science', 'Data science and machine learning', '#ef4444'),
('Lifestyle', 'lifestyle', 'Personal development and lifestyle', '#ec4899');
-- Insert sample tags
INSERT OR IGNORE INTO tags (name, slug, color, description) VALUES
('Go', 'go', '#00add8', 'Go programming language'),
('Python', 'python', '#3776ab', 'Python programming language'),
('JavaScript', 'javascript', '#f7df1e', 'JavaScript programming language'),
('Docker', 'docker', '#2496ed', 'Docker containerization'),
('Kubernetes', 'kubernetes', '#326ce5', 'Kubernetes orchestration'),
('React', 'react', '#61dafb', 'React framework'),
('Vue.js', 'vuejs', '#4fc08d', 'Vue.js framework'),
('Database', 'database', '#4479a1', 'Database technologies'),
('API', 'api', '#68d391', 'API development'),
('Security', 'security', '#f56565', 'Security practices');
-- Insert sample posts
INSERT OR IGNORE INTO posts (title, content, category, tags, author_id, status) VALUES
('Getting Started with Go and Fiber Framework', '# Getting Started with Go and Fiber
This is a comprehensive guide to building web applications with Go and the Fiber framework.
## Introduction
Fiber is an Express.js inspired web framework written in Go. It provides a robust set of features for building web applications and APIs.
## Installation
```bash
go get github.com/gofiber/fiber/v2
```
## Basic Usage
```go
package main
import "github.com/gofiber/fiber/v2"
func main() {
app := fiber.New()
app.Get("/", func(c *fiber.Ctx) error {
return c.SendString("Hello, World!")
})
app.Listen(":3000")
}
```
## Conclusion
Fiber provides an excellent foundation for building high-performance web applications in Go.', 'Programming', 'Go,Web Development,API', 2, 'published'),
('Modern Database Design Best Practices', '# Modern Database Design Best Practices
Learn the essential principles of designing scalable and maintainable databases.
## Key Principles
1. **Normalization**: Reduce data redundancy
2. **Indexing**: Improve query performance
3. **Data Types**: Choose appropriate data types
4. **Constraints**: Ensure data integrity
## Example Schema
Here''s an example of a well-designed user table:
```sql
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
```
## Performance Tips
- Use appropriate indexes
- Consider partitioning for large tables
- Regular database maintenance', 'Technology', 'Database,Design,Best Practices', 3, 'published'),
('Container Orchestration with Kubernetes', '# Container Orchestration with Kubernetes
Master the art of container orchestration with Kubernetes.
## What is Kubernetes?
Kubernetes is an open-source platform designed to automate deploying, scaling, and operating application containers.
## Key Features
- **Service Discovery**: Automatic container discovery
- **Load Balancing**: Distribute traffic across containers
- **Storage Orchestration**: Mount storage systems
- **Self-Healing**: Automatic container restarts
- **Secret Management**: Secure sensitive data
## Getting Started
```bash
# Start a local cluster
minikube start
# Deploy an application
kubectl create deployment hello-node --image=k8s.gcr.io/echoserver:1.4
# Expose the deployment
kubectl expose deployment hello-node --type=LoadBalancer --port=8080
```
## Best Practices
- Use namespaces for organization
- Implement health checks
- Monitor resource usage
- Use ConfigMaps for configuration', 'DevOps', 'Kubernetes,Docker,Containerization', 4, 'published'),
('Building RESTful APIs with Best Practices', '# Building RESTful APIs with Best Practices
Create robust and scalable RESTful APIs following industry standards.
## REST Principles
- **Stateless**: Each request contains all necessary information
- **Resource-Based**: URLs represent resources
- **HTTP Methods**: Use appropriate HTTP verbs (GET, POST, PUT, DELETE)
- **Status Codes**: Return appropriate HTTP status codes
## API Design Guidelines
### 1. Version Your API
```
/api/v1/users
/api/v2/users
```
### 2. Use Plural Nouns
```
GET /users
POST /users
GET /users/123
```
### 3. Filtering and Pagination
```
GET /users?page=1&limit=10&sort=name&order=asc
```
### 4. Error Handling
```json
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Email is required",
"details": []
}
}
```
## Security Considerations
- Use HTTPS
- Implement rate limiting
- Validate input data
- Use authentication and authorization', 'Programming', 'API,REST,Security', 2, 'published'),
('React State Management in 2024', '# React State Management in 2024
Explore modern state management solutions for React applications.
## State Management Options
### 1. React Context API
Built-in solution for sharing state across components.
### 2. Redux Toolkit
Predictable state container with great developer tools.
### 3. Zustand
Lightweight and simple state management.
### 4. Jotai
Atomic approach to state management.
## Choosing the Right Solution
| Solution | Best For | Bundle Size |
|----------|----------|-------------|
| Context | Simple apps | Small |
| Redux | Complex apps | Large |
| Zustand | Medium apps | Small |
| Jotai | Atomic state | Small |
## Code Example
```javascript
// Using Zustand
import { create } from ''zustand''
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}))
```
## Performance Tips
- Use React.memo for expensive components
- Implement proper memoization
- Consider code splitting for large state', 'Web Development', 'React,JavaScript,State Management', 3, 'published');
-- Insert sample comments
INSERT OR IGNORE INTO comments (post_id, user_id, content, status) VALUES
(1, 3, 'Great tutorial! Very helpful for getting started with Fiber.', 'approved'),
(1, 4, 'Thanks for sharing this. The examples are really clear.', 'approved'),
(2, 2, 'Excellent overview of database design principles!', 'approved'),
(3, 1, 'Kubernetes can be overwhelming, but this guide makes it approachable.', 'approved'),
(4, 3, 'RESTful API best practices are so important. Great article!', 'approved');
-- Insert post-tag relationships
INSERT OR IGNORE INTO post_tags (post_id, tag_id) VALUES
(1, 1), (1, 9), (1, 10),
(2, 8), (2, 9),
(3, 4), (3, 5),
(4, 9), (4, 10),
(5, 6), (5, 7), (5, 10);

346
config/schema.sql Normal file
View File

@@ -0,0 +1,346 @@
-- Database Render Application - Sample Database Schema
-- This file contains sample database tables for testing the application
-- Posts table (blog posts/articles)
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title VARCHAR(255) NOT NULL,
content TEXT,
category VARCHAR(50),
tags VARCHAR(255),
author_id INTEGER,
status VARCHAR(20) DEFAULT 'draft',
view_count INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Users table
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
full_name VARCHAR(100),
password_hash VARCHAR(255),
avatar_url VARCHAR(255),
bio TEXT,
status VARCHAR(20) DEFAULT 'active',
role VARCHAR(20) DEFAULT 'user',
last_login DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Categories table
CREATE TABLE IF NOT EXISTS categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(100) UNIQUE NOT NULL,
slug VARCHAR(100) UNIQUE NOT NULL,
description TEXT,
color VARCHAR(7),
sort_order INTEGER DEFAULT 0,
is_active BOOLEAN DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Tags table
CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(50) UNIQUE NOT NULL,
slug VARCHAR(50) UNIQUE NOT NULL,
color VARCHAR(7),
description TEXT,
usage_count INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Post-Tag relationship table
CREATE TABLE IF NOT EXISTS post_tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
post_id INTEGER NOT NULL,
tag_id INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE,
UNIQUE(post_id, tag_id)
);
-- Comments table
CREATE TABLE IF NOT EXISTS comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
post_id INTEGER NOT NULL,
user_id INTEGER,
author_name VARCHAR(100),
author_email VARCHAR(255),
content TEXT NOT NULL,
parent_id INTEGER,
status VARCHAR(20) DEFAULT 'pending',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
FOREIGN KEY (parent_id) REFERENCES comments(id) ON DELETE CASCADE
);
-- Media/Attachments table
CREATE TABLE IF NOT EXISTS media (
id INTEGER PRIMARY KEY AUTOINCREMENT,
filename VARCHAR(255) NOT NULL,
original_filename VARCHAR(255),
file_path VARCHAR(500),
file_size INTEGER,
mime_type VARCHAR(100),
alt_text VARCHAR(255),
title VARCHAR(255),
description TEXT,
uploaded_by INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (uploaded_by) REFERENCES users(id) ON DELETE SET NULL
);
-- Sample data insertion
-- Insert sample users
INSERT OR IGNORE INTO users (username, email, full_name, bio, role) VALUES
('admin', 'admin@example.com', 'System Administrator', 'System administrator account', 'admin'),
('john_doe', 'john@example.com', 'John Doe', 'Technology enthusiast and blogger', 'user'),
('jane_smith', 'jane@example.com', 'Jane Smith', 'Content creator and writer', 'user'),
('bob_wilson', 'bob@example.com', 'Bob Wilson', 'Software developer and tech reviewer', 'user');
-- Insert sample categories
INSERT OR IGNORE INTO categories (name, slug, description, color) VALUES
('Technology', 'technology', 'Latest technology trends and developments', '#3b82f6'),
('Programming', 'programming', 'Programming tutorials and best practices', '#8b5cf6'),
('Web Development', 'web-development', 'Web development articles and tutorials', '#10b981'),
('DevOps', 'devops', 'DevOps practices and tools', '#f59e0b'),
('Data Science', 'data-science', 'Data science and machine learning', '#ef4444'),
('Lifestyle', 'lifestyle', 'Personal development and lifestyle', '#ec4899');
-- Insert sample tags
INSERT OR IGNORE INTO tags (name, slug, color, description) VALUES
('Go', 'go', '#00add8', 'Go programming language'),
('Python', 'python', '#3776ab', 'Python programming language'),
('JavaScript', 'javascript', '#f7df1e', 'JavaScript programming language'),
('Docker', 'docker', '#2496ed', 'Docker containerization'),
('Kubernetes', 'kubernetes', '#326ce5', 'Kubernetes orchestration'),
('React', 'react', '#61dafb', 'React framework'),
('Vue.js', 'vuejs', '#4fc08d', 'Vue.js framework'),
('Database', 'database', '#4479a1', 'Database technologies'),
('API', 'api', '#68d391', 'API development'),
('Security', 'security', '#f56565', 'Security practices');
-- Insert sample posts
INSERT OR IGNORE INTO posts (title, content, category, tags, author_id, status) VALUES
('Getting Started with Go and Fiber Framework', '# Getting Started with Go and Fiber
This is a comprehensive guide to building web applications with Go and the Fiber framework.
## Introduction
Fiber is an Express.js inspired web framework written in Go. It provides a robust set of features for building web applications and APIs.
## Installation
```bash
go get github.com/gofiber/fiber/v2
```
## Basic Usage
```go
package main
import "github.com/gofiber/fiber/v2"
func main() {
app := fiber.New()
app.Get("/", func(c *fiber.Ctx) error {
return c.SendString("Hello, World!")
})
app.Listen(":3000")
}
```
## Conclusion
Fiber provides an excellent foundation for building high-performance web applications in Go.', 'Programming', 'Go,Web Development,API', 2, 'published'),
('Modern Database Design Best Practices', '# Modern Database Design Best Practices
Learn the essential principles of designing scalable and maintainable databases.
## Key Principles
1. **Normalization**: Reduce data redundancy
2. **Indexing**: Improve query performance
3. **Data Types**: Choose appropriate data types
4. **Constraints**: Ensure data integrity
## Example Schema
Here''s an example of a well-designed user table:
```sql
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
```
## Performance Tips
- Use appropriate indexes
- Consider partitioning for large tables
- Regular database maintenance', 'Technology', 'Database,Design,Best Practices', 3, 'published'),
('Container Orchestration with Kubernetes', '# Container Orchestration with Kubernetes
Master the art of container orchestration with Kubernetes.
## What is Kubernetes?
Kubernetes is an open-source platform designed to automate deploying, scaling, and operating application containers.
## Key Features
- **Service Discovery**: Automatic container discovery
- **Load Balancing**: Distribute traffic across containers
- **Storage Orchestration**: Mount storage systems
- **Self-Healing**: Automatic container restarts
- **Secret Management**: Secure sensitive data
## Getting Started
```bash
# Start a local cluster
minikube start
# Deploy an application
kubectl create deployment hello-node --image=k8s.gcr.io/echoserver:1.4
# Expose the deployment
kubectl expose deployment hello-node --type=LoadBalancer --port=8080
```
## Best Practices
- Use namespaces for organization
- Implement health checks
- Monitor resource usage
- Use ConfigMaps for configuration', 'DevOps', 'Kubernetes,Docker,Containerization', 4, 'published'),
('Building RESTful APIs with Best Practices', '# Building RESTful APIs with Best Practices
Create robust and scalable RESTful APIs following industry standards.
## REST Principles
- **Stateless**: Each request contains all necessary information
- **Resource-Based**: URLs represent resources
- **HTTP Methods**: Use appropriate HTTP verbs (GET, POST, PUT, DELETE)
- **Status Codes**: Return appropriate HTTP status codes
## API Design Guidelines
### 1. Version Your API
```
/api/v1/users
/api/v2/users
```
### 2. Use Plural Nouns
```
GET /users
POST /users
GET /users/123
```
### 3. Filtering and Pagination
```
GET /users?page=1&limit=10&sort=name&order=asc
```
### 4. Error Handling
```json
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Email is required",
"details": []
}
}
```
## Security Considerations
- Use HTTPS
- Implement rate limiting
- Validate input data
- Use authentication and authorization', 'Programming', 'API,REST,Security', 2, 'published'),
('React State Management in 2024', '# React State Management in 2024
Explore modern state management solutions for React applications.
## State Management Options
### 1. React Context API
Built-in solution for sharing state across components.
### 2. Redux Toolkit
Predictable state container with great developer tools.
### 3. Zustand
Lightweight and simple state management.
### 4. Jotai
Atomic approach to state management.
## Choosing the Right Solution
| Solution | Best For | Bundle Size |
|----------|----------|-------------|
| Context | Simple apps | Small |
| Redux | Complex apps | Large |
| Zustand | Medium apps | Small |
| Jotai | Atomic state | Small |
## Code Example
```javascript
// Using Zustand
import { create } from ''zustand''
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}))
```
## Performance Tips
- Use React.memo for expensive components
- Implement proper memoization
- Consider code splitting for large state', 'Web Development', 'React,JavaScript,State Management', 3, 'published');
-- Insert sample comments
INSERT OR IGNORE INTO comments (post_id, user_id, content, status) VALUES
(1, 3, 'Great tutorial! Very helpful for getting started with Fiber.', 'approved'),
(1, 4, 'Thanks for sharing this. The examples are really clear.', 'approved'),
(2, 2, 'Excellent overview of database design principles!', 'approved'),
(3, 1, 'Kubernetes can be overwhelming, but this guide makes it approachable.', 'approved'),
(4, 3, 'RESTful API best practices are so important. Great article!', 'approved');
-- Insert post-tag relationships
INSERT OR IGNORE INTO post_tags (post_id, tag_id) VALUES
(1, 1), (1, 9), (1, 10),
(2, 8), (2, 9),
(3, 4), (3, 5),
(4, 9), (4, 10),
(5, 6), (5, 7), (5, 10);

BIN
data/app.db Normal file

Binary file not shown.

27
database_render/.gitignore vendored Normal file
View File

@@ -0,0 +1,27 @@
# ---> Go
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
go.work.sum
# env file
.env

75
docker-compose.yml Normal file
View File

@@ -0,0 +1,75 @@
version: '3.8'
services:
# Database Render Application
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
volumes:
- ./config:/app/config
- ./data:/app/data
- ./logs:/app/logs
environment:
- CONFIG_FILE=/app/config/config.yaml
depends_on:
- mysql
- postgres
restart: unless-stopped
networks:
- app-network
# MySQL Database
mysql:
image: mysql:8.0
container_name: mysql-db
environment:
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_DATABASE: database_render
MYSQL_USER: appuser
MYSQL_PASSWORD: apppassword
ports:
- "3306:3306"
volumes:
- mysql-data:/var/lib/mysql
- ./config/schema.example.sql:/docker-entrypoint-initdb.d/01-schema.sql
restart: unless-stopped
networks:
- app-network
# PostgreSQL Database
postgres:
image: postgres:15-alpine
container_name: postgres-db
environment:
POSTGRES_DB: database_render
POSTGRES_USER: appuser
POSTGRES_PASSWORD: apppassword
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
- ./config/schema.example.sql:/docker-entrypoint-initdb.d/01-schema.sql
restart: unless-stopped
networks:
- app-network
# SQLite Database (for development)
sqlite-dev:
image: alpine:latest
container_name: sqlite-dev
volumes:
- ./data:/data
command: tail -f /dev/null
networks:
- app-network
volumes:
mysql-data:
postgres-data:
networks:
app-network:
driver: bridge

56
go.mod Normal file
View File

@@ -0,0 +1,56 @@
module github.com/rogeecn/database_render
go 1.24.1
require (
github.com/gofiber/fiber/v3 v3.0.0-beta.5
github.com/spf13/cobra v1.9.1
github.com/spf13/viper v1.18.2
gorm.io/driver/mysql v1.5.2
gorm.io/driver/postgres v1.5.4
gorm.io/driver/sqlite v1.5.4
gorm.io/gorm v1.25.5
)
require (
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/gofiber/schema v1.6.0 // indirect
github.com/gofiber/utils/v2 v2.0.0-beta.13 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.4.3 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.19 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tinylib/msgp v1.3.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.64.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.27.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

138
go.sum Normal file
View File

@@ -0,0 +1,138 @@
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/gofiber/fiber/v3 v3.0.0-beta.5 h1:MSGbiQZEYiYOqti2Ip2zMRkN4VvZw7Vo7dwZBa1Qjk8=
github.com/gofiber/fiber/v3 v3.0.0-beta.5/go.mod h1:XmI2Agulde26YcQrA2n8X499I1p98/zfCNbNObVUeP8=
github.com/gofiber/schema v1.6.0 h1:rAgVDFwhndtC+hgV7Vu5ItQCn7eC2mBA4Eu1/ZTiEYY=
github.com/gofiber/schema v1.6.0/go.mod h1:WNZWpQx8LlPSK7ZaX0OqOh+nQo/eW2OevsXs1VZfs/s=
github.com/gofiber/utils/v2 v2.0.0-beta.13 h1:dlpbGFLveQ9OduL2UHw4dtu4lXE+Gb3bHMc+8Yxp/dk=
github.com/gofiber/utils/v2 v2.0.0-beta.13/go.mod h1:qEZ175nSOkl5xciHmqxwNDsWzwiB39gB8RgU1d3U4mQ=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY=
github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/shamaton/msgpack/v2 v2.2.3 h1:uDOHmxQySlvlUYfQwdjxyybAOzjlQsD1Vjy+4jmO9NM=
github.com/shamaton/msgpack/v2 v2.2.3/go.mod h1:6khjYnkx73f7VQU7wjcFS9DFjs+59naVWJv1TB7qdOI=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.64.0 h1:QBygLLQmiAyiXuRhthf0tuRkqAFcrC42dckN2S+N3og=
github.com/valyala/fasthttp v1.64.0/go.mod h1:dGmFxwkWXSK0NbOSJuF7AMVzU+lkHz0wQVvVITv2UQA=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs=
gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8=
gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0=
gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=

236
internal/config/config.go Normal file
View File

@@ -0,0 +1,236 @@
package config
import (
"fmt"
"log/slog"
"strings"
"github.com/spf13/viper"
)
// Config represents the application configuration
type Config struct {
App AppConfig `mapstructure:"app"`
Database DatabaseConfig `mapstructure:"database"`
Tables map[string]TableConfig `mapstructure:"tables"`
}
// AppConfig holds application-level configuration
type AppConfig struct {
Name string `mapstructure:"name"`
Theme string `mapstructure:"theme"`
Language string `mapstructure:"language"`
Port int `mapstructure:"port"`
}
// DatabaseConfig holds database connection settings
type DatabaseConfig struct {
Type string `mapstructure:"type"`
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
DBName string `mapstructure:"dbname"`
Path string `mapstructure:"path"`
DSN string `mapstructure:"dsn"`
}
// TableConfig holds table-specific configuration
type TableConfig struct {
Alias string `mapstructure:"alias"`
Layout string `mapstructure:"layout"`
PageSize int `mapstructure:"page_size"`
Fields map[string]FieldConfig `mapstructure:"fields"`
Features map[string]interface{} `mapstructure:"features"`
Options map[string]interface{} `mapstructure:"options"`
}
// FieldConfig holds field-specific configuration
type FieldConfig struct {
Type string `mapstructure:"type"`
Hidden bool `mapstructure:"hidden"`
Searchable bool `mapstructure:"searchable"`
MaxLength int `mapstructure:"max_length"`
Length int `mapstructure:"length"`
Markdown bool `mapstructure:"markdown"`
Excerpt int `mapstructure:"excerpt"`
Size string `mapstructure:"size"`
Fit string `mapstructure:"fit"`
Format string `mapstructure:"format"`
Relative bool `mapstructure:"relative"`
Colors map[string]string `mapstructure:"colors"`
Separator string `mapstructure:"separator"`
Primary bool `mapstructure:"primary"`
AvatarField string `mapstructure:"avatar_field"`
Suffix string `mapstructure:"suffix"`
Prefix string `mapstructure:"prefix"`
Options map[string]interface{} `mapstructure:"options"`
}
// LoadConfig loads configuration from file and environment variables
func LoadConfig(configPath string) (*Config, error) {
logger := slog.With("component", "config")
// Set default values
viper.SetDefault("app.name", "Database Render")
viper.SetDefault("app.theme", "modern")
viper.SetDefault("app.language", "zh-CN")
viper.SetDefault("app.port", 8080)
// Database defaults
viper.SetDefault("database.type", "sqlite")
viper.SetDefault("database.host", "localhost")
viper.SetDefault("database.port", 3306)
viper.SetDefault("database.dbname", "testdb")
// Set config file
if configPath != "" {
viper.SetConfigFile(configPath)
} else {
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")
viper.AddConfigPath("./config")
viper.AddConfigPath("/etc/database-render")
}
// Environment variables
viper.SetEnvPrefix("DR")
viper.AutomaticEnv()
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
// Read config
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
logger.Error("failed to read config file", "error", err)
return nil, fmt.Errorf("failed to read config: %w", err)
}
logger.Warn("config file not found, using defaults")
}
// Parse database DSN if provided
if dsn := viper.GetString("database"); dsn != "" {
if err := parseDSN(dsn); err != nil {
return nil, err
}
}
// Unmarshal config
var config Config
if err := viper.Unmarshal(&config); err != nil {
logger.Error("failed to unmarshal config", "error", err)
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
}
// Validate config
if err := validateConfig(&config); err != nil {
logger.Error("config validation failed", "error", err)
return nil, fmt.Errorf("config validation failed: %w", err)
}
logger.Info("configuration loaded successfully",
"config_file", viper.ConfigFileUsed(),
"tables_count", len(config.Tables))
return &config, nil
}
// parseDSN parses database connection string
func parseDSN(dsn string) error {
if strings.HasPrefix(dsn, "sqlite://") {
path := strings.TrimPrefix(dsn, "sqlite://")
viper.Set("database.type", "sqlite")
viper.Set("database.path", path)
return nil
}
if strings.HasPrefix(dsn, "mysql://") {
dsn = strings.TrimPrefix(dsn, "mysql://")
parts := strings.Split(dsn, "@")
if len(parts) != 2 {
return fmt.Errorf("invalid mysql dsn format")
}
userPass := strings.Split(parts[0], ":")
if len(userPass) != 2 {
return fmt.Errorf("invalid mysql user:pass format")
}
hostPort := strings.Split(parts[1], "/")
if len(hostPort) != 2 {
return fmt.Errorf("invalid mysql host/db format")
}
host := strings.Split(hostPort[0], ":")
if len(host) == 2 {
viper.Set("database.port", host[1])
}
viper.Set("database.type", "mysql")
viper.Set("database.user", userPass[0])
viper.Set("database.password", userPass[1])
viper.Set("database.host", host[0])
viper.Set("database.dbname", hostPort[1])
return nil
}
if strings.HasPrefix(dsn, "postgres://") || strings.HasPrefix(dsn, "postgresql://") {
viper.Set("database.type", "postgres")
viper.Set("database.dsn", dsn)
return nil
}
return fmt.Errorf("unsupported database dsn format")
}
// validateConfig validates the loaded configuration
func validateConfig(config *Config) error {
if len(config.Tables) == 0 {
return fmt.Errorf("no tables configured")
}
for name, table := range config.Tables {
if name == "" {
return fmt.Errorf("table name cannot be empty")
}
if table.Alias == "" {
table.Alias = name
}
if table.Layout == "" {
table.Layout = "card"
}
if table.PageSize == 0 {
table.PageSize = 12
}
}
return nil
}
// GetTableConfig returns configuration for a specific table
func (c *Config) GetTableConfig(tableName string) (*TableConfig, bool) {
config, exists := c.Tables[tableName]
if !exists {
return nil, false
}
return &config, true
}
// GetDatabaseDSN returns the database connection string
func (c *Config) GetDatabaseDSN() string {
switch c.Database.Type {
case "sqlite":
return c.Database.Path
case "mysql":
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
c.Database.User, c.Database.Password, c.Database.Host, c.Database.Port, c.Database.DBName)
case "postgres":
if c.Database.DSN != "" {
return c.Database.DSN
}
return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
c.Database.Host, c.Database.Port, c.Database.User, c.Database.Password, c.Database.DBName)
default:
return c.Database.DSN
}
}

View File

@@ -0,0 +1,482 @@
package database
import (
"database/sql"
"fmt"
"log/slog"
"strings"
"time"
"github.com/rogeecn/database_render/internal/config"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// ConnectionManager manages database connections
type ConnectionManager struct {
db *gorm.DB
sqlDB *sql.DB
config *config.DatabaseConfig
logger *slog.Logger
}
// NewConnectionManager creates a new database connection manager
func NewConnectionManager(config *config.Config) (*ConnectionManager, error) {
logger := slog.With("component", "database")
cm := &ConnectionManager{
config: &config.Database,
logger: logger,
}
if err := cm.connect(); err != nil {
return nil, fmt.Errorf("failed to connect to database: %w", err)
}
if err := cm.configure(); err != nil {
return nil, fmt.Errorf("failed to configure database: %w", err)
}
logger.Info("database connection established",
"type", config.Database.Type,
"host", config.Database.Host,
"database", config.Database.DBName,
)
return cm, nil
}
// connect establishes the database connection
func (cm *ConnectionManager) connect() error {
var dialector gorm.Dialector
switch cm.config.Type {
case "sqlite":
dialector = sqlite.Open(cm.config.Path)
case "mysql":
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
cm.config.User, cm.config.Password, cm.config.Host, cm.config.Port, cm.config.DBName)
dialector = mysql.Open(dsn)
case "postgres":
if cm.config.DSN != "" {
dialector = postgres.Open(cm.config.DSN)
} else {
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
cm.config.Host, cm.config.Port, cm.config.User, cm.config.Password, cm.config.DBName)
dialector = postgres.Open(dsn)
}
default:
return fmt.Errorf("unsupported database type: %s", cm.config.Type)
}
gormConfig := &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
NowFunc: func() time.Time {
return time.Now().Local()
},
}
db, err := gorm.Open(dialector, gormConfig)
if err != nil {
return fmt.Errorf("failed to open database: %w", err)
}
cm.db = db
// Get underlying sql.DB for connection pooling
sqlDB, err := db.DB()
if err != nil {
return fmt.Errorf("failed to get sql.DB: %w", err)
}
cm.sqlDB = sqlDB
return nil
}
// configure sets up connection pool settings
func (cm *ConnectionManager) configure() error {
if cm.sqlDB == nil {
return fmt.Errorf("sql.DB is nil")
}
// Connection pool settings
cm.sqlDB.SetMaxIdleConns(10)
cm.sqlDB.SetMaxOpenConns(100)
cm.sqlDB.SetConnMaxLifetime(time.Hour)
// Ping to verify connection
if err := cm.sqlDB.Ping(); err != nil {
return fmt.Errorf("failed to ping database: %w", err)
}
return nil
}
// GetDB returns the GORM database instance
func (cm *ConnectionManager) GetDB() *gorm.DB {
return cm.db
}
// GetSQLDB returns the underlying SQL database instance
func (cm *ConnectionManager) GetSQLDB() *sql.DB {
return cm.sqlDB
}
// Close closes the database connection
func (cm *ConnectionManager) Close() error {
if cm.sqlDB != nil {
return cm.sqlDB.Close()
}
return nil
}
// Health checks the database health
func (cm *ConnectionManager) Health() error {
if cm.sqlDB == nil {
return fmt.Errorf("database not initialized")
}
return cm.sqlDB.Ping()
}
// GetTableNames returns all table names in the database
func (cm *ConnectionManager) GetTableNames() ([]string, error) {
var tableNames []string
switch cm.config.Type {
case "sqlite":
rows, err := cm.db.Raw("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").Rows()
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
return nil, err
}
tableNames = append(tableNames, name)
}
case "mysql":
rows, err := cm.db.Raw("SHOW TABLES").Rows()
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
return nil, err
}
tableNames = append(tableNames, name)
}
case "postgres":
rows, err := cm.db.Raw("SELECT tablename FROM pg_tables WHERE schemaname = 'public'").Rows()
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
return nil, err
}
tableNames = append(tableNames, name)
}
}
return tableNames, nil
}
// GetTableColumns returns column information for a table
func (cm *ConnectionManager) GetTableColumns(tableName string) ([]ColumnInfo, error) {
var columns []ColumnInfo
switch cm.config.Type {
case "sqlite":
rows, err := cm.db.Raw(fmt.Sprintf("PRAGMA table_info(%s)", tableName)).Rows()
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var col ColumnInfo
var notused interface{}
if err := rows.Scan(&col.Position, &col.Name, &col.Type, &col.NotNull, &notused, &col.DefaultValue); err != nil {
return nil, err
}
col.DatabaseType = cm.config.Type
columns = append(columns, col)
}
case "mysql":
rows, err := cm.db.Raw(fmt.Sprintf("DESCRIBE %s", tableName)).Rows()
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var col ColumnInfo
var key, extra, nullStr string
if err := rows.Scan(&col.Name, &col.Type, &nullStr, &key, &col.DefaultValue, &extra); err != nil {
return nil, err
}
col.NotNull = nullStr == "NO"
col.DatabaseType = cm.config.Type
columns = append(columns, col)
}
case "postgres":
query := `
SELECT
column_name,
data_type,
is_nullable,
column_default
FROM information_schema.columns
WHERE table_name = $1
ORDER BY ordinal_position
`
rows, err := cm.db.Raw(query, tableName).Rows()
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var col ColumnInfo
var nullable string
if err := rows.Scan(&col.Name, &col.Type, &nullable, &col.DefaultValue); err != nil {
return nil, err
}
col.NotNull = nullable == "NO"
col.DatabaseType = cm.config.Type
columns = append(columns, col)
}
}
return columns, nil
}
// ColumnInfo represents database column information
type ColumnInfo struct {
Name string
Type string
NotNull bool
DefaultValue interface{}
Position int
DatabaseType string
}
// GetTableData retrieves paginated data from a table
func (cm *ConnectionManager) GetTableData(
tableName string,
page, pageSize int,
search string,
sortField string,
sortOrder string,
) ([]map[string]interface{}, int64, error) {
var total int64
var data []map[string]interface{}
// Build count query for pagination
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM %s", tableName)
if search != "" {
// For search, we need to get column names first
columns, err := cm.GetTableColumns(tableName)
if err != nil {
return nil, 0, err
}
// Build WHERE clause for text columns
var whereConditions []string
for _, col := range columns {
if cm.isSearchableColumn(col.Type) {
whereConditions = append(whereConditions, fmt.Sprintf("%s LIKE '%%%s%%'", col.Name, search))
}
}
if len(whereConditions) > 0 {
countQuery += " WHERE " + strings.Join(whereConditions, " OR ")
}
}
if err := cm.db.Raw(countQuery).Scan(&total).Error; err != nil {
return nil, 0, err
}
// Build data query
dataQuery := fmt.Sprintf("SELECT * FROM %s", tableName)
if search != "" {
columns, err := cm.GetTableColumns(tableName)
if err != nil {
return nil, 0, err
}
var whereConditions []string
for _, col := range columns {
if cm.isSearchableColumn(col.Type) {
whereConditions = append(whereConditions, fmt.Sprintf("%s LIKE '%%%s%%'", col.Name, search))
}
}
if len(whereConditions) > 0 {
dataQuery += " WHERE " + strings.Join(whereConditions, " OR ")
}
}
// Add sorting
if sortField != "" {
order := "ASC"
if sortOrder == "desc" {
order = "DESC"
}
dataQuery += fmt.Sprintf(" ORDER BY %s %s", sortField, order)
}
// Add pagination
offset := (page - 1) * pageSize
dataQuery += fmt.Sprintf(" LIMIT %d OFFSET %d", pageSize, offset)
// Execute query
rows, err := cm.db.Raw(dataQuery).Rows()
if err != nil {
return nil, 0, err
}
defer rows.Close()
// Get column names
columnNames, err := rows.Columns()
if err != nil {
return nil, 0, err
}
// Scan data
for rows.Next() {
values := make([]interface{}, len(columnNames))
valuePtrs := make([]interface{}, len(columnNames))
for i := range values {
valuePtrs[i] = &values[i]
}
if err := rows.Scan(valuePtrs...); err != nil {
return nil, 0, err
}
row := make(map[string]interface{})
for i, col := range columnNames {
val := values[i]
// Handle NULL values
if val == nil {
row[col] = nil
continue
}
// Convert []byte to string for JSON compatibility
if b, ok := val.([]byte); ok {
row[col] = string(b)
} else {
row[col] = val
}
}
data = append(data, row)
}
return data, total, nil
}
// isSearchableColumn determines if a column type is searchable
func (cm *ConnectionManager) isSearchableColumn(columnType string) bool {
searchableTypes := []string{
"VARCHAR", "TEXT", "CHAR", "STRING",
"varchar", "text", "char", "string",
}
for _, t := range searchableTypes {
if strings.Contains(strings.ToUpper(columnType), strings.ToUpper(t)) {
return true
}
}
return false
}
// GetTableDataByID retrieves a single record by ID
func (cm *ConnectionManager) GetTableDataByID(tableName string, id interface{}) (map[string]interface{}, error) {
// Find primary key column
columns, err := cm.GetTableColumns(tableName)
if err != nil {
return nil, err
}
var primaryKey string
for _, col := range columns {
// Assume 'id' is the primary key if it exists
if col.Name == "id" || col.Name == "ID" {
primaryKey = col.Name
break
}
}
if primaryKey == "" {
// Fallback to first column
primaryKey = columns[0].Name
}
query := fmt.Sprintf("SELECT * FROM %s WHERE %s = ?", tableName, primaryKey)
rows, err := cm.db.Raw(query, id).Rows()
if err != nil {
return nil, err
}
defer rows.Close()
columnNames, err := rows.Columns()
if err != nil {
return nil, err
}
if rows.Next() {
values := make([]interface{}, len(columnNames))
valuePtrs := make([]interface{}, len(columnNames))
for i := range values {
valuePtrs[i] = &values[i]
}
if err := rows.Scan(valuePtrs...); err != nil {
return nil, err
}
row := make(map[string]interface{})
for i, col := range columnNames {
val := values[i]
if val == nil {
row[col] = nil
continue
}
if b, ok := val.([]byte); ok {
row[col] = string(b)
} else {
row[col] = val
}
}
return row, nil
}
return nil, fmt.Errorf("record not found")
}

View File

@@ -0,0 +1,161 @@
package handler
import (
"fmt"
"log/slog"
"net/http"
"strconv"
"github.com/gofiber/fiber/v3"
"github.com/rogeecn/database_render/internal/service"
)
// DataHandler handles HTTP requests for data operations
type DataHandler struct {
service *service.DataService
logger *slog.Logger
}
// NewDataHandler creates a new data handler
func NewDataHandler(service *service.DataService) *DataHandler {
return &DataHandler{
service: service,
logger: slog.With("component", "handler"),
}
}
// GetTables returns all configured tables
func (h *DataHandler) GetTables(c fiber.Ctx) error {
tables, err := h.service.GetTables()
if err != nil {
h.logger.Error("failed to get tables", "error", err)
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{
"error": "Failed to get tables",
})
}
h.logger.Debug("retrieved tables", "count", len(tables))
return c.JSON(fiber.Map{
"tables": tables,
})
}
// GetTableData returns paginated data for a table
func (h *DataHandler) GetTableData(c fiber.Ctx) error {
tableName := c.Params("table")
if tableName == "" {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
"error": "Table name is required",
})
}
// Parse query parameters
page, err := strconv.Atoi(c.Query("page", "1"))
if err != nil || page <= 0 {
page = 1
}
pageSize, err := strconv.Atoi(c.Query("per_page", "10"))
if err != nil || pageSize <= 0 {
pageSize = 10
}
search := c.Query("search", "")
sortField := c.Query("sort", "")
sortOrder := c.Query("order", "asc")
// Validate sort order
if sortOrder != "asc" && sortOrder != "desc" {
sortOrder = "asc"
}
// Get table data
data, err := h.service.GetTableData(tableName, page, pageSize, search, sortField, sortOrder)
if err != nil {
h.logger.Error("failed to get table data",
"table", tableName,
"page", page,
"error", err)
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{
"error": fmt.Sprintf("Failed to get table data: %v", err),
})
}
return c.JSON(data)
}
// GetTableDetail returns a single record detail
func (h *DataHandler) GetTableDetail(c fiber.Ctx) error {
tableName := c.Params("table")
if tableName == "" {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
"error": "Table name is required",
})
}
id := c.Params("id")
if id == "" {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
"error": "ID is required",
})
}
// Get table detail
detail, err := h.service.GetTableDetail(tableName, id)
if err != nil {
h.logger.Error("failed to get table detail",
"table", tableName,
"id", id,
"error", err)
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{
"error": fmt.Sprintf("Failed to get table detail: %v", err),
})
}
return c.JSON(detail)
}
// GetTableConfig returns the configuration for a specific table
func (h *DataHandler) GetTableConfig(c fiber.Ctx) error {
tableName := c.Params("table")
if tableName == "" {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
"error": "Table name is required",
})
}
config, err := h.service.GetTableConfig(tableName)
if err != nil {
h.logger.Error("failed to get table config",
"table", tableName,
"error", err)
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{
"error": fmt.Sprintf("Failed to get table config: %v", err),
})
}
return c.JSON(config)
}
// Health returns the health status
func (h *DataHandler) Health(c fiber.Ctx) error {
return c.JSON(fiber.Map{
"status": "ok",
"message": "Service is healthy",
})
}
// SetupRoutes sets up all the routes for the data handler
func (h *DataHandler) SetupRoutes(app *fiber.App) {
// API routes
api := app.Group("/api")
// Table routes
api.Get("/tables", h.GetTables)
api.Get("/data/:table", h.GetTableData)
api.Get("/data/:table/detail/:id", h.GetTableDetail)
api.Get("/config/:table", h.GetTableConfig)
// Health check
api.Get("/health", h.Health)
}

View File

@@ -0,0 +1,69 @@
package model
// TableConfig represents the configuration for a database table
type TableConfig struct {
Name string `json:"name" yaml:"name"`
Alias string `json:"alias" yaml:"alias"`
PageSize int `json:"page_size" yaml:"page_size"`
Columns []ColumnConfig `json:"columns" yaml:"columns"`
Filters []FilterConfig `json:"filters" yaml:"filters"`
SortFields []string `json:"sort_fields" yaml:"sort_fields"`
Options map[string]interface{} `json:"options" yaml:"options"`
}
// ColumnConfig represents the configuration for a table column
type ColumnConfig struct {
Name string `json:"name" yaml:"name"`
Alias string `json:"alias" yaml:"alias"`
RenderType string `json:"render_type" yaml:"render_type"`
Sortable bool `json:"sortable" yaml:"sortable"`
Searchable bool `json:"searchable" yaml:"searchable"`
ShowInList bool `json:"show_in_list" yaml:"show_in_list"`
IsPrimaryContent bool `json:"is_primary_content" yaml:"is_primary_content"`
MaxLength int `json:"max_length" yaml:"max_length"`
Width string `json:"width" yaml:"width"`
Format string `json:"format" yaml:"format"`
Values map[string]TagValue `json:"values" yaml:"values"`
Options map[string]interface{} `json:"options" yaml:"options"`
}
// FilterConfig represents the configuration for a table filter
type FilterConfig struct {
Name string `json:"name" yaml:"name"`
Type string `json:"type" yaml:"type"`
Options []interface{} `json:"options" yaml:"options"`
}
// TagValue represents a tag value with label and color
type TagValue struct {
Label string `json:"label" yaml:"label"`
Color string `json:"color" yaml:"color"`
}
// DataResponse represents the API response structure
type DataResponse struct {
Data []map[string]interface{} `json:"data"`
Total int64 `json:"total"`
Page int `json:"page"`
PerPage int `json:"per_page"`
Pages int `json:"pages"`
Table string `json:"table"`
Columns []ColumnConfig `json:"columns"`
Filters []FilterConfig `json:"filters"`
}
// TableResponse represents the table list response
type TableResponse struct {
Tables []TableInfo `json:"tables"`
}
// TableInfo represents basic table information
type TableInfo struct {
Name string `json:"name"`
Alias string `json:"alias"`
}
// DetailResponse represents a single record detail response
type DetailResponse struct {
Data map[string]interface{} `json:"data"`
}

View File

@@ -0,0 +1,192 @@
package repository
import (
"fmt"
"log/slog"
"github.com/rogeecn/database_render/internal/config"
"github.com/rogeecn/database_render/internal/database"
"github.com/rogeecn/database_render/internal/model"
)
// DataRepository handles data access operations
type DataRepository struct {
db *database.ConnectionManager
config *config.Config
logger *slog.Logger
}
// NewDataRepository creates a new data repository
func NewDataRepository(db *database.ConnectionManager, cfg *config.Config) *DataRepository {
return &DataRepository{
db: db,
config: cfg,
logger: slog.With("component", "repository"),
}
}
// GetTables returns all configured tables
func (r *DataRepository) GetTables() ([]model.TableInfo, error) {
var tables []model.TableInfo
for name, tableConfig := range r.config.Tables {
tables = append(tables, model.TableInfo{
Name: name,
Alias: tableConfig.Alias,
})
}
return tables, nil
}
// GetTableConfig returns the configuration for a specific table
func (r *DataRepository) GetTableConfig(tableName string) (*model.TableConfig, error) {
tableConfig, exists := r.config.Tables[tableName]
if !exists {
return nil, fmt.Errorf("table %s not found in configuration", tableName)
}
// Convert internal config to model config
config := &model.TableConfig{
Name: tableName,
Alias: tableConfig.Alias,
PageSize: tableConfig.PageSize,
Columns: []model.ColumnConfig{},
Filters: []model.FilterConfig{},
Options: tableConfig.Options,
}
// Convert field configurations
for fieldName, fieldConfig := range tableConfig.Fields {
column := model.ColumnConfig{
Name: fieldName,
Alias: fieldName,
RenderType: fieldConfig.Type,
Sortable: true, // Default to true
Searchable: fieldConfig.Searchable,
ShowInList: !fieldConfig.Hidden,
IsPrimaryContent: fieldConfig.Markdown,
MaxLength: fieldConfig.MaxLength,
Width: fieldConfig.Size,
Format: fieldConfig.Format,
Values: make(map[string]model.TagValue),
Options: fieldConfig.Options,
}
// Handle tag values if colors are provided
if len(fieldConfig.Colors) > 0 {
for key, color := range fieldConfig.Colors {
label := key
column.Values[key] = model.TagValue{
Label: label,
Color: color,
}
}
}
config.Columns = append(config.Columns, column)
}
return config, nil
}
// GetTableData retrieves paginated data from a table
func (r *DataRepository) GetTableData(tableName string, page, pageSize int, search string, sortField string, sortOrder string) ([]map[string]interface{}, int64, error) {
// Validate table exists in config
_, exists := r.config.Tables[tableName]
if !exists {
return nil, 0, fmt.Errorf("table %s not found in configuration", tableName)
}
// Get data from database
data, total, err := r.db.GetTableData(tableName, page, pageSize, search, sortField, sortOrder)
if err != nil {
r.logger.Error("failed to get table data",
"table", tableName,
"page", page,
"error", err)
return nil, 0, fmt.Errorf("failed to get table data: %w", err)
}
r.logger.Debug("retrieved table data",
"table", tableName,
"page", page,
"pageSize", pageSize,
"total", total,
"records", len(data))
return data, total, nil
}
// GetTableDataByID retrieves a single record by ID
func (r *DataRepository) GetTableDataByID(tableName string, id interface{}) (map[string]interface{}, error) {
// Validate table exists in config
_, exists := r.config.Tables[tableName]
if !exists {
return nil, fmt.Errorf("table %s not found in configuration", tableName)
}
// Get data from database
data, err := r.db.GetTableDataByID(tableName, id)
if err != nil {
r.logger.Error("failed to get table data by ID",
"table", tableName,
"id", id,
"error", err)
return nil, fmt.Errorf("failed to get table data by ID: %w", err)
}
r.logger.Debug("retrieved single record",
"table", tableName,
"id", id)
return data, nil
}
// GetTableColumns returns column information for a table
func (r *DataRepository) GetTableColumns(tableName string) ([]database.ColumnInfo, error) {
// Validate table exists in config
_, exists := r.config.Tables[tableName]
if !exists {
return nil, fmt.Errorf("table %s not found in configuration", tableName)
}
// Get column information from database
columns, err := r.db.GetTableColumns(tableName)
if err != nil {
r.logger.Error("failed to get table columns",
"table", tableName,
"error", err)
return nil, fmt.Errorf("failed to get table columns: %w", err)
}
return columns, nil
}
// ValidateTableConfig validates if the configured tables exist in the database
func (r *DataRepository) ValidateTableConfig() error {
// Get all table names from database
dbTables, err := r.db.GetTableNames()
if err != nil {
return fmt.Errorf("failed to get table names from database: %w", err)
}
// Create a map for quick lookup
dbTableMap := make(map[string]bool)
for _, table := range dbTables {
dbTableMap[table] = true
}
// Check if all configured tables exist
for tableName := range r.config.Tables {
if !dbTableMap[tableName] {
return fmt.Errorf("table %s not found in database", tableName)
}
}
r.logger.Info("table configuration validation completed",
"configured_tables", len(r.config.Tables),
"database_tables", len(dbTables))
return nil
}

View File

@@ -0,0 +1,204 @@
package service
import (
"fmt"
"log/slog"
"github.com/rogeecn/database_render/internal/config"
"github.com/rogeecn/database_render/internal/database"
"github.com/rogeecn/database_render/internal/model"
"github.com/rogeecn/database_render/internal/repository"
)
// DataService handles business logic for data operations
type DataService struct {
repo *repository.DataRepository
config *config.Config
logger *slog.Logger
}
// NewDataService creates a new data service
func NewDataService(repo *repository.DataRepository, cfg *config.Config) *DataService {
return &DataService{
repo: repo,
config: cfg,
logger: slog.With("component", "service"),
}
}
// GetTables returns all configured tables
func (s *DataService) GetTables() ([]model.TableInfo, error) {
tables, err := s.repo.GetTables()
if err != nil {
s.logger.Error("failed to get tables", "error", err)
return nil, err
}
s.logger.Debug("retrieved tables", "count", len(tables))
return tables, nil
}
// GetTableData returns paginated data for a table
func (s *DataService) GetTableData(tableName string, page, pageSize int, search string, sortField string, sortOrder string) (*model.DataResponse, error) {
// Validate table exists
tableConfig, err := s.repo.GetTableConfig(tableName)
if err != nil {
s.logger.Error("failed to get table config", "table", tableName, "error", err)
return nil, err
}
// Use configured page size if not provided
if pageSize <= 0 {
pageSize = tableConfig.PageSize
}
// Validate page and page size
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
if pageSize > 100 {
pageSize = 100 // Limit maximum page size
}
// Get data from repository
data, total, err := s.repo.GetTableData(tableName, page, pageSize, search, sortField, sortOrder)
if err != nil {
s.logger.Error("failed to get table data",
"table", tableName,
"page", page,
"pageSize", pageSize,
"search", search,
"error", err)
return nil, err
}
// Calculate total pages
totalPages := int((total + int64(pageSize) - 1) / int64(pageSize))
response := &model.DataResponse{
Data: data,
Total: total,
Page: page,
PerPage: pageSize,
Pages: totalPages,
Table: tableName,
Columns: tableConfig.Columns,
Filters: tableConfig.Filters,
}
s.logger.Info("retrieved table data",
"table", tableName,
"page", page,
"pageSize", pageSize,
"total", total,
"pages", totalPages)
return response, nil
}
// GetTableDetail returns a single record detail
func (s *DataService) GetTableDetail(tableName string, id interface{}) (*model.DetailResponse, error) {
// Validate table exists
_, err := s.repo.GetTableConfig(tableName)
if err != nil {
s.logger.Error("failed to get table config", "table", tableName, "error", err)
return nil, err
}
// Get data from repository
data, err := s.repo.GetTableDataByID(tableName, id)
if err != nil {
s.logger.Error("failed to get table detail",
"table", tableName,
"id", id,
"error", err)
return nil, err
}
response := &model.DetailResponse{
Data: data,
}
s.logger.Debug("retrieved table detail",
"table", tableName,
"id", id)
return response, nil
}
// GetTableColumns returns column information for a table
func (s *DataService) GetTableColumns(tableName string) ([]database.ColumnInfo, error) {
// Validate table exists
_, err := s.repo.GetTableConfig(tableName)
if err != nil {
s.logger.Error("failed to get table config", "table", tableName, "error", err)
return nil, err
}
// Get column information from repository
columns, err := s.repo.GetTableColumns(tableName)
if err != nil {
s.logger.Error("failed to get table columns",
"table", tableName,
"error", err)
return nil, err
}
return columns, nil
}
// ValidateConfiguration validates the entire configuration
func (s *DataService) ValidateConfiguration() error {
// Validate tables configuration
if len(s.config.Tables) == 0 {
return fmt.Errorf("no tables configured")
}
// Validate table existence in database
if err := s.repo.ValidateTableConfig(); err != nil {
return err
}
// Validate individual table configurations
for tableName, tableConfig := range s.config.Tables {
if tableConfig.Alias == "" {
return fmt.Errorf("table %s has empty alias", tableName)
}
if tableConfig.PageSize <= 0 {
return fmt.Errorf("table %s has invalid page size", tableName)
}
// Validate field configurations
for fieldName, fieldConfig := range tableConfig.Fields {
if fieldConfig.Type == "" {
return fmt.Errorf("field %s in table %s has empty type", fieldName, tableName)
}
}
}
s.logger.Info("configuration validation completed successfully")
return nil
}
// GetTableConfig returns the configuration for a specific table
func (s *DataService) GetTableConfig(tableName string) (*model.TableConfig, error) {
return s.repo.GetTableConfig(tableName)
}
// GetDefaultTable returns the first configured table name
func (s *DataService) GetDefaultTable() (string, error) {
if len(s.config.Tables) == 0 {
return "", fmt.Errorf("no tables configured")
}
// Return the first table name
for tableName := range s.config.Tables {
return tableName, nil
}
return "", fmt.Errorf("no tables configured")
}

View File

@@ -0,0 +1,250 @@
package template
import (
"encoding/json"
"fmt"
"html/template"
"log/slog"
"net/http"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/static"
"github.com/rogeecn/database_render/internal/config"
"github.com/rogeecn/database_render/internal/service"
)
// Renderer handles template rendering
type Renderer struct {
templates *template.Template
service *service.DataService
config *config.Config
logger *slog.Logger
}
// NewRenderer creates a new template renderer
func NewRenderer(service *service.DataService, cfg *config.Config) (*Renderer, error) {
r := &Renderer{
service: service,
config: cfg,
logger: slog.With("component", "renderer"),
}
if err := r.loadTemplates(); err != nil {
return nil, fmt.Errorf("failed to load templates: %w", err)
}
return r, nil
}
// loadTemplates loads all templates from the templates directory
func (r *Renderer) loadTemplates() error {
// Define template functions
funcMap := template.FuncMap{
"dict": func(values ...interface{}) map[string]interface{} {
dict := make(map[string]interface{})
for i := 0; i < len(values); i += 2 {
if i+1 < len(values) {
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 },
"mul": func(a, b int) int { return a * b },
"min": func(a, b int) int {
if a < b {
return a
}
return b
},
"max": func(a, b int) int {
if a > b {
return a
}
return b
},
"json": func(v interface{}) string {
b, _ := json.Marshal(v)
return string(b)
},
"split": func(s string, sep string) []string {
return strings.Split(s, sep)
},
"formatTime": func(t interface{}) string {
switch v := t.(type) {
case time.Time:
return v.Format("2006-01-02 15:04:05")
case string:
return v
default:
return fmt.Sprintf("%v", v)
}
},
"renderField": func(value interface{}, renderType string, column interface{}) template.HTML {
switch renderType {
case "time":
return template.HTML(r.formatTime(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":
// Return raw content for client-side markdown rendering
return template.HTML(fmt.Sprintf("%v", value))
default:
return template.HTML(fmt.Sprintf("%v", value))
}
},
"truncate": func(s string, length int) string {
if len(s) <= length {
return s
}
if length > 3 {
return s[:length-3] + "..."
}
return s[:length]
},
"eq": func(a, b interface{}) bool {
return fmt.Sprintf("%v", a) == fmt.Sprintf("%v", b)
},
}
// Load templates
templateDir := "web/templates"
pattern := filepath.Join(templateDir, "*.html")
tmpl, err := template.New("").Funcs(funcMap).ParseGlob(pattern)
if err != nil {
return fmt.Errorf("failed to parse templates: %w", err)
}
r.templates = tmpl
r.logger.Info("templates loaded successfully")
return nil
}
// formatTime formats time values for display
func (r *Renderer) formatTime(value interface{}) string {
switch v := value.(type) {
case time.Time:
return v.Format("2006-01-02 15:04:05")
case string:
// Try to parse as time
if t, err := time.Parse("2006-01-02T15:04:05Z", v); err == nil {
return t.Format("2006-01-02 15:04:05")
}
return v
default:
return fmt.Sprintf("%v", v)
}
}
// RenderList renders the list view for a table
func (r *Renderer) RenderList(c fiber.Ctx, tableName string) error {
// Parse query parameters
page, _ := strconv.Atoi(c.Query("page", "1"))
pageSize, _ := strconv.Atoi(c.Query("per_page", "10"))
search := c.Query("search", "")
sortField := c.Query("sort", "")
sortOrder := c.Query("order", "asc")
// Get table data
data, err := r.service.GetTableData(tableName, page, pageSize, search, sortField, sortOrder)
if err != nil {
r.logger.Error("failed to get table data", "error", err)
return c.Status(http.StatusInternalServerError).SendString("Failed to load table data")
}
// Get all tables for navigation
tables, err := r.service.GetTables()
if err != nil {
r.logger.Error("failed to get tables", "error", err)
return c.Status(http.StatusInternalServerError).SendString("Failed to load tables")
}
// Get table alias from config
tableConfig, err := r.service.GetTableConfig(tableName)
if err != nil {
r.logger.Error("failed to get table config", "error", err)
return c.Status(http.StatusInternalServerError).SendString("Failed to load table configuration")
}
// Prepare template data
templateData := map[string]interface{}{
"Table": tableName,
"TableAlias": tableConfig.Alias,
"Columns": data.Columns,
"Data": data.Data,
"Total": data.Total,
"Page": data.Page,
"PerPage": data.PerPage,
"Pages": data.Pages,
"Search": search,
"SortField": sortField,
"SortOrder": sortOrder,
"Tables": tables,
"CurrentPath": c.Path(),
}
// set content-type html
c.Response().Header.Set("Content-Type", "text/html; charset=utf-8")
// Render template
return r.templates.ExecuteTemplate(c.Response().BodyWriter(), "list.html", templateData)
}
// RenderIndex renders the index page
func (r *Renderer) RenderIndex(c fiber.Ctx) error {
// Get default table
defaultTable, err := r.service.GetDefaultTable()
if err != nil {
r.logger.Error("failed to get default table", "error", err)
return c.Status(http.StatusInternalServerError).SendString("No tables configured")
}
// Redirect to default table
return c.Redirect().To(fmt.Sprintf("/?table=%s", defaultTable))
}
// ServeStatic serves static files
func (r *Renderer) ServeStatic(app *fiber.App) {
// Serve static files
app.Use("/static/*", static.New("web/static"))
app.Use("/css/*", static.New("web/static/css"))
app.Use("/js/*", static.New("web/static/js"))
app.Use("/images/*", static.New("web/static/images"))
}
// NotFoundHandler handles 404 errors
func (r *Renderer) NotFoundHandler(c fiber.Ctx) error {
return c.Status(http.StatusNotFound).SendString("Page not found")
}
// ErrorHandler handles errors
func (r *Renderer) ErrorHandler(c fiber.Ctx, err error) error {
code := fiber.StatusInternalServerError
if e, ok := err.(*fiber.Error); ok {
code = e.Code
}
r.logger.Error("request error", "error", err, "code", code)
return c.Status(code).JSON(fiber.Map{
"error": err.Error(),
})
}

10
main.go Normal file
View File

@@ -0,0 +1,10 @@
/*
Copyright © 2025 NAME HERE <EMAIL ADDRESS>
*/
package main
import "github.com/rogeecn/database_render/cmd"
func main() {
cmd.Execute()
}

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+

345
scripts/build.sh Executable file
View File

@@ -0,0 +1,345 @@
#!/bin/bash
# Database Render Application - Build Script
# This script provides comprehensive build functionality for the application
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
BINARY_NAME="database-render"
DIST_DIR="$PROJECT_DIR/dist"
BIN_DIR="$PROJECT_DIR/bin"
# Functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Check if Go is installed
check_go() {
if ! command -v go >/dev/null 2>&1; then
log_error "Go is not installed. Please install Go first."
exit 1
fi
local go_version=$(go version | awk '{print $3}')
log_info "Go version: $go_version"
}
# Check if required tools are installed
check_tools() {
local tools=("go")
for tool in "${tools[@]}"; do
if command -v "$tool" >/dev/null 2>&1; then
log_success "$tool is available"
else
log_error "$tool is not installed"
exit 1
fi
done
}
# Clean build artifacts
clean() {
log_info "Cleaning build artifacts..."
if [ -d "$BIN_DIR" ]; then
rm -rf "$BIN_DIR"
log_success "Removed $BIN_DIR"
fi
if [ -d "$DIST_DIR" ]; then
rm -rf "$DIST_DIR"
log_success "Removed $DIST_DIR"
fi
go clean
log_success "Clean completed"
}
# Install dependencies
install_deps() {
log_info "Installing dependencies..."
cd "$PROJECT_DIR"
go mod tidy
go mod download
log_success "Dependencies installed"
}
# Build for current platform
build_current() {
log_info "Building for current platform..."
mkdir -p "$BIN_DIR"
cd "$PROJECT_DIR"
local build_time=$(date -u '+%Y-%m-%d_%H:%M:%S')
local git_commit=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
go build \
-ldflags "-X main.Version=dev -X main.BuildTime=$build_time -X main.GitCommit=$git_commit" \
-o "$BIN_DIR/$BINARY_NAME" \
./cmd/server
log_success "Built for current platform: $BIN_DIR/$BINARY_NAME"
}
# Build for multiple platforms
build_all() {
log_info "Building for multiple platforms..."
mkdir -p "$DIST_DIR"
cd "$PROJECT_DIR"
local build_time=$(date -u '+%Y-%m-%d_%H:%M:%S')
local git_commit=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
local ldflags="-X main.Version=dev -X main.BuildTime=$build_time -X main.GitCommit=$git_commit"
# Build targets
local targets=(
"linux/amd64"
"linux/arm64"
"darwin/amd64"
"darwin/arm64"
"windows/amd64"
)
for target in "${targets[@]}"; do
local os=$(echo "$target" | cut -d'/' -f1)
local arch=$(echo "$target" | cut -d'/' -f2)
local ext=""
if [ "$os" = "windows" ]; then
ext=".exe"
fi
log_info "Building for $os/$arch..."
GOOS=$os GOARCH=$arch go build \
-ldflags "$ldflags" \
-o "$DIST_DIR/${BINARY_NAME}-${os}-${arch}${ext}" \
./cmd/server
log_success "Built: ${BINARY_NAME}-${os}-${arch}${ext}"
done
log_success "Multi-platform build completed in $DIST_DIR/"
}
# Run tests
run_tests() {
log_info "Running tests..."
cd "$PROJECT_DIR"
go test -v ./...
log_success "Tests completed"
}
# Run tests with coverage
run_tests_coverage() {
log_info "Running tests with coverage..."
cd "$PROJECT_DIR"
go test -v -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
log_success "Tests completed with coverage report: coverage.html"
}
# Format code
format_code() {
log_info "Formatting code..."
cd "$PROJECT_DIR"
go fmt ./...
log_success "Code formatted"
}
# Check code formatting
check_format() {
log_info "Checking code formatting..."
cd "$PROJECT_DIR"
local unformatted=$(gofmt -l .)
if [ -n "$unformatted" ]; then
log_error "Code is not properly formatted. Run 'go fmt ./...' to fix."
echo "$unformatted"
exit 1
else
log_success "Code formatting is correct"
fi
}
# Run linter
run_linter() {
if command -v golangci-lint >/dev/null 2>&1; then
log_info "Running linter..."
cd "$PROJECT_DIR"
golangci-lint run
log_success "Linter completed"
else
log_warning "golangci-lint not found. Install with: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest"
fi
}
# Security scan
security_scan() {
if command -v gosec >/dev/null 2>&1; then
log_info "Running security scan..."
cd "$PROJECT_DIR"
gosec ./...
log_success "Security scan completed"
else
log_warning "gosec not found. Install with: go install github.com/securecodewarrior/gosec/v2/cmd/gosec@latest"
fi
}
# Initialize project
init_project() {
log_info "Initializing project..."
# Create directories
mkdir -p "$PROJECT_DIR/config"
mkdir -p "$PROJECT_DIR/data"
mkdir -p "$PROJECT_DIR/logs"
# Copy example configuration if it doesn't exist
if [ ! -f "$PROJECT_DIR/config/config.yaml" ]; then
cp "$PROJECT_DIR/config/config.example.yaml" "$PROJECT_DIR/config/config.yaml"
log_success "Created config.yaml from example"
fi
# Copy schema if it doesn't exist
if [ ! -f "$PROJECT_DIR/config/schema.sql" ]; then
cp "$PROJECT_DIR/config/schema.example.sql" "$PROJECT_DIR/config/schema.sql"
log_success "Created schema.sql from example"
fi
install_deps
log_success "Project initialized successfully!"
log_info "Please edit config/config.yaml to configure your database connection"
log_info "Then run: $0 build-current"
}
# Show help
show_help() {
echo "Database Render Application - Build Script"
echo ""
echo "Usage: $0 [command]"
echo ""
echo "Commands:"
echo " init - Initialize project with sample configuration"
echo " deps - Install dependencies"
echo " clean - Clean build artifacts"
echo " build - Build for current platform"
echo " build-all - Build for multiple platforms"
echo " test - Run tests"
echo " test-cov - Run tests with coverage"
echo " fmt - Format code"
echo " fmt-check - Check code formatting"
echo " lint - Run linter"
echo " security - Run security scan"
echo " all - Run full build pipeline (clean, deps, test, build)"
echo " help - Show this help message"
echo ""
echo "Examples:"
echo " $0 init"
echo " $0 build"
echo " $0 test-cov"
}
# Main function
main() {
case "${1:-help}" in
"init")
check_go
init_project
;;
"deps")
check_go
install_deps
;;
"clean")
clean
;;
"build")
check_go
check_tools
install_deps
build_current
;;
"build-all")
check_go
check_tools
install_deps
build_all
;;
"test")
check_go
run_tests
;;
"test-cov")
check_go
run_tests_coverage
;;
"fmt")
check_go
format_code
;;
"fmt-check")
check_go
check_format
;;
"lint")
check_go
run_linter
;;
"security")
security_scan
;;
"all")
check_go
check_tools
clean
install_deps
check_format
run_tests
build_current
log_success "Full build pipeline completed!"
;;
"help"|*)
show_help
;;
esac
}
# Run main function with all arguments
main "$@"

360
scripts/dev.sh Executable file
View File

@@ -0,0 +1,360 @@
#!/bin/bash
# Database Render Application - Development Script
# This script provides development utilities for the application
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
BINARY_NAME="database-render"
CONFIG_FILE="$PROJECT_DIR/config/config.yaml"
# Functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Check if service is running
check_service() {
local port=${1:-8080}
if curl -sf http://localhost:$port/health >/dev/null; then
return 0
else
return 1
fi
}
# Wait for service to be ready
wait_for_service() {
local port=${1:-8080}
local max_attempts=30
local attempt=1
log_info "Waiting for service to be ready on port $port..."
while [ $attempt -le $max_attempts ]; do
if check_service $port; then
log_success "Service is ready!"
return 0
fi
log_info "Attempt $attempt/$max_attempts - waiting..."
sleep 2
((attempt++))
done
log_error "Service failed to start within $max_attempts attempts"
return 1
}
# Initialize SQLite database
init_sqlite() {
local db_file="$PROJECT_DIR/data/app.db"
if [ ! -f "$db_file" ]; then
log_info "Creating SQLite database at $db_file..."
mkdir -p "$(dirname "$db_file")"
sqlite3 "$db_file" < "$PROJECT_DIR/config/schema.example.sql"
log_success "SQLite database initialized"
else
log_info "SQLite database already exists at $db_file"
fi
}
# Generate sample data
generate_data() {
local db_file="$PROJECT_DIR/data/app.db"
if [ -f "$db_file" ]; then
log_info "Generating sample data..."
sqlite3 "$db_file" <<EOF
-- Insert sample data if tables are empty
INSERT OR IGNORE INTO users (username, email, full_name, role) VALUES
('admin', 'admin@example.com', 'System Administrator', 'admin'),
('developer', 'dev@example.com', 'Developer User', 'user'),
('editor', 'editor@example.com', 'Content Editor', 'user');
INSERT OR IGNORE INTO posts (title, content, category, tags, author_id, status) VALUES
('Welcome to Database Render', '# Welcome to Database Render
This is your first post created by the development script!
## Features
- Easy database visualization
- Modern card-based UI
- Responsive design
- Multiple database support
## Getting Started
1. Configure your database connection in config/config.yaml
2. Run the application
3. Start exploring your data!', 'Technology', 'welcome,getting-started', 1, 'published'),
('Development Tips', '# Development Tips
Here are some useful development tips:
## Hot Reload
Use air for hot reloading during development:
```bash
make watch
```
## Database Setup
The development script can initialize your SQLite database automatically.
## Configuration
Edit config/config.yaml to customize your setup.', 'Programming', 'development,tips', 2, 'published');
EOF
log_success "Sample data generated"
else
log_error "Database file not found: $db_file"
return 1
fi
}
# Start development server
start_dev() {
log_info "Starting development server..."
# Ensure config exists
if [ ! -f "$CONFIG_FILE" ]; then
log_info "Creating development configuration..."
cp "$PROJECT_DIR/config/config.example.yaml" "$CONFIG_FILE"
# Update config for development
sed -i.bak 's/path: "data\/app.db"/path: "data\/app.db"/' "$CONFIG_FILE"
sed -i.bak 's/debug: false/debug: true/' "$CONFIG_FILE"
sed -i.bak 's/level: "info"/level: "debug"/' "$CONFIG_FILE"
rm -f "$CONFIG_FILE.bak"
log_success "Development configuration created"
fi
# Initialize database
init_sqlite
# Generate sample data
generate_data
# Build and run
cd "$PROJECT_DIR"
if [ -f "Makefile" ]; then
log_info "Building with Makefile..."
make build
if [ -f "$PROJECT_DIR/bin/$BINARY_NAME" ]; then
log_success "Starting application..."
CONFIG_FILE="$CONFIG_FILE" "$PROJECT_DIR/bin/$BINARY_NAME"
else
log_error "Binary not found: $PROJECT_DIR/bin/$BINARY_NAME"
exit 1
fi
else
log_error "Makefile not found"
exit 1
fi
}
# Start with hot reload
start_hot_reload() {
log_info "Starting with hot reload..."
# Check if air is installed
if command -v air >/dev/null 2>&1; then
log_info "Using air for hot reload..."
cd "$PROJECT_DIR"
air
else
log_warning "air not found. Installing..."
go install github.com/cosmtrek/air@latest
if command -v air >/dev/null 2>&1; then
cd "$PROJECT_DIR"
air
else
log_error "Failed to install air"
exit 1
fi
fi
}
# Database utilities
reset_db() {
local db_file="$PROJECT_DIR/data/app.db"
if [ -f "$db_file" ]; then
log_info "Resetting database..."
rm -f "$db_file"
init_sqlite
generate_data
log_success "Database reset completed"
else
log_info "Database not found, creating new..."
init_sqlite
generate_data
fi
}
# Health check
health_check() {
local port=${1:-8080}
if check_service $port; then
log_success "Service is healthy on port $port"
curl -s http://localhost:$port/health | jq . 2>/dev/null || curl -s http://localhost:$port/health
else
log_error "Service is not responding on port $port"
return 1
fi
}
# Show logs
tail_logs() {
local log_file="$PROJECT_DIR/logs/app.log"
if [ -f "$log_file" ]; then
log_info "Tailing application logs..."
tail -f "$log_file"
else
log_info "Log file not found, checking stdout..."
log_info "Application is logging to stdout/stderr"
fi
}
# Open browser
open_browser() {
local port=${1:-8080}
local url="http://localhost:$port"
log_info "Opening browser at $url..."
case "$(uname -s)" in
Darwin*)
open "$url"
;;
Linux*)
if command -v xdg-open >/dev/null 2>&1; then
xdg-open "$url"
elif command -v gnome-open >/dev/null 2>&1; then
gnome-open "$url"
else
log_info "Please open your browser and navigate to: $url"
fi
;;
CYGWIN*|MINGW*|MSYS*)
start "$url"
;;
*)
log_info "Please open your browser and navigate to: $url"
;;
esac
}
# Show help
show_help() {
echo "Database Render Application - Development Script"
echo ""
echo "Usage: $0 [command] [options]"
echo ""
echo "Commands:"
echo " start - Start development server"
echo " watch - Start with hot reload (using air)"
echo " init-db - Initialize SQLite database"
echo " reset-db - Reset database with sample data"
echo " health - Check service health"
echo " logs - Tail application logs"
echo " open - Open browser to application"
echo " help - Show this help message"
echo ""
echo "Options:"
echo " --port PORT - Specify port (default: 8080)"
echo ""
echo "Examples:"
echo " $0 start"
echo " $0 watch"
echo " $0 reset-db"
echo " $0 health --port 8080"
echo " $0 open --port 8080"
}
# Parse command line arguments
parse_args() {
local command="$1"
local port=8080
while [[ $# -gt 0 ]]; do
case $1 in
--port)
port="$2"
shift 2
;;
*)
shift
;;
esac
done
case "$command" in
"start")
start_dev
;;
"watch")
start_hot_reload
;;
"init-db")
init_sqlite
generate_data
;;
"reset-db")
reset_db
;;
"health")
health_check $port
;;
"logs")
tail_logs
;;
"open")
open_browser $port
;;
"help"|*)
show_help
;;
esac
}
# Main execution
main() {
cd "$PROJECT_DIR"
parse_args "$@"
}
# Run main function
main "$@"

278
web/templates/list.html Normal file
View File

@@ -0,0 +1,278 @@
<!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;
}
.card {
@apply bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow;
}
.card-body {
@apply p-6;
}
.loading {
@apply animate-pulse;
}
</style>
</head>
<body class="bg-gray-50">
<div class="min-h-screen">
<!-- Header -->
<header class="bg-white shadow-sm border-b">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center py-4">
<div>
<h1 class="text-2xl font-bold text-gray-900">数据管理系统</h1>
<p class="text-sm text-gray-600">{{.TableAlias}}</p>
</div>
<div class="flex items-center space-x-4">
<select id="tableSelector" class="border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" onchange="changeTable(this.value)">
{{range .Tables}}
<option value="{{.Name}}" {{if eq .Name $.Table}}selected{{end}}>{{.Alias}}</option>
{{end}}
</select>
<div class="text-sm text-gray-600">
共 {{.Total}} 条记录
</div>
</div>
</div>
</div>
</header>
<!-- Main Content -->
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Search and Filters -->
<div class="mb-6 bg-white p-4 rounded-lg shadow">
<div class="flex flex-col sm:flex-row gap-4">
<div class="flex-1">
<input type="text" id="searchInput" placeholder="搜索..." value="{{.Search}}"
class="w-full border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div class="flex gap-2">
<button onclick="performSearch()" class="px-4 py-2 bg-blue-600 text-white rounded-md text-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500">
搜索
</button>
<button onclick="clearSearch()" class="px-4 py-2 bg-gray-300 text-gray-700 rounded-md text-sm hover:bg-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-500">
清除
</button>
</div>
</div>
</div>
<!-- Data Display -->
<div class="grid gap-6">
<!-- Table View -->
<div class="bg-white shadow overflow-hidden sm:rounded-md">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
{{range $col := .Columns}}
{{if $col.ShowInList}}
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{{$col.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 $row := .Data}}
<tr class="hover:bg-gray-50">
{{range $col := $.Columns}}
{{if $col.ShowInList}}
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{{if eq $col.RenderType "text"}}
{{truncate (index $row $col.Name) 50}}
{{else}}
{{index $row $col.Name}}
{{end}}
</td>
{{end}}
{{end}}
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
<button onclick="showDetail('{{index $row "id"}}')" class="text-blue-600 hover:text-blue-800">
查看详情
</button>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
<!-- Pagination -->
{{if gt .Pages 1}}
<div class="mt-6 bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
<div class="flex-1 flex justify-between sm:hidden">
{{if gt .Page 1}}
<a href="?table={{.Table}}&page={{sub .Page 1}}&per_page={{.PerPage}}&search={{.Search}}&sort={{.SortField}}&order={{.SortOrder}}"
class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
上一页
</a>
{{end}}
{{if lt .Page .Pages}}
<a href="?table={{.Table}}&page={{add .Page 1}}&per_page={{.PerPage}}&search={{.Search}}&sort={{.SortField}}&order={{.SortOrder}}"
class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
下一页
</a>
{{end}}
</div>
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p class="text-sm text-gray-700">
显示第 <span class="font-medium">{{add (mul (sub .Page 1) .PerPage) 1}}</span>
<span class="font-medium">{{if lt (mul .Page .PerPage) .Total}}{{mul .Page .PerPage}}{{else}}{{.Total}}{{end}}</span>
条,共 <span class="font-medium">{{.Total}}</span> 条记录
</p>
</div>
<div>
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px">
{{if gt .Page 1}}
<a href="?table={{.Table}}&page={{sub .Page 1}}&per_page={{.PerPage}}&search={{.Search}}&sort={{.SortField}}&order={{.SortOrder}}"
class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
上一页
</a>
{{end}}
{{$start := sub .Page 2}}
{{$end := add .Page 2}}
{{if lt $start 1}}
{{$start = 1}}
{{$end = min 5 .Pages}}
{{end}}
{{if gt $end .Pages}}
{{$end = .Pages}}
{{$start = max 1 (sub .Pages 4)}}
{{end}}
{{range $i := $start | $end}}
{{if eq $i .Page}}
<span class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-blue-50 text-sm font-medium text-blue-600">
{{$i}}
</span>
{{else}}
<a href="?table={{.Table}}&page={{$i}}&per_page={{.PerPage}}&search={{.Search}}&sort={{.SortField}}&order={{.SortOrder}}"
class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50">
{{$i}}
</a>
{{end}}
{{end}}
{{if lt .Page .Pages}}
<a href="?table={{.Table}}&page={{add .Page 1}}&per_page={{.PerPage}}&search={{.Search}}&sort={{.SortField}}&order={{.SortOrder}}"
class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
下一页
</a>
{{end}}
</nav>
</div>
</div>
</div>
{{end}}
</main>
</div>
<!-- Detail Modal -->
<div id="detailModal" class="modal">
<div class="modal-content">
<div class="p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-medium">详情信息</h3>
<button onclick="closeModal()" class="text-gray-400 hover:text-gray-600">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<div id="detailContent" class="space-y-4">
<!-- Content will be loaded here -->
</div>
</div>
</div>
</div>
<script>
// Utility functions
function changeTable(tableName) {
window.location.href = `/?table=${tableName}&page=1`;
}
function performSearch() {
const search = document.getElementById('searchInput').value;
const table = new URLSearchParams(window.location.search).get('table') || '{{.Table}}';
window.location.href = `/?table=${table}&page=1&search=${encodeURIComponent(search)}`;
}
function clearSearch() {
const table = new URLSearchParams(window.location.search).get('table') || '{{.Table}}';
document.getElementById('searchInput').value = '';
window.location.href = `/?table=${table}&page=1`;
}
function showDetail(id) {
const table = new URLSearchParams(window.location.search).get('table') || '{{.Table}}';
fetch(`/api/data/${table}/detail/${id}`)
.then(response => response.json())
.then(data => {
let content = '';
for (const [key, value] of Object.entries(data.data)) {
content += `
<div class="border-b pb-3">
<label class="block text-sm font-medium text-gray-700">${key}</label>
<div class="mt-1 text-sm text-gray-900">${value || ''}</div>
</div>
`;
}
document.getElementById('detailContent').innerHTML = content;
document.getElementById('detailModal').classList.remove('hidden');
})
.catch(error => {
console.error('Error loading detail:', error);
alert('加载详情失败');
});
}
function closeModal() {
document.getElementById('detailModal').classList.add('hidden');
}
// Event listeners
document.getElementById('searchInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
performSearch();
}
});
document.getElementById('detailModal').addEventListener('click', function(e) {
if (e.target === this) {
closeModal();
}
});
// Initialize
document.addEventListener('DOMContentLoaded', function() {
// Add enter key support for search
const searchInput = document.getElementById('searchInput');
if (searchInput) {
searchInput.focus();
}
});
</script>
</body>
</html>