Runxi Yu’s Personal Forge
Login

melonsurvey

Web survey software for melons
Commit info
ID
8987a773164bbaa25135ec8e3c53e0ec06a21d80
Author
Runxi Yu <me@runxiyu.org>
Author date
Sat, 21 Jun 2025 18:08:49 +0800
Committer
Runxi Yu <me@runxiyu.org>
Committer date
Sat, 21 Jun 2025 18:08:49 +0800
Actions
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())
}