Browse Source

Merge branch 'eta/2.4.5'

Roc 1 week ago
parent
commit
6b77e8b368

+ 60 - 0
controllers/data_manage/chart_common.go

@@ -11,6 +11,7 @@ import (
 	"eta/eta_mobile/models"
 	"eta/eta_mobile/models/data_manage"
 	"eta/eta_mobile/models/system"
+	"eta/eta_mobile/services"
 	"eta/eta_mobile/services/data"
 	"eta/eta_mobile/services/data/excel"
 	"eta/eta_mobile/utils"
@@ -228,3 +229,62 @@ func getBalanceChartInfoDetailFromUniqueCode(chartInfo *data_manage.ChartInfoVie
 
 	return
 }
+
+// GeneralChartToken
+// @Title 根据图表唯一code生成token
+// @Description 根据编码获取图表详情接口
+// @Param   UniqueCode   query   string  true       "图表/表格唯一编码"
+// @Param   Source   query   string  true       "来源,枚举值:chart、table"
+// @Success 200 {object} data_manage.ChartInfoDetailFromUniqueCodeResp
+// @router /chart_info/common/general_token [get]
+func (this *ChartInfoController) GeneralChartToken() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	uniqueCode := this.GetString("UniqueCode")
+	if uniqueCode == "" {
+		br.Msg = "参数错误"
+		br.ErrMsg = "参数错误,uniqueCode is empty"
+		return
+	}
+	source := this.GetString("Source", "chart")
+
+	businessConf, err := models.GetBusinessConfByKey(models.BusinessConfIsOpenChartExpired)
+	if err != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = "获取配置失败,Err:" + err.Error()
+		return
+	}
+
+	var token string
+	if businessConf.ConfVal == `true` {
+		// 缓存key
+		sourceType := source
+		if source == `table` {
+			sourceType = source
+		}
+		token, err = services.GeneralChartToken(sourceType, uniqueCode, 30*time.Minute)
+		if err != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = "获取失败"
+			return
+		}
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+	br.Data = token
+
+	return
+}

+ 93 - 4
controllers/english_report/report.go

