package controllers import ( "bytes" "encoding/base64" "encoding/json" "fmt" "image" "image/color" "image/draw" _ "image/gif" "image/jpeg" "image/png" "io" "net/http" "net/url" "os" "shudao-chat-go/models" "shudao-chat-go/utils" "strings" "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/s3" "github.com/beego/beego/v2/server/web" "github.com/fogleman/gg" ) type HazardController struct { web.Controller } // 图片压缩配置 - 使用shudaooss.go中的配置 type HazardRequest struct { //场景名称 SceneName string `json:"scene_name"` //图片 Image string `json:"image"` // 用户ID、手机号、用户姓名从token中获取 //日期 Date string `json:"date"` } // YOLOResponse 定义YOLO响应结构 type YOLOResponse struct { Boxes [][]float64 `json:"boxes"` Labels []string `json:"labels"` Scores []float64 `json:"scores"` ModelType string `json:"model_type"` } // HazardResponse 定义隐患识别响应结构 type HazardResponse struct { Code int `json:"code"` Msg string `json:"msg"` Data map[string]interface{} `json:"data"` } // 隐患识别 func (c *HazardController) Hazard() { // 从token中获取用户信息 userInfo, err := utils.GetUserInfoFromContext(c.Ctx.Input.GetData("userInfo")) if err != nil { c.Data["json"] = HazardResponse{ Code: 401, Msg: "获取用户信息失败: " + err.Error(), } c.ServeJSON() return } var requestData HazardRequest if err := json.Unmarshal(c.Ctx.Input.RequestBody, &requestData); err != nil { c.Data["json"] = HazardResponse{ Code: 400, Msg: "参数错误", } c.ServeJSON() return } fmt.Println("requestData", requestData) // 验证参数 if requestData.SceneName == "" || requestData.Image == "" { c.Data["json"] = HazardResponse{ Code: 400, Msg: "场景名称和图片链接不能为空", } c.ServeJSON() return } // 从OSS下载图片 imageData, err := downloadImageFromOSS(requestData.Image) if err != nil { c.Data["json"] = HazardResponse{ Code: 500, Msg: "下载图片失败: " + err.Error(), } c.ServeJSON() return } // 验证图片数据 if len(imageData) == 0 { c.Data["json"] = HazardResponse{ Code: 500, Msg: "下载的图片数据为空", } c.ServeJSON() return } fmt.Printf("下载图片成功,大小: %d 字节\n", len(imageData)) // 将图片转换为base64 imageBase64 := base64.StdEncoding.EncodeToString(imageData) // 获取YOLO API配置 yoloBaseURL, err := web.AppConfig.String("yolo_base_url") if err != nil || yoloBaseURL == "" { yoloBaseURL = "http://172.16.35.50:18080" // 默认值 } // 构建YOLO请求 yoloRequest := map[string]interface{}{ "modeltype": requestData.SceneName, "image": imageBase64, "conf_threshold": 0.5, // 默认置信度 } jsonData, err := json.Marshal(yoloRequest) if err != nil { c.Data["json"] = HazardResponse{ Code: 500, Msg: "序列化请求失败: " + err.Error(), } c.ServeJSON() return } // 调用YOLO API resp, err := http.Post( fmt.Sprintf("%s/predict", yoloBaseURL), "application/json", bytes.NewBuffer(jsonData), ) if err != nil { c.Data["json"] = HazardResponse{ Code: 500, Msg: "调用YOLO API失败: " + err.Error(), } c.ServeJSON() return } defer resp.Body.Close() // 读取响应 body, err := io.ReadAll(resp.Body) if err != nil { c.Data["json"] = HazardResponse{ Code: 500, Msg: "读取YOLO响应失败: " + err.Error(), } c.ServeJSON() return } fmt.Println("body111", string(body)) if resp.StatusCode != http.StatusOK { c.Data["json"] = HazardResponse{ Code: resp.StatusCode, Msg: "YOLO API返回错误: " + string(body), } c.ServeJSON() return } // 解析YOLO响应 var yoloResp YOLOResponse if err := json.Unmarshal(body, &yoloResp); err != nil { c.Data["json"] = HazardResponse{ Code: 500, Msg: "解析YOLO响应失败: " + err.Error(), } c.ServeJSON() return } fmt.Println("yoloResp222", yoloResp) //如果labels为空,则返回错误 if len(yoloResp.Labels) == 0 { c.Data["json"] = HazardResponse{ Code: 500, Msg: "没有识别到任何隐患", } c.ServeJSON() return } // 构建返回数据 detectionResults := make([]map[string]interface{}, 0) for i, label := range yoloResp.Labels { if i < len(yoloResp.Scores) && i < len(yoloResp.Boxes) { result := map[string]interface{}{ "label": label, "score": yoloResp.Scores[i], "box": yoloResp.Boxes[i], "percent": fmt.Sprintf("%.2f%%", yoloResp.Scores[i]*100), } detectionResults = append(detectionResults, result) } } // 生成标注后的图片并上传到OSS var annotatedImageURL string if len(detectionResults) > 0 { // 解码原始图片 fmt.Printf("开始解码图片,数据大小: %d 字节\n", len(imageData)) // 检查图片格式 img, format, err := image.Decode(bytes.NewReader(imageData)) if err != nil { fmt.Printf("图片解码失败: %v\n", err) fmt.Printf("图片数据前20字节: %x\n", imageData[:min(20, len(imageData))]) // 尝试手动检测PNG格式并转换 if len(imageData) >= 8 && string(imageData[:8]) == "\x89PNG\r\n\x1a\n" { fmt.Printf("检测到PNG格式,尝试特殊处理...\n") // 尝试使用PNG解码器 img, err = decodePNGImage(imageData) if err != nil { fmt.Printf("PNG特殊解码也失败: %v\n", err) c.Data["json"] = HazardResponse{ Code: 500, Msg: "PNG图片解码失败: " + err.Error() + ",请检查图片是否损坏", } c.ServeJSON() return } format = "png" fmt.Printf("PNG特殊解码成功\n") } else { c.Data["json"] = HazardResponse{ Code: 500, Msg: "解码图片失败: " + err.Error() + ",请检查图片格式是否支持(JPEG、PNG、GIF等)", } c.ServeJSON() return } } fmt.Printf("图片解码成功,格式: %s,尺寸: %dx%d\n", format, img.Bounds().Dx(), img.Bounds().Dy()) // 绘制边界框 - 使用token中的用户信息 annotatedImg := drawBoundingBox(img, yoloResp.Boxes, yoloResp.Labels, yoloResp.Scores, userInfo.Name, userInfo.ContactNumber, requestData.Date) // 将标注后的图片编码为JPEG var buf bytes.Buffer if err := jpeg.Encode(&buf, annotatedImg, &jpeg.Options{Quality: 95}); err != nil { c.Data["json"] = HazardResponse{ Code: 500, Msg: "编码标注图片失败: " + err.Error(), } c.ServeJSON() return } // 生成文件名 utcNow := time.Now().UTC() timestamp := utcNow.Unix() fileName := fmt.Sprintf("hazard_annotated/%d/%s_%d.jpg", utcNow.Year(), utcNow.Format("0102"), timestamp) // 上传标注后的图片到OSS annotatedImageURL, err = uploadImageToOSS(buf.Bytes(), fileName) if err != nil { c.Data["json"] = HazardResponse{ Code: 500, Msg: "上传标注图片到OSS失败: " + err.Error(), } c.ServeJSON() return } fmt.Printf("标注图片上传成功: %s\n", fileName) } // fmt.Println("annotatedImageURL", annotatedImageURL) // 使用token中的user_id user_id := int(userInfo.ID) if user_id == 0 { user_id = 1 } //查询场景名称 var scene models.Scene models.DB.Where("scene_en_name = ? and is_deleted = ?", requestData.SceneName, 0).First(&scene) if scene.ID == 0 { c.Data["json"] = map[string]interface{}{ "statusCode": 500, "msg": "场景名称不存在", } c.ServeJSON() return } var tx = models.DB.Begin() recognitionRecord := &models.RecognitionRecord{ OriginalImageUrl: requestData.Image, RecognitionImageUrl: annotatedImageURL, UserID: user_id, Labels: removeDuplicateLabels(yoloResp.Labels), // 存储YOLO识别的标签(去重) Title: scene.SceneName, TagType: requestData.SceneName, SecondScene: "", // 默认空字符串 ThirdScene: "", // 默认空字符串 } if err := tx.Create(recognitionRecord).Error; err != nil { tx.Rollback() c.Data["json"] = map[string]interface{}{ "statusCode": 500, "msg": "识别失败: " + err.Error(), } c.ServeJSON() return } //获取labels对应的二级场景和三级场景 var thirdSceneNames []string // 判断是否为高速公路场景 isHighwayScene := strings.Contains(scene.SceneName, "运营高速公路") //隧道和简支梁 isTunnelScene := strings.Contains(scene.SceneName, "隧道") isSimplySupportedBeamScene := strings.Contains(scene.SceneName, "简支梁") fmt.Println("yoloResp.Labels", yoloResp.Labels) for _, label := range yoloResp.Labels { // 处理标签:对于高速公路场景,需要去掉前缀 processedLabel := label if isHighwayScene || isTunnelScene || isSimplySupportedBeamScene { processedLabel = processHighwayLabel(label) } var secondScene models.SecondScene models.DB.Where("second_scene_name = ? and is_deleted = ?", processedLabel, 0).First(&secondScene) if secondScene.ID != 0 { tx.Create(&models.RecognitionRecordSecondScene{ RecognitionRecordID: int(recognitionRecord.ID), SecondSceneID: int(secondScene.ID), }) // 通过secondScene.ID查询ThirdScene var thirdScene []models.ThirdScene models.DB.Where("second_scene_id = ? and is_deleted = ?", secondScene.ID, 0).Find(&thirdScene) if len(thirdScene) > 0 { for _, thirdScene := range thirdScene { thirdSceneNames = append(thirdSceneNames, thirdScene.ThirdSceneName) } } } } //对thirdSceneNames去重 thirdSceneNames = removeDuplicates(thirdSceneNames) // 将三级场景名称数组更新到recognitionRecord的Description if len(thirdSceneNames) > 0 { // 将数组转换为空格分隔的字符串 description := strings.Join(thirdSceneNames, " ") tx.Model(recognitionRecord).Update("Description", description) } tx.Commit() fmt.Println("thirdSceneNames", thirdSceneNames) // 返回成功响应 c.Data["json"] = HazardResponse{ Code: 200, Msg: "识别成功", Data: map[string]interface{}{ "scene_name": requestData.SceneName, "total_detections": len(detectionResults), "detections": detectionResults, "model_type": yoloResp.ModelType, "original_image": requestData.Image, "annotated_image": annotatedImageURL, //前端用这个预链接渲染 "labels": strings.Join(yoloResp.Labels, ", "), "third_scenes": thirdSceneNames, //三级场景名称数组 }, } c.ServeJSON() } // min 返回两个整数中的较小值 func min(a, b int) int { if a < b { return a } return b } // decodePNGImage 专门解码PNG图片 func decodePNGImage(imageData []byte) (image.Image, error) { // 直接使用PNG解码器 img, err := png.Decode(bytes.NewReader(imageData)) if err != nil { return nil, fmt.Errorf("PNG解码失败: %v", err) } return img, nil } // isImageContentType 检查Content-Type是否是图片类型 func isImageContentType(contentType string) bool { imageTypes := []string{ "image/jpeg", "image/jpg", "image/png", "image/gif", "image/bmp", "image/webp", "image/tiff", } for _, imgType := range imageTypes { if contentType == imgType { return true } } return false } // downloadImageFromOSS 从OSS链接下载图片(支持代理URL) func downloadImageFromOSS(imageURL string) ([]byte, error) { fmt.Printf("开始下载图片: %s\n", imageURL) // 检查是否是代理URL,如果是则直接使用代理接口 if strings.Contains(imageURL, "/apiv1/oss/parse/?url=") { fmt.Printf("检测到代理URL,直接使用代理接口\n") return downloadImageFromProxy(imageURL) } // 原始OSS URL的处理 client := &http.Client{ Timeout: 30 * time.Second, } resp, err := client.Get(imageURL) if err != nil { return nil, fmt.Errorf("下载图片失败: %v", err) } defer resp.Body.Close() fmt.Printf("下载响应状态码: %d\n", resp.StatusCode) fmt.Printf("响应头: %+v\n", resp.Header) if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("下载图片失败,状态码: %d", resp.StatusCode) } imageData, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("读取图片数据失败: %v", err) } fmt.Printf("图片下载完成,大小: %d 字节\n", len(imageData)) // 检查Content-Type contentType := resp.Header.Get("Content-Type") if contentType != "" { fmt.Printf("图片Content-Type: %s\n", contentType) // 检查是否是图片类型 if !isImageContentType(contentType) { fmt.Printf("警告: Content-Type不是图片类型: %s\n", contentType) } } return imageData, nil } // downloadImageFromProxy 通过代理接口下载图片 func downloadImageFromProxy(proxyURL string) ([]byte, error) { fmt.Printf("通过代理接口下载图片: %s\n", proxyURL) // 从代理URL中提取原始OSS URL // 格式: /apiv1/oss/parse/?url=http://172.16.17.52:8060/... parsedURL, err := url.Parse(proxyURL) if err != nil { return nil, fmt.Errorf("解析代理URL失败: %v", err) } // 获取url参数 originalURL := parsedURL.Query().Get("url") if originalURL == "" { return nil, fmt.Errorf("代理URL中缺少原始URL参数") } fmt.Printf("从代理URL提取的原始URL: %s\n", originalURL) // 直接使用原始URL下载图片 client := &http.Client{ Timeout: 30 * time.Second, } resp, err := client.Get(originalURL) if err != nil { return nil, fmt.Errorf("通过原始URL下载图片失败: %v", err) } defer resp.Body.Close() fmt.Printf("原始URL下载响应状态码: %d\n", resp.StatusCode) if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("原始URL下载图片失败,状态码: %d", resp.StatusCode) } imageData, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("读取原始URL图片数据失败: %v", err) } fmt.Printf("原始URL图片下载完成,大小: %d 字节\n", len(imageData)) // 检查Content-Type contentType := resp.Header.Get("Content-Type") if contentType != "" { fmt.Printf("原始URL图片Content-Type: %s\n", contentType) // 检查是否是图片类型 if !isImageContentType(contentType) { fmt.Printf("警告: 原始URL Content-Type不是图片类型: %s\n", contentType) } } return imageData, nil } // drawBoundingBox 在图像上绘制边界框和标签 func drawBoundingBox(img image.Image, boxes [][]float64, labels []string, scores []float64, username, account, date string) image.Image { // 创建可绘制的图像副本 bounds := img.Bounds() drawableImg := image.NewRGBA(bounds) draw.Draw(drawableImg, bounds, img, image.Point{}, draw.Src) // 在左上角添加logo图片 drawableImg = addLogoToImage(drawableImg) // 添加文字水印 - 传递动态参数 drawableImg = addTextWatermark(drawableImg, username, account, date) // 红色边界框 red := image.NewUniform(color.RGBA{255, 0, 0, 255}) for _, box := range boxes { if len(box) >= 4 { x1, y1, x2, y2 := int(box[0]), int(box[1]), int(box[2]), int(box[3]) // 绘制边界框 drawRect(drawableImg, x1, y1, x2, y2, red) } } return drawableImg } // addLogoToImage 在图片左上角添加logo func addLogoToImage(img *image.RGBA) *image.RGBA { // 读取本地logo图片 logoPath := "static/image/1.png" logoFile, err := os.Open(logoPath) if err != nil { fmt.Printf("无法打开logo文件: %v\n", err) return img // 如果无法打开logo,返回原图 } defer logoFile.Close() // 解码logo图片 logoImg, err := png.Decode(logoFile) if err != nil { fmt.Printf("解码logo图片失败: %v\n", err) return img // 如果解码失败,返回原图 } // 获取logo图片尺寸 logoBounds := logoImg.Bounds() originalWidth := logoBounds.Dx() originalHeight := logoBounds.Dy() // 缩小两倍 logoWidth := originalWidth / 2 logoHeight := originalHeight / 2 // 设置logo在左上角的位置(留一些边距) margin := 10 logoX := margin logoY := margin // 确保logo不会超出图片边界 imgBounds := img.Bounds() if logoX+logoWidth > imgBounds.Max.X { logoX = imgBounds.Max.X - logoWidth - margin if logoX < 0 { logoX = 0 } } if logoY+logoHeight > imgBounds.Max.Y { logoY = imgBounds.Max.Y - logoHeight - margin if logoY < 0 { logoY = 0 } } // 创建缩放后的logo图像 scaledLogo := image.NewRGBA(image.Rect(0, 0, logoWidth, logoHeight)) // 使用简单的最近邻缩放算法 for y := 0; y < logoHeight; y++ { for x := 0; x < logoWidth; x++ { // 计算原始图像中的对应位置 srcX := x * 2 srcY := y * 2 // 确保不超出原始图像边界 if srcX < originalWidth && srcY < originalHeight { scaledLogo.Set(x, y, logoImg.At(srcX, srcY)) } } } // 将缩放后的logo绘制到图片上 logoRect := image.Rect(logoX, logoY, logoX+logoWidth, logoY+logoHeight) draw.Draw(img, logoRect, scaledLogo, image.Point{}, draw.Over) fmt.Printf("成功在位置(%d,%d)添加logo,原始尺寸: %dx%d,缩放后尺寸: %dx%d\n", logoX, logoY, originalWidth, originalHeight, logoWidth, logoHeight) return img } // drawRect 绘制矩形 func drawRect(img *image.RGBA, x1, y1, x2, y2 int, color *image.Uniform) { bounds := img.Bounds() // 确保坐标在图像边界内 if x1 < bounds.Min.X { x1 = bounds.Min.X } if y1 < bounds.Min.Y { y1 = bounds.Min.Y } if x2 >= bounds.Max.X { x2 = bounds.Max.X - 1 } if y2 >= bounds.Max.Y { y2 = bounds.Max.Y - 1 } // 设置线条粗细(像素数) lineThickness := 6 // 绘制上边(粗线) for i := 0; i < lineThickness; i++ { y := y1 + i if y >= bounds.Max.Y { break } for x := x1; x <= x2; x++ { img.Set(x, y, color) } } // 绘制下边(粗线) for i := 0; i < lineThickness; i++ { y := y2 - i if y < bounds.Min.Y { break } for x := x1; x <= x2; x++ { img.Set(x, y, color) } } // 绘制左边(粗线) for i := 0; i < lineThickness; i++ { x := x1 + i if x >= bounds.Max.X { break } for y := y1; y <= y2; y++ { img.Set(x, y, color) } } // 绘制右边(粗线) for i := 0; i < lineThickness; i++ { x := x2 - i if x < bounds.Min.X { break } for y := y1; y <= y2; y++ { img.Set(x, y, color) } } } // removeDuplicates 去除字符串数组中的重复元素 func removeDuplicates(strs []string) []string { keys := make(map[string]bool) var result []string for _, str := range strs { if !keys[str] { keys[str] = true result = append(result, str) } } return result } // OSS配置信息 - 使用shudaooss.go中的配置 // uploadImageToOSS 上传图片数据到OSS并返回预签名URL func uploadImageToOSS(imageData []byte, fileName string) (string, error) { // 压缩图片 if EnableImageCompression { fmt.Printf("开始压缩标注图片到200KB以下...\n") compressedBytes, err := compressImage(imageData, MaxImageWidth, MaxImageHeight, 0) if err != nil { fmt.Printf("图片压缩失败,使用原始图片: %v\n", err) // 压缩失败时使用原始图片 } else { fmt.Printf("标注图片压缩完成,最终大小: %.2f KB\n", float64(len(compressedBytes))/1024) imageData = compressedBytes } } else { fmt.Printf("图片压缩已禁用,使用原始图片\n") } // 获取S3会话 s3Config := &aws.Config{ Credentials: credentials.NewStaticCredentials(ossAccessKey, ossSecretKey, ""), Endpoint: aws.String(ossEndpoint), Region: aws.String(ossRegion), S3ForcePathStyle: aws.Bool(true), } sess, err := session.NewSession(s3Config) if err != nil { return "", fmt.Errorf("创建S3会话失败: %v", err) } // 创建S3服务 s3Client := s3.New(sess) // 上传图片到S3 _, err = s3Client.PutObject(&s3.PutObjectInput{ Bucket: aws.String(ossBucket), Key: aws.String(fileName), Body: aws.ReadSeekCloser(strings.NewReader(string(imageData))), ContentType: aws.String("image/jpeg"), ACL: aws.String("public-read"), }) if err != nil { return "", fmt.Errorf("上传图片到OSS失败: %v", err) } // // 生成预签名URL(24小时有效期) // req, _ := s3Client.GetObjectRequest(&s3.GetObjectInput{ // Bucket: aws.String(ossBucket), // Key: aws.String(fileName), // }) // presignedURL, err := req.Presign(24 * time.Hour) presignedURL := fmt.Sprintf("%s/%s/%s", ossEndpoint, ossBucket, fileName) // 使用代理接口包装URL,前端需要显示图片 proxyURL := utils.GetProxyURL(presignedURL) if err != nil { return "", fmt.Errorf("生成预签名URL失败: %v", err) } return proxyURL, nil } // TestWatermarkFunction 测试文字水印功能的简单函数 func TestWatermarkFunction() { fmt.Println("开始测试文字水印功能...") // 读取测试图片 imagePath := "static/image/1.png" imageFile, err := os.Open(imagePath) if err != nil { fmt.Printf("无法打开测试图片: %v\n", err) return } defer imageFile.Close() // 解码图片 img, err := png.Decode(imageFile) if err != nil { fmt.Printf("解码测试图片失败: %v\n", err) return } fmt.Printf("成功读取测试图片,尺寸: %dx%d\n", img.Bounds().Dx(), img.Bounds().Dy()) // 转换为可绘制的RGBA图像 bounds := img.Bounds() drawableImg := image.NewRGBA(bounds) draw.Draw(drawableImg, bounds, img, image.Point{}, draw.Src) // 添加logo fmt.Println("添加logo...") drawableImg = addLogoToImage(drawableImg) // 添加文字水印 fmt.Println("添加文字水印...") drawableImg = addTextWatermark(drawableImg, "测试用户", "1234", "2025/01/15") // 将处理后的图片保存到static/image目录 outputPath := "static/image/test_output.jpg" outputFile, err := os.Create(outputPath) if err != nil { fmt.Printf("创建输出文件失败: %v\n", err) return } defer outputFile.Close() // 编码为JPEG if err := jpeg.Encode(outputFile, drawableImg, &jpeg.Options{Quality: 95}); err != nil { fmt.Printf("编码图片失败: %v\n", err) return } fmt.Printf("测试完成!处理后的图片已保存到: %s\n", outputPath) fmt.Println("请查看图片效果,确认logo和文字水印是否正确显示") } // addTextWatermark 使用gg库添加45度角的文字水印(改进版) func addTextWatermark(img *image.RGBA, username, account, date string) *image.RGBA { // 水印文本 - 使用传入的动态数据 watermarks := []string{username, account, date} // 获取图片尺寸 bounds := img.Bounds() width, height := bounds.Max.X, bounds.Max.Y // 创建一个新的绘图上下文 dc := gg.NewContextForImage(img) // 设置字体和颜色 fontSize := 20.0 // 从30调整为20,字体更小 // 尝试多种字体加载方案 fontLoaded := false // 方案1:优先使用项目中的字体文件 localFontPaths := []string{ "static/font/AlibabaPuHuiTi-3-55-Regular.ttf", // 阿里巴巴普惠体(项目字体) } for _, fontPath := range localFontPaths { if err := dc.LoadFontFace(fontPath, fontSize); err == nil { fmt.Printf("成功加载本地字体: %s\n", fontPath) fontLoaded = true break } } // 方案2:如果本地字体失败,尝试使用内置字体 if !fontLoaded { fmt.Printf("本地字体加载失败,尝试使用内置字体\n") // 使用空字符串加载默认字体,这在大多数情况下都能工作 if err := dc.LoadFontFace("", fontSize); err == nil { fmt.Printf("成功加载内置默认字体\n") fontLoaded = true } else { fmt.Printf("内置字体加载失败: %v\n", err) } } // 方案3:如果所有字体都失败,使用像素绘制(备用方案) if !fontLoaded { fmt.Printf("所有字体加载失败,将使用像素绘制方案\n") // 这里可以添加像素绘制的备用方案 } // 设置更深的颜色(从灰色改为深灰色) dc.SetColor(color.RGBA{R: 120, G: 120, B: 120, A: 120}) // 深灰色,更不透明 // 设置旋转角度 angle := gg.Radians(-45) // 设置为-45度 // 循环绘制水印 textWidthEstimate := 150.0 // 估算文本宽度(字体变小,间距也相应调整) textHeightEstimate := 80.0 // 估算文本高度,作为行间距(字体变小,间距也相应调整) // 旋转整个画布来绘制 dc.Rotate(angle) // 计算旋转后的绘制范围 // 为了覆盖整个图片,我们需要在更大的范围内绘制 // 简单起见,我们循环一个足够大的范围 // 这里的x,y是旋转后的坐标 for y := -float64(height); y < float64(height)*1.5; y += textHeightEstimate { for x := -float64(width); x < float64(width)*1.5; x += textWidthEstimate { // 每排使用相同的内容:根据y坐标确定使用哪个文本 rowIndex := int(y/textHeightEstimate) % len(watermarks) if rowIndex < 0 { rowIndex = (rowIndex%len(watermarks) + len(watermarks)) % len(watermarks) } text := watermarks[rowIndex] dc.DrawString(text, x, y) } } fmt.Printf("成功添加45度角文字水印(改进版:颜色更深,每排内容相同)\n") return dc.Image().(*image.RGBA) } // 前端传值步骤、json文件、封面图过来 type SaveStepRequest struct { AIConversationID uint64 `json:"ai_conversation_id"` Step int `json:"step"` PPTJsonUrl string `json:"ppt_json_url"` CoverImage string `json:"cover_image"` PPTJsonContent string `json:"ppt_json_content"` } func (c *HazardController) SaveStep() { var requestData SaveStepRequest if err := json.Unmarshal(c.Ctx.Input.RequestBody, &requestData); err != nil { c.Data["json"] = map[string]interface{}{ "statusCode": 400, "msg": "请求数据解析失败", } c.ServeJSON() return } tx := models.DB.Begin() //更新到ai_conversation表 if err := tx.Model(&models.AIConversation{}).Where("id = ?", requestData.AIConversationID).Updates(map[string]interface{}{ "step": requestData.Step, "ppt_json_url": requestData.PPTJsonUrl, "cover_image": requestData.CoverImage, "ppt_json_content": requestData.PPTJsonContent, }).Error; err != nil { tx.Rollback() c.Data["json"] = map[string]interface{}{ "statusCode": 500, "msg": "更新步骤失败", } c.ServeJSON() return } tx.Commit() c.Data["json"] = map[string]interface{}{ "statusCode": 200, "msg": "success", } c.ServeJSON() } // removeDuplicateLabels 去除重复的标签并返回逗号分隔的字符串 func removeDuplicateLabels(labels []string) string { if len(labels) == 0 { return "" } // 使用map来去重 labelMap := make(map[string]bool) var uniqueLabels []string for _, label := range labels { // 去除前后空格 label = strings.TrimSpace(label) if label != "" && !labelMap[label] { labelMap[label] = true uniqueLabels = append(uniqueLabels, label) } } return strings.Join(uniqueLabels, ", ") } // processHighwayLabel 处理高速公路场景的标签 // 例如:"绿化_路侧植株_路侧植株" -> "路侧植株_路侧植株" // 去掉前缀(第一个下划线及其之前的内容) func processHighwayLabel(label string) string { // 查找第一个下划线的位置 underscoreIndex := strings.Index(label, "_") if underscoreIndex == -1 { // 如果没有下划线,直接返回原标签 return label } // 返回从下划线之后的内容 return label[underscoreIndex+1:] }