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 }