share_poster.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  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_resource"
  9. "hongze/hongze_yb/models/tables/yb_suncode_pars"
  10. "hongze/hongze_yb/services/alarm_msg"
  11. "hongze/hongze_yb/services/wx_app"
  12. "hongze/hongze_yb/utils"
  13. "io/ioutil"
  14. "net/http"
  15. "os"
  16. "strings"
  17. "time"
  18. )
  19. var ServerUrl = "http://127.0.0.1:5008/"
  20. // SharePosterReq 分享海报请求体
  21. type SharePosterReq struct {
  22. CodePage string `json:"code_page" description:"太阳码page"`
  23. CodeScene string `json:"code_scene" description:"太阳码scene"`
  24. Source string `json:"source" description:"来源"`
  25. Version string `json:"version" description:"海报版本号" `
  26. Pars string `json:"pars" description:"海报动态信息"`
  27. }
  28. // CreatePosterFromSource 生成分享海报
  29. func CreatePosterFromSource(codePage, codeScene, source, version, pars string) (imgUrl string, err error) {
  30. var errMsg string
  31. defer func() {
  32. if err != nil {
  33. global.LOG.Critical(fmt.Sprintf("CreatePosterFromSource: source=%s, pars:%s, errMsg:%s", source, pars, errMsg))
  34. go alarm_msg.SendAlarmMsg("CreatePosterFromSource生成分享海报失败, Msg:"+errMsg+";Err:"+err.Error(), 2)
  35. }
  36. }()
  37. if codePage == "" || source == "" || pars == "" {
  38. errMsg = "参数有误"
  39. err = errors.New(errMsg)
  40. return
  41. }
  42. path := fmt.Sprint(codePage, "?", codeScene)
  43. // 非列表来源获取历史图片,无则生成
  44. if !strings.Contains(source, "list") {
  45. poster, tmpErr := yb_poster_resource.GetPosterByCondition(path, "poster", version)
  46. if tmpErr != nil && tmpErr != utils.ErrNoRow {
  47. err = tmpErr
  48. return
  49. }
  50. if poster != nil && poster.ImgURL != "" {
  51. imgUrl = poster.ImgURL
  52. return
  53. }
  54. }
  55. // 图片长宽
  56. heightMap := map[string]int{
  57. "activity_detail": 1210,
  58. "activity_list": 1948,
  59. "special_column_detail": 1480,
  60. "special_column_list": 1548,
  61. "chart_detail": 1536,
  62. "chart_list": 1352,
  63. "report_detail": 1420,
  64. "report_list": 1344,
  65. }
  66. widthMap := map[string]int{
  67. "activity_detail": 1280,
  68. "activity_list": 1280,
  69. "special_column_detail": 1176,
  70. "special_column_list": 1176,
  71. "chart_detail": 1176,
  72. "chart_list": 1176,
  73. "report_detail": 1176,
  74. "report_list": 1176,
  75. }
  76. width := widthMap[source]
  77. height := heightMap[source]
  78. if width == 0 || height == 0 {
  79. errMsg = "图片长度有误"
  80. err = errors.New(errMsg)
  81. return "", err
  82. }
  83. // 生成太阳码
  84. sunCodeUrl, err := CreateAndUploadSunCode(codePage, codeScene, version)
  85. if err != nil {
  86. return
  87. }
  88. // 填充html内容
  89. contentStr, newHeight, err := fillContent2Html(source, pars, sunCodeUrl, height)
  90. if err != nil {
  91. errMsg = "html内容有误"
  92. return
  93. }
  94. // 请求python服务htm2img
  95. htm2ImgReq := make(map[string]interface{})
  96. htm2ImgReq["html_content"] = contentStr
  97. htm2ImgReq["width"] = width
  98. htm2ImgReq["height"] = newHeight
  99. res, err := postHtml2Img(htm2ImgReq)
  100. if err != nil || res == nil {
  101. errMsg = "html转图片请求失败"
  102. return
  103. }
  104. if res.Code != 200 {
  105. errMsg = "html转图片请求失败"
  106. err = errors.New("html转图片失败: " + res.Msg)
  107. return
  108. }
  109. imgUrl = res.Data
  110. // 记录海报信息
  111. newPoster := &yb_poster_resource.YbPosterResource{
  112. Path: path,
  113. ImgURL: imgUrl,
  114. Type: "poster",
  115. Version: version,
  116. CreateTime: time.Now(),
  117. }
  118. err = newPoster.Create()
  119. return
  120. }
  121. type Html2ImgResp struct {
  122. Code int `json:"code"`
  123. Msg string `json:"msg"`
  124. Data string `json:"data"`
  125. }
  126. // postHtml2Img 请求htm2img接口
  127. func postHtml2Img(param map[string]interface{}) (resp *Html2ImgResp, err error) {
  128. // 目前仅此处调用该接口,暂不加授权、校验等
  129. postUrl := ServerUrl + "htm2img"
  130. postData, err := json.Marshal(param)
  131. if err != nil {
  132. return
  133. }
  134. result, err := Html2ImgHttpPost(postUrl, string(postData), "application/json")
  135. if err != nil {
  136. return
  137. }
  138. if err = json.Unmarshal(result, &resp); err != nil {
  139. return
  140. }
  141. return resp, nil
  142. }
  143. // Html2ImgHttpPost post请求
  144. func Html2ImgHttpPost(url, postData string, params ...string) ([]byte, error) {
  145. body := ioutil.NopCloser(strings.NewReader(postData))
  146. client := &http.Client{}
  147. req, err := http.NewRequest("POST", url, body)
  148. if err != nil {
  149. return nil, err
  150. }
  151. contentType := "application/x-www-form-urlencoded;charset=utf-8"
  152. if len(params) > 0 && params[0] != "" {
  153. contentType = params[0]
  154. }
  155. req.Header.Set("Content-Type", contentType)
  156. resp, err := client.Do(req)
  157. if err != nil {
  158. return nil, err
  159. }
  160. defer resp.Body.Close()
  161. b, err := ioutil.ReadAll(resp.Body)
  162. fmt.Println("HttpPost:" + string(b))
  163. return b, err
  164. }
  165. // CreateAndUploadSunCode 生成太阳码并上传OSS
  166. func CreateAndUploadSunCode(page, scene, version string) (imgUrl string, err error) {
  167. if page == "" {
  168. err = errors.New("page不能为空")
  169. return
  170. }
  171. path := fmt.Sprint(page, "?", scene)
  172. exist, err := yb_poster_resource.GetPosterByCondition(path, "qrcode", version)
  173. if err != nil && err != utils.ErrNoRow {
  174. return
  175. }
  176. if exist != nil && exist.ImgURL != "" {
  177. return exist.ImgURL, nil
  178. }
  179. // scene超过32位会生成失败,md5处理至32位
  180. sceneMD5 := "a=1"
  181. if scene != "" {
  182. sceneMD5 = utils.MD5(scene)
  183. }
  184. picByte, err := wx_app.GetSunCode(page, sceneMD5)
  185. if err != nil {
  186. return
  187. }
  188. // 生成图片
  189. localPath := "./static/img"
  190. fileName := utils.GetRandStringNoSpecialChar(28) + ".png"
  191. fpath := fmt.Sprint(localPath, "/", fileName)
  192. f, err := os.Create(fpath)
  193. if err != nil {
  194. return
  195. }
  196. if _, err = f.Write(picByte); err != nil {
  197. return
  198. }
  199. defer func() {
  200. f.Close()
  201. os.Remove(fpath)
  202. }()
  203. // 上传OSS
  204. fileDir := "yb/suncode/"
  205. imgUrl, err = UploadAliyunToDir(fileName, fpath, fileDir)
  206. if err != nil {
  207. return
  208. }
  209. // 记录二维码信息
  210. newPoster := &yb_poster_resource.YbPosterResource{
  211. Path: path,
  212. ImgURL: imgUrl,
  213. Type: "qrcode",
  214. Version: version,
  215. CreateTime: time.Now(),
  216. }
  217. err = newPoster.Create()
  218. if err != nil {
  219. return
  220. }
  221. // 记录参数md5
  222. if scene != "" {
  223. newPars := &yb_suncode_pars.YbSuncodePars{
  224. Scene: scene,
  225. SceneKey: sceneMD5,
  226. CreateTime: time.Now(),
  227. }
  228. err = newPars.Create()
  229. }
  230. return
  231. }
  232. type PosterParsReq struct {
  233. ActivityTitle string `json:"activity_title"`
  234. ActivityAvatar string `json:"activity_avatar"`
  235. ActivitySpeaker string `json:"activity_speaker"`
  236. ActivityTime string `json:"activity_time"`
  237. ChartName string `json:"chart_name"`
  238. ChartImage string `json:"chart_image"`
  239. ReportType string `json:"report_type"`
  240. ReportAvatar string `json:"report_avatar"`
  241. ReportTitle string `json:"report_title"`
  242. ReportAbstract string `json:"report_abstract"`
  243. Stage1 string `json:"stage_1"`
  244. Avatar1 string `json:"avatar_1"`
  245. Title1 string `json:"title_1"`
  246. Author1 string `json:"author_1"`
  247. Tag1 string `json:"tag_1"`
  248. Img1 string `json:"img_1"`
  249. Time1 string `json:"time_1"`
  250. Abstract1 string `json:"abstract_1"`
  251. Status1 string `json:"status_1"`
  252. Speaker1 string `json:"speaker_1"`
  253. Stage2 string `json:"stage_2"`
  254. Avatar2 string `json:"avatar_2"`
  255. Title2 string `json:"title_2"`
  256. Author2 string `json:"author_2"`
  257. Tag2 string `json:"tag_2"`
  258. Img2 string `json:"img_2"`
  259. Abstract2 string `json:"abstract_2"`
  260. Time2 string `json:"time_2"`
  261. Status2 string `json:"status_2"`
  262. Speaker2 string `json:"speaker_2"`
  263. ListTitle string `json:"list_title"`
  264. }
  265. // fillContent2Html 填充HTML动态内容
  266. func fillContent2Html(source, pars, sunCodeUrl string, height int) (contentStr string, newHeight int, err error) {
  267. params := new(PosterParsReq)
  268. if err = json.Unmarshal([]byte(pars), &params); err != nil {
  269. return
  270. }
  271. template := fmt.Sprint("static/htm2img/", source, ".html")
  272. contentByte, err := ioutil.ReadFile(template)
  273. if err != nil {
  274. return
  275. }
  276. newHeight = height
  277. contentStr = string(contentByte)
  278. // 列表的动态内容不完整的用默认内容的填充
  279. var emptyTime1, emptyTime2 bool
  280. if strings.Contains(source, "list") {
  281. if params.Title1 == "" || params.Title2 == "" {
  282. defaultAvatar := "https://hzstatic.hzinsights.com/static/images/202112/20211210/wn6c3oYKTfT4NbTZgRGflRuIBZaa.png"
  283. switch source {
  284. case "activity_list":
  285. if params.Title1 == "" {
  286. params.ListTitle = "线下沙龙"
  287. params.Status1 = "未开始"
  288. params.Avatar1 = defaultAvatar
  289. params.Title1 = "周度报告"
  290. params.Speaker1 = "FICC研究员"
  291. params.Time1 = "2022-5-10"
  292. emptyTime1 = true
  293. }
  294. params.Status2 = params.Status1
  295. params.Avatar2 = defaultAvatar
  296. params.Title2 = "周度报告"
  297. params.Speaker2 = "FICC研究员"
  298. params.Time2 = "2022-5-10"
  299. emptyTime2 = true
  300. case "special_column_list":
  301. if params.Title1 == "" {
  302. params.Stage1 = "第1期"
  303. params.Avatar1 = defaultAvatar
  304. params.Title1 = "弘则FICC专栏"
  305. params.Author1 = "弘则研究"
  306. params.Tag1 = "FICC研究员"
  307. }
  308. params.Stage2 = "第2期"
  309. params.Avatar2 = defaultAvatar
  310. params.Title2 = "弘则FICC专栏"
  311. params.Author2 = "弘则研究"
  312. params.Tag2 = "FICC研究员"
  313. case "chart_list":
  314. if params.Title1 == "" {
  315. params.Title1 = "螺纹仓单-热卷仓单季节性"
  316. params.Img1 = "https://hzstatic.hzinsights.com/static/images/202112/20211227/8VBIH1l6VraYpLCoBS6qOIXA5Zoq.png"
  317. }
  318. params.Title2 = "卷螺期货现货价差"
  319. params.Img2 = "https://hzstatic.hzinsights.com/static/images/202204/20220427/d8GRfdR3Xfrvk397SnYudcwVs9pV.png"
  320. case "report_list":
  321. if params.Title1 == "" {
  322. params.Img1 = defaultAvatar
  323. params.Title1 = "弘则FICC研报"
  324. params.Time1 = "1期"
  325. }
  326. params.Img2 = defaultAvatar
  327. params.Title2 = "弘则FICC研报"
  328. params.Time2 = "2期"
  329. }
  330. }
  331. }
  332. // 填充指定内容
  333. switch source {
  334. case "activity_detail":
  335. contentStr = strings.Replace(contentStr, "{{ACTIVITY_TITLE}}", params.ActivityTitle, 1)
  336. contentStr = strings.Replace(contentStr, "{{ACTIVITY_AVATAR}}", params.ActivityAvatar, 1)
  337. contentStr = strings.Replace(contentStr, "{{ACTIVITY_SPEAKER}}", params.ActivitySpeaker, 1)
  338. contentStr = strings.Replace(contentStr, "{{ACTIVITY_TIME}}", params.ActivityTime, 1)
  339. case "special_column_detail":
  340. contentStr = strings.Replace(contentStr, "{{REPORT_AVATAR}}", params.ReportAvatar, 1)
  341. contentStr = strings.Replace(contentStr, "{{REPORT_TITLE}}", params.ReportTitle, 1)
  342. contentStr = strings.Replace(contentStr, "{{REPORT_ABSTRACT}}", params.ReportAbstract, 1)
  343. case "chart_detail":
  344. contentStr = strings.Replace(contentStr, "{{CHART_NAME}}", params.ChartName, 1)
  345. contentStr = strings.Replace(contentStr, "{{CHART_IMAGE}}", params.ChartImage, 1)
  346. case "report_detail":
  347. doc, tmpErr := goquery.NewDocumentFromReader(strings.NewReader(params.ReportAbstract))
  348. if tmpErr != nil {
  349. err = tmpErr
  350. return
  351. }
  352. abstract := ""
  353. doc.Find("p").Each(func(i int, s *goquery.Selection) {
  354. phtml, tmpErr := s.Html()
  355. if tmpErr != nil {
  356. err = tmpErr
  357. return
  358. }
  359. st := s.Text()
  360. if st != "" && st != "<br>" && st != "<br style=\"max-width: 100%;\">" && !strings.Contains(phtml, "iframe") {
  361. abstract = abstract + "<p>" + phtml + "</p>"
  362. }
  363. })
  364. contentStr = strings.Replace(contentStr, "{{REPORT_TYPE}}", params.ReportType, 1)
  365. contentStr = strings.Replace(contentStr, "{{REPORT_TITLE}}", params.ReportTitle, 1)
  366. contentStr = strings.Replace(contentStr, "{{REPORT_ABSTRACT}}", abstract, 1)
  367. case "special_column_list":
  368. contentStr = strings.Replace(contentStr, "{{LIST_TITLE}}", params.ListTitle, 1)
  369. contentStr = strings.Replace(contentStr, "{{STAGE_1}}", "第" + params.Stage1 + "期", 1)
  370. contentStr = strings.Replace(contentStr, "{{AVATAR_1}}", params.Avatar1, 1)
  371. contentStr = strings.Replace(contentStr, "{{TITLE_1}}", params.Title1, 1)
  372. contentStr = strings.Replace(contentStr, "{{AUTHOR_1}}", params.Author1, 1)
  373. contentStr = strings.Replace(contentStr, "{{TAG_1}}", params.Tag1, 1)
  374. contentStr = strings.Replace(contentStr, "{{STAGE_2}}", "第" + params.Stage2 + "期", 1)
  375. contentStr = strings.Replace(contentStr, "{{AVATAR_2}}", params.Avatar2, 1)
  376. contentStr = strings.Replace(contentStr, "{{TITLE_2}}", params.Title2, 1)
  377. contentStr = strings.Replace(contentStr, "{{AUTHOR_2}}", params.Author2, 1)
  378. contentStr = strings.Replace(contentStr, "{{TAG_2}}", params.Tag2, 1)
  379. case "chart_list":
  380. contentStr = strings.Replace(contentStr, "{{LIST_TITLE}}", params.ListTitle, 1)
  381. contentStr = strings.Replace(contentStr, "{{TITLE_1}}", params.Title1, 1)
  382. contentStr = strings.Replace(contentStr, "{{IMG_1}}", params.Img1, 1)
  383. contentStr = strings.Replace(contentStr, "{{TITLE_2}}", params.Title2, 1)
  384. contentStr = strings.Replace(contentStr, "{{IMG_2}}", params.Img2, 1)
  385. case "report_list":
  386. contentStr = strings.Replace(contentStr, "{{LIST_TITLE}}", params.ListTitle, 1)
  387. contentStr = strings.Replace(contentStr, "{{TITLE_1}}", params.Title1, 1)
  388. contentStr = strings.Replace(contentStr, "{{ABSTRACT_1}}", params.Abstract1, 1)
  389. contentStr = strings.Replace(contentStr, "{{IMG_1}}", params.Img1, 1)
  390. contentStr = strings.Replace(contentStr, "{{TIME_1}}", params.Time1, 1)
  391. contentStr = strings.Replace(contentStr, "{{TITLE_2}}", params.Title2, 1)
  392. contentStr = strings.Replace(contentStr, "{{ABSTRACT_2}}", params.Abstract2, 1)
  393. contentStr = strings.Replace(contentStr, "{{IMG_2}}", params.Img2, 1)
  394. contentStr = strings.Replace(contentStr, "{{TIME_2}}", params.Time2, 1)
  395. case "activity_list":
  396. bgColorMap := map[string]string{
  397. "未开始": "#E3B377",
  398. "进行中": "#3385FF",
  399. "已结束": "#A2A2A2",
  400. }
  401. statusItemMap := map[string]string{
  402. "未开始": "block",
  403. "进行中": "none",
  404. "已结束": "none",
  405. }
  406. offlineMap := map[string]string{
  407. "线上会议": "none",
  408. "线下沙龙": "block",
  409. }
  410. onlineMap := map[string]string{
  411. "线上会议": "block",
  412. "线下沙龙": "none",
  413. }
  414. if params.Status1 != "未开始" {
  415. newHeight = 1715
  416. }
  417. contentStr = strings.Replace(contentStr, "{{LIST_TITLE}}", "弘则FICC周度电话会安排", 1)
  418. contentStr = strings.Replace(contentStr, "{{BG_COLORE_1}}", bgColorMap[params.Status1], 1)
  419. contentStr = strings.Replace(contentStr, "{{STATUS_1}}", params.Status1, 1)
  420. contentStr = strings.Replace(contentStr, "{{AVATAR_1}}", params.Avatar1, 1)
  421. contentStr = strings.Replace(contentStr, "{{TITLE_1}}", params.Title1, 1)
  422. contentStr = strings.Replace(contentStr, "{{SPEAKER_1}}", params.Speaker1, 1)
  423. contentStr = strings.Replace(contentStr, "{{TIME_1}}", params.Time1, 1)
  424. contentStr = strings.Replace(contentStr, "{{SHOW_ITEM_1}}", statusItemMap[params.Status1], 1)
  425. contentStr = strings.Replace(contentStr, "{{SHOW_OFFLINE_1}}", offlineMap[params.ListTitle], 1)
  426. contentStr = strings.Replace(contentStr, "{{SHOW_ONLINE_1}}", onlineMap[params.ListTitle], 1)
  427. contentStr = strings.Replace(contentStr, "{{BG_COLORE_2}}", bgColorMap[params.Status2], 1)
  428. contentStr = strings.Replace(contentStr, "{{STATUS_2}}", params.Status2, 1)
  429. contentStr = strings.Replace(contentStr, "{{AVATAR_2}}", params.Avatar2, 1)
  430. contentStr = strings.Replace(contentStr, "{{TITLE_2}}", params.Title2, 1)
  431. contentStr = strings.Replace(contentStr, "{{SPEAKER_2}}", params.Speaker2, 1)
  432. contentStr = strings.Replace(contentStr, "{{TIME_2}}", params.Time2, 1)
  433. contentStr = strings.Replace(contentStr, "{{SHOW_ITEM_2}}", statusItemMap[params.Status2], 1)
  434. contentStr = strings.Replace(contentStr, "{{SHOW_OFFLINE_2}}", offlineMap[params.ListTitle], 1)
  435. contentStr = strings.Replace(contentStr, "{{SHOW_ONLINE_2}}", onlineMap[params.ListTitle], 1)
  436. // 用默认内容填充的活动时间字体颜色调至看不见
  437. color1 := "#999"
  438. color2 := "#999"
  439. if emptyTime1 {
  440. color1 = "#fff"
  441. }
  442. if emptyTime2 {
  443. color2 = "#fff"
  444. }
  445. contentStr = strings.Replace(contentStr, "{{TIME_COLOR_1}}", color1, 1)
  446. contentStr = strings.Replace(contentStr, "{{TIME_COLOR_2}}", color2, 1)
  447. }
  448. contentStr = strings.Replace(contentStr, "{{SUN_CODE}}", sunCodeUrl, 1)
  449. return
  450. }