package services import ( "encoding/json" "errors" "fmt" "github.com/PuerkitoBio/goquery" "hongze/hongze_yb/global" "hongze/hongze_yb/models/tables/yb_poster_config" "hongze/hongze_yb/models/tables/yb_poster_resource" "hongze/hongze_yb/models/tables/yb_resource" "hongze/hongze_yb/models/tables/yb_suncode_pars" "hongze/hongze_yb/services/alarm_msg" "hongze/hongze_yb/services/wx_app" "hongze/hongze_yb/utils" "io/ioutil" "net/http" "os" "os/exec" "strings" "time" ) var ( ServerUrl = "http://127.0.0.1:5008/" VoiceBroadcastShareImgSource = "voice_broadcast_share_img" ) // SharePosterReq 分享海报请求体 type SharePosterReq struct { CodePage string `json:"code_page" description:"太阳码page"` CodeScene string `json:"code_scene" description:"太阳码scene"` Source string `json:"source" description:"来源"` Version string `json:"version" description:"海报版本号" ` Pars string `json:"pars" description:"海报动态信息"` } type Html2ImgResp struct { Code int `json:"code"` Msg string `json:"msg"` Data string `json:"data"` } // postHtml2Img 请求htm2img接口 func postHtml2Img(param map[string]interface{}) (resp *Html2ImgResp, err error) { // 目前仅此处调用该接口,暂不加授权、校验等 postUrl := ServerUrl + "htm2img" postData, err := json.Marshal(param) if err != nil { return } result, err := Html2ImgHttpPost(postUrl, string(postData), "application/json") if err != nil { return } if err = json.Unmarshal(result, &resp); err != nil { return } return resp, nil } // Html2ImgHttpPost post请求 func Html2ImgHttpPost(url, postData string, params ...string) ([]byte, error) { body := ioutil.NopCloser(strings.NewReader(postData)) client := &http.Client{} req, err := http.NewRequest("POST", url, body) if err != nil { return nil, err } contentType := "application/x-www-form-urlencoded;charset=utf-8" if len(params) > 0 && params[0] != "" { contentType = params[0] } req.Header.Set("Content-Type", contentType) resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() b, err := ioutil.ReadAll(resp.Body) fmt.Println("HttpPost:" + string(b)) return b, err } // CreateAndUploadSunCode 生成太阳码并上传OSS func CreateAndUploadSunCode(page, scene, version, copyYb string) (imgUrl string, err error) { if page == "" { err = errors.New("page不能为空") return } path := fmt.Sprint(page, "?", scene) exist, err := yb_poster_resource.GetPosterByCondition(path, "qrcode", version) if err != nil && err != utils.ErrNoRow { return } if exist != nil && exist.ImgURL != "" { return exist.ImgURL, nil } // scene超过32位会生成失败,md5处理至32位 sceneMD5 := "a=1" if scene != "" { sceneMD5 = utils.MD5(scene) } picByte, err := wx_app.GetSunCode(page, sceneMD5, copyYb) if err != nil { return } // 生成图片 localPath := "./static/img" fileName := utils.GetRandStringNoSpecialChar(28) + ".png" fpath := fmt.Sprint(localPath, "/", fileName) f, err := os.Create(fpath) if err != nil { return } if _, err = f.Write(picByte); err != nil { return } defer func() { f.Close() os.Remove(fpath) }() // 上传OSS fileDir := "images/yb/suncode/" imgUrl, err = UploadAliyunToDir(fileName, fpath, fileDir) if err != nil { return } // 记录二维码信息 newPoster := &yb_poster_resource.YbPosterResource{ Path: path, ImgURL: imgUrl, Type: "qrcode", Version: version, CreateTime: time.Now(), } err = newPoster.Create() if err != nil { return } // 记录参数md5 if scene != "" { newPars := &yb_suncode_pars.YbSuncodePars{ Scene: scene, SceneKey: sceneMD5, CreateTime: time.Now(), } err = newPars.Create() } // 记录文件 go func() { re := new(yb_resource.YbResource) re.ResourceUrl = imgUrl re.ResourceType = yb_resource.ResourceTypeImg re.CreateTime = time.Now().Local() if e := re.Create(); e != nil { return } }() return } // CreatePosterFromSourceV2 生成分享海报(通过配置获取相关信息) func CreatePosterFromSourceV2(codePage, codeScene, source, version, pars, copyYb string) (imgUrl string, err error) { var errMsg string defer func() { if err != nil { global.LOG.Critical(fmt.Sprintf("CreatePosterFromSource: source=%s, pars:%s, errMsg:%s", source, pars, errMsg)) reqSlice := make([]string, 0) reqSlice = append(reqSlice, fmt.Sprint("CodePage:", codePage, "\n")) reqSlice = append(reqSlice, fmt.Sprint("CodeScene:", codeScene, "\n")) reqSlice = append(reqSlice, fmt.Sprint("Source:", source, "\n")) reqSlice = append(reqSlice, fmt.Sprint("Version:", version, "\n")) reqSlice = append(reqSlice, fmt.Sprint("Pars:", pars, "\n")) go alarm_msg.SendAlarmMsg("CreatePosterFromSource生成分享海报失败, Msg:"+errMsg+";Err:"+err.Error()+"\n;Req:\n"+strings.Join(reqSlice, ";"), 3) } }() if codePage == "" || source == "" || pars == "" { errMsg = "参数有误" err = errors.New(errMsg) return } path := fmt.Sprint(codePage, "?", codeScene) // 非列表来源获取历史图片,无则生成 if !strings.Contains(source, "list") && source != "price_driven" { poster, tmpErr := yb_poster_resource.GetPosterByCondition(path, "poster", version) if tmpErr != nil && tmpErr != utils.ErrNoRow { err = tmpErr return } if poster != nil && poster.ImgURL != "" { imgUrl = poster.ImgURL return } } ybPosterConfig, err := yb_poster_config.GetBySource(source) if err != nil { return } width := ybPosterConfig.Width height := ybPosterConfig.Hight //生成太阳码 sunCodeUrl, err := CreateAndUploadSunCode(codePage, codeScene, version, copyYb) if err != nil { return } //sunCodeUrl := `` // 填充html内容 contentStr, newHeight, err := fillContent2HtmlV2(source, pars, sunCodeUrl, height, *ybPosterConfig) if err != nil { errMsg = "html内容有误" return } global.LOG.Critical(contentStr) //return // 请求python服务htm2img htm2ImgReq := make(map[string]interface{}) htm2ImgReq["html_content"] = contentStr htm2ImgReq["width"] = width htm2ImgReq["height"] = newHeight res, err := postHtml2Img(htm2ImgReq) if err != nil || res == nil { errMsg = "html转图片请求失败" return } if res.Code != 200 { errMsg = "html转图片请求失败" err = errors.New("html转图片失败: " + res.Msg) return } imgUrl = res.Data // 记录海报信息 newPoster := &yb_poster_resource.YbPosterResource{ Path: path, ImgURL: imgUrl, Type: "poster", Version: version, CreateTime: time.Now(), } err = newPoster.Create() return } // HtmlReplaceConfig html替换配置 type HtmlReplaceConfig struct { TemplateStr string `json:"template_str"` ReplaceStr string `json:"replace_str"` } // DefaultValueConfig 默认值的配置 type DefaultValueConfig struct { Key string `json:"key"` UseOtherKey string `json:"use_other_key"` Value string `json:"value"` ConditionKey string `json:"condition_key"` } // fillContent2Html 填充HTML动态内容 func fillContent2HtmlV2(source, pars, sunCodeUrl string, height float64, ybPosterConfig yb_poster_config.YbPosterConfig) (contentStr string, newHeight float64, err error) { paramsMap := make(map[string]string) if err = json.Unmarshal([]byte(pars), ¶msMap); err != nil { return } //fmt.Println(paramsMap) //html替换规则 htmlReplaceConfigList := make([]HtmlReplaceConfig, 0) if err = json.Unmarshal([]byte(ybPosterConfig.HTMLReplaceConfig), &htmlReplaceConfigList); err != nil { return } newHeight = height contentStr = ybPosterConfig.HTMLTemplate // 默认数据替换 defaultValueConfigMap := make([]DefaultValueConfig, 0) if ybPosterConfig.DefaultValueConfig != `` { if err = json.Unmarshal([]byte(ybPosterConfig.DefaultValueConfig), &defaultValueConfigMap); err != nil { return } } // 列表的动态内容不完整的用默认内容的填充 //var emptyTime1, emptyTime2 bool conditionKeyValMap := make(map[string]string) for _, v := range defaultValueConfigMap { if v.ConditionKey == `` { continue } conditionKeyVal, ok := conditionKeyValMap[v.ConditionKey] if !ok { conditionKeyVal = paramsMap[v.ConditionKey] conditionKeyValMap[v.ConditionKey] = conditionKeyVal } if conditionKeyVal == `` { paramsMap[v.Key] = v.Value if v.UseOtherKey != `` { if tmpVal, ok := paramsMap[v.UseOtherKey]; ok { paramsMap[v.Key] = tmpVal } } } } // 填充指定内容 switch source { case "report_detail": //需要将简介处理下 reportAbstract := paramsMap["report_abstract"] doc, tmpErr := goquery.NewDocumentFromReader(strings.NewReader(reportAbstract)) if tmpErr != nil { err = tmpErr return } abstract := "" doc.Find("p").Each(func(i int, s *goquery.Selection) { phtml, tmpErr := s.Html() if tmpErr != nil { err = tmpErr return } st := s.Text() if st != "" && st != "
" && st != "
" && !strings.Contains(phtml, "iframe") { abstract = abstract + "

