package handler import ( "encoding/json" "fmt" "net/http" "time" "github.com/google/uuid" "go.uber.org/zap" "github.com/topfans/laserCompositor/compositor" "github.com/topfans/laserCompositor/config" "github.com/topfans/laserCompositor/uploader" ) // ComposeHandler 合成 HTTP handler type ComposeHandler struct { cfg *config.Config uploader *uploader.OSSUploader logger *zap.Logger } // NewComposeHandler 创建合成 handler func NewComposeHandler(cfg *config.Config, log *zap.Logger) *ComposeHandler { return &ComposeHandler{ cfg: cfg, uploader: uploader.NewOSSUploader(cfg.OSS), logger: log, } } // ComposeSingle POST /compose — 合成单张镭射卡 func (h *ComposeHandler) ComposeSingle(w http.ResponseWriter, r *http.Request) { var req compositor.ComposeRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]interface{}{ "error": fmt.Sprintf("invalid request: %v", err), }) return } h.logger.Info("ComposeSingle started", zap.Int("variant_index", req.VariantIndex), zap.Float64("sheen_angle", req.GratingConfig.SheenBandAngle), zap.String("bg_url_prefix", req.BackgroundURL[:min(50, len(req.BackgroundURL))]), zap.String("cutout_url_prefix", req.CutoutURL[:min(50, len(req.CutoutURL))]), zap.String("overlay_url_prefix", req.OverlayURL[:min(50, len(req.OverlayURL))]), ) start := time.Now() // 执行合成 pngData, err := compositor.Compose(req) if err != nil { h.logger.Error("Compose failed", zap.Error(err)) writeJSON(w, http.StatusInternalServerError, map[string]interface{}{ "error": fmt.Sprintf("compose failed: %v", err), }) return } // 如果指定了 OSS key,上传到 OSS var ossResult *uploader.UploadResult if req.OutputOSSKey != "" { filename := fmt.Sprintf("%s.png", req.OutputOSSKey) ossResult, err = h.uploader.UploadPNG(pngData, "laser-card/", filename) if err != nil { h.logger.Error("OSS upload failed", zap.Error(err)) // 不中断:返回 base64 作为降级 writeJSON(w, http.StatusOK, map[string]interface{}{ "status": "partial", "variant_index": req.VariantIndex, "width": req.CanvasW(), "height": req.CanvasH(), "base64": fmt.Sprintf("data:image/png;base64,PNG_DATA"), "warning": fmt.Sprintf("OSS upload failed: %v", err), }) return } } elapsed := time.Since(start) h.logger.Info("ComposeSingle done", zap.Int("variant_index", req.VariantIndex), zap.Duration("elapsed", elapsed), ) resp := map[string]interface{}{ "status": "succeeded", "variant_index": req.VariantIndex, "width": req.CanvasW(), "height": req.CanvasH(), } if ossResult != nil { resp["oss_key"] = ossResult.OSSKey resp["signed_url"] = ossResult.SignedURL } writeJSON(w, http.StatusOK, resp) } // ComposeBatch POST /compose/batch — 批量合成多张 func (h *ComposeHandler) ComposeBatch(w http.ResponseWriter, r *http.Request) { var req struct { Variants []compositor.ComposeRequest `json:"variants"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]interface{}{ "error": fmt.Sprintf("invalid request: %v", err), }) return } if len(req.Variants) == 0 { writeJSON(w, http.StatusBadRequest, map[string]interface{}{ "error": "variants array is empty", }) return } results := make([]map[string]interface{}, 0, len(req.Variants)) warnings := make([]string, 0) for _, variant := range req.Variants { start := time.Now() pngData, err := compositor.Compose(variant) result := map[string]interface{}{ "variant_index": variant.VariantIndex, } if err != nil { result["status"] = "failed" result["error"] = err.Error() warnings = append(warnings, fmt.Sprintf("variant %d failed: %v", variant.VariantIndex, err)) } else { filename := fmt.Sprintf("%s_%s.png", uuid.New().String()[:8], fmt.Sprint(variant.VariantIndex)) ossResult, ossErr := h.uploader.UploadPNG(pngData, "laser-card/", filename) if ossErr != nil { result["status"] = "partial" result["warning"] = ossErr.Error() } else { result["status"] = "succeeded" result["oss_key"] = ossResult.OSSKey result["signed_url"] = ossResult.SignedURL } result["width"] = variant.CanvasW() result["height"] = variant.CanvasH() } result["elapsed_ms"] = time.Since(start).Milliseconds() results = append(results, result) } resp := map[string]interface{}{ "batch_status": "completed", "variants": results, "warnings": warnings, } writeJSON(w, http.StatusOK, resp) } func writeJSON(w http.ResponseWriter, status int, data interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(data) }