This commit is contained in:
xkm
2025-11-12 19:55:29 +08:00
commit deec440038
7 changed files with 319 additions and 0 deletions

197
handler.go Normal file
View File

@@ -0,0 +1,197 @@
package main
import (
"context"
"database/sql"
"encoding/json"
"errors"
"net/http"
"strconv"
"strings"
"time"
)
func healthHandler(w http.ResponseWriter, r *http.Request) {
now := time.Now().Format(time.RFC3339)
ok(w, &map[string]string{"status": "UP", "time": now})
}
func studentsHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
listStudents(w, r)
case http.MethodPost:
createStudent(w, r)
default:
w.Header().Set("Allow", "GET, POST")
fail(w, http.StatusMethodNotAllowed, "method not allowed")
}
}
func studentByIDHandler(w http.ResponseWriter, r *http.Request) {
idStr := strings.TrimPrefix(r.URL.Path, "/students/")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
fail(w, http.StatusBadRequest, "invalid student id")
return
}
switch r.Method {
case http.MethodGet:
getStudent(w, r, id)
case http.MethodPut:
updateStudent(w, r, id)
case http.MethodDelete:
deleteStudent(w, r, id)
default:
w.Header().Set("Allow", "GET, PUT, DELETE")
fail(w, http.StatusMethodNotAllowed, "method not allowed")
}
}
func getStudent(w http.ResponseWriter, r *http.Request, id int64) {
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
var s Student
err := db.QueryRowContext(ctx,
`SELECT id,name,age,email,created_at,updated_at FROM students WHERE id=?`, id,
).Scan(&s.ID, &s.Name, &s.Age, &s.Email, &s.CreatedAt, &s.UpdatedAt)
if errors.Is(err, sql.ErrNoRows) {
fail(w, http.StatusNotFound, "student not found")
return
} else if err != nil {
fail(w, http.StatusInternalServerError, "db query error: "+err.Error())
return
}
ok(w, &s)
}
func updateStudent(w http.ResponseWriter, r *http.Request, id int64) {
if ct := r.Header.Get("Content-Type"); !strings.HasPrefix(strings.ToLower(ct), "application/json") {
fail(w, http.StatusUnsupportedMediaType, "Content-Type must be application/json")
return
}
var in struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email"`
}
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
fail(w, http.StatusBadRequest, "invalid json: "+err.Error())
return
}
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
res, err := db.ExecContext(ctx,
`UPDATE students SET name=?, age=?, email=? WHERE id=?`,
in.Name, in.Age, in.Email, id)
if err != nil {
if strings.Contains(strings.ToLower(err.Error()), "duplicate") {
fail(w, http.StatusConflict, "email already exists")
return
}
fail(w, http.StatusInternalServerError, "update error: "+err.Error())
return
}
n, _ := res.RowsAffected()
if n == 0 {
fail(w, http.StatusNotFound, "student not found")
return
}
getStudent(w, r, id)
}
func deleteStudent(w http.ResponseWriter, r *http.Request, id int64) {
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
res, err := db.ExecContext(ctx, `DELETE FROM students WHERE id=?`, id)
if err != nil {
fail(w, http.StatusInternalServerError, "delete error: "+err.Error())
return
}
n, _ := res.RowsAffected()
if n == 0 {
fail(w, http.StatusNotFound, "student not found")
return
}
ok(w, &map[string]any{"deleted": id})
}
func createStudent(w http.ResponseWriter, r *http.Request) {
if ct := r.Header.Get("Content-Type"); !strings.HasPrefix(strings.ToLower(ct), "application/json") {
fail(w, http.StatusUnsupportedMediaType, "Context-Type must be application/json")
return
}
var in struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email"`
}
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
fail(w, http.StatusBadRequest, "invalid json: "+err.Error())
return
}
if strings.TrimSpace(in.Name) == "" || in.Age <= 0 || strings.TrimSpace(in.Email) == "" {
fail(w, http.StatusBadRequest, "name/age/email required")
return
}
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
res, err := db.ExecContext(ctx,
`INSERT INTO students(name, age, email) VALUES(?,?,?)`,
in.Name, in.Age, in.Email)
if err != nil {
fail(w, http.StatusInternalServerError, "insert error: "+err.Error())
return
}
id, _ := res.LastInsertId()
var out Student
err = db.QueryRowContext(ctx,
`SELECT id,name,age,email,created_at,updated_at FROM students WHERE id=?`, id,
).Scan(&out.ID, &out.Name, &out.Age, &out.Email, &out.CreatedAt, &out.UpdatedAt)
if err != nil {
fail(w, http.StatusInternalServerError, "fetch created row error: "+err.Error())
return
}
created(w, "/students/"+strconv.FormatInt(out.ID, 10), &out)
}
func listStudents(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
rows, err := db.QueryContext(ctx,
`SELECT id,name,age,email,created_at,updated_at FROM students ORDER BY id`)
if err != nil {
fail(w, http.StatusInternalServerError, "db query error: "+err.Error())
return
}
defer rows.Close()
var items []Student
for rows.Next() {
var s Student
if err := rows.Scan(&s.ID, &s.Name, &s.Age, &s.Email, &s.CreatedAt, &s.UpdatedAt); err != nil {
fail(w, http.StatusInternalServerError, "scan error: "+err.Error())
return
}
items = append(items, s)
}
if err := rows.Err(); err != nil {
fail(w, http.StatusInternalServerError, "rows error: "+err.Error())
return
}
ok(w, &items)
}