package handler import ( "bytes" "errors" "fmt" "io" "log" "mime/multipart" "net/http" "path" "regexp" "strings" "time" "github.com/gin-gonic/gin" "wx_service/config" "wx_service/internal/common/imageutil" oss "wx_service/internal/common/oss" uploadservice "wx_service/internal/common/upload/service" "wx_service/internal/marketing/service" "wx_service/internal/middleware" "wx_service/internal/model" ) var adminExtPattern = regexp.MustCompile(`^\.[a-z0-9]{1,10}$`) type DownloadHandler struct { svc *service.DownloadService } func NewDownloadHandler(svc *service.DownloadService) *DownloadHandler { return &DownloadHandler{svc: svc} } func (h *DownloadHandler) Create(c *gin.Context) { user := middleware.MustCurrentUser(c) var req service.CreateDownloadRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "请求参数错误")) return } dl, err := h.svc.Create(user.ID, req) if err != nil { if service.IsNotFoundError(err) { c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "模板不存在")) return } if service.IsBadRequestError(err) { c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, err.Error())) return } c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "服务器错误")) return } c.JSON(http.StatusOK, model.Success(dl)) } type adCallbackRequest struct { DownloadID uint `json:"download_id" binding:"required"` } func (h *DownloadHandler) AdCallback(c *gin.Context) { var req adCallbackRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "请求参数错误")) return } if err := h.svc.MarkAdCompleted(req.DownloadID); err != nil { if service.IsNotFoundError(err) { c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "记录不存在")) return } c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "服务器错误")) return } c.JSON(http.StatusOK, model.Success(nil)) } func (h *DownloadHandler) ListByUser(c *gin.Context) { user := middleware.MustCurrentUser(c) page := parseIntQuery(c, "page", 1) pageSize := parseIntQuery(c, "page_size", 20) resp, err := h.svc.ListByUser(user.ID, page, pageSize) if err != nil { c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "服务器错误")) return } c.JSON(http.StatusOK, model.Success(resp)) } func (h *DownloadHandler) AdminStats(c *gin.Context) { stats, err := h.svc.GetStats() if err != nil { c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "服务器错误")) return } c.JSON(http.StatusOK, model.Success(stats)) } type adminUploadTokenRequest struct { Filename string `json:"filename"` } type adminUploadTokenResponse struct { Token string `json:"token,omitempty"` Key string `json:"key"` UploadURL string `json:"upload_url"` ExpireAt int64 `json:"expire,omitempty"` CDNDomain string `json:"cdn_domain,omitempty"` OSSAccessKey string `json:"oss_access_key_id,omitempty"` OSSPolicy string `json:"oss_policy,omitempty"` OSSSignature string `json:"oss_signature,omitempty"` } func (h *DownloadHandler) AdminUploadToken(c *gin.Context) { var req adminUploadTokenRequest _ = c.ShouldBindJSON(&req) cfg := config.AppConfig.OSS if cfg.AccessKey == "" || cfg.SecretKey == "" || cfg.Bucket == "" { c.JSON(http.StatusServiceUnavailable, model.Error(http.StatusServiceUnavailable, "未配置上传服务")) return } cdnDomain := strings.TrimSpace(cfg.CDNDomain) if oss.IsOSSDomain(cdnDomain) { ext := path.Ext(req.Filename) if ext == "" || !adminExtPattern.MatchString(strings.ToLower(ext)) { ext = ".jpg" } keyPrefix := strings.Trim(cfg.KeyPrefix, "/") key := fmt.Sprintf("%s/admin/%s/%x%s", keyPrefix, time.Now().Format("20060102"), time.Now().UnixNano()&0xffffffff, ext) endpoint := oss.ParseOSSEndpoint(cdnDomain) expireSeconds := cfg.TokenExpireSeconds if expireSeconds <= 0 { expireSeconds = 300 } policy, signature, err := oss.PostPolicy(cfg.Bucket, endpoint, key, cfg.SecretKey, expireSeconds) if err != nil { c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "生成上传凭证失败")) return } uploadURL := oss.UploadHost(cfg.Bucket, endpoint) cdnHost := "https://" + cfg.Bucket + "." + endpoint + ".aliyuncs.com" c.JSON(http.StatusOK, model.Success(adminUploadTokenResponse{ Key: key, UploadURL: uploadURL, CDNDomain: cdnHost, OSSAccessKey: cfg.AccessKey, OSSPolicy: policy, OSSSignature: signature, })) return } uploadSvc := uploadservice.NewUploadService(cfg) token, err := uploadSvc.CreateUploadToken(0, 0, req.Filename) if err != nil { if errors.Is(err, uploadservice.ErrUploadNotConfigured) { c.JSON(http.StatusServiceUnavailable, model.Error(http.StatusServiceUnavailable, "未配置上传服务")) return } c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "获取上传凭证失败")) return } c.JSON(http.StatusOK, model.Success(token)) } // AdminUploadFile 接收管理后台上传的文件,服务端代理转发到 OSS, // 同时自动生成一张压缩缩略图一并上传,减少 CDN 占用。 func (h *DownloadHandler) AdminUploadFile(c *gin.Context) { file, header, err := c.Request.FormFile("file") if err != nil { c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "请选择要上传的文件")) return } defer file.Close() if header.Size > 10*1024*1024 { c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "文件大小不能超过 10MB")) return } cfg := config.AppConfig.OSS if cfg.AccessKey == "" || cfg.SecretKey == "" || cfg.Bucket == "" { c.JSON(http.StatusServiceUnavailable, model.Error(http.StatusServiceUnavailable, "未配置上传服务")) return } cdnDomain := strings.TrimSpace(cfg.CDNDomain) if !oss.IsOSSDomain(cdnDomain) { c.JSON(http.StatusServiceUnavailable, model.Error(http.StatusServiceUnavailable, "仅支持 OSS 上传")) return } fileData, err := io.ReadAll(file) if err != nil { c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "读取文件失败")) return } ext := strings.ToLower(path.Ext(header.Filename)) if ext == "" || !adminExtPattern.MatchString(ext) { ext = ".jpg" } keyPrefix := strings.Trim(cfg.KeyPrefix, "/") ts := fmt.Sprintf("%x", time.Now().UnixNano()&0xffffffff) origKey := fmt.Sprintf("%s/admin/%s/%s%s", keyPrefix, time.Now().Format("20060102"), ts, ext) thumbKey := fmt.Sprintf("%s/admin/%s/%s_thumb.jpg", keyPrefix, time.Now().Format("20060102"), ts) endpoint := oss.ParseOSSEndpoint(cdnDomain) cdnHost := "https://" + cfg.Bucket + "." + endpoint + ".aliyuncs.com" origURL, err := uploadToOSS(cfg, endpoint, origKey, header.Filename, fileData) if err != nil { log.Printf("[admin_upload] upload original error: %v", err) c.JSON(http.StatusBadGateway, model.Error(http.StatusBadGateway, "上传原图到 OSS 失败")) return } thumbURL := "" thumbData, thumbErr := imageutil.GenerateThumbnail(fileData, header.Header.Get("Content-Type"), imageutil.DefaultResizeOptions()) if thumbErr != nil { log.Printf("[admin_upload] thumbnail generation skipped: %v", thumbErr) } else { if url, err := uploadToOSS(cfg, endpoint, thumbKey, "thumb.jpg", thumbData); err != nil { log.Printf("[admin_upload] upload thumbnail error: %v", err) } else { thumbURL = url } } if origURL == "" { origURL = cdnHost + "/" + origKey } result := gin.H{"url": origURL} if thumbURL != "" { result["thumbnail_url"] = thumbURL } c.JSON(http.StatusOK, model.Success(result)) } func uploadToOSS(cfg config.OSSConfig, endpoint, key, filename string, data []byte) (string, error) { expireSeconds := cfg.TokenExpireSeconds if expireSeconds <= 0 { expireSeconds = 300 } policy, signature, err := oss.PostPolicy(cfg.Bucket, endpoint, key, cfg.SecretKey, expireSeconds) if err != nil { return "", fmt.Errorf("generate policy: %w", err) } uploadURL := oss.UploadHost(cfg.Bucket, endpoint) var body bytes.Buffer writer := multipart.NewWriter(&body) writer.WriteField("key", key) writer.WriteField("policy", policy) writer.WriteField("OSSAccessKeyId", cfg.AccessKey) writer.WriteField("Signature", signature) fw, err := writer.CreateFormFile("file", filename) if err != nil { return "", fmt.Errorf("create form file: %w", err) } fw.Write(data) writer.Close() req, err := http.NewRequest(http.MethodPost, uploadURL, &body) if err != nil { return "", fmt.Errorf("new request: %w", err) } req.Header.Set("Content-Type", writer.FormDataContentType()) client := &http.Client{Timeout: 60 * time.Second} resp, err := client.Do(req) if err != nil { return "", fmt.Errorf("do request: %w", err) } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { respBody, _ := io.ReadAll(resp.Body) return "", fmt.Errorf("oss status=%d body=%s", resp.StatusCode, string(respBody)) } cdnHost := "https://" + cfg.Bucket + "." + endpoint + ".aliyuncs.com" return cdnHost + "/" + key, nil }