Files
WorkBuddy/cnc-sales-backend/internal/api/router.go
T
2026-08-16 16:29:06 +08:00

124 lines
4.5 KiB
Go

package api
import (
"crypto/rand"
"database/sql"
"encoding/hex"
"encoding/json"
"log"
"mime"
"net/http"
"strconv"
"strings"
"time"
"cnc-sales-backend/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)))
// ---- 客户档案 ----
s.mux.Handle("GET /api/customers", auth.Middleware(http.HandlerFunc(s.listCustomers)))
s.mux.Handle("POST /api/customers", auth.Middleware(http.HandlerFunc(s.createCustomer)))
s.mux.Handle("GET /api/customers/{id}", auth.Middleware(http.HandlerFunc(s.getCustomer)))
s.mux.Handle("PUT /api/customers/{id}", auth.Middleware(http.HandlerFunc(s.updateCustomer)))
s.mux.Handle("DELETE /api/customers/{id}", auth.Middleware(http.HandlerFunc(s.deleteCustomer)))
s.mux.Handle("POST /api/customers/{id}/contacts", auth.Middleware(http.HandlerFunc(s.addContact)))
s.mux.Handle("DELETE /api/customers/{id}/contacts/{cid}", auth.Middleware(http.HandlerFunc(s.deleteContact)))
s.mux.Handle("POST /api/customers/{id}/followups", auth.Middleware(http.HandlerFunc(s.addFollowUp)))
s.mux.Handle("DELETE /api/customers/{id}/followups/{fid}", auth.Middleware(http.HandlerFunc(s.deleteFollowUp)))
// ---- 任务 ----
s.mux.Handle("GET /api/tasks", auth.Middleware(http.HandlerFunc(s.listTasks)))
s.mux.Handle("POST /api/tasks", auth.Middleware(http.HandlerFunc(s.createTask)))
s.mux.Handle("PUT /api/tasks/{id}", auth.Middleware(http.HandlerFunc(s.updateTask)))
s.mux.Handle("DELETE /api/tasks/{id}", auth.Middleware(http.HandlerFunc(s.deleteTask)))
s.mux.Handle("POST /api/tasks/{id}/toggle", auth.Middleware(http.HandlerFunc(s.toggleTask)))
// ---- 资料库 ----
s.mux.Handle("GET /api/notes", auth.Middleware(http.HandlerFunc(s.listNotes)))
s.mux.Handle("POST /api/notes", auth.Middleware(http.HandlerFunc(s.createNote)))
s.mux.Handle("PUT /api/notes/{id}", auth.Middleware(http.HandlerFunc(s.updateNote)))
s.mux.Handle("DELETE /api/notes/{id}", auth.Middleware(http.HandlerFunc(s.deleteNote)))
// ---- 聚合视图 ----
s.mux.Handle("GET /api/pipeline/stages", auth.Middleware(http.HandlerFunc(s.pipelineStages)))
s.mux.Handle("GET /api/stats/overview", auth.Middleware(http.HandlerFunc(s.statsOverview)))
s.mux.Handle("GET /api/today", auth.Middleware(http.HandlerFunc(s.todayPanel)))
// ---- 备份 ----
s.mux.Handle("GET /api/backup", auth.Middleware(http.HandlerFunc(s.exportBackup)))
s.mux.Handle("POST /api/backup", auth.Middleware(http.HandlerFunc(s.importBackup)))
// ---- 静态资源(PWA 前端) ----
mime.AddExtensionType(".webmanifest", "application/manifest+json")
mime.AddExtensionType(".js", "text/javascript")
// 根路径重定向到工作台页面,其余路径交给静态文件服务
fileServer := http.FileServer(http.Dir(staticDir))
s.mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
http.Redirect(w, r, "/cnc-sales-workbench.html", http.StatusFound)
return
}
fileServer.ServeHTTP(w, r)
})
}
// ---- 通用工具 ----
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)
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() string {
var sb strings.Builder
sb.WriteString("id")
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))
}