feat: add docfill mvp cli

This commit is contained in:
赵义仑
2026-06-18 20:03:47 +08:00
commit ca444bd5d0
22 changed files with 2539 additions and 0 deletions

274
internal/docfill/p0_test.go Normal file
View File

@@ -0,0 +1,274 @@
package docfill
import (
"database/sql"
"os"
"path/filepath"
"strings"
"testing"
)
func TestRunResolvesConfigRelativePathsAndSanitizesOutputFileNames(t *testing.T) {
dir := t.TempDir()
taskDir := filepath.Join(dir, "task")
if err := os.MkdirAll(taskDir, 0o755); err != nil {
t.Fatalf("make task dir: %v", err)
}
writeWorkbook(t, filepath.Join(taskDir, "data.xlsx"), "Daily", [][]string{
{"name", "done"},
{"张/三:测试", "已完成"},
})
writeWorkbook(t, filepath.Join(taskDir, "template.xlsx"), "日报", [][]string{
{"{{name}}", "{{done}}"},
})
configPath := filepath.Join(taskDir, "task.json")
writeJSON(t, configPath, map[string]any{
"data": map[string]any{
"type": "excel",
"path": "./data.xlsx",
"sheet": "Daily",
},
"template": map[string]any{
"path": "./template.xlsx",
},
"output": map[string]any{
"path": "./out/日报_{{name}}.xlsx",
},
})
result, err := RunWithOptions(configPath, RunOptions{})
if err != nil {
t.Fatalf("RunWithOptions returned error: %v", err)
}
wantPath := filepath.Join(taskDir, "out", "日报_张_三_测试.xlsx")
if len(result.Files) != 1 || result.Files[0] != wantPath {
t.Fatalf("result files = %#v, want %q", result.Files, wantPath)
}
assertWorkbookCell(t, wantPath, "日报", "A1", "张/三:测试")
}
func TestRunUsesVarsForSQLiteParamsAndTemplateDefaults(t *testing.T) {
dir := t.TempDir()
dbPath := filepath.Join(dir, "daily.db")
templatePath := filepath.Join(dir, "template.docx")
configPath := filepath.Join(dir, "task.json")
seedP0SQLite(t, dbPath, `
create table daily (name text, date text, done text);
insert into daily values
('张三', '2026-06-18', '完成旧日报'),
('李四', '2026-06-19', '完成新日报');
`)
writeMinimalDocx(t, templatePath, "批次:{{batch}}\n日期{{date}}\n员工{{name}}\n内容{{done}}")
writeJSON(t, configPath, map[string]any{
"vars": map[string]any{
"date": "2026-06-18",
"batch": "默认批次",
},
"data": map[string]any{
"type": "sqlite",
"path": dbPath,
"query": "select name, date, done from daily where date = ?",
"params": []any{"{{date}}"},
},
"template": map[string]any{
"path": templatePath,
},
"output": map[string]any{
"path": filepath.Join(dir, "out", "日报_{{batch}}_{{date}}_{{name}}.docx"),
},
})
result, err := RunWithOptions(configPath, RunOptions{
Vars: map[string]string{
"date": "2026-06-19",
"batch": "命令行批次",
},
})
if err != nil {
t.Fatalf("RunWithOptions returned error: %v", err)
}
if len(result.Files) != 1 {
t.Fatalf("expected 1 generated file, got %#v", result.Files)
}
got := readZipEntry(t, result.Files[0], "word/document.xml")
if !strings.Contains(got, "批次:命令行批次") || !strings.Contains(got, "员工:李四") {
t.Fatalf("unexpected rendered docx body: %q", got)
}
if strings.Contains(got, "张三") || strings.Contains(result.Files[0], "默认批次") {
t.Fatalf("CLI vars did not override config vars: file=%q body=%q", result.Files[0], got)
}
}
func TestRunSupportsExcelHeaderRow(t *testing.T) {
dir := t.TempDir()
dataPath := filepath.Join(dir, "data.xlsx")
templatePath := filepath.Join(dir, "template.xlsx")
configPath := filepath.Join(dir, "task.json")
writeWorkbook(t, dataPath, "Daily", [][]string{
{"日报导出数据"},
{"name", "done"},
{"张三", "完成日报"},
})
writeWorkbook(t, templatePath, "日报", [][]string{
{"员工", "{{name}}"},
{"完成", "{{done}}"},
})
writeJSON(t, configPath, map[string]any{
"data": map[string]any{
"type": "excel",
"path": dataPath,
"sheet": "Daily",
"header_row": 2,
},
"template": map[string]any{
"path": templatePath,
},
"output": map[string]any{
"path": filepath.Join(dir, "out", "日报_{{name}}.xlsx"),
},
})
if err := Run(configPath); err != nil {
t.Fatalf("Run returned error: %v", err)
}
assertWorkbookCell(t, filepath.Join(dir, "out", "日报_张三.xlsx"), "日报", "B1", "张三")
}
func TestRunRefusesToOverwriteUnlessConfigured(t *testing.T) {
dir := t.TempDir()
dataPath := filepath.Join(dir, "data.xlsx")
templatePath := filepath.Join(dir, "template.xlsx")
configPath := filepath.Join(dir, "task.json")
outputPath := filepath.Join(dir, "out", "日报_张三.xlsx")
writeWorkbook(t, dataPath, "Daily", [][]string{
{"name"},
{"张三"},
})
writeWorkbook(t, templatePath, "日报", [][]string{{"{{name}}"}})
if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil {
t.Fatalf("make output dir: %v", err)
}
if err := os.WriteFile(outputPath, []byte("existing"), 0o644); err != nil {
t.Fatalf("write existing output: %v", err)
}
writeJSON(t, configPath, map[string]any{
"data": map[string]any{
"type": "excel",
"path": dataPath,
"sheet": "Daily",
},
"template": map[string]any{
"path": templatePath,
},
"output": map[string]any{
"path": outputPath,
},
})
err := Run(configPath)
if err == nil || !strings.Contains(err.Error(), "already exists") {
t.Fatalf("expected already exists error, got %v", err)
}
writeJSON(t, configPath, map[string]any{
"data": map[string]any{
"type": "excel",
"path": dataPath,
"sheet": "Daily",
},
"template": map[string]any{
"path": templatePath,
},
"output": map[string]any{
"path": outputPath,
"overwrite": true,
},
})
if err := Run(configPath); err != nil {
t.Fatalf("Run with overwrite returned error: %v", err)
}
assertWorkbookCell(t, outputPath, "日报", "A1", "张三")
}
func TestRunFailsWhenTemplatePlaceholderIsMissing(t *testing.T) {
dir := t.TempDir()
dataPath := filepath.Join(dir, "data.xlsx")
templatePath := filepath.Join(dir, "template.xlsx")
configPath := filepath.Join(dir, "task.json")
writeWorkbook(t, dataPath, "Daily", [][]string{
{"name"},
{"张三"},
})
writeWorkbook(t, templatePath, "日报", [][]string{{"{{name}}", "{{missing}}"}})
writeJSON(t, configPath, map[string]any{
"data": map[string]any{
"type": "excel",
"path": dataPath,
"sheet": "Daily",
},
"template": map[string]any{
"path": templatePath,
},
"output": map[string]any{
"path": filepath.Join(dir, "out", "日报_{{name}}.xlsx"),
},
})
err := Run(configPath)
if err == nil || !strings.Contains(err.Error(), "missing template field") {
t.Fatalf("expected missing template field error, got %v", err)
}
}
func TestRunFailsWhenRenderedTemplateStillHasPlaceholder(t *testing.T) {
dir := t.TempDir()
dataPath := filepath.Join(dir, "data.xlsx")
templatePath := filepath.Join(dir, "template.xlsx")
configPath := filepath.Join(dir, "task.json")
writeWorkbook(t, dataPath, "Daily", [][]string{
{"name", "done"},
{"张三", "{{still_pending}}"},
})
writeWorkbook(t, templatePath, "日报", [][]string{{"{{name}}", "{{done}}"}})
writeJSON(t, configPath, map[string]any{
"data": map[string]any{
"type": "excel",
"path": dataPath,
"sheet": "Daily",
},
"template": map[string]any{
"path": templatePath,
},
"output": map[string]any{
"path": filepath.Join(dir, "out", "日报_{{name}}.xlsx"),
},
})
err := Run(configPath)
if err == nil || !strings.Contains(err.Error(), "unresolved placeholder") {
t.Fatalf("expected unresolved placeholder error, got %v", err)
}
}
func seedP0SQLite(t *testing.T, path, statements string) {
t.Helper()
db, err := sql.Open("sqlite", path)
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
defer db.Close()
if _, err := db.Exec(statements); err != nil {
t.Fatalf("seed sqlite: %v", err)
}
}