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

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.env
.envrc
mysql/
run_db.sh

30
db.go Normal file
View File

@@ -0,0 +1,30 @@
package main
import (
"context"
"database/sql"
"log"
"os"
)
func openDB() (*sql.DB, error) {
dsn := os.Getenv("MYSQL_DSN")
if dsn == "" {
log.Fatal("MYSQL_DSN is empty!")
}
return sql.Open("mysql", dsn)
}
func migrate(ctx context.Context, db *sql.DB) error {
ddl := `
CREATE TABLE IF NOT EXISTS students (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
age INT NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`
_, err := db.ExecContext(ctx, ddl)
return err
}

8
go.mod Normal file
View File

@@ -0,0 +1,8 @@
module work3
go 1.25.4
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
)

4
go.sum Normal file
View File

@@ -0,0 +1,4 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=

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)
}

45
main.go Normal file
View File

@@ -0,0 +1,45 @@
package main
import (
"context"
"database/sql"
"log"
"net/http"
"time"
_ "github.com/go-sql-driver/mysql"
)
var db *sql.DB
type Student struct {
ID int64 `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/health", healthHandler)
mux.HandleFunc("/students", studentsHandler)
mux.HandleFunc("/students/", studentByIDHandler)
var err error
db, err = openDB()
if err != nil {
log.Fatal(err)
}
if err = migrate(context.Background(), db); err != nil {
log.Fatal("migrate error: "+err.Error())
}
addr := ":8080"
log.Printf("Start server at %s\n", addr)
if err := http.ListenAndServe(addr, mux); err != nil {
log.Fatal(err)
}
}

31
respose.go Normal file
View File

@@ -0,0 +1,31 @@
package main
import (
"encoding/json"
"net/http"
)
type Result[T any] struct {
Code int `json:"code"`
Message string `json:"message"`
Data *T `json:"data,omitempty"`
}
func writeJSON[T any](w http.ResponseWriter, status int, payload Result[T]) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
json.NewEncoder(w).Encode(payload)
}
func ok[T any](w http.ResponseWriter, data *T) {
writeJSON(w, http.StatusOK, Result[T]{Code: 0, Message: "OK", Data: data})
}
func created[T any](w http.ResponseWriter, location string, data *T) {
w.Header().Set("Location", location)
writeJSON(w, http.StatusCreated, Result[T]{Code: 0, Message: "Created", Data: data})
}
func fail(w http.ResponseWriter, status int, msg string) {
writeJSON[any](w, status, Result[any]{Code: status, Message: msg, Data: nil})
}