Runxi Yu’s Personal Forge
order the list fields
package main import ( "bytes" "embed" "encoding/csv" "encoding/json" "fmt" "io/fs" "io/ioutil" "log" "net" "net/http" "os" "os/exec" "path/filepath"
"sort"
"strings" "time" ) //go:embed static/* var embeddedStatic embed.FS func main() { fs, err := fs.Sub(embeddedStatic, "static") if err != nil { panic(err) } http.Handle("/", http.FileServer(http.FS(fs))) http.HandleFunc("/submit", handleForm) http.HandleFunc("/fwdaiusyflaidsunfuoiawenufwylnfkalhjdslkjfhjlwadk.csv", handleCSV) if err := os.MkdirAll("responses", 0755); err != nil { log.Fatalf("unable to create responses folder: %v", err) } log.Println("listening 127.0.0.1:9074") log.Fatal(http.ListenAndServe("127.0.0.1:9074", nil)) } func handleForm(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Invalid request method", http.StatusMethodNotAllowed) return } if err := r.ParseForm(); err != nil { http.Error(w, "Unable to parse form", http.StatusBadRequest) return } data := make(map[string]string) for key, values := range r.PostForm { if len(values) > 0 { data[key] = values[0] } else { data[key] = "" } } ip := r.Header.Get("X-Forwarded-For") if ip != "" { ip = strings.Split(ip, ",")[0] ip = strings.TrimSpace(ip) } else { ip, _, _ = net.SplitHostPort(r.RemoteAddr) } data["ip_address"] = ip filename := time.Now().Format("20060102_150405.000") + ".json" filePath := filepath.Join("responses", filename) file, err := os.Create(filePath) if err != nil { http.Error(w, "无法打开保存文件:"+err.Error(), http.StatusInternalServerError) return } defer file.Close() encoder := json.NewEncoder(file) encoder.SetIndent("", "\t") if err := encoder.Encode(data); err != nil { http.Error(w, "无法写入保存文件:"+err.Error(), http.StatusInternalServerError) return } go func(data map[string]string) { cmd := "/sbin/sendmail" args := []string{"-t", "-i"} bodyBuilder := &strings.Builder{} fmt.Fprintf(bodyBuilder, "From: tiffany@runxiyu.org\n") fmt.Fprintf(bodyBuilder, "To: tiffany@runxiyu.org\n") fmt.Fprintf(bodyBuilder, "Subject: Survey response from %s\n", data["ip_address"]) fmt.Fprintf(bodyBuilder, "MIME-Version: 1.0\n") fmt.Fprintf(bodyBuilder, "Content-Type: text/plain; charset=UTF-8\n") fmt.Fprintf(bodyBuilder, "Content-Transfer-Encoding: 8bit\n\n") jsonBytes, err := json.MarshalIndent(data, "", "\t") if err == nil { bodyBuilder.Write(jsonBytes) sendmail := exec.Command(cmd, args...) stdin, err := sendmail.StdinPipe() if err == nil { if err := sendmail.Start(); err == nil { stdin.Write([]byte(bodyBuilder.String())) stdin.Close() sendmail.Wait() } } } }(data) fmt.Fprintf(w, `恭喜您完成所有测试并衷心感谢您的参与! 如有兴趣了解实验数据分析结果,请关注微信公众号 @WIT studio。`) } func handleCSV(w http.ResponseWriter, r *http.Request) { files, err := ioutil.ReadDir("responses") if err != nil { http.Error(w, "无法读取 responses 目录:"+err.Error(), http.StatusInternalServerError) return } records := []map[string]string{} fieldSet := make(map[string]struct{}) // First pass: collect all unique field names for _, file := range files { if !file.IsDir() && strings.HasSuffix(file.Name(), ".json") { content, err := ioutil.ReadFile(filepath.Join("responses", file.Name())) if err != nil { continue } var data map[string]string if err := json.Unmarshal(content, &data); err != nil { continue } for k := range data { fieldSet[k] = struct{}{} } records = append(records, data) } }
fieldSet["Time"] = struct{}{}
// Define the desired column order preferredOrder := []string{ "ip_address", "gender", "age", "dialect", "guanhua_details", "wuyu_details", "other_details", "usage_frequency", "fluency", "foreign_language", "music_training", "music_freq", "absolute_pitch", } // Add q1-q20 for i := 1; i <= 20; i++ { preferredOrder = append(preferredOrder, fmt.Sprintf("q%d", i)) } // Add cadence1-4 for i := 1; i <= 4; i++ { preferredOrder = append(preferredOrder, fmt.Sprintf("cadence%d", i)) } // Add cadence_sample_1-4 for i := 1; i <= 4; i++ { preferredOrder = append(preferredOrder, fmt.Sprintf("cadence_sample_%d", i)) } // Add style columns preferredOrder = append(preferredOrder, "style1trap", "style2drill", "style3drumbass", "style4reggaetton", "style5rb", ) // Add style_sample_1-5 for i := 1; i <= 5; i++ { preferredOrder = append(preferredOrder, fmt.Sprintf("style_sample_%d", i)) }
// Build field list (columns)
// Build final field list with preferred order first, then remaining fields
fields := make([]string, 0, len(fieldSet))
remainingFields := make([]string, 0) // First add fields that are in preferred order and exist in the data for _, field := range preferredOrder { if _, exists := fieldSet[field]; exists { fields = append(fields, field) delete(fieldSet, field) // Remove from set to track remaining fields } } // Then add any remaining fields that weren't in preferred order
for field := range fieldSet {
fields = append(fields, field)
remainingFields = append(remainingFields, field)
}
// Sort remaining fields alphabetically for consistency sort.Strings(remainingFields) fields = append(fields, remainingFields...)
// Prepare CSV buffer var buf bytes.Buffer buf.WriteString("\uFEFF") // UTF-8 BOM writer := csv.NewWriter(&buf) writer.Write(fields) for _, record := range records { row := make([]string, len(fields)) for i, field := range fields {
if field == "Time" { } else { row[i] = record[field] }
row[i] = record[field] // Will be empty string if field doesn't exist
} writer.Write(row) } writer.Flush() w.Header().Set("Content-Type", "text/csv; charset=utf-8") w.Header().Set("Content-Disposition", "attachment; filename=\"responses.csv\"") w.Write(buf.Bytes()) }