share_poster.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. package services
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "github.com/PuerkitoBio/goquery"
  7. "hongze/hongze_yb/global"
  8. "hongze/hongze_yb/models/tables/yb_poster_config"
  9. "hongze/hongze_yb/models/tables/yb_poster_resource"
  10. "hongze/hongze_yb/models/tables/yb_resource"
  11. "hongze/hongze_yb/models/tables/yb_suncode_pars"
  12. "hongze/hongze_yb/services/alarm_msg"
  13. "hongze/hongze_yb/services/wx_app"
  14. "hongze/hongze_yb/utils"
  15. "io/ioutil"
  16. "net/http"
  17. "os"
  18. "os/exec"
  19. "strings"
  20. "time"
  21. )
  22. var (
  23. ServerUrl = "http://127.0.0.1:5008/"
  24. VoiceBroadcastShareImgSource = "voice_broadcast_share_img"
  25. )
  26. // SharePosterReq 分享海报请求体
  27. type SharePosterReq struct {
  28. CodePage string `json:"code_page" description:"太阳码page"`
  29. CodeScene string `json:"code_scene" description:"太阳码scene"`
  30. Source string `json:"source" description:"来源"`
  31. Version string `json:"version" description:"海报版本号" `
  32. Pars string `json:"pars" description:"海报动态信息"`
  33. }
  34. type Html2ImgResp struct {
  35. Code int `json:"code"`
  36. Msg string `json:"msg"`
  37. Data string `json:"data"`
  38. }
  39. // postHtml2Img 请求htm2img接口
  40. func postHtml2Img(param map[string]interface{}) (resp *Html2ImgResp, err error) {
  41. // 目前仅此处调用该接口,暂不加授权、校验等
  42. postUrl := ServerUrl + "htm2img"
  43. postData, err := json.Marshal(param)
  44. if err != nil {
  45. return
  46. }
  47. result, err := Html2ImgHttpPost(postUrl, string(postData), "application/json")
  48. if err != nil {
  49. return
  50. }
  51. if err = json.Unmarshal(result, &resp); err != nil {
  52. return
  53. }
  54. return resp, nil
  55. }
  56. // Html2ImgHttpPost post请求
  57. func Html2ImgHttpPost(url, postData string, params ...string) ([]byte, error) {
  58. body := ioutil.NopCloser(strings.NewReader(postData))
  59. client := &http.Client{}
  60. req, err := http.NewRequest("POST", url, body)
  61. if err != nil {
  62. return nil, err
  63. }
  64. contentType := "application/x-www-form-urlencoded;charset=utf-8"
  65. if len(params) > 0 && params[0] != "" {
  66. contentType = params[0]
  67. }
  68. req.Header.Set("Content-Type", contentType)
  69. resp, err := client.Do(req)
  70. if err != nil {
  71. return nil, err
  72. }
  73. defer resp.Body.Close()
  74. b, err := ioutil.ReadAll(resp.Body)
  75. fmt.Println("HttpPost:" + string(b))
  76. return b, err
  77. }
  78. // CreateAndUploadSunCode 生成太阳码并上传OSS
  79. func CreateAndUploadSunCode(page, scene, version, copyYb string) (imgUrl string, err error) {
  80. if page == "" {
  81. err = errors.New("page不能为空")
  82. return
  83. }
  84. path := fmt.Sprint(page, "?", scene)
  85. exist, err := yb_poster_resource.GetPosterByCondition(path, "qrcode", version)
  86. if err != nil && err != utils.ErrNoRow {
  87. return
  88. }
  89. if exist != nil && exist.ImgURL != "" {
  90. return exist.ImgURL, nil
  91. }
  92. // scene超过32位会生成失败,md5处理至32位
  93. sceneMD5 := "a=1"
  94. if scene != "" {
  95. sceneMD5 = utils.MD5(scene)
  96. }
  97. picByte, err := wx_app.GetSunCode(page, sceneMD5, copyYb)
  98. if err != nil {
  99. return
  100. }
  101. // 生成图片
  102. localPath := "./static/img"
  103. fileName := utils.GetRandStringNoSpecialChar(28) + ".png"
  104. fpath := fmt.Sprint(localPath, "/", fileName)
  105. f, err := os.Create(fpath)
  106. if err != nil {
  107. return
  108. }
  109. if _, err = f.Write(picByte); err != nil {
  110. return
  111. }
  112. defer func() {
  113. f.Close()
  114. os.Remove(fpath)
  115. }()
  116. // 上传OSS
  117. fileDir := "images/yb/suncode/"
  118. imgUrl, err = UploadAliyunToDir(fileName, fpath, fileDir)
  119. if err != nil {
  120. return
  121. }
  122. // 记录二维码信息
  123. newPoster := &yb_poster_resource.YbPosterResource{
  124. Path: path,
  125. ImgURL: imgUrl,
  126. Type: "qrcode",
  127. Version: version,
  128. CreateTime: time.Now(),
  129. }
  130. err = newPoster.Create()
  131. if err != nil {
  132. return
  133. }
  134. // 记录参数md5
  135. if scene != "" {
  136. newPars := &yb_suncode_pars.YbSuncodePars{
  137. Scene: scene,
  138. SceneKey: sceneMD5,
  139. CreateTime: time.Now(),
  140. }
  141. err = newPars.Create()
  142. }
  143. // 记录文件
  144. go func() {
  145. re := new(yb_resource.YbResource)
  146. re.ResourceUrl = imgUrl
  147. re.ResourceType = yb_resource.ResourceTypeImg
  148. re.CreateTime = time.Now().Local()
  149. if e := re.Create(); e != nil {
  150. return
  151. }
  152. }()
  153. return
  154. }
  155. // CreatePosterFromSourceV2 生成分享海报(通过配置获取相关信息)
  156. func CreatePosterFromSourceV2(codePage, codeScene, source, version, pars, copyYb string) (imgUrl string, err error) {
  157. var errMsg string
  158. defer func() {
  159. if err != nil {
  160. global.LOG.Critical(fmt.Sprintf("CreatePosterFromSource: source=%s, pars:%s, errMsg:%s", source, pars, errMsg))
  161. reqSlice := make([]string, 0)
  162. reqSlice = append(reqSlice, fmt.Sprint("CodePage:", codePage, "\n"))
  163. reqSlice = append(reqSlice, fmt.Sprint("CodeScene:", codeScene, "\n"))
  164. reqSlice = append(reqSlice, fmt.Sprint("Source:", source, "\n"))
  165. reqSlice = append(reqSlice, fmt.Sprint("Version:", version, "\n"))
  166. reqSlice = append(reqSlice, fmt.Sprint("Pars:", pars, "\n"))
  167. go alarm_msg.SendAlarmMsg("CreatePosterFromSource生成分享海报失败, Msg:"+errMsg+";Err:"+err.Error()+"\n;Req:\n"+strings.Join(reqSlice, ";"), 3)
  168. }
  169. }()
  170. if codePage == "" || source == "" || pars == "" {
  171. errMsg = "参数有误"
  172. err = errors.New(errMsg)
  173. return
  174. }
  175. path := fmt.Sprint(codePage, "?", codeScene)
  176. // 非列表来源获取历史图片,无则生成
  177. if !strings.Contains(source, "list") && source != "price_driven" {
  178. poster, tmpErr := yb_poster_resource.GetPosterByCondition(path, "poster", version)
  179. if tmpErr != nil && tmpErr != utils.ErrNoRow {
  180. err = tmpErr
  181. return
  182. }
  183. if poster != nil && poster.ImgURL != "" {
  184. imgUrl = poster.ImgURL
  185. return
  186. }
  187. }
  188. ybPosterConfig, err := yb_poster_config.GetBySource(source)
  189. if err != nil {
  190. return
  191. }
  192. width := ybPosterConfig.Width
  193. height := ybPosterConfig.Hight
  194. //生成太阳码
  195. sunCodeUrl, err := CreateAndUploadSunCode(codePage, codeScene, version, copyYb)
  196. if err != nil {
  197. return
  198. }
  199. //sunCodeUrl := ``
  200. // 填充html内容
  201. contentStr, newHeight, err := fillContent2HtmlV2(source, pars, sunCodeUrl, height, *ybPosterConfig)
  202. if err != nil {
  203. errMsg = "html内容有误"
  204. return
  205. }
  206. global.LOG.Critical(contentStr)
  207. //return
  208. // 请求python服务htm2img
  209. htm2ImgReq := make(map[string]interface{})
  210. htm2ImgReq["html_content"] = contentStr
  211. htm2ImgReq["width"] = width
  212. htm2ImgReq["height"] = newHeight
  213. res, err := postHtml2Img(htm2ImgReq)
  214. if err != nil || res == nil {
  215. errMsg = "html转图片请求失败"
  216. return
  217. }
  218. if res.Code != 200 {
  219. errMsg = "html转图片请求失败"
  220. err = errors.New("html转图片失败: " + res.Msg)
  221. return
  222. }
  223. imgUrl = res.Data
  224. // 记录海报信息
  225. newPoster := &yb_poster_resource.YbPosterResource{
  226. Path: path,
  227. ImgURL: imgUrl,
  228. Type: "poster",
  229. Version: version,
  230. CreateTime: time.Now(),
  231. }
  232. err = newPoster.Create()
  233. return
  234. }
  235. // HtmlReplaceConfig html替换配置
  236. type HtmlReplaceConfig struct {
  237. TemplateStr string `json:"template_str"`
  238. ReplaceStr string `json:"replace_str"`
  239. }
  240. // DefaultValueConfig 默认值的配置
  241. type DefaultValueConfig struct {
  242. Key string `json:"key"`
  243. UseOtherKey string `json:"use_other_key"`
  244. Value string `json:"value"`
  245. ConditionKey string `json:"condition_key"`
  246. }
  247. // fillContent2Html 填充HTML动态内容
  248. func fillContent2HtmlV2(source, pars, sunCodeUrl string, height float64, ybPosterConfig yb_poster_config.YbPosterConfig) (contentStr string, newHeight float64, err error) {
  249. paramsMap := make(map[string]string)
  250. if err = json.Unmarshal([]byte(pars), &paramsMap); err != nil {
  251. return
  252. }
  253. //fmt.Println(paramsMap)
  254. //html替换规则
  255. htmlReplaceConfigList := make([]HtmlReplaceConfig, 0)
  256. if err = json.Unmarshal([]byte(ybPosterConfig.HTMLReplaceConfig), &htmlReplaceConfigList); err != nil {
  257. return
  258. }
  259. newHeight = height
  260. contentStr = ybPosterConfig.HTMLTemplate
  261. // 默认数据替换
  262. defaultValueConfigMap := make([]DefaultValueConfig, 0)
  263. if ybPosterConfig.DefaultValueConfig != `` {
  264. if err = json.Unmarshal([]byte(ybPosterConfig.DefaultValueConfig), &defaultValueConfigMap); err != nil {
  265. return
  266. }
  267. }
  268. // 列表的动态内容不完整的用默认内容的填充
  269. //var emptyTime1, emptyTime2 bool
  270. conditionKeyValMap := make(map[string]string)
  271. for _, v := range defaultValueConfigMap {
  272. if v.ConditionKey == `` {
  273. continue
  274. }
  275. conditionKeyVal, ok := conditionKeyValMap[v.ConditionKey]
  276. if !ok {
  277. conditionKeyVal = paramsMap[v.ConditionKey]
  278. conditionKeyValMap[v.ConditionKey] = conditionKeyVal
  279. }
  280. if conditionKeyVal == `` {
  281. paramsMap[v.Key] = v.Value
  282. if v.UseOtherKey != `` {
  283. if tmpVal, ok := paramsMap[v.UseOtherKey]; ok {
  284. paramsMap[v.Key] = tmpVal
  285. }
  286. }
  287. }
  288. }
  289. // 填充指定内容
  290. switch source {
  291. case "report_detail": //需要将简介处理下
  292. reportAbstract := paramsMap["report_abstract"]
  293. doc, tmpErr := goquery.NewDocumentFromReader(strings.NewReader(reportAbstract))
  294. if tmpErr != nil {
  295. err = tmpErr
  296. return
  297. }
  298. abstract := ""
  299. doc.Find("p").Each(func(i int, s *goquery.Selection) {
  300. phtml, tmpErr := s.Html()
  301. if tmpErr != nil {
  302. err = tmpErr
  303. return
  304. }
  305. st := s.Text()
  306. if st != "" && st != "<br>" && st != "<br style=\"max-width: 100%;\">" && !strings.Contains(phtml, "iframe") {
  307. abstract = abstract + "<p>" + phtml + "</p>"
  308. }
  309. })
  310. paramsMap["report_abstract"] = abstract
  311. case "activity_list":
  312. bgColorMap := map[string]string{
  313. "未开始": "#E3B377",
  314. "进行中": "#3385FF",
  315. "已结束": "#A2A2A2",
  316. }
  317. statusItemMap := map[string]string{
  318. "未开始": "block",
  319. "进行中": "none",
  320. "已结束": "none",
  321. }
  322. offlineMap := map[string]string{
  323. "线上会议": "none",
  324. "线下沙龙": "block",
  325. }
  326. onlineMap := map[string]string{
  327. "线上会议": "block",
  328. "线下沙龙": "none",
  329. }
  330. listTitle := paramsMap["list_title"]
  331. status1 := paramsMap["status_1"]
  332. if status1 != "未开始" {
  333. newHeight = 1715
  334. }
  335. status2 := paramsMap["status_2"]
  336. paramsMap["list_title"] = "弘则FICC周度电话会安排"
  337. paramsMap["bg_color_1"] = bgColorMap[status1]
  338. paramsMap["show_item_1"] = statusItemMap[status1]
  339. paramsMap["show_offline_1"] = offlineMap[listTitle]
  340. paramsMap["show_online_1"] = onlineMap[listTitle]
  341. paramsMap["bg_color_2"] = bgColorMap[status2]
  342. paramsMap["show_item_2"] = statusItemMap[status2]
  343. paramsMap["show_offline_2"] = offlineMap[listTitle]
  344. paramsMap["show_online_2"] = onlineMap[listTitle]
  345. // 用默认内容填充的活动时间字体颜色调至看不见
  346. color1 := "#999"
  347. color2 := "#999"
  348. if paramsMap["empty_time_1"] == "true" {
  349. color1 = "#fff"
  350. }
  351. if paramsMap["empty_time_2"] == "true" {
  352. color2 = "#fff"
  353. }
  354. paramsMap["time_color_1"] = color1
  355. paramsMap["time_color_2"] = color2
  356. }
  357. contentStr = strings.Replace(contentStr, "{{SUN_CODE}}", sunCodeUrl, 1)
  358. for _, v := range htmlReplaceConfigList {
  359. tmpVal, ok := paramsMap[v.ReplaceStr]
  360. if !ok {
  361. tmpVal = ``
  362. }
  363. contentStr = strings.Replace(contentStr, v.TemplateStr, tmpVal, 1)
  364. }
  365. return
  366. }
  367. // GetDynamicShareImg 生成动态分享图
  368. func GetDynamicShareImg(source, pars string, reportId, reportChapterId int, version string) (imgUrl string, err error) {
  369. if source == "" {
  370. err = errors.New("图片来源有误")
  371. return
  372. }
  373. // 报告章节详情无需重复生成
  374. var path string
  375. if reportId > 0 {
  376. path = fmt.Sprintf("reportDetailCover?ReportId=%d&ReportChapterId=%d", reportId, reportChapterId)
  377. poster, e := yb_poster_resource.GetPosterByCondition(path, "poster", version)
  378. if e != nil && e != utils.ErrNoRow {
  379. err = fmt.Errorf("获取报告已生成海报失败, %e", e)
  380. return
  381. }
  382. if poster != nil && poster.ImgURL != "" {
  383. imgUrl = poster.ImgURL
  384. return
  385. }
  386. }
  387. // 生成海报
  388. imgConfig, e := yb_poster_config.GetBySource(source)
  389. if e != nil {
  390. err = errors.New("获取图片配置失败")
  391. return
  392. }
  393. content, newHeight, e := fillContent2HtmlV2(source, pars, "", imgConfig.Hight, *imgConfig)
  394. if e != nil {
  395. err = errors.New("html内容有误")
  396. return
  397. }
  398. htm2ImgReq := make(map[string]interface{})
  399. htm2ImgReq["html_content"] = content
  400. htm2ImgReq["width"] = imgConfig.Width
  401. htm2ImgReq["height"] = newHeight
  402. res, e := postHtml2Img(htm2ImgReq)
  403. if e != nil || res == nil {
  404. err = errors.New("html转图片请求失败")
  405. return
  406. }
  407. if res.Code != 200 {
  408. err = errors.New("html转图片请求失败: " + res.Msg)
  409. return
  410. }
  411. imgUrl = res.Data
  412. // 报告详情-记录海报信息
  413. if reportId > 0 {
  414. newPoster := &yb_poster_resource.YbPosterResource{
  415. Path: path,
  416. ImgURL: imgUrl,
  417. Type: "poster",
  418. Version: version,
  419. CreateTime: time.Now(),
  420. }
  421. err = newPoster.Create()
  422. }
  423. return
  424. }
  425. func ReportToJpeg(reportUrl, filePath string) (err error) {
  426. pyCode := `
  427. import asyncio
  428. from pyppeteer import launch, errors
  429. async def main():
  430. try:
  431. # 启动浏览器
  432. browser = await launch({
  433. 'executablePath': '%s',
  434. 'headless': True,
  435. 'args': ['--disable-infobars', '--no-sandbox']
  436. })
  437. # 新建页面
  438. page = await browser.newPage()
  439. # 设置视口大小
  440. await page.setViewport({
  441. 'width': 750,
  442. 'height': 1080
  443. })
  444. # 导航到页面
  445. await page.goto('%s', {
  446. 'waitUntil': 'networkidle0',
  447. 'timeout': 1000000 # 设置超时时间为 100 秒
  448. })
  449. await page.screenshot({
  450. 'path': "%s",
  451. 'fullPage': True,
  452. 'quality':100
  453. })
  454. except errors.BrowserError as e:
  455. print('Browser closed unexpectedly:', e)
  456. except Exception as e:
  457. print('An error occurred:', e)
  458. finally:
  459. # 确保浏览器关闭
  460. if browser is not None:
  461. await browser.close()
  462. # 获取当前事件循环
  463. loop = asyncio.get_event_loop()
  464. # 运行事件循环直到main协程完成
  465. try:
  466. loop.run_until_complete(main())
  467. except Exception as e:
  468. print('Error during event loop execution:', e)
  469. finally:
  470. # 关闭事件循环
  471. loop.close()
  472. `
  473. pyCode = fmt.Sprintf(pyCode, global.CONFIG.System.ChromePath, reportUrl, filePath)
  474. global.LOG.Info("jpeg pyCode: \n" + pyCode)
  475. cmd := exec.Command("python3", "-c", pyCode)
  476. output, e := cmd.CombinedOutput()
  477. if e != nil {
  478. err = e
  479. global.LOG.Info("ReportToJpeg failed: , error: \n" + err.Error())
  480. global.LOG.Info("Output: %s\n", string(output))
  481. go alarm_msg.SendAlarmMsg("ReportToJpeg failed:"+err.Error(), 3)
  482. go alarm_msg.SendAlarmMsg("Output :"+string(output), 3)
  483. }
  484. defer func() {
  485. cmd.Process.Kill()
  486. }()
  487. return
  488. }