share_poster.go 16 KB

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