package services import ( "encoding/json" "errors" "eta/eta_api/models" "eta/eta_api/models/company" "eta/eta_api/models/system" "eta/eta_api/services/alarm_msg" "eta/eta_api/services/public_api" "eta/eta_api/utils" "fmt" "github.com/PuerkitoBio/goquery" "github.com/rdlucklib/rdluck_tools/http" "html" "os" "regexp" "strconv" "strings" "time" ) func GetReportContentSub(content string) (contentSub string, err error) { content = html.UnescapeString(content) doc, err := goquery.NewDocumentFromReader(strings.NewReader(content)) if err != nil { fmt.Println("create doc err:", err.Error()) return } n := 0 doc.Find("p").Each(func(i int, s *goquery.Selection) { if n >= 5 { return } n++ phtml, err := s.Html() if err != nil { fmt.Println("get html err", err.Error()) return } if s.Text() != "" || strings.Contains(phtml, "src") { contentSub = contentSub + "

" + phtml + "

" } }) return } type ZgParam struct { } // 找钢网 func ZhaoGangSend(report *models.ReportDetail) (err error) { defer func() { if err != nil { go alarm_msg.SendAlarmMsg("发送报告至找刚网失败,Err"+err.Error(), 3) //go utils.SendEmail(utils.APPNAME+"【"+utils.RunMode+"】"+"失败提醒", "发送报告至找刚网失败 ErrMsg:"+err.Error(), utils.EmailSendToUsers) } }() reportIdStr := strconv.Itoa(report.Id) articleId := utils.MD5(reportIdStr) var reportType int if report.ClassifyNameSecond == "知白守黑日评" { reportType = 1 } else { reportType = 2 } contentHtml := html.UnescapeString(report.Content) doc, err := goquery.NewDocumentFromReader(strings.NewReader(contentHtml)) if err != nil { fmt.Println("Create Doc Err:" + err.Error()) err = errors.New("Create Doc Err:" + err.Error()) return } doc.Find("p").Each(func(i int, p *goquery.Selection) { phtml, err := p.Html() if err != nil { fmt.Println("Err:" + err.Error()) return } if phtml == "
" { if i == 0 { p.Remove() } else { p.ReplaceWithHtml("
") } } else { p.SetAttr("style", "line-height:30px") } }) doc.Find("img").Each(func(i int, img *goquery.Selection) { img.SetAttr("style", "width:100%;") }) contentHtml, _ = doc.Find("body").Html() createDate, _ := time.Parse(utils.FormatDateTime, report.CreateTime) createDay := createDate.Format("0102") title := "【第 " + strconv.Itoa(report.Stage) + "期|FICC" + "】 " + report.Title + "(" + createDay + ")" contentSummary := report.Abstract postUrl := `http://appserver.index.zhaogang.com/double.index.appserver.service/api/v1/wechat/article/hongze/push` signStr := "zhaogang_data_vip" + "articleId" + articleId + "type" + strconv.Itoa(reportType) + "title" + title + "contentSummary" + contentSummary + "zhaogang_data_vip" fmt.Println("signStr:", signStr) sign := utils.MD5(signStr) disclaimers := `

1、本报告仅供弘则弥道(上海)投资咨询有限公司正式签约的机构客户使用,不会仅因接收人/接受机构收到本报告而将其视为客户。

        

2、本报告根据国际和行业通行的准则,以合法渠道获得这些信息,尽可能保证可靠、准确和完整,但并不保证报告所述信息的准确性和完整性,也不保证本报告所包含的信息或建议在本报告发出后不会发生任何变更。本报告中所提供的信息仅供参考。

        

3、报告中的内容不对投资者做出的最终操作建议做任何的担保,也没有任何形式的分享投资收益或者分担投资损失的书面或口头承诺。不作为客户在投资、法律、会计或税务等方面的最终操作建议,也不作为道义的、责任的和法律的依据或者凭证,无论是否已经明示或者暗示。

        

4、在任何情况下,本公司不对客户/接受人/接受机构因使用报告中内容所引致的一切损失负责任,客户/接受人/接受机构需自行承担全部风险。

` param := make(map[string]interface{}) dataMap := make(map[string]interface{}) contentMap := make(map[string]interface{}) dataMap["articleId"] = articleId dataMap["type"] = reportType dataMap["title"] = title dataMap["contentSummary"] = contentSummary contentMap["contentHtml"] = contentHtml contentMap["articleAuthor"] = report.Author //contentMap["publishedTime"] = report.PublishTime.Format(utils.FormatDateTime) contentMap["publishedTime"] = report.PublishTime contentMap["audioName"] = report.VideoName contentMap["audioUrl"] = report.VideoUrl videoPlaySeconds := report.VideoPlaySeconds f, _ := strconv.ParseFloat(videoPlaySeconds, 64) contentMap["audioLength"] = f * 1000 contentMap["disclaimers"] = disclaimers param["cardData"] = dataMap param["contentData"] = contentMap param["sign"] = strings.ToUpper(sign) paramJson, err := json.Marshal(param) if err != nil { fmt.Println("param json.Marshal Err:" + err.Error()) err = errors.New("param json.Marshal Err:" + err.Error()) return } utils.FileLog.Info("ZhaoGangSend parms:%s", string(paramJson)) result, err := http.Post(postUrl, string(paramJson), "application/json") if err != nil { fmt.Println("post err:" + err.Error()) err = errors.New("post Err:" + err.Error()) return } utils.FileLog.Info("ZhaoGangSend Result:%s", string(result)) //返回数据校验 mapResult := make(map[string]interface{}) err = json.Unmarshal(result, &mapResult) if err != nil { fmt.Println("找钢网返回数据转json失败: err:", err.Error(), ";返回数据:", string(result)) err = errors.New(fmt.Sprint("找钢网返回数据转json失败: err:", err.Error(), ";返回数据:", string(result))) return } if resultCode, ok := mapResult["code"]; ok { tmpResultCode := resultCode.(float64) if tmpResultCode != 200 { fmt.Println("找钢网返回数据异常,code返回参异常;返回数据:", string(result)) err = errors.New(fmt.Sprint("找钢网返回数据异常,code返回参异常;返回数据:", string(result))) return } } else { fmt.Println("找钢网返回数据异常,缺少code返回参;返回数据:", string(result)) err = errors.New(fmt.Sprint("找钢网返回数据异常,缺少code返回参;返回数据:", string(result))) return } utils.FileLog.Info("%s", string(result)) return } /*func init() { fmt.Println("start") vint := 845 report, err := models.GetReportById(vint) if err != nil { fmt.Println("GetReportById Err", err.Error()) return } ZhaoGangSend(report) fmt.Println("end") return }*/ // PublishDayWeekReport 发布晨周报 func PublishDayWeekReport(reportId int) (tips string, err error) { report, err := models.GetReportByReportId(reportId) if err != nil { return } if report.State == 2 { return } chapters, err := models.GetChapterListByReportId(reportId) if err != nil { return } chapterLen := len(chapters) if chapterLen <= 0 { err = errors.New("报告章节为空,不可发布") return } reportType := chapters[0].ReportType // 校验章节 publishReport, tips, publishIdArr, unPublishIdArr, err := checkDayWeekChapterWrite(chapters, reportType) if err != nil { return } publishLen := len(publishIdArr) if publishLen <= 0 { err = errors.New("报告章节均不可发布") return } // 需发布整期 updateCols := make([]string, 0) if publishReport { updateCols = append(updateCols, "Title", "State", "ModifyTime") // 发布后标题调整 title := report.Title title = strings.ReplaceAll(title, "【弘则FICC晨报】", "") title = strings.ReplaceAll(title, "【弘则FICC周报】", "") if title == "" { // 取第一个需发布章节的标题 firstId := publishIdArr[0] firstTitle := "" for i := 0; i < chapterLen; i++ { if chapters[i].ReportChapterId == firstId { firstTitle = chapters[i].Title break } } title = firstTitle } report.Title = title report.State = 2 // 研报后台4.4 只在没有发布过时更新发布时间,其余均按模版消息发送时间当作发布时间 if report.MsgIsSend == 0 || report.PublishTime.IsZero() { report.PublishTime = time.Now().Local() updateCols = append(updateCols, "PublishTime") } report.ModifyTime = time.Now().Local() } publishIdStr := utils.IntArr2joinString(publishIdArr, ",") //unPublishIdStr := utils.IntArr2joinString(unPublishIdArr, ",") if e := models.PublishReportAndChapter(report, publishIdArr, unPublishIdArr, publishReport, updateCols); e != nil { err = errors.New("发布报告及章节失败") return } // 生成章节音频 go func() { _ = UpdateChaptersVideo(publishIdStr) }() // 更新报告ES go func() { _ = UpdateReportEs(report.Id, 2) }() // 发布时备份内容 go SaveReportLogs(report, chapters, report.AdminId, report.AdminRealName) return } // UpdateChaptersVideo 更新章节音频 func UpdateChaptersVideo(chapterIds string) (err error) { defer func() { if err != nil { utils.FileLog.Error("UpdateChaptersVideo, chapterIds:%s, Err:%s", chapterIds, err.Error()) go alarm_msg.SendAlarmMsg("更新章节音频失败, 章节ID: "+chapterIds+", Err: "+err.Error(), 3) } }() if chapterIds == "" { return } ids := make([]int, 0) chapterIdArr := strings.Split(chapterIds, ",") for _, v := range chapterIdArr { id, e := strconv.Atoi(v) if e != nil { return } ids = append(ids, id) } chapterList, err := models.GetChapterListByChapterIds(ids) if err != nil { return } // 生成video nowTime := time.Now() updateCols := make([]string, 0) updateCols = append(updateCols, "VideoUrl", "VideoName", "VideoSize", "VideoPlaySeconds") for i := 0; i < len(chapterList); i++ { item := chapterList[i] // 忽略已有音频的章节 if item.VideoUrl != "" && item.VideoName != "" && item.VideoSize != "" && item.VideoPlaySeconds != "" { continue } videoUrl, videoName, videoSize, videoPlaySeconds, e := CreateReportVideo(item.Title, html.UnescapeString(item.Content), nowTime.Format(utils.FormatDateTime)) if e != nil { err = e return } item.VideoUrl = videoUrl item.VideoName = videoName item.VideoSize = videoSize item.VideoPlaySeconds = fmt.Sprintf("%.2f", videoPlaySeconds) if e = item.UpdateChapter(updateCols); e != nil { err = e } } return } // PublishTodayDayReport 发布今日晨报 func PublishTodayDayReport() (err error) { nowTime := time.Now() startTime := time.Date(nowTime.Year(), nowTime.Month(), nowTime.Day(), 0, 0, 0, 0, time.Local) endTime := time.Date(nowTime.Year(), nowTime.Month(), nowTime.Day(), 23, 59, 59, 0, time.Local) todayReport, err := models.GetUnPublishDayReport(startTime, endTime) if err != nil { if err.Error() == utils.ErrNoRow() { //如果是找不到待发送的晨报,那么需要将err置空 err = nil } return } if todayReport != nil { if _, tmpErr := PublishDayWeekReport(todayReport.Id); tmpErr != nil { err = tmpErr return } // 定时发布的晨报自动推送客群 reportDetail, tmpErr := models.GetReportById(todayReport.Id) if tmpErr != nil { err = tmpErr return } // 推送模板消息 if tmpErr = SendMiniProgramReportWxMsg(todayReport.Id); tmpErr != nil { err = tmpErr return } if tmpErr = models.ModifyReportThsMsgIsSend(reportDetail); tmpErr != nil { err = tmpErr return } } return } // UpdateReportEs 更新报告/章节Es func UpdateReportEs(reportId int, publishState int) (err error) { if reportId <= 0 { return } reportInfo, err := models.GetReportByReportId(reportId) if err != nil { return } categories := "" if reportInfo.HasChapter == 1 { // 晨周报 chapterList, tmpErr := models.GetPublishedChapterListByReportId(reportInfo.Id) if tmpErr != nil { return } if len(chapterList) > 0 { for i := 0; i < len(chapterList); i++ { // 章节对应的品种 permissionList, tmpErr := models.GetChapterTypePermissionByTypeIdAndResearchType(chapterList[i].TypeId, chapterList[i].ReportType) if tmpErr != nil { return } categoryArr := make([]string, 0) if len(permissionList) > 0 { for ii := 0; ii < len(permissionList); ii++ { categoryArr = append(categoryArr, permissionList[ii].PermissionName) } } aliasArr, _ := addCategoryAliasToArr(categoryArr) chapterCategories := strings.Join(aliasArr, ",") esChapter := &models.ElasticReportDetail{ ReportId: chapterList[i].ReportId, ReportChapterId: chapterList[i].ReportChapterId, Title: chapterList[i].Title, Abstract: chapterList[i].Abstract, BodyContent: utils.TrimHtml(html.UnescapeString(chapterList[i].Content)), PublishTime: chapterList[i].PublishTime.Format(utils.FormatDateTime), PublishState: chapterList[i].PublishState, Author: chapterList[i].Author, ClassifyIdFirst: chapterList[i].ClassifyIdFirst, ClassifyNameFirst: chapterList[i].ClassifyNameFirst, ClassifyIdSecond: 0, ClassifyNameSecond: "", Categories: chapterCategories, StageStr: strconv.Itoa(chapterList[i].Stage), } chapterDocId := fmt.Sprintf("%d-%d", reportInfo.Id, chapterList[i].ReportChapterId) if err = EsAddOrEditReport(utils.EsReportIndexName, chapterDocId, esChapter); err != nil { return } } } } else { if utils.BusinessCode == utils.BusinessCodeRelease || utils.BusinessCode == utils.BusinessCodeSandbox { permissionList, tmpErr := models.GetChartPermissionNameFromMappingByKeyword(reportInfo.ClassifyNameSecond, "rddp") if tmpErr != nil { return } categoryArr := make([]string, 0) for i := 0; i < len(permissionList); i++ { categoryArr = append(categoryArr, permissionList[i].PermissionName) } aliasArr, _ := addCategoryAliasToArr(categoryArr) categories = strings.Join(aliasArr, ",") } } // 新增报告ES esReport := &models.ElasticReportDetail{ ReportId: reportInfo.Id, ReportChapterId: 0, Title: reportInfo.Title, Abstract: reportInfo.Abstract, BodyContent: utils.TrimHtml(html.UnescapeString(reportInfo.Content)), PublishTime: reportInfo.PublishTime.Format(utils.FormatDateTime), PublishState: publishState, Author: reportInfo.Author, ClassifyIdFirst: reportInfo.ClassifyIdFirst, ClassifyNameFirst: reportInfo.ClassifyNameFirst, ClassifyIdSecond: reportInfo.ClassifyIdSecond, ClassifyNameSecond: reportInfo.ClassifyNameSecond, Categories: categories, StageStr: strconv.Itoa(reportInfo.Stage), } docId := fmt.Sprintf("%d-%d", reportInfo.Id, 0) if err = EsAddOrEditReport(utils.EsReportIndexName, docId, esReport); err != nil { return } return } // addCategoryAliasToArr 品种别名 func addCategoryAliasToArr(categoryArr []string) (aliasArr []string, err error) { aliasArr = categoryArr if len(categoryArr) > 0 { for i := 0; i < len(categoryArr); i++ { if strings.Contains(categoryArr[i], "沥青") { aliasArr = append(aliasArr, "BU") } if strings.Contains(categoryArr[i], "MEG") { aliasArr = append(aliasArr, "EG", "乙二醇") } if strings.Contains(categoryArr[i], "聚酯") { aliasArr = append(aliasArr, "长丝", "短纤", "瓶片") } if strings.Contains(categoryArr[i], "纯苯+苯乙烯") { aliasArr = append(aliasArr, "EB") } if strings.Contains(categoryArr[i], "聚乙烯") { aliasArr = append(aliasArr, "PP", "PE") } if strings.Contains(categoryArr[i], "玻璃纯碱") { aliasArr = append(aliasArr, "玻璃", "纯碱", "FG", "SA") } if strings.Contains(categoryArr[i], "甲醇") { aliasArr = append(aliasArr, "甲醇", "MA") } if strings.Contains(categoryArr[i], "橡胶") { aliasArr = append(aliasArr, "橡胶", "RU") } } } return } // UpdateReportChapterEs 更新报告章节ES func UpdateReportChapterEs(reportChapterId int) (err error) { if reportChapterId <= 0 { return } chapterInfo, err := models.GetReportChapterInfoById(reportChapterId) if err != nil { return } // 章节对应的品种 permissionList, tmpErr := models.GetChapterTypePermissionByTypeIdAndResearchType(chapterInfo.TypeId, chapterInfo.ReportType) if tmpErr != nil { return } categoryArr := make([]string, 0) if len(permissionList) > 0 { for ii := 0; ii < len(permissionList); ii++ { categoryArr = append(categoryArr, permissionList[ii].PermissionName) } } aliasArr, _ := addCategoryAliasToArr(categoryArr) categories := strings.Join(aliasArr, ",") // 新增/编辑ES esChapter := &models.ElasticReportDetail{ ReportId: chapterInfo.ReportId, ReportChapterId: chapterInfo.ReportChapterId, Title: chapterInfo.Title, Abstract: chapterInfo.Abstract, BodyContent: utils.TrimHtml(html.EscapeString(chapterInfo.Content)), PublishTime: chapterInfo.PublishTime.Format(utils.FormatDateTime), PublishState: chapterInfo.PublishState, Author: chapterInfo.Author, ClassifyIdFirst: chapterInfo.ClassifyIdFirst, ClassifyNameFirst: chapterInfo.ClassifyNameFirst, ClassifyIdSecond: 0, ClassifyNameSecond: "", Categories: categories, StageStr: strconv.Itoa(chapterInfo.Stage), } chapterDocId := fmt.Sprintf("%d-%d", chapterInfo.ReportId, chapterInfo.ReportChapterId) if err = EsAddOrEditReport(utils.EsReportIndexName, chapterDocId, esChapter); err != nil { return } return } // DeleteReportAndChapter 删除报告及章节 func DeleteReportAndChapter(reportId int) (err error) { reportInfo, err := models.GetReportByReportId(reportId) if err != nil { err = errors.New("报告信息有误, Err: " + err.Error()) return } if reportInfo.State == 2 { err = errors.New("报告已发布,不可删除") return } // 更新ES _ = UpdateReportEs(reportId, 1) // 删除 if reportInfo.HasChapter == 1 && (reportInfo.ChapterType == utils.REPORT_TYPE_DAY || reportInfo.ChapterType == utils.REPORT_TYPE_WEEK) { err = models.DeleteDayWeekReportAndChapter(reportId) } else { err = models.DeleteReport(reportId) } if err != nil { err = errors.New("删除失败, Err: " + err.Error()) return } // 重置PPT关联报告 go func() { _ = ResetPPTReport(reportId, false) }() return } // UpdatePublishedReportToEs 更新已发布的报告ES func UpdatePublishedReportToEs() (err error) { // 获取所有已发布的报告 var condition string var pars []interface{} condition = ` AND state IN (2, 6) ` reportList, err := models.GetReportList(condition, pars, "ficc", 1, 5000) count := 0 failCount := 0 for i := 0; i < len(reportList); i++ { if err = UpdateReportEs(reportList[i].Id, 2); err != nil { fmt.Printf("更新失败, report_id: %d, Err: %s\n", reportList[i].Id, err.Error()) failCount += 1 } else { count += 1 } } fmt.Printf("报告总数:%d, 更新成功数: %d, 更新失败数: %d", len(reportList), count, failCount) return } // 替换报告内容中的base64图片 func replaceReportBase64ToImg(content string) (newContent string, err error) { if content == "" { return } pattern := "data:([a-z]+\\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)?)?(;base64)?,([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?" re, _ := regexp.Compile(pattern) matcher := re.FindAllString(content, 999) if len(matcher) > 0 { for _, v := range matcher { imgUrl, tmpErr := reportBase64ToImg(v) if tmpErr != nil { err = tmpErr return } content = strings.ReplaceAll(content, v, imgUrl) } } newContent = content return } // 转换base64图片为img并上传 func reportBase64ToImg(imageBase64 string) (resourceUrl string, err error) { if imageBase64 == "" { err = errors.New("图片为空") return } ext := ".png" uploadDir := "./static" randStr := utils.GetRandStringNoSpecialChar(28) fileName := randStr + ext fpath := uploadDir + "/" + fileName b, _ := regexp.MatchString(`^data:\s*image\/(\w+);base64,`, imageBase64) if !b { err = errors.New("图片格式不正确") return } re, _ := regexp.Compile(`^data:\s*image\/(\w+);base64,`) base64Str := re.ReplaceAllString(imageBase64, "") base64Str = strings.Replace(base64Str, " ", "", -1) err = utils.SaveBase64ToFile(base64Str, fpath) if err != nil { err = errors.New("图片保存失败" + err.Error()) return } defer os.Remove(fpath) hzUploadDir := "static/images/" savePath := hzUploadDir + time.Now().Format("200601/20060102/") savePath += fileName //上传到阿里云 和 minio if utils.ObjectStorageClient == "minio" { err = UploadFileToMinIo(fileName, fpath, savePath) if err != nil { err = errors.New("文件上传失败" + err.Error()) return } resourceUrl = utils.MinIoImghost + savePath } else { err = UploadFileToAliyun(fileName, fpath, savePath) if err != nil { err = errors.New("文件上传失败" + err.Error()) return } resourceUrl = utils.Imghost + savePath } item := new(models.Resource) item.ResourceUrl = resourceUrl item.ResourceType = 1 item.CreateTime = time.Now() _, err = models.AddResource(item) if err != nil { err = errors.New("资源上传失败" + err.Error()) return } return } // UpdateReportVideo 更新报告及其章节音频 func UpdateReportVideo(reportId int) (err error) { defer func() { if err != nil { utils.FileLog.Error("UpdateReportVideo, reportId:%s, Err:%s", strconv.Itoa(reportId), err.Error()) go alarm_msg.SendAlarmMsg("更新报告音频失败, 报告ID: "+strconv.Itoa(reportId)+", Err: "+err.Error(), 3) //go utils.SendEmail(utils.APPNAME+"【"+utils.RunMode+"】"+"失败提醒", "更新报告音频失败, 报告ID: " + reportIdStr + ", Err: "+err.Error(), utils.EmailSendToUsers) } }() if reportId == 0 { return } reportInfo, err := models.GetReportByReportId(reportId) if err != nil { return } if reportInfo.HasChapter == 1 { // 更新章节音频 chapterList, tmpErr := models.GetPublishedChapterListByReportId(reportInfo.Id) if tmpErr != nil { err = tmpErr return } chapterIdArr := make([]string, 0) for i := 0; i < len(chapterList); i++ { chapterIdArr = append(chapterIdArr, strconv.Itoa(chapterList[i].ReportChapterId)) } chapterIds := strings.Join(chapterIdArr, ",") //go UpdateChaptersVideo(chapterIds) err = UpdateChaptersVideo(chapterIds) } else { // 更新报告音频 if reportInfo.VideoUrl != "" { return } nowTime := time.Now() updateCols := make([]string, 0) updateCols = append(updateCols, "VideoUrl", "VideoName", "VideoSize", "VideoPlaySeconds") videoUrl, videoName, videoSize, videoPlaySeconds, tmpErr := CreateReportVideo(reportInfo.Title, html.UnescapeString(reportInfo.Content), nowTime.Format(utils.FormatDateTime)) reportInfo.VideoUrl = videoUrl reportInfo.VideoName = videoName reportInfo.VideoSize = videoSize reportInfo.VideoPlaySeconds = fmt.Sprintf("%.2f", videoPlaySeconds) tmpErr = reportInfo.UpdateReport(updateCols) if tmpErr != nil { err = tmpErr return } } return } func UpdateEmptyVideoReportVideo() (err error) { list, err := models.GetSyncEmptyVideoReport() if err != nil { return } listLen := len(list) if listLen <= 0 { fmt.Println("无报告需要更新音频") return } fmt.Println("Start 待更新报告音频数: ", listLen) for i := 0; i < listLen; i++ { if err = UpdateReportVideo(list[i].Id); err != nil { fmt.Printf("更新音频失败") fmt.Println(err.Error()) return } } fmt.Println("End 报告音频更新完毕") return } // checkDayWeekChapterWrite 校验晨周报已写章节与本期应写章节 func checkDayWeekChapterWrite(chapters []*models.ReportChapter, reportType string) (publishReport bool, tips string, publishIdArr, unPublishIdArr []int, err error) { nowTime := time.Now().Local() updateTypeArr := make([]int, 0) // 需更新的章节类型IDs publishIdArr = make([]int, 0) // 需发布的章节IDs unPublishIdArr = make([]int, 0) // 需取消发布/未发布的章节IDs // 校验章节内容 if reportType == utils.REPORT_TYPE_DAY { // 晨报章节不能都为空 isEmpty := true for i := 0; i < len(chapters); i++ { if chapters[i].Content != "" && chapters[i].Title != "" { isEmpty = false break } } if isEmpty { err = errors.New("报告章节内容均为空或标题为空,不可发布") return } } else { // 周报章节需至少有一篇已编辑且有标题 editNum := 0 for i := 0; i < len(chapters); i++ { if chapters[i].IsEdit == 1 && chapters[i].Title != "" { editNum += 1 } } if editNum == 0 { err = errors.New("报告均未编辑或标题为空,不可发布") return } } // 章节类型列表 types, e := models.GetReportChapterTypeListByResearchType(reportType) if e != nil { err = errors.New("获取章节类型列表失败") return } // 本期需更新的章节IDs typeLen := len(types) for i := 0; i < typeLen; i++ { if types[i].IsSet != 1 && types[i].Enabled != 0 { // 正常更新 updateTypeArr = append(updateTypeArr, types[i].ReportChapterTypeId) } else { // 被设置为零值的也算作正常更新 if types[i].PauseStartTime == utils.EmptyDateStr && types[i].PauseEndTime == utils.EmptyDateStr { updateTypeArr = append(updateTypeArr, types[i].ReportChapterTypeId) continue } // 暂停更新需校验时间 startTime, _ := time.Parse(utils.FormatDate, types[i].PauseStartTime) endTime, _ := time.Parse(utils.FormatDate, types[i].PauseEndTime) if nowTime.Before(startTime) || nowTime.After(endTime.AddDate(0, 0, 1)) { updateTypeArr = append(updateTypeArr, types[i].ReportChapterTypeId) } } } // 校验本期需更新的章节是否都已编辑 chapterLen := len(chapters) updateTypeLen := len(updateTypeArr) tipsArr := make([]string, 0) for i := 0; i < chapterLen; i++ { isWrite := false for ii := 0; ii < updateTypeLen; ii++ { // 本期应发布的章节 if chapters[i].TypeId == updateTypeArr[ii] { // 标题或者内容为空的情况下, 记录tips提示信息且不发布该章节 if chapters[i].Title == "" || chapters[i].Content == "" { tipsArr = append(tipsArr, chapters[i].TypeName) break } isWrite = true break } } if isWrite { publishIdArr = append(publishIdArr, chapters[i].ReportChapterId) } else { unPublishIdArr = append(unPublishIdArr, chapters[i].ReportChapterId) } } if len(tipsArr) > 0 { tips = "部分章节未发布:" + strings.Join(tipsArr, "、") + "未填写标题/内容" } // 周报需发布的章节与需更新的章节数相等则表示可发布整期, 晨报无限制 if reportType == utils.REPORT_TYPE_DAY { publishReport = true } else { if len(publishIdArr) == updateTypeLen { publishReport = true } } return } // PcCreateAndUploadSunCode 生成太阳码并上传OSS func PcCreateAndUploadSunCode(scene, page string) (imgUrl string, err error) { if page == "" { err = errors.New("page不能为空") return } // scene超过32位会生成失败,md5处理至32位 sceneMD5 := "a=1" if scene != "" { sceneMD5 = utils.MD5(scene) } picByte, err := GetSunCode(page, sceneMD5) if err != nil { return } // 生成图片 localPath := "./static/imgs" fileName := utils.GetRandStringNoSpecialChar(28) + ".png" fpath := fmt.Sprint(localPath, "/", fileName) f, err := os.Create(fpath) if err != nil { fmt.Println("11111") return } if _, err = f.Write(picByte); err != nil { return } defer func() { f.Close() os.Remove(fpath) }() // 上传OSS fileDir := "yb/suncode/" //上传到阿里云 和 minio if utils.ObjectStorageClient == "minio" { imgUrl, err = UploadMinIoToDir(fileName, fpath, "", fileDir) if err != nil { return } } else { imgUrl, err = UploadAliyunToDir(fileName, fpath, "", fileDir) if err != nil { return } } if err != nil { return } // 记录参数 if scene != "" { newSuncode := &models.YbPcSuncode{ Scene: scene, SceneMd5: sceneMD5, CodePage: page, SuncodeUrl: imgUrl, CreateTime: time.Now(), } err = models.AddYbPcSunCode(newSuncode) } // 记录参数md5 if scene != "" { newPars := &models.YbSuncodePars{ Scene: scene, SceneKey: sceneMD5, CreateTime: time.Now(), } err = models.AddYbSuncodePars(newPars) } return } // CreateNewReport 创建新报告 func CreateNewReport(req models.AddReq, adminInfo *system.Admin) (newReportId int64, reportCode, errMsg string, err error) { contentSub := "" if req.Content != "" { contentClean, e := FilterReportContentBr(req.Content) if e != nil { errMsg = "内容去除前后空格失败" err = errors.New("内容去除前后空格失败, Err: " + e.Error()) return } req.Content = contentClean sub, e := GetReportContentSub(req.Content) if e != nil { go alarm_msg.SendAlarmMsg("ContentSub 失败,Err:"+e.Error(), 3) } contentSub = sub } maxStage, e := models.GetReportStage(req.ClassifyIdFirst, req.ClassifyIdSecond) if e != nil { errMsg = "期数获取失败!" err = errors.New("期数获取失败,Err:" + e.Error()) return } item := new(models.Report) item.AddType = req.AddType item.ClassifyIdFirst = req.ClassifyIdFirst item.ClassifyNameFirst = req.ClassifyNameFirst item.ClassifyIdSecond = req.ClassifyIdSecond item.ClassifyNameSecond = req.ClassifyNameSecond item.Title = req.Title item.Abstract = req.Abstract item.Author = req.Author item.Frequency = req.Frequency item.State = req.State item.Content = html.EscapeString(req.Content) item.Stage = maxStage + 1 item.ContentSub = html.EscapeString(contentSub) item.CreateTime = req.CreateTime item.ModifyTime = time.Now() item.ReportVersion = req.ReportVersion item.AdminId = adminInfo.AdminId item.AdminRealName = adminInfo.RealName newReportId, e = models.AddReport(item) if e != nil { errMsg = "保存失败" err = errors.New("保存失败,Err:" + e.Error()) return } // 处理权限 if utils.BusinessCode == utils.BusinessCodeRelease || utils.BusinessCode == utils.BusinessCodeSandbox { go func() { permissionItems, e := models.GetPermission(req.ClassifyNameSecond) if e != nil { alarm_msg.SendAlarmMsg("获取权限失败,Err:"+err.Error(), 3) } for _, v := range permissionItems { e = models.AddChartPermissionChapterMapping(v.ChartPermissionId, newReportId) if e != nil { alarm_msg.SendAlarmMsg("新增权限失败,Err:"+err.Error(), 3) } } }() } reportCode = utils.MD5(strconv.Itoa(int(newReportId))) //修改唯一编码 { go models.ModifyReportCode(newReportId, reportCode) } return } // FilterReportContentBr 过滤报告正文前后换行符 func FilterReportContentBr(content string) (res string, err error) { newContent := content //content = `










四季度验证投产预期

 

PTA:近端短缺的格局出现缓解的迹象,表现在:PX进口回升;国内重整及常减压提负;PTA、PX投产在即。正套部分或全部止盈。

 

乙二醇:到港预报集中的情况下仍在去库,4200-4600区间震荡操作,节前没有明显方向,不建议在节前备货的时间段布局空单

 

1、PX国内供应本周回落,PTA工厂负荷变动滞后于PX装置。

PX装置变动:

海南炼化一期66万吨PX装置因故障停车检修,重启时间待跟踪,其二期100万吨PX装置预计在此装置重启后停车检修。

天津石化一套100万吨重整已于20日重启中,其39万吨PX预计下周初出产品。

韩国SK 位于仁川的130万吨PX装置按计划在23日停车检修,计划检修时长45天左右。

截至周五,中国国内PX负荷小幅回落至73.4%(前值76.8%),亚洲PX负荷小幅回落至68.7%(72%)。

PXN本周延续回落至380美金附近。

PX平衡表

海关统计,国内8月PX进口总量在78.8万吨,环比增加17.3万吨,增幅28%;同比减少30.8万吨,降幅28.1%;8月PX出口量0.47万吨,环比下降5万吨。

PX 8月进口量大幅回升至79万吨附近,其中8月从韩国进口的PX量达到33.65万吨。9月目前公布的1-20号从韩国进口PX的量已经接近8月全月的水平,即环比8月仍是大幅增加的情况:据悉,9月1-20日韩国PX出口总量在30.9万吨,其中出口至中国27.9万吨。

预计9月进口量环比8月进一步增加。

原油本周偏弱,欧美汽油利润本周均出现了明显回升。

美国飓风将在下周登陆墨西哥湾,届时或将影响海上钻机的运行以及炼厂开工。 

2、乙烯连续跌价至900美金,PX投产压力临近PXN延续压缩。石脑油亏损本周延续修复。

PTA:按照PX11-12月上1054美金(石脑油672美金,Brent86.15美金)计算,醋酸3075元,目前含醋酸的原料成本在5670元附近。可以发现虽然Brent和PX环比上周都出现了明显的下跌,但石脑油相对上周环比上涨,石脑油亏损本周延续修复,目前石脑油-Brent价差回升至0以上。

给到200-300的最低加工成本,PTA的估值在5870-5970元。周五日盘收盘后TA11合约5742,11月及之后的PTA合约均亏损。

PTA基差再度回升至1000附近,周五小幅走弱至970。

乙二醇:静态来看,按照盘面(煤价按照900元),锚定石脑油672美金,乙烯900美金,甲醇2640元,北美乙烷价格38美分/加仑计算,乙二醇的综合成本仍在5050元附近。周五日盘收盘01合约按照综合成本亏损700元附近。

国内外采乙烷制乙二醇目前扭亏为盈。

  1. 3、需求端本周仍偏弱:继上周加弹负荷下滑后,本周织造负荷下滑。延续坯布库存回升同时原料库存下降,聚酯连续累库(瓶片低库存优势也明显减弱),两家大厂减产执行过程和节前备货中和,因此减产去库成效甚微 
  2. 终端:江浙加弹综合开工微幅回升至77%(前值76%);江浙织机综合开工回落至69%(前值71%);江浙印染综合开工维持在77% 。
  3. 本周,涤丝仅周三稍有放量,整体涤丝销售氛围偏弱为主。终端在新订单氛围走弱和成本端偏弱氛围下,原料不再进一步跟进,消化前期备货为主,综合原料备货有所下降。截至目前,原料备货集中在10-15天,偏高备货至10月底。
  4. 订单情况:近期的织造端新单氛围整体走弱明显,前期较好的圆机和经编工厂尤为明显,出货量也有所放缓,部分工厂生产前期订单为主。
    直接需求:近期装置轮动检修与重启,长丝大厂陆续执行减产动作,但也有几套切片装置恢复,整体而言聚酯负荷仍以区间波动为主。截至本周五,初步核算聚酯负荷在83.9%(前值84.3%)。

轻纺城成交量节前震荡回升但仍在偏低水平。








PTA库存结构变化跟踪:本期PTA库存大幅去化17.9万吨附近(本周仓单集中注销,仓单库存大幅下降至0附近),聚酯成品折算PTA库存大幅累库9.3万吨,叠加PTA库存在聚酯成品库存累库的情况下去库8.6万吨。

PTA平衡表——按照Q4有500万吨新装置投产(东营威联化学250万吨及嘉通能源250万吨分别在11、12月计入产能基数)计算,按照9-10月聚酯月均负荷84%(下调1%)、87%(下调1%)预估,8-12月出口预计25万吨附近。8-10月目前预估均为去库格局。

PX折算PTA与PTA合计9月去库幅度修正后大幅收窄。


5、乙二醇行情沙盘推演2022.9.23:到港预报集中的情况下仍在去库,4200-4600区间震荡操作,节前没有明显方向,不建议在节前备货的时间段布局空单。


乙二醇国内供需:

乙二醇到港预报与实际到港:

隆众口径:截至9月29日,国内乙二醇华东总到港量预计在19.21万吨,较上一期增加3.45万吨,提升21.93个百分点。 

港口发货节前回升。


海外装置:

印度IOC 32.5万吨装置将于近期停车技改,预计停车将持续至12月份。

伊朗 Marun 44.5万吨装置目前处于停车状态,该装置此前货源供应印度市场为主。

美国Sasol 28万吨装置计划于10月上旬停车检修,预计检修时长在一个月附近。









` content = html.UnescapeString(content) if content == "" { return } // 过滤编辑器版权html content = strings.Replace(content, "

Powered by Froala Editor

", "", -1) defer func() { if err != nil { go alarm_msg.SendAlarmMsg("过滤报告正文前后换行符及空格失败, ErrMsg: "+err.Error(), 3) } }() // 做一个配置,有问题的时候随时关闭 configKey := "report_filter_br" conf, e := company.GetConfigDetailByCode(configKey) if e != nil { err = errors.New("获取报告过滤配置失败, Err: " + e.Error()) return } if conf.ConfigValue != "1" { return content, nil } // 找出所有

标签,

标签的索引 re := regexp.MustCompile(`(?is:)`) arr := re.FindAllString(content, -1) indexArr := re.FindAllIndex([]byte(content), -1) // 空

正则 emptyRe := `]*>(
|
)+

` startIsBr := false countEmptyBr := 0 // 需要连续替换的空

总数 lastBrRange := 0 // 最后一个空

右侧index, 用来判断是否为连续的空

// 注:以下逻辑只适用于去除前面的空行, 由于编辑器始终会在文章最后面跟上自己的html标签, 此处不再进行后面空行的去除=_=! for i := range arr { byteRange := indexArr[i] if len(byteRange) == 2 { // 内容开头不为

直接跳出遍历, 否则才进行空

的判断 if i == 0 && byteRange[0] == 0 { startIsBr = true } if !startIsBr { break } if lastBrRange != 0 { // 说明不是连续的空

, 中间出现了其他标签, 那么结束遍历, 进行最终的文本替换 if lastBrRange != byteRange[0] { break } } // 正则匹配为空

则计数, 记录该空

右侧index m, e := regexp.Match(emptyRe, []byte(arr[i])) if e != nil { err = e return } if m { countEmptyBr += 1 lastBrRange = byteRange[1] continue } // 遍历到该

标签不为空了, 结束遍历 break } } if countEmptyBr > 0 { reg, e := regexp.Compile(emptyRe) if e != nil { err = errors.New("正则解析失败, Err: " + e.Error()) return } counted := 0 // 已替换数 res = reg.ReplaceAllStringFunc(content, func(s string) string { counted += 1 if counted <= countEmptyBr { return "" } else { return s } }) if res == "" { res = newContent } } else { res = content if res == "" { res = newContent } } return } // GetEnglishReportOverview 获取英文研报overview部分 func GetEnglishReportOverview(content string) (res string, err error) { content = html.UnescapeString(content) doc, e := goquery.NewDocumentFromReader(strings.NewReader(content)) if e != nil { err = errors.New("Create Doc Err: " + e.Error()) return } target := "overview" label := "" start := -1 end := -1 doc.Find("p").Each(func(i int, s *goquery.Selection) { h, e := s.Html() if e != nil { err = errors.New("Get Html1 Err: " + e.Error()) return } h = strings.ToLower(h) t := s.Text() t = strings.ToLower(t) if strings.Contains(h, label) && t != "" && strings.Contains(t, target) { start = i } if start != -1 && end == -1 && i > start && strings.Contains(h, label) { end = i } }) if start != -1 && end != -1 { doc.Find("p").Each(func(i int, s *goquery.Selection) { if i > start && i < end { h, e := s.Html() if e != nil { err = errors.New("Get Html2 Err: " + e.Error()) return } // 包含iframe则过滤掉 if strings.Contains(h, "iframe") { return } res += `

` + h + `

` } }) } return } // GetReportContentSubWithoutIframe 获取报告正文前几段,过滤iframe func GetReportContentSubWithoutIframe(content string) (contentSub string, err error) { content = html.UnescapeString(content) doc, err := goquery.NewDocumentFromReader(strings.NewReader(content)) if err != nil { fmt.Println("create doc err:", err.Error()) return } label := "iframe" n := 0 doc.Find("p").Each(func(i int, s *goquery.Selection) { if n >= 5 { return } n++ h, err := s.Html() if err != nil { fmt.Println("get html err", err.Error()) return } // 包含iframe则过滤掉 if strings.Contains(h, label) { return } if s.Text() != "" || strings.Contains(h, "src") { contentSub = contentSub + "

" + h + "

" } }) return } // UpdateReportEditMark 更新研报当前更新状态 // status 枚举值 1:编辑中,0:完成编辑, 2:只做查询 func UpdateReportEditMark(reportId, nowUserId, status int, nowUserName string) (ret models.MarkReportResp, err error) { //更新标记key key := fmt.Sprint(`crm:report:edit:`, reportId) ret.Status = 0 ret.Msg = "无人编辑" opUserId, e := utils.Rc.RedisInt(key) var opUser models.MarkReportItem var classifyNameFirst string if e != nil { opUserInfoStr, tErr := utils.Rc.RedisString(key) if tErr == nil { tErr = json.Unmarshal([]byte(opUserInfoStr), &opUser) if tErr == nil { opUserId = opUser.AdminId } } } //判断是否是晨报或者周报,如果是则跳过 var reportInfo *models.ReportDetail classifyNameFirst = opUser.ReportClassifyNameFirst if reportId > 0 && status != 2 && classifyNameFirst == "" { //查询报告ID信息 reportInfo, err = models.GetReportById(reportId) if err != nil { err = fmt.Errorf("报告不存在") return } classifyNameFirst = reportInfo.ClassifyNameFirst } if classifyNameFirst == "晨报" || classifyNameFirst == "周报" { return } if opUserId > 0 && opUserId != nowUserId { editor := opUser.Editor if editor == "" { //查询账号的用户姓名 otherInfo, e := system.GetSysAdminById(opUserId) if e != nil { err = fmt.Errorf("查询其他编辑者信息失败") return } editor = otherInfo.RealName } ret.Status = 1 ret.Msg = fmt.Sprintf("当前%s正在编辑报告", editor) ret.Editor = editor return } if status == 1 { nowUser := &models.MarkReportItem{AdminId: nowUserId, Editor: nowUserName, ReportClassifyNameFirst: classifyNameFirst} bt, e := json.Marshal(nowUser) if e != nil { err = fmt.Errorf("格式化编辑者信息失败") return } if opUserId > 0 { utils.Rc.Do("SETEX", key, int64(180), string(bt)) //3分钟缓存 } else { utils.Rc.SetNX(key, string(bt), time.Second*60*3) //3分钟缓存 } } else if status == 0 { //清除编辑缓存 _ = utils.Rc.Delete(key) } return } // HandleVideoDecibel 处理报告中的音频文件 func HandleVideoDecibel(chapterInfo *models.ReportChapter) { public_api.HandleVideoDecibel(chapterInfo.ReportChapterId) return } // SaveReportLogs 记录报告日志 func SaveReportLogs(item *models.Report, chapters []*models.ReportChapter, adminId int, adminRealName string) { if item == nil && len(chapters) == 0 { return } var err error defer func() { if err != nil { tips := fmt.Sprintf("报告日志记录, SaveReportLogs error: %s", err.Error()) go alarm_msg.SendAlarmMsg(tips, 2) } }() if item != nil { e := models.AddReportSaveLog(item.Id, item.AdminId, item.Content, item.ContentSub, item.AdminRealName) if e != nil { err = fmt.Errorf("AddReportSaveLog: %s", e.Error()) return } } if len(chapters) > 0 { e := models.MultiAddReportChaptersSaveLog(chapters, adminId, adminRealName) if e != nil { err = fmt.Errorf("MultiAddReportChaptersSaveLog: %s", e.Error()) return } } return }