@@ -64,6 +64,8 @@ func (this *EnglishReportController) Add() {
 
 	var contentSub string
 	if req.Content != "" {
+		req.Content = services.HandleReportContent(req.Content, "del", nil)
+
 		content, e := services.FilterReportContentBr(req.Content)
 		if e != nil {
 			br.Msg = "内容去除前后空格失败"
@@ -181,6 +183,7 @@ func (this *EnglishReportController) Edit() {
 	}
 	var contentSub string
 	if req.Content != "" {
+		req.Content = services.HandleReportContent(req.Content, "del", nil)
 		content, e := services.FilterReportContentBr(req.Content)
 		if e != nil {
 			br.Msg = "内容去除前后空格失败"
@@ -298,6 +301,17 @@ func (this *EnglishReportController) Detail() {
 	item.Content = html.UnescapeString(item.Content)
 	item.ContentSub = html.UnescapeString(item.ContentSub)
 
+	businessConf, err := models.GetBusinessConfByKey(models.BusinessConfIsOpenChartExpired)
+	if err != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = "获取配置失败,Err:" + err.Error()
+		return
+	}
+	if businessConf.ConfVal == `true` {
+		tokenMap := make(map[string]string)
+		item.Content = services.HandleReportContent(item.Content, "add", tokenMap)
+	}
+
 	classifyNameMap := make(map[int]*models.EnglishClassifyFullName)
 	if item.ClassifyIdSecond > 0 {
 		nameList, tErr := models.GetEnglishClassifyFullNameByIds([]int{item.ClassifyIdSecond})
@@ -682,8 +696,9 @@ func (this *EnglishReportController) PublishReport() {
 			}()
 
 			// 生成报告pdf和长图
-			if req.ReportUrl != "" {
-				go services.Report2pdfAndJpeg(req.ReportUrl, report.Id, 2)
+			pdfUrl := services.GetGeneralEnglishReportPdfUrl(report.Id, report.ReportCode)
+			if pdfUrl != "" {
+				go services.Report2pdfAndJpeg(pdfUrl, report.Id, 2)
 			}
 		} else {
 			// 从无审批切换为有审批, 状态重置
@@ -785,8 +800,9 @@ func (this *EnglishReportController) PrePublishReport() {
 	}
 
 	// 生成报告pdf和长图
-	if req.ReportUrl != "" {
-		go services.Report2pdfAndJpeg(req.ReportUrl, report.Id, 2)
+	pdfUrl := services.GetGeneralEnglishReportPdfUrl(report.Id, report.ReportCode)
+	if pdfUrl != "" {
+		go services.Report2pdfAndJpeg(pdfUrl, report.Id, 2)
 	}
 
 	br.Ret = 200
@@ -945,6 +961,7 @@ func (this *EnglishReportController) SaveReportContent() {
 	}
 
 	if noChangeFlag != 1 {
+		req.Content = services.HandleReportContent(req.Content, "del", nil)
 		content := req.Content
 		if content == "" {
 			content = this.GetString("Content")
@@ -1020,6 +1037,18 @@ func (this *EnglishReportController) ClassifyIdDetail() {
 		item.Content = html.UnescapeString(item.Content)
 		item.ContentSub = html.UnescapeString(item.ContentSub)
 
+		businessConf, err := models.GetBusinessConfByKey(models.BusinessConfIsOpenChartExpired)
+		if err != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = "获取配置失败,Err:" + err.Error()
+			return
+		}
+
+		if businessConf.ConfVal == `true` {
+			tokenMap := make(map[string]string)
+			item.Content = services.HandleReportContent(item.Content, "add", tokenMap)
+		}
+
 		classifyNameMap := make(map[int]*models.EnglishClassifyFullName)
 		if item.ClassifyIdSecond > 0 {
 			nameList, tErr := models.GetEnglishClassifyFullNameByIds([]int{item.ClassifyIdSecond})
@@ -1421,3 +1450,63 @@ func (this *EnglishReportController) CancelApprove() {
 	br.Success = true
 	br.Msg = "操作成功"
 }
+
+// @Title 获取报告分享链接
+// @Description 获取报告分享链接
+// @Param   ReportId   query   int  true       "报告id"
+// @Success 200 {object} models.EnglishReportDetailView
+// @router /share_url [get]
+func (this *EnglishReportController) GetShareUrl() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	/*var req models.ReportDetailReq
+	err := json.Unmarshal(this.Ctx.Input.RequestBody, &req)
+	if err != nil {
+		br.Msg = "参数解析异常!"
+		br.ErrMsg = "参数解析失败,Err:" + err.Error()
+		return
+	}
+	if req.ReportId <= 0 {
+		br.Msg = "参数错误"
+		return
+	}*/
+	reportId, err := this.GetInt("ReportId")
+	if err != nil {
+		br.Msg = "获取参数失败!"
+		br.ErrMsg = "获取参数失败,Err:" + err.Error()
+		return
+	}
+	if reportId <= 0 {
+		br.Msg = "参数错误"
+		return
+	}
+	item, err := models.GetEnglishReportById(reportId)
+	if err != nil {
+		if err.Error() == utils.ErrNoRow() {
+			br.Msg = "报告已被删除"
+			return
+		}
+		br.Msg = "获取失败"
+		br.ErrMsg = "获取失败,Err:" + err.Error()
+		return
+	}
+
+	token, err := services.GetEnglishReportToken(reportId, item.ReportCode)
+	if err != nil {
+		if err.Error() == utils.ErrNoRow() {
+			br.Msg = "报告已被删除"
+			return
+		}
+		br.Msg = "获取失败"
+		br.ErrMsg = "获取失败,Err:" + err.Error()
+		return
+	}
+
+	br.Data = token
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+}

+ 81 - 3
controllers/report.go

@@ -8,9 +8,6 @@ import (
 	"eta/eta_mobile/services/data"
 	"eta/eta_mobile/utils"
 	"fmt"
-	"github.com/beego/beego/v2/server/web"
-	"github.com/h2non/filetype"
-	"github.com/tealeg/xlsx"
 	"html"
 	"io"
 	"os"
@@ -19,6 +16,10 @@ import (
 	"strconv"
 	"strings"
 	"time"
+
+	"github.com/beego/beego/v2/server/web"
+	"github.com/h2non/filetype"
+	"github.com/tealeg/xlsx"
 )
 
 // ReportController 报告
@@ -1986,3 +1987,80 @@ func (this *ReportController) CheckDayWeekReportChapterVideo() {
 	br.Msg = "保存成功"
 	br.Data = typeNameArr
 }
+
+// ShareGenerate
+// @Title 获取复制链接
+// @Description 获取复制链接
+// @Param	request	body models.ReportShartLinkReq true "type json string"
+// @Success 200 Ret=200 操作成功
+// @router /share/generate [post]
+func (this *ReportController) ShareGenerate() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+
+	var req models.ReportShartUrlReq
+	if e := json.Unmarshal(this.Ctx.Input.RequestBody, &req); e != nil {
+		br.Msg = "参数有误"
+		br.ErrMsg = "参数解析失败, Err: " + e.Error()
+		return
+	}
+	var title string
+	reportItem, _ := models.GetReportByReportId(req.ReportId)
+	if reportItem != nil && reportItem.Title != "" {
+		title = reportItem.Title
+	}
+
+	link, err := services.GetReportShareUrlToken(req, this.SysUser.AdminId)
+	if err != nil || link == "" {
+		br.Msg = "复制链接失败"
+		br.ErrMsg = "获取复制链接失败, Err: " + err.Error()
+		return
+	}
+
+	resp := new(models.ReportShartUrlResp)
+	resp.UrlToken = fmt.Sprint(link, " ", title)
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+	br.Data = resp
+}
+
+// ShareTransform
+// @Title 获取原始链接
+// @Description 获取原始链接
+// @Param	Token   query   string  true    "复制链接的token"
+// @Success 200 Ret=200 操作成功
+// @router /share/link [get]
+func (this *ReportCommonController) ShareTransform() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+
+	token := this.GetString("Token")
+	tokenArr := strings.Split(token, " ")
+	if len(tokenArr) > 0 {
+		token = tokenArr[0]
+	}
+
+	link, msg, err := services.TransfromToOriginUrl(token)
+	if err != nil {
+		if msg == "" {
+			msg = "获取失败"
+		}
+		br.Msg = msg
+		br.ErrMsg = "获取复制链接失败, Err: " + err.Error()
+		return
+	}
+
+	this.Ctx.Redirect(302, link)
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+}

+ 18 - 0
controllers/report_chapter.go

@@ -363,6 +363,7 @@ func (this *ReportController) EditDayWeekChapter() {
 	// 更新章节及指标
 	contentSub := ""
 	if req.Content != "" {
+		req.Content = services.HandleReportContent(req.Content, "del", nil)
 		e := utils.ContentXssCheck(req.Content)
 		if e != nil {
 			br.Msg = "存在非法标签"
@@ -405,6 +406,10 @@ func (this *ReportController) EditDayWeekChapter() {
 	reportChapterInfo.LastModifyAdminId = sysUser.AdminId
 	reportChapterInfo.LastModifyAdminName = sysUser.RealName
 	reportChapterInfo.ContentModifyTime = time.Now()
+
+	if req.ContentStruct != `` {
+		req.ContentStruct = services.HandleReportContentStruct(req.ContentStruct, "del", nil)
+	}
 	reportChapterInfo.ContentStruct = html.EscapeString(req.ContentStruct)
 
 	updateCols = append(updateCols, "Author", "Content", "ContentSub", "IsEdit", "ModifyTime")
@@ -776,6 +781,19 @@ func (this *ReportController) GetDayWeekChapter() {
 	chapterItem.ContentSub = html.UnescapeString(chapterItem.ContentSub)
 	chapterItem.ContentStruct = html.UnescapeString(chapterItem.ContentStruct)
 
+	businessConf, err := models.GetBusinessConfByKey(models.BusinessConfIsOpenChartExpired)
+	if err != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = "获取配置失败,Err:" + err.Error()
+		return
+	}
+
+	if businessConf.ConfVal == `true` {
+		tokenMap := make(map[string]string)
+		chapterItem.Content = services.HandleReportContent(chapterItem.Content, "add", tokenMap)
+		chapterItem.ContentStruct = services.HandleReportContentStruct(chapterItem.ContentStruct, "add", tokenMap)
+	}
+
 	// 授权用户列表map
 	chapterGrantIdList := make([]int, 0)
 	// 关联品种id列表map

+ 30 - 1
controllers/report_v2.go

@@ -391,6 +391,7 @@ func (this *ReportController) Add() {
 
 	var contentSub string
 	if req.Content != "" {
+		req.Content = services.HandleReportContent(req.Content, "del", nil)
 		e := utils.ContentXssCheck(req.Content)
 		if e != nil {
 			br.Msg = "存在非法标签"
@@ -412,6 +413,10 @@ func (this *ReportController) Add() {
 		}
 	}
 
+	if req.ContentStruct != `` {
+		req.ContentStruct = services.HandleReportContentStruct(req.ContentStruct, "del", nil)
+	}
+
 	// 报告期数
 	maxStage, err := models.GetReportStage(req.ClassifyIdFirst, req.ClassifyIdSecond, req.ClassifyIdThird)
 	if err != nil {
@@ -587,6 +592,8 @@ func (this *ReportController) Edit() {
 		return
 	}
 
+	req.Content = services.HandleReportContent(req.Content, "del", nil)
+	req.ContentStruct = services.HandleReportContentStruct(req.ContentStruct, "del", nil)
 	// 编辑报告信息
 	err, errMsg := services.EditReport(reportInfo, req, sysUser)
 	if err != nil {
@@ -698,6 +705,24 @@ func (this *ReportController) Detail() {
 		item.EndStyle = endResource.Style
 	}
 
+	businessConf, err := models.GetBusinessConfByKey(models.BusinessConfIsOpenChartExpired)
+	if err != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = "获取配置失败,Err:" + err.Error()
+		return
+	}
+
+	if businessConf.ConfVal == `true` {
+		tokenMap := make(map[string]string)
+		item.Content = services.HandleReportContent(item.Content, "add", tokenMap)
+		item.ContentStruct = services.HandleReportContentStruct(item.ContentStruct, "add", tokenMap)
+		for _, v := range chapterList {
+			v.Content = services.HandleReportContent(v.Content, "add", tokenMap)
+			v.ContentStruct = services.HandleReportContentStruct(v.ContentStruct, "add", tokenMap)
+		}
+
+	}
+
 	resp := &models.ReportDetailView{
 		ReportDetail: item,
 		ChapterList:  chapterList,
@@ -775,6 +800,8 @@ func (this *ReportController) SaveReportContent() {
 		if content == "" {
 			content = this.GetString("Content")
 		}
+		content = services.HandleReportContent(content, "del", nil)
+
 		if content != "" {
 			e := utils.ContentXssCheck(req.Content)
 			if e != nil {
@@ -790,6 +817,8 @@ func (this *ReportController) SaveReportContent() {
 			}
 			content = contentClean
 
+			req.ContentStruct = services.HandleReportContentStruct(req.ContentStruct, "del", nil)
+
 			contentSub, err := services.GetReportContentSub(content)
 			if err != nil {
 				go alarm_msg.SendAlarmMsg("解析 ContentSub 失败,Err:"+err.Error(), 3)
@@ -1443,7 +1472,7 @@ func (this *ReportController) PrePublishReport() {
 
 	// 生成报告pdf和长图
 	{
-		reportPdfUrl := services.GetGeneralPdfUrl(reportDetail.ReportCode, reportDetail.ReportLayout)
+		reportPdfUrl := services.GetGeneralPdfUrl(reportDetail.Id, reportDetail.ReportCode, reportDetail.ReportLayout)
 		go services.Report2pdfAndJpeg(reportPdfUrl, reportDetail.Id, 1)
 	}
 

+ 4 - 3
go.mod

@@ -23,8 +23,10 @@ require (
 	github.com/go-xorm/xorm v0.7.9
 	github.com/gonum/stat v0.0.0-20181125101827-41a0da705a5b
 	github.com/gorilla/websocket v1.5.1
+	github.com/h2non/filetype v1.1.3
 	github.com/jung-kurt/gofpdf v1.16.2
 	github.com/kgiannakakis/mp3duration v0.0.0-20191013070830-d834f8d5ed53
+	github.com/microcosm-cc/bluemonday v1.0.27
 	github.com/minio/minio-go/v7 v7.0.69
 	github.com/mojocn/base64Captcha v1.3.6
 	github.com/nosixtools/solarlunar v0.0.0-20211112060703-1b6dea7b4a19
@@ -33,6 +35,7 @@ require (
 	github.com/rdlucklib/rdluck_tools v1.0.3
 	github.com/shopspring/decimal v1.3.1
 	github.com/silenceper/wechat/v2 v2.1.6
+	github.com/spaolacci/murmur3 v1.1.0
 	github.com/tealeg/xlsx v1.0.5
 	github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/asr v1.0.897
 	github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.897
@@ -40,6 +43,7 @@ require (
 	github.com/xuri/excelize/v2 v2.8.1
 	github.com/yidane/formula v0.0.0-20220322063702-c9da84ba3476
 	go.mongodb.org/mongo-driver v1.15.0
+	golang.org/x/net v0.26.0
 	gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
 )
 
@@ -75,7 +79,6 @@ require (
 	github.com/gonum/matrix v0.0.0-20181209220409-c518dec07be9 // indirect
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/gorilla/css v1.0.1 // indirect
-	github.com/h2non/filetype v1.1.3 // indirect
 	github.com/hashicorp/golang-lru v0.5.4 // indirect
 	github.com/jmespath/go-jmespath v0.4.0 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
@@ -85,7 +88,6 @@ require (
 	github.com/leodido/go-urn v1.2.0 // indirect
 	github.com/mailru/easyjson v0.7.7 // indirect
 	github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
-	github.com/microcosm-cc/bluemonday v1.0.27 // indirect
 	github.com/minio/md5-simd v1.1.2 // indirect
 	github.com/minio/sha256-simd v1.0.1 // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
@@ -117,7 +119,6 @@ require (
 	github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
 	golang.org/x/crypto v0.24.0 // indirect
 	golang.org/x/image v0.14.0 // indirect
-	golang.org/x/net v0.26.0 // indirect
 	golang.org/x/sync v0.7.0 // indirect
 	golang.org/x/sys v0.21.0 // indirect
 	golang.org/x/text v0.16.0 // indirect

+ 2 - 9
go.sum

@@ -412,6 +412,8 @@ github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
 github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
 github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
+github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
 github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA=
 github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
 github.com/ssdb/gossdb v0.0.0-20180723034631-88f6b59b84ec/go.mod h1:QBvMkMya+gXctz3kmljlUCu/yB3GZ6oee+dUozsezQE=
@@ -501,8 +503,6 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0
 golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
 golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
 golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
-golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
-golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
 golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
 golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
 golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -553,8 +553,6 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
 golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
 golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
-golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
-golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
 golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
 golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -570,8 +568,6 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
-golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
 golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -607,8 +603,6 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
-golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
 golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -630,7 +624,6 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
-golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
 golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=

+ 21 - 2
models/business_conf.go

@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"github.com/beego/beego/v2/client/orm"
 	"html"
+	"strconv"
 	"strings"
 	"time"
 )
@@ -51,8 +52,15 @@ const (
 	BusinessConfTencentApiSecretKey          = "TencentApiSecretKey"          // 腾讯云API-密钥对
 	BusinessConfTencentApiRecTaskCallbackUrl = "TencentApiRecTaskCallbackUrl" // 腾讯云API-语音识别回调地址
 	BusinessConfSmsJhgjVariable              = "SmsJhgjVariable"              // 聚合国际短信变量
-	BusinessConfEsIndexNameExcel             = "EsIndexNameExcel"             // ES索引名称-表格
-	BusinessConfEsIndexNameDataSource        = "EsIndexNameDataSource"        // ES索引名称-数据源
+
+	BusinessConfEdbStopRefreshRule = "EdbStopRefreshRule" // 是否停止指标刷新规则
+	BusinessConfReport2ImgUrl      = "Report2ImgUrl"      // 报告转长图地址(用于兼容内外网环境的)
+	BusinessConfReportViewUrl      = "ReportViewUrl"      // 报告详情地址     // 报告详情地址
+
+	BusinessConfEsIndexNameExcel       = "EsIndexNameExcel"       // ES索引名称-表格
+	BusinessConfEsIndexNameDataSource  = "EsIndexNameDataSource"  // 聚合国际短信变量
+	BusinessConfIsOpenChartExpired     = "IsOpenChartExpired"     // 是否开启图表有效期鉴权/报告禁止复制
+	BusinessConfReportChartExpiredTime = "ReportChartExpiredTime" // 图表有效期鉴权时间,单位:分钟
 )
 
 const (
@@ -255,4 +263,15 @@ func InitBusinessConf() {
 	if BusinessConfMap[BusinessConfEsIndexNameDataSource] != "" {
 		utils.EsDataSourceIndexName = BusinessConfMap[BusinessConfEsIndexNameDataSource]
 	}
+
+	// 图表有效期的过期时间
+	if BusinessConfMap[BusinessConfReportChartExpiredTime] != "" {
+		reportChartExpiredTime, _ := strconv.Atoi(BusinessConfMap[BusinessConfReportChartExpiredTime])
+		if reportChartExpiredTime <= 0 {
+			reportChartExpiredTime = 30
+		}
+		utils.BusinessConfReportChartExpiredTime = time.Duration(reportChartExpiredTime) * time.Minute
+	} else {
+		utils.BusinessConfReportChartExpiredTime = 30 * time.Minute
+	}
 }

+ 12 - 2
models/report.go

@@ -4,10 +4,11 @@ import (
 	"errors"
 	"eta/eta_mobile/utils"
 	"fmt"
-	"github.com/beego/beego/v2/client/orm"
-	"github.com/rdlucklib/rdluck_tools/paging"
 	"strings"
 	"time"
+
+	"github.com/beego/beego/v2/client/orm"
+	"github.com/rdlucklib/rdluck_tools/paging"
 )
 
 // 报告状态
@@ -1901,3 +1902,12 @@ b.abstract,b.admin_id,b.admin_real_name,b.last_modify_admin_id,b.last_modify_adm
 	_, err = o.Raw(sql, params...).QueryRows(&items)
 	return items, err
 }
+
+type ReportShartUrlReq struct {
+	Url      string `description:"分享链接"`
+	ReportId int    `description:"报告ID"`
+}
+
+type ReportShartUrlResp struct {
+	UrlToken string `description:"分享链接token"`
+}

+ 27 - 0
routers/commentsRouter.go

@@ -2068,6 +2068,15 @@ func init() {
             Filters: nil,
             Params: nil})
 
+    beego.GlobalControllerRouter["eta/eta_mobile/controllers/data_manage:ChartInfoController"] = append(beego.GlobalControllerRouter["eta/eta_mobile/controllers/data_manage:ChartInfoController"],
+        beego.ControllerComments{
+            Method: "GeneralChartToken",
+            Router: `/chart_info/common/general_token`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
     beego.GlobalControllerRouter["eta/eta_mobile/controllers/data_manage:ChartInfoController"] = append(beego.GlobalControllerRouter["eta/eta_mobile/controllers/data_manage:ChartInfoController"],
         beego.ControllerComments{
             Method: "ChartInfoConvertDetail",
@@ -5524,6 +5533,15 @@ func init() {
             Filters: nil,
             Params: nil})
 
+    beego.GlobalControllerRouter["eta/eta_mobile/controllers:ReportCommonController"] = append(beego.GlobalControllerRouter["eta/eta_mobile/controllers:ReportCommonController"],
+        beego.ControllerComments{
+            Method: "ShareTransform",
+            Router: `/share/link`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
     beego.GlobalControllerRouter["eta/eta_mobile/controllers:ReportController"] = append(beego.GlobalControllerRouter["eta/eta_mobile/controllers:ReportController"],
         beego.ControllerComments{
             Method: "CheckDayWeekReportChapterVideo",
@@ -5947,6 +5965,15 @@ func init() {
             Filters: nil,
             Params: nil})
 
+    beego.GlobalControllerRouter["eta/eta_mobile/controllers:ReportController"] = append(beego.GlobalControllerRouter["eta/eta_mobile/controllers:ReportController"],
+        beego.ControllerComments{
+            Method: "ShareGenerate",
+            Router: `/share/generate`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
     beego.GlobalControllerRouter["eta/eta_mobile/controllers:ReportController"] = append(beego.GlobalControllerRouter["eta/eta_mobile/controllers:ReportController"],
         beego.ControllerComments{
             Method: "ThsSendTemplateMsg",

+ 4 - 0
routers/router.go

@@ -33,6 +33,8 @@ import (
 	"eta/eta_mobile/controllers/smart_report"
 	"eta/eta_mobile/controllers/speech_recognition"
 	"eta/eta_mobile/controllers/trade_analysis"
+	"eta/eta_mobile/services"
+
 	"github.com/beego/beego/v2/server/web"
 	"github.com/beego/beego/v2/server/web/filter/cors"
 )
@@ -46,6 +48,8 @@ func init() {
 		ExposeHeaders:    []string{"Content-Length", "Access-Control-Allow-Origin", "Access-Control-Allow-Headers", "Content-Type"},
 		AllowCredentials: true,
 	}))
+
+	web.InsertFilter("/v1/share/*", web.BeforeRouter, services.FilterShareUrl())
 	ns := web.NewNamespace("/v1",
 		web.NSNamespace("/ppt",
 			web.NSInclude(

+ 42 - 0
services/english_report.go

@@ -964,3 +964,45 @@ func UpdateEnglishCompanyEnabledByCompanyId(companyId int) (err error) {
 	}
 	return
 }
+
+// GetEnglishReportToken
+// @Description: 获取token
+// @author: Roc
+// @datetime 2025-03-18 10:35:11
+// @param reportId int
+// @param reportCode string
+// @return token string
+// @return err error
+func GetEnglishReportToken(reportId int, reportCode string) (token string, err error) {
+	// 图表授权token
+	token = utils.MD5(fmt.Sprint(reportCode, time.Now().UnixNano()/1e6))
+	err = generalReportAuthToken(token, `en:`, reportId)
+
+	return
+}
+
+func GetGeneralEnglishReportPdfUrl(reportId int, reportCode string) (pdfUrl string) {
+	// 优先取Report2ImgUrl(用于兼容内外网环境的), 没有的话取报告详情地址
+	var reportUrl string
+	conf, _ := models.GetBusinessConfByKey(models.BusinessConfReport2ImgUrl)
+	if conf != nil && conf.ConfVal != "" {
+		reportUrl = conf.ConfVal
+	}
+	if reportUrl == "" {
+		conf, e := models.GetBusinessConfByKey(models.BusinessConfReportViewUrl)
+		if e != nil {
+			return
+		}
+		reportUrl = conf.ConfVal
+	}
+
+	token := utils.MD5(fmt.Sprint(pdfUrl, time.Now().UnixNano()/1e6))
+	e := generalReportAuthToken(token, `en:`, reportId)
+	if e == nil {
+		pdfUrl = fmt.Sprintf("%s&authToken=%s", pdfUrl, token)
+	}
+
+	pdfUrl = fmt.Sprintf("%s/reportshare_pdf_en?code=%s&authToken=%s", reportUrl, reportCode, token)
+
+	return
+}

+ 1 - 1
services/report_approve.go

@@ -841,7 +841,7 @@ func AfterReportApprovePass(reportType, reportId int) (err error) {
 
 		// 生成报告pdf和长图
 		{
-			reportPdfUrl := GetGeneralPdfUrl(reportInfo.ReportCode, reportInfo.ReportLayout)
+			reportPdfUrl := GetGeneralPdfUrl(reportInfo.Id, reportInfo.ReportCode, reportInfo.ReportLayout)
 			go Report2pdfAndJpeg(reportPdfUrl, reportId, 1)
 		}
 

+ 415 - 3
services/report_v2.go

@@ -2,6 +2,7 @@ package services
 
 import (
 	"archive/zip"
+	"encoding/json"
 	"errors"
 	"eta/eta_mobile/models"
 	"eta/eta_mobile/models/report"
@@ -11,10 +12,17 @@ import (
 	"fmt"
 	"github.com/rdlucklib/rdluck_tools/file"
 	"github.com/rdlucklib/rdluck_tools/http"
+	html2 "golang.org/x/net/html"
+	"net/url"
 	"os"
 	"path"
 	"strconv"
+	"strings"
 	"time"
+
+	"github.com/beego/beego/v2/server/web"
+	"github.com/beego/beego/v2/server/web/context"
+	"github.com/go-redis/redis/v8"
 )
 
 // AddReportAndChapter
@@ -1207,7 +1215,7 @@ func PublishReport(reportId int, reportUrl string, sysUser *system.Admin) (tips
 
 	// 生成报告pdf和长图
 	{
-		reportPdfUrl := GetGeneralPdfUrl(reportInfo.ReportCode, reportInfo.ReportLayout)
+		reportPdfUrl := GetGeneralPdfUrl(reportInfo.Id, reportInfo.ReportCode, reportInfo.ReportLayout)
 		go Report2pdfAndJpeg(reportPdfUrl, reportId, 1)
 	}
 
@@ -1334,7 +1342,7 @@ func PublishChapterReport(reportInfo *models.Report, reportUrl string, sysUser *
 
 	// 生成报告pdf和长图
 	{
-		reportPdfUrl := GetGeneralPdfUrl(reportInfo.ReportCode, reportInfo.ReportLayout)
+		reportPdfUrl := GetGeneralPdfUrl(reportInfo.Id, reportInfo.ReportCode, reportInfo.ReportLayout)
 		go Report2pdfAndJpeg(reportPdfUrl, reportId, 1)
 	}
 
@@ -1495,7 +1503,7 @@ func UpdateReportVideo(reportInfo *models.Report) {
 // @param reportCode string
 // @param reportLayout int8
 // @return pdfUrl string
-func GetGeneralPdfUrl(reportCode string, reportLayout int8) (pdfUrl string) {
+func GetGeneralPdfUrl(reportId int, reportCode string, reportLayout int8) (pdfUrl string) {
 	conf, e := models.GetBusinessConfByKey("ReportViewUrl")
 	if e != nil {
 		return
@@ -1510,5 +1518,409 @@ func GetGeneralPdfUrl(reportCode string, reportLayout int8) (pdfUrl string) {
 		pdfUrl = fmt.Sprintf("%s/reportshare_smart_pdf?code=%s", conf.ConfVal, reportCode)
 	}
 
+	if pdfUrl != "" {
+		token := utils.MD5(fmt.Sprint(pdfUrl, time.Now().UnixNano()/1e6))
+		e := generalReportAuthToken(token, ``, reportId)
+		if e == nil {
+			pdfUrl = fmt.Sprintf("%s&authToken=%s", pdfUrl, token)
+		}
+	}
+
+	return
+}
+
+// HandleReportContent
+// @Description: 处理报告内容(动态图表/表格添加授权token)
+// @author: Roc
+// @datetime 2025-01-07 10:03:15
+// @param body string
+// @return newBody string
+func HandleReportContent(body string, opType string, tokenMap map[string]string) (newBody string) {
+	if body == `` {
+		return
+	}
+	newBody = body
+
+	// 解析HTML
+	doc, err := html2.Parse(strings.NewReader(body))
+	if err != nil {
+		fmt.Println("Error parsing HTML:", err)
+		return
+	}
+
+	replaceIframeSrc(doc, opType, tokenMap)
+
+	// 输出修改后的HTML
+	var modifiedHtml strings.Builder
+	err = html2.Render(&modifiedHtml, doc)
+	if err != nil {
+		fmt.Println("Error rendering HTML:", err)
+		return
+	}
+
+	newBody = modifiedHtml.String()
+	fmt.Println(newBody)
+
+	return
+}
+
+// replaceIframeSrc 遍历HTML节点,替换iframe的src属性
+func replaceIframeSrc(n *html2.Node, opType string, tokenMap map[string]string) {
+	if n.Type == html2.ElementNode && n.Data == "iframe" {
+		for i, attr := range n.Attr {
+			if attr.Key == "src" {
+				newLink := attr.Val
+				// 处理链接
+				switch opType {
+				case `add`:
+					newLink = linkAddToken(attr.Val, tokenMap)
+				case `del`:
+					newLink = linkDelToken(attr.Val)
+				}
+				// 替换原来的链接
+				n.Attr[i].Val = newLink
+				break
+			}
+		}
+	}
+	// 递归处理子节点
+	for c := n.FirstChild; c != nil; c = c.NextSibling {
+		replaceIframeSrc(c, opType, tokenMap)
+	}
+}
+
+// linkAddToken 链接添加token
+func linkAddToken(link string, tokenMap map[string]string) string {
+	var err error
+	defer func() {
+		if err != nil {
+			utils.FileLog.Info("处理链接失败,ERR:" + err.Error())
+		}
+	}()
+	parsedURL, err := url.Parse(link)
+	if err != nil {
+		return link
+	}
+
+	// 获取查询参数
+	queryParams := parsedURL.Query()
+
+	// 先移除authToken参数,避免莫名其妙的这个值入库了
+	queryParams.Del("authToken")
+
+	// 获取code参数
+	code := queryParams.Get("code")
+	if code == "" {
+		return link
+	}
+
+	showType := `chart`
+	if strings.Contains(parsedURL.Path, "sheetshow") {
+		showType = `excel`
+	}
+
+	// 避免报告里面一个图表/表格重复生成token
+	var token string
+	key := fmt.Sprint(showType, `:`, code)
+	if tokenMap != nil {
+		token = tokenMap[key]
+	}
+
+	// 如果之前没有token,那么就重新生成token
+	if token == `` {
+		token, err = GeneralChartToken(showType, code, 30*time.Minute)
+		if err != nil {
+			return link
+		}
+	}
+
+	if tokenMap != nil {
+		tokenMap[key] = token
+	}
+
+	// 在链接后面添加一个token值
+	queryParams.Add("authToken", token)
+
+	// 更新URL的查询参数
+	parsedURL.RawQuery = queryParams.Encode()
+
+	return parsedURL.String()
+}
+
+// linkDelToken 链接添加token
+func linkDelToken(link string) string {
+	var err error
+	defer func() {
+		if err != nil {
+			utils.FileLog.Info("处理链接失败,ERR:" + err.Error())
+		}
+	}()
+	parsedURL, err := url.Parse(link)
+	if err != nil {
+		return link
+	}
+
+	// 获取查询参数
+	queryParams := parsedURL.Query()
+
+	// 移除authToken参数
+	queryParams.Del("authToken")
+
+	// 更新URL的查询参数
+	parsedURL.RawQuery = queryParams.Encode()
+
+	return parsedURL.String()
+}
+
+// GeneralChartToken
+// @Description: 生产图表/表格授权token
+// @author: Roc
+// @datetime 2025-01-07 10:41:36
+// @param showType string
+// @param uniqueCode string
+// @param expireTime time.Duration
+// @return token string
+// @return err error
+func GeneralChartToken(showType, uniqueCode string, expireTime time.Duration) (token string, err error) {
+	// 缓存key
+	token = utils.MD5(fmt.Sprint(showType+`:`, uniqueCode, time.Now().UnixNano()/1e6))
+	key := fmt.Sprint(utils.CACHE_CHART_AUTH, token)
+	err = utils.Rc.Put(key, uniqueCode, expireTime)
+
+	return
+}
+
+// GeneralReportToken
+// @Description: 生成报告授权token
+// @author: Roc
+// @datetime 2025-01-07 10:41:36
+// @param uniqueCode string
+// @return token string
+// @return err error
+func GeneralReportToken(linkToken string, reportId int) (token string, err error) {
+	// 图表授权token
+	token = utils.MD5(fmt.Sprint(linkToken, time.Now().UnixNano()/1e6))
+
+	// 缓存key
+	reportKey := getReportShareTokenKey(linkToken)
+	err = utils.Rc.Put(reportKey, token, utils.BusinessConfReportChartExpiredTime)
+	if err != nil {
+		return
+	}
+
+	// 生成报告的图表授权token
+	err = generalReportAuthToken(token, ``, reportId)
+	if err != nil {
+		return
+	}
+
 	return
 }
+
+// generalReportAuthToken
+// @Description: 生成报告的图表授权token
+// @author: Roc
+// @datetime 2025-03-17 17:47:07
+// @param token string
+// @param reportId int
+// @return err error
+func generalReportAuthToken(token, source string, reportId int) (err error) {
+	// 缓存key
+	reportTokenKey := getReportTokenKey(token, source)
+	err = utils.Rc.Put(reportTokenKey, reportId, utils.BusinessConfReportChartExpiredTime)
+
+	return
+}
+
+// getReportShareTokenKey
+// @Description:
+// @author: Roc
+// @datetime 2025-03-17 14:00:14
+// @param linkToken string
+// @return string
+func getReportShareTokenKey(linkToken string) string {
+	return fmt.Sprint(utils.CACHE_REPORT_SHARE_AUTH, utils.MD5(linkToken))
+}
+
+// GetReportAuthToken
+// @Description: 获取报告token
+// @author: Roc
+// @datetime 2025-03-17 16:48:38
+// @param linkToken string
+// @return string
+func GetReportAuthToken(linkToken string) string {
+	key := getReportShareTokenKey(linkToken)
+	return utils.Rc.GetStr(key)
+
+}
+
+// getReportTokenKey
+// @Description:
+// @author: Roc
+// @datetime 2025-03-17 14:00:14
+// @param linkToken string
+// @return string
+func getReportTokenKey(token, source string) string {
+	return fmt.Sprint(utils.CACHE_REPORT_AUTH, source, token)
+}
+
+// HandleReportContentStruct
+// @Description: 处理内容组件的链接
+// @author: Roc
+// @datetime 2025-01-07 13:38:39
+// @param body string
+// @param opType string
+// @return newBody string
+func HandleReportContentStruct(body string, opType string, tokenMap map[string]string) (newBody string) {
+	if body == `` {
+		return
+	}
+	newBody = body
+
+	// 解析JSON数据到map[string]interface{}
+	var jsonData []map[string]interface{}
+	if err := json.Unmarshal([]byte(body), &jsonData); err != nil {
+		fmt.Println("Error parsing JSON:", err)
+		return
+	}
+
+	// 处理每个组件
+	for i := range jsonData {
+		if err := processMap(jsonData[i], opType, tokenMap); err != nil {
+			fmt.Println("Error processing component:", err)
+			return
+		}
+	}
+
+	// 将处理后的数据转换回JSON字符串
+	modifiedJSON, err := json.MarshalIndent(jsonData, "", "  ")
+	if err != nil {
+		fmt.Println("Error marshaling JSON:", err)
+		return
+	}
+	newBody = string(modifiedJSON)
+
+	return
+}
+
+// processMap 递归处理map中的content字段
+func processMap(data map[string]interface{}, opType string, tokenMap map[string]string) error {
+	for key, value := range data {
+		switch v := value.(type) {
+		case string:
+			if key == "content" {
+				newContent := v
+				// 处理链接
+				switch opType {
+				case `add`:
+					newContent = linkAddToken(v, tokenMap)
+				case `del`:
+					newContent = linkDelToken(v)
+				}
+				data[key] = newContent
+			}
+		case map[string]interface{}:
+			if err := processMap(v, opType, tokenMap); err != nil {
+				return err
+			}
+		case []interface{}:
+			for i := range v {
+				if m, ok := v[i].(map[string]interface{}); ok {
+					if err := processMap(m, opType, tokenMap); err != nil {
+						return err
+					}
+				}
+			}
+		}
+	}
+	return nil
+}
+
+// GetReportShareUrlToken 获取报告分享链接token
+func GetReportShareUrlToken(req models.ReportShartUrlReq, adminId int) (linkToken string, err error) {
+	defer func() {
+		if err == nil && linkToken != `` {
+			GeneralReportToken(linkToken, req.ReportId)
+		}
+	}()
+	cacheLinkKey := utils.CACHE_REPORT_SHARE_SHORT_Url + strconv.Itoa(req.ReportId) + "userId:" + strconv.Itoa(adminId)
+	linkToken, _ = utils.Rc.RedisString(cacheLinkKey)
+	if linkToken != "" && utils.Rc.IsExist(fmt.Sprint(utils.CACHE_REPORT_SHARE_ORIGIN_Url, utils.MD5(linkToken))) {
+		return
+	}
+	var tokenKey string
+
+	var ok bool
+	// 冲突检测
+	for i := 0; i < 3; i++ {
+		linkToken = req.Url
+		if i > 0 {
+			linkToken += "_" + utils.GetRandDigit(3)
+		}
+		hashUrl := utils.MurmurHash64([]byte(linkToken))
+		linkToken = utils.ConvertNumToBase62(hashUrl)
+		// 拼上报告标题
+		//linkToken = fmt.Sprintf("%s %s", linkToken, req.Title)
+
+		tokenKey = fmt.Sprint(utils.CACHE_REPORT_SHARE_ORIGIN_Url, utils.MD5(linkToken))
+		ok = utils.Rc.IsExist(tokenKey)
+		if !ok {
+			break
+		}
+	}
+	if !ok {
+		after := time.Now().AddDate(0, 0, 7)
+		err = utils.Rc.Put(cacheLinkKey, linkToken, time.Until(after))
+		if err != nil {
+			return
+		}
+		err = utils.Rc.Put(tokenKey, req.Url, time.Until(after))
+		if err != nil {
+			return
+		}
+	} else {
+		linkToken = ""
+		err = errors.New("生成链接失败")
+	}
+	return
+}
+
+func TransfromToOriginUrl(linkToken string) (originLink string, msg string, err error) {
+	cacheLinkKey := fmt.Sprint(utils.CACHE_REPORT_SHARE_ORIGIN_Url, utils.MD5(linkToken))
+	originLink, err = utils.Rc.RedisString(cacheLinkKey)
+	if err != nil {
+		if err == redis.Nil {
+			msg = "链接已失效, 请重新获取"
+			return
+		}
+		msg = "获取链接失败"
+		return
+	}
+	if originLink == "" {
+		msg = "链接已失效, 请重新获取"
+		return
+	}
+
+	reportToken := GetReportAuthToken(linkToken)
+	if reportToken != "" {
+		originLink += `&authToken=` + reportToken
+	}
+
+	return
+}
+
+func FilterShareUrl() web.FilterFunc {
+	return func(c *context.Context) {
+		path := c.Input.Context.Request.URL.Path
+		tokenArr := strings.Split(path, "/")
+		token := tokenArr[len(tokenArr)-1]
+
+		newPath := "/v1/report/share/link"
+		q := c.Input.Context.Request.URL.Query()
+		q.Add("Token", token)
+		c.Input.Context.Request.URL.Path = newPath
+		c.Input.Context.Request.URL.RawQuery = q.Encode()
+
+		utils.ApiLog.Info(fmt.Sprintf("原始请求为:%s, 已修改请求路径为:%s?%s", path, newPath, q.Encode()))
+	}
+}

+ 5 - 0
utils/business_conf.go

@@ -0,0 +1,5 @@
+package utils
+
+import "time"
+
+var BusinessConfReportChartExpiredTime time.Duration //图表有效期鉴权时间,单位:分钟

+ 40 - 4
utils/common.go

@@ -11,10 +11,6 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-	"github.com/PuerkitoBio/goquery"
-	"github.com/microcosm-cc/bluemonday"
-	"github.com/shopspring/decimal"
-	xhtml "golang.org/x/net/html"
 	"html"
 	"image"
 	"image/png"
@@ -31,6 +27,12 @@ import (
 	"strconv"
 	"strings"
 	"time"
+
+	"github.com/PuerkitoBio/goquery"
+	"github.com/microcosm-cc/bluemonday"
+	"github.com/shopspring/decimal"
+	"github.com/spaolacci/murmur3"
+	xhtml "golang.org/x/net/html"
 )
 
 // 随机数种子
@@ -2562,3 +2564,37 @@ func GetDuration(filePath string) (duration string, err error) {
 
 	return duration, nil
 }
+
+// MurmurHash64 计算字符串的64位哈希值
+func MurmurHash64(val []byte) uint64 {
+	hash64 := murmur3.New64()
+	hash64.Write(val)
+	return hash64.Sum64()
+}
+
+const base62Chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+
+type TUint interface {
+	uint64 | uint32 | uint16 | uint8 | uint
+}
+
+// ConvertNumToBase62 转换数字为base62编码
+func ConvertNumToBase62[T TUint](num T) string {
+	if num == 0 {
+		return string(base62Chars[0])
+	}
+
+	var result []byte
+	for num > 0 {
+		remainder := num % 62
+		result = append(result, base62Chars[remainder])
+		num /= 62
+	}
+
+	// 反转结果
+	for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
+		result[i], result[j] = result[j], result[i]
+	}
+
+	return string(result)
+}

+ 5 - 0
utils/constants.go

@@ -218,6 +218,11 @@ const (
 	CACHE_SMART_REPORT_EDITING        = "eta:smart_report:editing:"         // 智能研报用户编辑中
 	CACHE_SMART_REPORT_SEND_MSG       = "eta:smart_report:sending:"         // 智能研报用户报告推送
 	CACHE_KEY_REPLACE_EDB             = "eta:replace_edb"                   //系统用户操作日志队列
+	CACHE_REPORT_SHARE_SHORT_Url      = "eta:report_share_url:report_id:"   //报告短链映射key
+	CACHE_REPORT_SHARE_ORIGIN_Url     = "eta:report_share_url:token:"       //短链与原始报告链接的映射key
+	CACHE_CHART_AUTH                  = "eta:chart:auth:"                   //图表数据授权
+	CACHE_REPORT_SHARE_AUTH           = "eta:report:auth:share:"            //报告短链与报告图表授权映射key
+	CACHE_REPORT_AUTH                 = "eta:report:auth:"                  //报告图表数据授权
 )
 
 // 模板消息推送类型

+ 2 - 0
utils/redis.go

@@ -7,6 +7,8 @@ import (
 
 type RedisClient interface {
 	Get(key string) interface{}
+	GetStr(key string) string
+	GetUInt64(key string) (uint64, error)
 	RedisBytes(key string) (data []byte, err error)
 	RedisString(key string) (data string, err error)
 	RedisInt(key string) (data int, err error)

+ 19 - 0
utils/redis/cluster_redis.go

@@ -86,6 +86,25 @@ func (rc *ClusterRedisClient) Get(key string) interface{} {
 	return data
 }
 
+// GetStr
+// @Description: 根据key获取字符串数据
+// @receiver rc
+// @param key
+// @return string
+func (rc *ClusterRedisClient) GetStr(key string) string {
+	return rc.redisClient.Get(context.TODO(), key).Val()
+}
+
+// GetUInt64
+// @Description: 根据key获取uint64数据
+// @receiver rc
+// @param key
+// @return int
+// @return error
+func (rc *ClusterRedisClient) GetUInt64(key string) (uint64, error) {
+	return rc.redisClient.Get(context.TODO(), key).Uint64()
+}
+
 // RedisBytes
 // @Description: 根据key获取字节编码数据
 // @receiver rc

+ 19 - 0
utils/redis/standalone_redis.go

@@ -78,6 +78,25 @@ func (rc *StandaloneRedisClient) Get(key string) interface{} {
 	return data
 }
 
+// GetStr
+// @Description: 根据key获取字符串数据
+// @receiver rc
+// @param key
+// @return string
+func (rc *StandaloneRedisClient) GetStr(key string) string {
+	return rc.redisClient.Get(context.TODO(), key).Val()
+}
+
+// GetUInt64
+// @Description: 根据key获取uint64数据
+// @receiver rc
+// @param key
+// @return int
+// @return error
+func (rc *StandaloneRedisClient) GetUInt64(key string) (uint64, error) {
+	return rc.redisClient.Get(context.TODO(), key).Uint64()
+}
+
 // RedisBytes
 // @Description: 根据key获取字节编码数据
 // @receiver rc