Files
WorkBuddy/freight-sfa-server/internal/api/router.go
T

162 lines
4.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package api
import (
"crypto/rand"
"database/sql"
"encoding/hex"
"encoding/json"
"log"
"mime"
"net/http"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"time"
"freight-sfa-server/internal/auth"
)
// Server API 服务
type Server struct {
db *sql.DB
mux *http.ServeMux
}
// New 组装路由与中间件,返回 http.Handler
func New(db *sql.DB, staticDir string) http.Handler {
s := &Server{db: db, mux: http.NewServeMux()}
s.routes(staticDir)
return s.mux
}
func (s *Server) routes(staticDir string) {
// ---- 认证(公开) ----
s.mux.HandleFunc("POST /api/auth/register", s.register)
s.mux.HandleFunc("POST /api/auth/login", s.login)
// ---- 认证(JWT 保护) ----
s.mux.Handle("GET /api/auth/me", auth.Middleware(http.HandlerFunc(s.me)))
// ---- 通用业务记录(client/inquiry/booking/followup ----
s.mux.Handle("GET /api/records/{kind}", auth.Middleware(http.HandlerFunc(s.listRecords)))
s.mux.Handle("POST /api/records/{kind}", auth.Middleware(http.HandlerFunc(s.createRecord)))
s.mux.Handle("GET /api/records/{kind}/{id}", auth.Middleware(http.HandlerFunc(s.getRecord)))
s.mux.Handle("PUT /api/records/{kind}/{id}", auth.Middleware(http.HandlerFunc(s.updateRecord)))
s.mux.Handle("DELETE /api/records/{kind}/{id}", auth.Middleware(http.HandlerFunc(s.deleteRecord)))
// ---- 选项字典 ----
s.mux.Handle("GET /api/options", auth.Middleware(http.HandlerFunc(s.getOptions)))
s.mux.Handle("PUT /api/options", auth.Middleware(http.HandlerFunc(s.updateOptions)))
// ---- 看板聚合 ----
s.mux.Handle("GET /api/dashboard", auth.Middleware(http.HandlerFunc(s.dashboard)))
// ---- 备份/恢复 ----
s.mux.Handle("GET /api/backup", auth.Middleware(http.HandlerFunc(s.exportBackup)))
s.mux.Handle("POST /api/backup", auth.Middleware(http.HandlerFunc(s.importBackup)))
// ---- 审计日志 ----
s.mux.Handle("GET /api/audit-logs", auth.Middleware(http.HandlerFunc(s.listAuditLogs)))
// ---- 静态资源(前端) ----
mime.AddExtensionType(".webmanifest", "application/manifest+json")
mime.AddExtensionType(".js", "text/javascript")
s.mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
http.Redirect(w, r, "/index.html", http.StatusFound)
return
}
serveStatic(w, r, staticDir)
})
}
// serveStatic 提供 html 目录下的静态文件(含路径遍历防护)
func serveStatic(w http.ResponseWriter, r *http.Request, staticDir string) {
rel := strings.TrimPrefix(path.Clean("/"+r.URL.Path), "/")
full := filepath.Join(staticDir, filepath.FromSlash(rel))
base, err := filepath.Abs(staticDir)
if err != nil {
http.NotFound(w, r)
return
}
abs, err := filepath.Abs(full)
if err != nil {
http.NotFound(w, r)
return
}
// 规范化后必须仍在 staticDir 内,防目录遍历
if abs != base && !strings.HasPrefix(abs, base+string(filepath.Separator)) {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
st, err := os.Stat(abs)
if err != nil || st.IsDir() {
http.NotFound(w, r)
return
}
f, err := os.Open(abs)
if err != nil {
http.NotFound(w, r)
return
}
defer f.Close()
if ctype := mime.TypeByExtension(strings.ToLower(filepath.Ext(abs))); ctype != "" {
w.Header().Set("Content-Type", ctype)
}
http.ServeContent(w, r, filepath.Base(abs), st.ModTime(), f)
}
// ---- 通用工具 ----
func writeJSON(w http.ResponseWriter, code int, v any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(code)
if err := json.NewEncoder(w).Encode(v); err != nil {
log.Printf("JSON 序列化失败: %v", err)
}
}
func writeErr(w http.ResponseWriter, code int, msg string) {
writeJSON(w, code, map[string]string{"error": msg})
}
func decodeJSON(r *http.Request, v any) error {
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
return dec.Decode(v)
}
func nowStr() string { return time.Now().Format("2006-01-02 15:04") }
func todayStr() string { return time.Now().Format("2006-01-02") }
// genID 生成与前端 uid() 兼容的 ID(前缀 + 时间戳36进制 + 随机数)
func genID(p string) string {
var sb strings.Builder
sb.WriteString(p)
sb.WriteString("_")
sb.WriteString(strconv.FormatInt(time.Now().UnixMilli(), 36))
b := make([]byte, 4)
if _, err := rand.Read(b); err == nil {
sb.WriteString(hex.EncodeToString(b))
}
return sb.String()
}
func pathID(r *http.Request, key string) string {
return strings.TrimSpace(r.PathValue(key))
}
// auditLog 记录审计日志
func (s *Server) auditLog(r *http.Request, action, entity, entityID, detail string) {
_, err := s.db.Exec(`INSERT INTO audit_logs (user_id, username, action, entity, entity_id, detail, created_at)
VALUES (?,?,?,?,?,?,?)`,
auth.UserID(r), auth.Username(r), action, entity, entityID, detail, nowStr())
if err != nil {
log.Printf("写入审计日志失败: %v", err)
}
}