" + phtml + "

" } }) paramsMap["report_abstract"] = abstract case "activity_list": bgColorMap := map[string]string{ "未开始": "#E3B377", "进行中": "#3385FF", "已结束": "#A2A2A2", } statusItemMap := map[string]string{ "未开始": "block", "进行中": "none", "已结束": "none", } offlineMap := map[string]string{ "线上会议": "none", "线下沙龙": "block", } onlineMap := map[string]string{ "线上会议": "block", "线下沙龙": "none", } listTitle := paramsMap["list_title"] status1 := paramsMap["status_1"] if status1 != "未开始" { newHeight = 1715 } status2 := paramsMap["status_2"] paramsMap["list_title"] = "弘则FICC周度电话会安排" paramsMap["bg_color_1"] = bgColorMap[status1] paramsMap["show_item_1"] = statusItemMap[status1] paramsMap["show_offline_1"] = offlineMap[listTitle] paramsMap["show_online_1"] = onlineMap[listTitle] paramsMap["bg_color_2"] = bgColorMap[status2] paramsMap["show_item_2"] = statusItemMap[status2] paramsMap["show_offline_2"] = offlineMap[listTitle] paramsMap["show_online_2"] = onlineMap[listTitle] // 用默认内容填充的活动时间字体颜色调至看不见 color1 := "#999" color2 := "#999" if paramsMap["empty_time_1"] == "true" { color1 = "#fff" } if paramsMap["empty_time_2"] == "true" { color2 = "#fff" } paramsMap["time_color_1"] = color1 paramsMap["time_color_2"] = color2 } contentStr = strings.Replace(contentStr, "{{SUN_CODE}}", sunCodeUrl, 1) for _, v := range htmlReplaceConfigList { tmpVal, ok := paramsMap[v.ReplaceStr] if !ok { tmpVal = `` } contentStr = strings.Replace(contentStr, v.TemplateStr, tmpVal, 1) } return } // GetDynamicShareImg 生成动态分享图 func GetDynamicShareImg(source, pars string) (imgUrl string, err error) { if source == "" { err = errors.New("图片来源有误") return } imgConfig, e := yb_poster_config.GetBySource(source) if e != nil { err = errors.New("获取图片配置失败") return } // 填充html内容 content, newHeight, e := fillContent2HtmlV2(source, pars, "", imgConfig.Hight, *imgConfig) if e != nil { err = errors.New("html内容有误") return } htm2ImgReq := make(map[string]interface{}) htm2ImgReq["html_content"] = content htm2ImgReq["width"] = imgConfig.Width htm2ImgReq["height"] = newHeight res, e := postHtml2Img(htm2ImgReq) if e != nil || res == nil { err = errors.New("html转图片请求失败") return } if res.Code != 200 { err = errors.New("html转图片请求失败: " + res.Msg) return } imgUrl = res.Data return } func ReportToJpeg(reportUrl, filePath string) (err error) { pyCode := ` import asyncio from pyppeteer import launch, errors async def main(): try: # 启动浏览器 browser = await launch({ 'executablePath': '%s', 'headless': True, 'args': ['--disable-infobars', '--no-sandbox'] }) # 新建页面 page = await browser.newPage() # 设置视口大小 await page.setViewport({ 'width': 750, 'height': 1080 }) # 导航到页面 await page.goto('%s', { 'waitUntil': 'networkidle0', 'timeout': 1000000 # 设置超时时间为 100 秒 }) await page.screenshot({ 'path': "%s", 'fullPage': True, 'quality':100 }) except errors.BrowserError as e: print('Browser closed unexpectedly:', e) except Exception as e: print('An error occurred:', e) finally: # 确保浏览器关闭 if browser is not None: await browser.close() # 获取当前事件循环 loop = asyncio.get_event_loop() # 运行事件循环直到main协程完成 try: loop.run_until_complete(main()) except Exception as e: print('Error during event loop execution:', e) finally: # 关闭事件循环 loop.close() ` pyCode = fmt.Sprintf(pyCode, global.CONFIG.System.ChromePath, reportUrl, filePath) global.LOG.Info("jpeg pyCode: \n" + pyCode) cmd := exec.Command("python3", "-c", pyCode) output, e := cmd.CombinedOutput() if e != nil { err = e global.LOG.Info("ReportToJpeg failed: , error: \n" + err.Error()) global.LOG.Info("Output: %s\n", string(output)) go alarm_msg.SendAlarmMsg("ReportToJpeg failed:"+err.Error(), 3) go alarm_msg.SendAlarmMsg("Output :"+string(output), 3) } defer func() { cmd.Process.Kill() }() return }