Переглянути джерело

Merge remote-tracking branch 'origin/debug' into debug

Roc 5 місяців тому
батько
коміт
9627bedcc4

+ 19 - 21
controllers/data_stat/edb_source_stat.go

@@ -804,24 +804,26 @@ func (this *EdbSourceStatController) EdbUpdateFailedList() {
 
 	terminalCode := this.GetString("TerminalCode", "")
 	createTime := this.GetString("CreateTime", "")
+	condition := " and source = ? and terminal_code = ?"
+	var pars []interface{}
+	pars = append(pars, utils.DATA_SOURCE_MYSTEEL_CHEMICAL, terminalCode)
 
-	if terminalCode == "" {
-		br.Msg = "请选择对应的终端信息"
-		return
-	}
-	terminalInfo, err := data_manage.GetEdbTerminalByTerminalCode(terminalCode)
-	if err != nil {
-		if err.Error() == utils.ErrNoRow() {
-			br.Msg = "终端不存在"
+	terminalName := ""
+	terminalDir := ""
+	if terminalCode != "" {
+		terminalInfo, err := data_manage.GetEdbTerminalByTerminalCode(terminalCode)
+		if err != nil {
+			if err.Error() == utils.ErrNoRow() {
+				br.Msg = "终端不存在"
+				return
+			}
+			br.Msg = "查询终端信息出错"
+			br.ErrMsg = "查询终端信息出错 Err:" + err.Error()
 			return
 		}
-		br.Msg = "查询终端信息出错"
-		br.ErrMsg = "查询终端信息出错 Err:" + err.Error()
-		return
+		terminalName = terminalInfo.Name
+		terminalDir = terminalInfo.DirPath
 	}
-	condition := " and source = ? and terminal_code = ?"
-	var pars []interface{}
-	pars = append(pars, utils.DATA_SOURCE_MYSTEEL_CHEMICAL, terminalCode)
 
 	if createTime != "" {
 		startT, err := time.ParseInLocation(utils.FormatDate, createTime, time.Local)
@@ -856,9 +858,9 @@ func (this *EdbSourceStatController) EdbUpdateFailedList() {
 	}
 	resp := data_stat.GetEdbUpdateFailedResp{
 		List:             list,
-		Name:             terminalInfo.Name,
+		Name:             terminalName,
 		TerminalCode:     terminalCode,
-		DirPath:          terminalInfo.DirPath,
+		DirPath:          terminalDir,
 		UpdateSuccessNum: successNum,
 		UpdateFailedNum:  failedNum,
 	}
@@ -909,16 +911,12 @@ func (this *EdbSourceStatController) EdbUpdateFailedDetailList() {
 		return
 	}
 
-	if terminalCode == "" {
-		br.Msg = "请选择对应的终端信息"
-		return
-	}
 	if frequency == "" {
 		br.Msg = "请选择对应的频度"
 		return
 	}
 
-	condition := " and source = ? and terminal_code = ? and frequency=? and data_update_failed_reason=? and data_update_result = 2"
+	condition := " and source = ? AND terminal_code = ? and frequency=? and data_update_failed_reason=? and data_update_result = 2"
 	var pars []interface{}
 	pars = append(pars, utils.DATA_SOURCE_MYSTEEL_CHEMICAL, terminalCode, frequency, sourceUpdateFailedReason)
 

+ 3 - 67
controllers/material/material.go

@@ -26,70 +26,6 @@ type MaterialController struct {
 	controllers.BaseAuthController
 }
 
-// ClassifyMaterialItems
-// @Title 获取所有素材库分类接口-包含素材库
-// @Description 获取所有素材库分类接口-包含素材库
-// @Param   IsShowMe   query   bool  true       "是否只看我的,true、false"
-// @Success 200 {object} data_manage.ChartClassifyListResp
-// @router /classify/materialList [get]
-func (this *MaterialController) ClassifyMaterialItems() {
-	br := new(models.BaseResponse).Init()
-	defer func() {
-		this.Data["json"] = br
-		this.ServeJSON()
-	}()
-
-	resp := new(material.MaterialClassifyListResp)
-	MaterialClassifyId, _ := this.GetInt("MaterialClassifyId")
-
-	isShowMe, _ := this.GetBool("IsShowMe")
-	if isShowMe {
-		errMsg, err := materialService.GetMaterialClassifyListForMe(*this.SysUser, resp, MaterialClassifyId)
-		if err != nil {
-			br.Msg = errMsg
-			br.ErrMsg = err.Error()
-			return
-		}
-		// 移除没有权限的图表
-
-		br.Ret = 200
-		br.Success = true
-		br.Msg = "获取成功"
-		br.Data = resp
-		fmt.Println("source my classify")
-		return
-	}
-
-	rootList, err := material.GetMaterialClassifyAndInfoByParentId(MaterialClassifyId)
-	if err != nil && err.Error() != utils.ErrNoRow() {
-		br.Msg = "获取失败"
-		br.ErrMsg = "获取数据失败,Err:" + err.Error()
-		return
-	}
-
-	classifyAll, err := material.GetMaterialClassifyAndInfoByParentId(MaterialClassifyId)
-	if err != nil && err.Error() != utils.ErrNoRow() {
-		br.Msg = "获取失败"
-		br.ErrMsg = "获取数据失败,Err:" + err.Error()
-		return
-	}
-
-	nodeAll := make([]*material.MaterialClassifyItems, 0)
-	for k := range rootList {
-		rootNode := rootList[k]
-		materialService.MaterialClassifyItemsMakeTreeV2(this.SysUser, classifyAll, rootNode)
-		nodeAll = append(nodeAll, rootNode)
-	}
-
-	//newAll := materialService.MaterialItemsMakeTree(nodeAll, sandListMap, MaterialClassifyId)
-
-	resp.AllNodes = nodeAll
-	br.Ret = 200
-	br.Success = true
-	br.Msg = "获取成功"
-	br.Data = resp
-}
-
 // AddMaterialClassify
 // @Title 新增素材库分类
 // @Description 新增材库分类接口
@@ -978,14 +914,14 @@ func (this *MaterialController) MyChartSaveAsMaterial() {
 			return
 		}
 		if _, ok := namesMap[v.MaterialName]; ok {
-			br.Msg = "素材名称不能重复"
+			br.Msg = "图片名称不能重复"
 			return
 		}
 		namesMap[v.MaterialName] = struct{}{}
 		materialNames = append(materialNames, v.MaterialName)
 	}
 	if len(materialNames) == 0 {
-		br.Msg = "请填写素材名称"
+		br.Msg = "请填写图片名称"
 		return
 	}
 	existList := make([]*material.Material, 0)
@@ -1289,7 +1225,7 @@ func (this *MaterialController) BatchAdd() {
 			return
 		}
 		if _, ok := namesMap[v.MaterialName]; ok {
-			br.Msg = "素材名称不能重复"
+			br.Msg = "图片名称不能重复"
 			return
 		}
 		namesMap[v.MaterialName] = struct{}{}

+ 224 - 0
controllers/residual_analysis/residual_analysis.go

@@ -0,0 +1,224 @@
+package residual_analysis
+
+import (
+	"encoding/json"
+	"eta/eta_api/controllers"
+	"eta/eta_api/models"
+	"eta/eta_api/models/residual_analysis_model"
+	"eta/eta_api/services/residual_analysis_service"
+)
+
+// ResidualAnalysisController 残差分析
+type ResidualAnalysisController struct {
+	controllers.BaseAuthController
+}
+
+// ResidualAnalysisPreview
+// @Title 残差分析预览
+// @Description 残差分析预览
+// @Success 200 {object} residual_analysis_model.ResidualAnalysisResp
+// @router /residual/analysis/preview [post]
+func (this *ResidualAnalysisController) ResidualAnalysisPreview() {
+	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
+	}
+
+	var req residual_analysis_model.ResidualAnalysisReq
+	err := json.Unmarshal(this.Ctx.Input.RequestBody, &req)
+	if err != nil {
+		br.Msg = "参数解析异常!"
+		br.ErrMsg = "参数解析失败,Err:" + err.Error()
+		return
+	}
+
+	resp, err := residual_analysis_service.ResidualAnalysisPreview(req)
+	if err != nil {
+		br.Ret = 408
+		br.Msg = "获取失败"
+		br.ErrMsg = err.Error()
+		return
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+	br.Data = resp
+}
+
+// ContrastPreview
+// @Title 对比指标预览
+// @Description 对比指标预览
+// @Success 200 {object} residual_analysis_model.ResidualAnalysisChartEdbInfoMapping
+// @router /contrast/preview [get]
+func (this *ResidualAnalysisController) ContrastPreview() {
+	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
+	}
+
+	IndexCode := this.GetString("IndexCode")
+	if IndexCode == "" {
+		br.Ret = 408
+		br.Msg = "IndexCode不能为空"
+		br.ErrMsg = "IndexCode不能为空"
+		return
+	}
+
+	resp, err := residual_analysis_service.ContrastPreview(IndexCode)
+	if err != nil {
+		return
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+	br.Data = resp
+}
+
+// SaveResidualAnalysis
+// @Title 保存残差分析指标
+// @Description 保存残差分析指标
+// @Success 200
+// @router /save/residual/analysis [post]
+func (this *ResidualAnalysisController) SaveResidualAnalysis() {
+	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
+	}
+
+	var req residual_analysis_model.ResidualAnalysisIndexSaveReq
+	err := json.Unmarshal(this.Ctx.Input.RequestBody, &req)
+	if err != nil {
+		br.Msg = "参数解析异常!"
+		br.ErrMsg = "参数解析失败,Err:" + err.Error()
+		return
+	}
+
+	if req.ClassifyId == 0 {
+		br.Msg = "分类id不能为空"
+		br.ErrMsg = "分类id不能为空"
+		return
+	}
+
+	err = residual_analysis_service.SaveResidualAnalysis(req, sysUser)
+	if err != nil {
+		br.Ret = 408
+		br.Msg = "获取失败"
+		br.ErrMsg = err.Error()
+		return
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "添加成功"
+}
+
+// SaveResidualAnalysisConfig
+// @Title 保存残差指标配置
+// @Description 保存残差指标配置
+// @Success 200
+// @router /save/residual/analysis/config [post]
+func (this *ResidualAnalysisController) SaveResidualAnalysisConfig() {
+	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
+	}
+
+	var req residual_analysis_model.ResidualAnalysisReq
+	err := json.Unmarshal(this.Ctx.Input.RequestBody, &req)
+	if err != nil {
+		br.Msg = "参数解析异常!"
+		br.ErrMsg = "参数解析失败,Err:" + err.Error()
+		return
+	}
+
+	err = residual_analysis_service.SaveResidualAnalysisConfig(req, sysUser)
+	if err != nil {
+		br.Ret = 408
+		br.Msg = "保存失败"
+		br.ErrMsg = err.Error()
+		return
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "添加成功"
+}
+
+// ResidualAnalysisDetail
+// @Title 残差分析指标详情
+// @Description 残差分析指标详情
+// @Success 200 {object} []*data_manage.EdbInfoList
+// @router /residual/analysis/detail [get]
+func (this *ResidualAnalysisController) ResidualAnalysisDetail() {
+	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
+	}
+
+	EdbInfoId, err := this.GetInt("EdbInfoId")
+	if err != nil {
+		br.Msg = "EdbInfoId参数异常!"
+		br.ErrMsg = "EdbInfoId参数解析失败,Err:" + err.Error()
+	}
+	if EdbInfoId <= 0 {
+		br.Msg = "EdbInfoId参数异常!"
+		br.ErrMsg = "EdbInfoId参数异常!"
+	}
+
+	resp, err := residual_analysis_service.ResidualAnalysisDetail(EdbInfoId)
+	if err != nil {
+		return
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+	br.Data = resp
+}

+ 1 - 127
models/material/material.go

@@ -1,9 +1,7 @@
 package material
 
 import (
-	"eta/eta_api/models/system"
 	"eta/eta_api/utils"
-	"fmt"
 	"github.com/beego/beego/v2/client/orm"
 	"github.com/rdlucklib/rdluck_tools/paging"
 	"time"
@@ -60,28 +58,6 @@ func AddMultiMaterial(materialInfoList []*Material) (err error) {
 	return
 }
 
-// UpdateMaterial 更新素材
-func UpdateMaterial(materialInfo *Material, updateMaterialColumn []string) (err error) {
-	o := orm.NewOrmUsingDB("rddp")
-	to, err := o.Begin()
-	if err != nil {
-		return
-	}
-	defer func() {
-		if err != nil {
-			_ = to.Rollback()
-		} else {
-			_ = to.Commit()
-		}
-	}()
-
-	_, err = to.Update(materialInfo, updateMaterialColumn...)
-	if err != nil {
-		return
-	}
-	return
-}
-
 // MaterialListItem 素材推演列表数据
 type MaterialListItem struct {
 	MaterialId      int    `description:"素材id"`
@@ -99,55 +75,6 @@ type MaterialSaveResp struct {
 	*Material
 }
 
-func GetMaterialAll() (list []*MaterialClassifyItems, err error) {
-	o := orm.NewOrmUsingDB("rddp")
-	sql := `SELECT material_id,classify_id,material_name AS classify_name, sort
-		FROM material `
-	_, err = o.Raw(sql).QueryRows(&list)
-	return
-}
-
-// CheckOpMaterialPermission 判断素材操作权限
-func CheckOpMaterialPermission(sysUser *system.Admin, createUserId int) (ok bool) {
-	if sysUser.RoleTypeCode == utils.ROLE_TYPE_CODE_ADMIN || sysUser.RoleTypeCode == utils.ROLE_TYPE_CODE_FICC_ADMIN {
-		ok = true
-	}
-	// 如果图表创建人与当前操作人相同的话,那么就是允许操作
-	if ok == false && createUserId == sysUser.AdminId {
-		ok = true
-	}
-	return
-}
-
-// GetMaterialInfoByAdminId 获取所有我创建的素材,用于分类展示
-func GetMaterialInfoByAdminId(adminId int) (items []*MaterialClassifyItems, err error) {
-	o := orm.NewOrmUsingDB("rddp")
-	sql := ` SELECT material_id,classify_id,material_name AS classify_name,code,
-             sys_user_id,sys_user_real_name
-            FROM material where sys_user_id = ? ORDER BY sort asc,create_time ASC `
-	_, err = o.Raw(sql, adminId).QueryRows(&items)
-	return
-}
-
-func GetMaterialClassify(classifyId int) (classify_id string, err error) {
-	o := orm.NewOrmUsingDB("rddp")
-	sql := `SELECT GROUP_CONCAT(t.classify_id) AS classify_id FROM (
-			SELECT a.classify_id FROM material_classify AS a 
-			WHERE a.classify_id=?
-			UNION ALL
-			SELECT a.classify_id FROM material_classify AS a 
-			WHERE a.parent_id=? UNION ALL
-	SELECT
-		classify_id 
-	FROM
-		material_classify 
-WHERE
-	parent_id IN ( SELECT classify_id FROM material_classify WHERE parent_id = ? )
-			)AS t`
-	err = o.Raw(sql, classifyId, classifyId, classifyId).QueryRow(&classify_id)
-	return
-}
-
 type MaterialListItems struct {
 	Material
 	ModifyTime string `description:"修改时间"`
@@ -198,20 +125,6 @@ func AddMaterial(item *Material) (lastId int64, err error) {
 	return
 }
 
-type MoveMaterialReq struct {
-	MaterialId     int `description:"素材ID"`
-	PrevMaterialId int `description:"上一个素材ID"`
-	NextMaterialId int `description:"下一个素材ID"`
-	ClassifyId     int `description:"分类id"`
-}
-
-func GetMaterialClassifyCountById(classifyId int) (count int, err error) {
-	o := orm.NewOrmUsingDB("rddp")
-	sql := `SELECT count(1) AS count FROM material_classify WHERE classify_id=? `
-	err = o.Raw(sql, classifyId).QueryRow(&count)
-	return
-}
-
 // GetMaterialByClassifyIdAndName 根据分类id和素材名获取图表信息
 func GetMaterialByClassifyIdAndName(classifyId int, name string) (item *Material, err error) {
 	o := orm.NewOrmUsingDB("rddp")
@@ -228,37 +141,12 @@ func GetMaterialByIds(ids []int) (items []*MaterialListItems, err error) {
 	return
 }
 
-// UpdateMaterialSortByClassifyId 根据素材id更新排序
-func UpdateMaterialSortByClassifyId(classifyId, nowSort, prevMaterialId int, updateSort string) (err error) {
-	o := orm.NewOrmUsingDB("rddp")
-	sql := ` UPDATE  material set sort = ` + updateSort + ` WHERE classify_id=?  AND `
-	if prevMaterialId > 0 {
-		sql += ` (sort > ? or (material_id > ` + fmt.Sprint(prevMaterialId) + ` and sort = ` + fmt.Sprint(nowSort) + `))`
-	}
-	_, err = o.Raw(sql, classifyId, nowSort).Exec()
-	return
-}
-
-// GetFirstMaterialByClassifyId 获取当前分类下,且排序数相同 的排序第一条的数据
-func GetFirstMaterialByClassifyId(classifyId int) (item *Material, err error) {
-	o := orm.NewOrmUsingDB("rddp")
-	sql := ` SELECT * FROM material WHERE classify_id=? order by sort asc,material_id asc limit 1`
-	err = o.Raw(sql, classifyId).QueryRow(&item)
-	return
-}
-
-type DetailParams struct {
-	Code       string `json:"code"`
-	Id         int    `json:"id"`
-	ClassifyId int    `json:"classifyId"`
-}
-
 // SaveAsMaterialReq 添加素材的请求数据
 type SaveAsMaterialReq struct {
 	MaterialName string `description:"素材名称"`
 	ClassifyId   int    `description:"分类id"`
 	ObjectId     int    `description:"对象id"`
-	ObjectType   string `description:"对象类型:chart,excel,sandbox"`
+	ObjectType   string `description:"对象类型:chart,excel,sandbox,sa_doc"`
 }
 
 // MyChartSaveAsMaterialReq 添加素材的
@@ -348,20 +236,6 @@ func GetMaterialByNameEn(materialName string) (item *Material, err error) {
 	return
 }
 
-func GetMaterialCountByName(materialName string) (count int, err error) {
-	o := orm.NewOrmUsingDB("rddp")
-	sql := `SELECT COUNT(1) AS count FROM material WHERE material_name=? `
-	err = o.Raw(sql, materialName).QueryRow(&count)
-	return
-}
-
-func GetMaterialCountByNameEn(materialName string) (count int, err error) {
-	o := orm.NewOrmUsingDB("rddp")
-	sql := `SELECT COUNT(1) AS count FROM material WHERE material_name_en=? `
-	err = o.Raw(sql, materialName).QueryRow(&count)
-	return
-}
-
 // GetMaterialMaxSort 获取最大的排序数
 func GetMaterialMaxSort() (sort int, err error) {
 	o := orm.NewOrmUsingDB("rddp")

+ 176 - 0
models/residual_analysis_model/calculate_residual_analysis_config.go

@@ -0,0 +1,176 @@
+package residual_analysis_model
+
+import (
+	"eta/eta_api/models/data_manage"
+	"github.com/beego/beego/v2/client/orm"
+)
+
+type CalculateResidualAnalysisConfig struct {
+	CalculateResidualAnalysisConfigId int    `orm:"column(calculate_residual_analysis_config_id);pk;auto" description:"自增id"`
+	Config                            string `orm:"column(config)" description:"计算参数配置"`
+	SysUserId                         int    `orm:"column(sys_user_id)" description:"操作人id"`
+	CreateTime                        string `orm:"column(create_time)" description:"创建时间"`
+	ModifyTime                        string `orm:"column(modify_time)" description:"修改时间"`
+}
+
+func init() {
+	orm.RegisterModel(new(CalculateResidualAnalysisConfig))
+}
+
+// ResidualAnalysisReq 残差分析预览请求
+type ResidualAnalysisReq struct {
+	EdbInfoIdA       int     `description:"指标A"`
+	EdbInfoIdB       int     `description:"指标B"`
+	EdbInfoId        int     `description:"残差指标id"`
+	DateType         int     `description:"时间类型 -1-自定义时间 0-至今 n-枚举时间(近n年)"`
+	StartDate        string  `description:"自定义开始日期"`
+	EndDate          string  `description:"自定义结束日期"`
+	IsOrder          bool    `description:"true:正序,false:逆序"`
+	IndexType        int     `description:"1-标准指标 2-领先指标"`
+	LeadValue        int     `description:"领先值"`
+	LeadFrequency    string  `description:"领先频度"`
+	LeftIndexMin     float64 `description:"指标A左侧下限"`
+	LeftIndexMax     float64 `description:"指标A左侧上限"`
+	RightIndexMin    float64 `description:"指标B右侧下限"`
+	RightIndexMax    float64 `description:"指标B右侧上限"`
+	ResidualIndexMin float64 `description:"残差指标下限"`
+	ResidualIndexMax float64 `description:"残差指标上限"`
+	ContrastIndexMin float64 `description:"对比指标下限"`
+	ContrastIndexMax float64 `description:"对比指标上限"`
+}
+
+// ResidualAnalysisResp 残差分析预览响应
+type ResidualAnalysisResp struct {
+	OriginalChartData ChartResp `description:"原始图数据"`
+	MappingChartData  ChartResp `description:"映射图数据"`
+	ResidualChartData ChartResp `description:"残差图数据"`
+}
+
+type ChartResp struct {
+	ChartInfo   *ResidualAnalysisChartInfo
+	EdbInfoList []*ResidualAnalysisChartEdbInfoMapping
+}
+
+type ResidualAnalysisChartInfo struct {
+	ChartName     string  `description:"来源名称"`
+	ChartNameEn   string  `description:"英文图表名称"`
+	Unit          string  `description:"中文单位名称"`
+	UnitEn        string  `description:"英文单位名称"`
+	UniqueCode    string  `description:"图表唯一编码"`
+	DateType      int     `description:"时间类型 0-自定义时间 1-至今 n-枚举时间(近n年)"`
+	StartDate     string  `description:"自定义开始日期"`
+	EndDate       string  `description:"自定义结束日期"`
+	ChartType     int     `description:"生成样式:1:曲线图,2:季节性图"`
+	ChartWidth    float64 `description:"线条大小"`
+	Calendar      string  `description:"公历/农历"`
+	Disabled      int     `description:"是否禁用,0:启用,1:禁用,默认:0"`
+	Source        int     `description:"1:ETA图库;2:商品价格曲线;3:相关性图表"`
+	ChartSource   string  `description:"图表来源str"`
+	ChartSourceEn string  `description:"图表来源(英文)"`
+	SourcesFrom   string  `description:"图表来源"`
+	Instructions  string  `description:"图表说明"`
+}
+
+type ResidualAnalysisChartEdbInfoMapping struct {
+	EdbInfoId           int     `description:"指标id"`
+	SourceName          string  `description:"来源名称"`
+	Source              int     `description:"来源id"`
+	EdbCode             string  `description:"指标编码"`
+	EdbName             string  `description:"指标名称"`
+	EdbNameEn           string  `description:"英文指标名称"`
+	EdbType             int     `description:"指标类型:1:基础指标,2:计算指标"`
+	Frequency           string  `description:"频率"`
+	FrequencyEn         string  `description:"英文频率"`
+	Unit                string  `description:"单位"`
+	UnitEn              string  `description:"英文单位"`
+	StartDate           string  `description:"起始日期"`
+	EndDate             string  `description:"终止日期"`
+	ModifyTime          string  `description:"指标最后更新时间"`
+	MaxData             float64 `description:"上限"`
+	MinData             float64 `description:"下限"`
+	IsOrder             bool    `description:"true:正序,false:逆序"`
+	IsAxis              int     `description:"1:左轴,0:右轴"`
+	EdbInfoType         int     `description:"1:标准指标,0:领先指标"`
+	EdbInfoCategoryType int     `description:"0:普通指标,1:预测指标"`
+	LeadValue           int     `description:"领先值"`
+	LeadUnit            string  `description:"领先单位"`
+	LeadUnitEn          string  `description:"领先英文单位"`
+	ChartStyle          string  `description:"图表类型"`
+	ChartColor          string  `description:"颜色"`
+	PredictChartColor   string  `description:"预测数据的颜色"`
+	ChartWidth          float64 `description:"线条大小"`
+	ChartType           int     `description:"生成样式:1:曲线图,2:季节性图,3:面积图,4:柱状图,5:散点图,6:组合图,7:柱方图,8:商品价格曲线图,9:相关性图"`
+	LatestDate          string  `description:"数据最新日期"`
+	LatestValue         float64 `description:"数据最新值"`
+	MinValue            float64 `json:"-" description:"最小值"`
+	MaxValue            float64 `json:"-" description:"最大值"`
+	DataList            interface{}
+}
+
+type ResidualAnalysisIndexSaveReq struct {
+	EdbCode      string                 `description:"指标编码"`
+	EdbName      string                 `description:"指标名称"`
+	EdbNameEn    string                 `description:"英文指标名称"`
+	EdbType      int                    `description:"指标类型:1:基础指标,2:计算指标"`
+	Frequency    string                 `description:"频率"`
+	FrequencyEn  string                 `description:"英文频率"`
+	Unit         string                 `description:"单位"`
+	UnitEn       string                 `description:"英文单位"`
+	ClassifyId   int                    `description:"分类id"`
+	ConfigId     int                    `description:"残差配置id"`
+	ResidualType int                    `orm:"column(residual_type)" description:"残差类型: 1-映射残差 2-拟合残差"`
+	DataList     []ResidualAnalysisData `description:"指标数据"`
+}
+
+// ResidualAnalysisConfigVo 残差分析配置vo
+type ResidualAnalysisConfigVo struct {
+	DateType         int     `description:"时间类型 -1-自定义时间 0-至今 n-枚举时间(近n年)"`
+	StartDate        string  `description:"自定义开始日期"`
+	EndDate          string  `description:"自定义结束日期"`
+	IsOrder          bool    `description:"true:正序,false:逆序"`
+	IndexType        int     `description:"1-标准指标 2-领先指标"`
+	LeadValue        int     `description:"领先值"`
+	LeadFrequency    string  `description:"领先频度"`
+	LeftIndexMin     float64 `description:"指标A左侧下限"`
+	LeftIndexMax     float64 `description:"指标A左侧上限"`
+	RightIndexMin    float64 `description:"指标B右侧下限"`
+	RightIndexMax    float64 `description:"指标B右侧上限"`
+	ResidualIndexMin float64 `description:"残差指标下限"`
+	ResidualIndexMax float64 `description:"残差指标上限"`
+	ContrastIndexMin float64 `description:"对比指标下限"`
+	ContrastIndexMax float64 `description:"对比指标上限"`
+}
+
+// ResidualAnalysisDetailResp 详情响应接口
+type ResidualAnalysisDetailResp struct {
+	ConfigInfo  *CalculateResidualAnalysisConfig
+	EdbInfoList []*data_manage.EdbInfoList
+}
+
+// GetResidualAnalysisConfigById 根据配置id查询配置信息
+func GetResidualAnalysisConfigById(configId int) (residualAnalysisConfig CalculateResidualAnalysisConfig, err error) {
+	o := orm.NewOrm()
+	sql := "SELECT * FROM residual_analysis_config WHERE calculate_residual_analysis_config_id=?"
+	err = o.Raw(sql, configId).QueryRow(&residualAnalysisConfig)
+	return residualAnalysisConfig, nil
+}
+
+// UpdateResidualAnalysisConfig 更新配置信息
+func UpdateResidualAnalysisConfig(config CalculateResidualAnalysisConfig) (err error) {
+	o := orm.NewOrm()
+	_, err = o.Update(config)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+// SaveResidualAnalysisConfig 保存配置信息
+func SaveResidualAnalysisConfig(config CalculateResidualAnalysisConfig) (id int64, err error) {
+	o := orm.NewOrm()
+	id, err = o.Insert(config)
+	if err != nil {
+		return 0, err
+	}
+	return id, nil
+}

+ 46 - 0
models/residual_analysis_model/calculate_residual_analysis_config_mapping.go

@@ -0,0 +1,46 @@
+package residual_analysis_model
+
+import "github.com/beego/beego/v2/client/orm"
+
+type CalculateResidualAnalysisConfigMapping struct {
+	CalculateResidualAnalysisConfigMappingId int    `orm:"column(calculate_residual_analysis_config_mapping_id);pk;auto" description:"自增id"`
+	CalculateResidualAnalysisConfigId        int    `orm:"column(calculate_residual_analysis_config_id)" description:"残差分析配置id"`
+	EdbInfoId                                int64  `orm:"column(edb_info_id)" description:"指标id"`
+	ResidualType                             int    `orm:"column(residual_type)" description:"残差类型: 1-映射残差 2-拟合残差"`
+	CreateTime                               string `orm:"column(create_time)" description:"创建时间"`
+	ModifyTime                               string `orm:"column(modify_time)" description:"修改时间"`
+}
+
+func init() {
+	orm.RegisterModel(new(CalculateResidualAnalysisConfigMapping))
+}
+
+// GetConfigMappingListByConfigId 根据配置配置id查询配置信息
+func GetConfigMappingListByConfigId(configId int) (items []CalculateResidualAnalysisConfigMapping, err error) {
+	o := orm.NewOrmUsingDB("data")
+	sql := `select * from calculate_residual_analysis_config_mapping where calculate_residual_analysis_config_id = ?`
+
+	_, err = o.Raw(sql, configId).QueryRows(&items)
+	return items, nil
+}
+
+// GetConfigMappingListByCondition 通过条件查询指标配置映射
+func GetConfigMappingListByCondition(condition string, pars []interface{}) (items []CalculateResidualAnalysisConfigMapping, err error) {
+	o := orm.NewOrmUsingDB("data")
+	sql := `select * from calculate_residual_analysis_config_mapping where 1=1 `
+
+	sql += condition
+
+	_, err = o.Raw(sql, pars).QueryRows(&items)
+	return items, nil
+}
+
+// SaveConfigMapping 保存指标和配置的映射关系
+func SaveConfigMapping(mapping CalculateResidualAnalysisConfigMapping) (id int64, err error) {
+	o := orm.NewOrm()
+	id, err = o.Insert(mapping)
+	if err != nil {
+		return 0, err
+	}
+	return id, nil
+}

+ 35 - 0
models/residual_analysis_model/edb_data_residual_analysis.go

@@ -0,0 +1,35 @@
+package residual_analysis_model
+
+import "github.com/beego/beego/v2/client/orm"
+
+type ResidualAnalysisData struct {
+	EdbDataId     int     `orm:"column(edb_data_id);pk;auto" description:"自增id"`
+	EdbInfoId     int     `orm:"column(edb_info_id)" description:"指标id"`
+	EdbCode       string  `orm:"column(edb_code)" description:"指标编码"`
+	DataTime      string  `orm:"column(data_time)" description:"数据日期"`
+	Value         float64 `orm:"column(value)" description:"数据值"`
+	DataTimeStamp int64   `orm:"column(data_timestamp)"`
+}
+
+func init() {
+	orm.RegisterModel(new(ResidualAnalysisData))
+}
+
+// DeleteResidualAnalysisDataByEdbCode 根据指标编码删除数据
+func DeleteResidualAnalysisDataByEdbCode(edbCode string) error {
+	o := orm.NewOrmUsingDB("data")
+	sql := `delete from edb_data_residual_analysis where edb_code = ?`
+	_, err := o.Raw(sql, edbCode).Exec()
+	return err
+}
+
+// AddResidualAnalysisData 新增指标数据
+func AddResidualAnalysisData(dataList []ResidualAnalysisData) (num int64, err error) {
+	o := orm.NewOrmUsingDB("data")
+	num, err = o.InsertMulti(len(dataList), dataList)
+	if err != nil {
+		return 0, err
+	}
+
+	return num, nil
+}

+ 45 - 9
routers/commentsRouter.go

@@ -7927,15 +7927,6 @@ func init() {
             Filters: nil,
             Params: nil})
 
-    beego.GlobalControllerRouter["eta/eta_api/controllers/material:MaterialController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/material:MaterialController"],
-        beego.ControllerComments{
-            Method: "ClassifyMaterialItems",
-            Router: `/classify/materialList`,
-            AllowHTTPMethods: []string{"get"},
-            MethodParams: param.Make(),
-            Filters: nil,
-            Params: nil})
-
     beego.GlobalControllerRouter["eta/eta_api/controllers/material:MaterialController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/material:MaterialController"],
         beego.ControllerComments{
             Method: "ClassifyMove",
@@ -8143,6 +8134,51 @@ func init() {
             Filters: nil,
             Params: nil})
 
+    beego.GlobalControllerRouter["eta/eta_api/controllers/residual_analysis:ResidualAnalysisController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/residual_analysis:ResidualAnalysisController"],
+        beego.ControllerComments{
+            Method: "ContrastPreview",
+            Router: `/contrast/preview`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/residual_analysis:ResidualAnalysisController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/residual_analysis:ResidualAnalysisController"],
+        beego.ControllerComments{
+            Method: "ResidualAnalysisDetail",
+            Router: `/residual/analysis/detail`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/residual_analysis:ResidualAnalysisController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/residual_analysis:ResidualAnalysisController"],
+        beego.ControllerComments{
+            Method: "ResidualAnalysisPreview",
+            Router: `/residual/analysis/preview`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/residual_analysis:ResidualAnalysisController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/residual_analysis:ResidualAnalysisController"],
+        beego.ControllerComments{
+            Method: "SaveResidualAnalysis",
+            Router: `/save/residual/analysis`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/residual_analysis:ResidualAnalysisController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/residual_analysis:ResidualAnalysisController"],
+        beego.ControllerComments{
+            Method: "SaveResidualAnalysisConfig",
+            Router: `/save/residual/analysis/config`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
     beego.GlobalControllerRouter["eta/eta_api/controllers/roadshow:CalendarController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/roadshow:CalendarController"],
         beego.ControllerComments{
             Method: "ResearcherList",

+ 6 - 0
routers/router.go

@@ -30,6 +30,7 @@ import (
 	"eta/eta_api/controllers/fe_calendar"
 	"eta/eta_api/controllers/material"
 	"eta/eta_api/controllers/report_approve"
+	"eta/eta_api/controllers/residual_analysis"
 	"eta/eta_api/controllers/roadshow"
 	"eta/eta_api/controllers/sandbox"
 	"eta/eta_api/controllers/semantic_analysis"
@@ -432,6 +433,11 @@ func init() {
 				&material.MaterialController{},
 			),
 		),
+		web.NSNamespace("/residual_analysis",
+			web.NSInclude(
+				&residual_analysis.ResidualAnalysisController{},
+			),
+		),
 	)
 	web.AddNamespace(ns)
 }

+ 1 - 1
services/data/edb_info.go

@@ -1857,7 +1857,7 @@ func EdbInfoAdd(source, subSource, classifyId int, edbCode, edbName, frequency,
 		edbType = 2 //计算指标
 	}
 	// 从缓存中获取
-	terminalCode, serverUrl, sourceIndexName, e := GetEdbTerminalCodeBySource(edbInfo.Source, edbInfo.EdbCode, edbInfo.StockCode)
+	terminalCode, serverUrl, sourceIndexName, e := GetEdbTerminalCodeBySource(edbInfo.Source, edbCode, edbInfo.StockCode)
 	if e != nil {
 		errMsg = "获取可以使用的终端地址失败"
 		err = errors.New("获取可以使用的终端地址失败,Err:" + e.Error())

+ 22 - 0
services/material/material.go

@@ -6,6 +6,7 @@ import (
 	"eta/eta_api/models/data_manage/excel"
 	"eta/eta_api/models/material"
 	"eta/eta_api/models/sandbox"
+	"eta/eta_api/models/semantic_analysis"
 	"eta/eta_api/models/system"
 	"eta/eta_api/services"
 	_interface "eta/eta_api/services/interface"
@@ -158,6 +159,27 @@ func AddToMaterial(req material.SaveAsMaterialReq, opUserId int, opUserName stri
 			return
 		}
 		oldRsourceUrl = excelInfo.ExcelImage
+	case "sa_doc":
+		// 获取文档封面地址
+		docObj := new(semantic_analysis.SaCompare)
+		e := docObj.GetItemById(req.ObjectId)
+		if e != nil { // 获取文档信息
+			if e.Error() == utils.ErrNoRow() {
+				errMsg = "文档不存在"
+				err = fmt.Errorf("文档不存在")
+				return
+			}
+			errMsg = "获取文档信息失败"
+			err = e
+			return
+		}
+		if docObj.ResultImg == "" {
+			errMsg = "文档封面为空"
+			err = fmt.Errorf("文档封面为空")
+			return
+		}
+
+		oldRsourceUrl = docObj.ResultImg
 	default:
 		errMsg = "不支持的类型"
 		err = fmt.Errorf("不支持的类型")

+ 509 - 0
services/residual_analysis_service/residual_analysis_service.go

@@ -0,0 +1,509 @@
+package residual_analysis_service
+
+import (
+	"encoding/json"
+	"eta/eta_api/models/data_manage"
+	"eta/eta_api/models/residual_analysis_model"
+	"eta/eta_api/models/system"
+	"eta/eta_api/utils"
+	"fmt"
+	"time"
+)
+
+func ResidualAnalysisPreview(req residual_analysis_model.ResidualAnalysisReq) (residual_analysis_model.ResidualAnalysisResp, error) {
+
+	if req.DateType < 0 {
+		return residual_analysis_model.ResidualAnalysisResp{}, fmt.Errorf("时间类型错误", nil)
+	}
+
+	mappingList, err := data_manage.GetChartEdbMappingListByEdbInfoIdList([]int{req.EdbInfoIdA, req.EdbInfoIdB})
+	if err != nil {
+		return residual_analysis_model.ResidualAnalysisResp{}, fmt.Errorf("获取图表,指标信息失败,Err:%s", err.Error())
+	}
+
+	var edbInfoMappingA, edbInfoMappingB *data_manage.ChartEdbInfoMapping
+	for _, v := range mappingList {
+		if v.EdbInfoId == req.EdbInfoIdA {
+			edbInfoMappingA = v
+		}
+		if v.EdbInfoId == req.EdbInfoIdB {
+			edbInfoMappingB = v
+		}
+	}
+	if edbInfoMappingA == nil {
+		return residual_analysis_model.ResidualAnalysisResp{}, fmt.Errorf("指标A不存在", nil)
+	}
+	if edbInfoMappingB == nil {
+		return residual_analysis_model.ResidualAnalysisResp{}, fmt.Errorf("指标B不存在", nil)
+	}
+
+	// 时间处理
+	var startDate, endDate string
+	switch req.DateType {
+	case 0:
+		startDate = req.StartDate
+		endDate = req.EndDate
+	case 1:
+		startDate = req.StartDate
+		endDate = ""
+	default:
+		startDate = utils.GetPreYear(req.DateType)
+		endDate = ""
+	}
+
+	resp := residual_analysis_model.ResidualAnalysisResp{}
+
+	// 图表基础信息
+	baseChartInfo := new(residual_analysis_model.ResidualAnalysisChartInfo)
+	baseChartInfo.Calendar = `公历`
+	baseChartInfo.Source = utils.CHART_SOURCE_DEFAULT
+	baseChartInfo.DateType = req.DateType
+	baseChartInfo.StartDate = startDate
+	baseChartInfo.EndDate = endDate
+	baseChartInfo.ChartType = utils.CHART_TYPE_CURVE
+
+	// 原始图表信息
+	var OriginalEdbList []*residual_analysis_model.ResidualAnalysisChartEdbInfoMapping
+
+	OriginalEdbList, err = fillOriginalChart(req, mappingList, startDate, endDate, edbInfoMappingA, edbInfoMappingB, OriginalEdbList)
+	if err != nil {
+		return residual_analysis_model.ResidualAnalysisResp{}, err
+	}
+	baseChartInfo.ChartName = edbInfoMappingA.EdbName + "与" + edbInfoMappingB.EdbName
+
+	resp.OriginalChartData = residual_analysis_model.ChartResp{
+		ChartInfo:   baseChartInfo,
+		EdbInfoList: OriginalEdbList,
+	}
+
+	dataAList, ok := edbInfoMappingA.DataList.([]data_manage.EdbDataList)
+	if !ok {
+		return residual_analysis_model.ResidualAnalysisResp{}, fmt.Errorf("数据类型转换失败", nil)
+	}
+	indexADataMap := map[string]data_manage.EdbDataList{}
+	for _, indexData := range dataAList {
+		indexADataMap[indexData.DataTime] = indexData
+	}
+
+	// 映射图表信息
+	mappingEdbList, err := fillMappingChartInfo(req, edbInfoMappingA, edbInfoMappingB, OriginalEdbList, indexADataMap)
+	if err != nil {
+		return residual_analysis_model.ResidualAnalysisResp{}, err
+	}
+	baseChartInfo.ChartName = edbInfoMappingA.EdbName + "与" + edbInfoMappingB.EdbName + "映射" + edbInfoMappingA.EdbName
+
+	resp.MappingChartData = residual_analysis_model.ChartResp{
+		ChartInfo:   baseChartInfo,
+		EdbInfoList: mappingEdbList,
+	}
+
+	// 残差图表信息
+	ResidualEdbList, err := fillResidualChartInfo(edbInfoMappingA, edbInfoMappingB, mappingEdbList)
+	if err != nil {
+		return residual_analysis_model.ResidualAnalysisResp{}, err
+	}
+
+	baseChartInfo.ChartName = edbInfoMappingA.EdbName + "与" + edbInfoMappingA.EdbName + "映射残差/" + edbInfoMappingB.EdbName
+
+	resp.MappingChartData = residual_analysis_model.ChartResp{
+		ChartInfo:   baseChartInfo,
+		EdbInfoList: ResidualEdbList,
+	}
+
+	return resp, nil
+}
+
+func fillResidualChartInfo(edbInfoMappingA *data_manage.ChartEdbInfoMapping, edbInfoMappingB *data_manage.ChartEdbInfoMapping, mappingEdbList []*residual_analysis_model.ResidualAnalysisChartEdbInfoMapping) ([]*residual_analysis_model.ResidualAnalysisChartEdbInfoMapping, error) {
+	// 计算公式 映射残差 = 因变量指标 - 映射指标
+	var edbInfoA, edbInfoB *residual_analysis_model.ResidualAnalysisChartEdbInfoMapping
+	if mappingEdbList[0].EdbInfoId == edbInfoMappingA.EdbInfoId {
+		edbInfoA = mappingEdbList[0]
+		edbInfoB = mappingEdbList[1]
+	} else {
+		edbInfoA = mappingEdbList[1]
+		edbInfoB = mappingEdbList[0]
+	}
+	dataAList, ok := edbInfoA.DataList.([]data_manage.EdbDataList)
+	if !ok {
+		return nil, fmt.Errorf("数据类型转换失败", nil)
+	}
+	edbData := dataAList
+	dataBList, ok := edbInfoB.DataList.([]data_manage.EdbDataList)
+	if !ok {
+		return nil, fmt.Errorf("数据类型转换失败", nil)
+	}
+	var indexDataBMap map[string]data_manage.EdbDataList
+	for _, data := range dataBList {
+		indexDataBMap[data.DataTime] = data
+	}
+	var valueB float64
+	for _, indexData := range edbData {
+		if dataB, ok := indexDataBMap[indexData.DataTime]; ok {
+			valueB = dataB.Value
+		} else {
+			valueB = 0
+		}
+		indexData.Value = indexData.Value - valueB
+	}
+	for _, mapping := range mappingEdbList {
+		if mapping.EdbInfoId != edbInfoMappingA.EdbInfoId {
+			mapping.DataList = edbData
+			mapping.EdbName = edbInfoMappingA.EdbName + "映射残差/" + edbInfoMappingB.EdbName
+		}
+	}
+
+	return mappingEdbList, nil
+}
+
+func fillMappingChartInfo(req residual_analysis_model.ResidualAnalysisReq, edbInfoMappingA *data_manage.ChartEdbInfoMapping, edbInfoMappingB *data_manage.ChartEdbInfoMapping, originalEdbList []*residual_analysis_model.ResidualAnalysisChartEdbInfoMapping, indexADataMap map[string]data_manage.EdbDataList) ([]*residual_analysis_model.ResidualAnalysisChartEdbInfoMapping, error) {
+	// 计算公式:Y=aX+b,Y为映射后的指标,X为自变量指标
+	// 正序:a=(L2-L1)/(R2-R1)	b=L2-R2*a
+	// 逆序:a=(L2-L1)/(R1-R2)	b=L2-R1*a
+	// L2:左轴下限 R2:右轴上限 L1:左轴上限 R1:右轴下限
+	var a, b float64
+	if req.IsOrder {
+		a = (req.LeftIndexMax - req.LeftIndexMin) / (req.RightIndexMax - req.RightIndexMin)
+		b = req.LeftIndexMax - req.RightIndexMax*a
+	} else {
+		a = (req.LeftIndexMax - req.LeftIndexMin) / (req.RightIndexMin - req.RightIndexMax)
+		b = req.LeftIndexMax - req.RightIndexMin*a
+	}
+
+	dataList, ok := edbInfoMappingB.DataList.([]data_manage.EdbDataList)
+	if !ok {
+		return nil, fmt.Errorf("数据类型转换失败", nil)
+	}
+
+	// 领先指标 dataList进行数据处理
+	if req.IndexType == 2 {
+		if req.LeadValue < 0 {
+			return nil, fmt.Errorf("领先值不能小于0", nil)
+		} else if req.LeadValue > 0 {
+			for _, indexData := range dataList {
+				switch req.LeadFrequency {
+				case "天":
+					indexData.DataTime = utils.GetNextDayN(indexData.DataTime, req.LeadValue)
+				case "周":
+					indexData.DataTime = utils.GetNextDayN(indexData.DataTime, req.LeadValue*7)
+				case "月":
+					indexData.DataTime = utils.TimeToString(utils.AddDate(utils.StringToTime(indexData.DataTime), 0, req.LeadValue), utils.YearMonthDay)
+				case "季":
+					indexData.DataTime = utils.TimeToString(utils.AddDate(utils.StringToTime(indexData.DataTime), 0, req.LeadValue*3), utils.YearMonthDay)
+				case "年":
+					indexData.DataTime = utils.TimeToString(utils.AddDate(utils.StringToTime(indexData.DataTime), req.LeadValue, 0), utils.YearMonthDay)
+				}
+			}
+		}
+	}
+
+	for index, indexData := range dataList {
+		// 计算映射值
+		indexData.Value = a*indexData.Value + b
+
+		// 从最早时间开始 补充时间为自然日
+		beforeIndexData := dataList[index]
+		afterIndexData := dataList[index+1]
+		if utils.IsMoreThanOneDay(beforeIndexData.DataTime, afterIndexData.DataTime) {
+			replenishIndexData := data_manage.EdbDataList{
+				DataTime:      utils.GetNextDay(beforeIndexData.DataTime),
+				DataTimestamp: time.Now().UnixMilli(),
+				Value:         beforeIndexData.Value,
+			}
+			dataList = append(dataList, replenishIndexData)
+		}
+	}
+	// 根据指标A的时间key,在B的映射指标中筛选出对应的值
+	var dataBList []data_manage.EdbDataList
+	for _, indexData := range dataList {
+		if _, ok := indexADataMap[indexData.DataTime]; ok {
+			dataBList = append(dataBList, indexData)
+		}
+	}
+
+	mappingEdbList := originalEdbList
+	for _, mapping := range mappingEdbList {
+		if mapping.EdbInfoId == req.EdbInfoIdB {
+			mapping.EdbInfoId = 0
+			mapping.EdbCode = ""
+			mapping.EdbName = edbInfoMappingB.EdbName + "映射" + edbInfoMappingA.EdbName
+			mapping.DataList = dataList
+		}
+	}
+	return mappingEdbList, nil
+}
+
+func fillOriginalChart(req residual_analysis_model.ResidualAnalysisReq, mappingList []*data_manage.ChartEdbInfoMapping, startDate string, endDate string, edbInfoMappingA *data_manage.ChartEdbInfoMapping, edbInfoMappingB *data_manage.ChartEdbInfoMapping, OriginalEdbList []*residual_analysis_model.ResidualAnalysisChartEdbInfoMapping) ([]*residual_analysis_model.ResidualAnalysisChartEdbInfoMapping, error) {
+	for _, v := range mappingList {
+		var edbInfoMapping *residual_analysis_model.ResidualAnalysisChartEdbInfoMapping
+		edbInfoMapping.EdbInfoType = 1
+		edbInfoMapping.IsOrder = false
+		edbInfoMapping.IsAxis = 1
+		edbInfoMapping.ChartColor = `#00F`
+		edbInfoMapping.ChartWidth = 3
+
+		// 获取图表中的指标数据
+		dataList, err := data_manage.GetEdbDataList(v.Source, v.SubSource, v.EdbInfoId, startDate, endDate)
+		if err != nil {
+			return nil, fmt.Errorf("获取指标数据失败,Err:%s", err.Error())
+		}
+
+		if v.EdbInfoId == req.EdbInfoIdB {
+			edbInfoMapping.LeadValue = req.LeadValue
+			edbInfoMapping.LeadUnit = req.LeadFrequency
+			edbInfoMapping.EdbInfoType = req.IndexType
+			edbInfoMapping.IsOrder = req.IsOrder
+			edbInfoMapping.IsAxis = 0
+			edbInfoMapping.ChartColor = `#F00`
+			edbInfoMapping.ChartWidth = 1
+
+			edbInfoMappingB.DataList = dataList
+		} else {
+			edbInfoMappingA.DataList = dataList
+		}
+		edbInfoMapping.EdbInfoId = v.EdbInfoId
+		edbInfoMapping.EdbName = v.EdbName
+		edbInfoMapping.EdbCode = v.EdbCode
+		edbInfoMapping.Unit = v.Unit
+		edbInfoMapping.Frequency = v.Frequency
+		edbInfoMapping.Source = v.Source
+		edbInfoMapping.SourceName = v.SourceName
+
+		edbInfoMapping.DataList = dataList
+		OriginalEdbList = append(OriginalEdbList, edbInfoMapping)
+	}
+	return OriginalEdbList, nil
+}
+
+func ContrastPreview(indexCode string) (residual_analysis_model.ResidualAnalysisChartEdbInfoMapping, error) {
+	var condition string
+	var pars []interface{}
+
+	if indexCode != "" {
+		condition += " and edb_code=?"
+		pars = append(pars, indexCode)
+	}
+
+	edbInfo, err := data_manage.GetEdbInfoByCondition(condition, pars)
+	if err != nil {
+		return residual_analysis_model.ResidualAnalysisChartEdbInfoMapping{}, err
+	}
+	if edbInfo == nil {
+		return residual_analysis_model.ResidualAnalysisChartEdbInfoMapping{}, fmt.Errorf("指标不存在")
+	}
+
+	dataList, err := data_manage.GetEdbDataList(edbInfo.Source, edbInfo.SubSource, edbInfo.EdbInfoId, "", "")
+
+	var resp residual_analysis_model.ResidualAnalysisChartEdbInfoMapping
+	resp.EdbInfoId = edbInfo.EdbInfoId
+	resp.SourceName = edbInfo.SourceName
+	resp.EdbName = edbInfo.EdbName
+	resp.Unit = edbInfo.Unit
+	resp.Frequency = edbInfo.Frequency
+	resp.DataList = dataList
+	return resp, nil
+}
+
+func SaveResidualAnalysis(req residual_analysis_model.ResidualAnalysisIndexSaveReq, sysUser *system.Admin) error {
+
+	// 验证分类是否存在
+	classifyCount, err := data_manage.GetEdbClassifyCountById(req.ClassifyId)
+	if err != nil {
+		return err
+	}
+	if classifyCount <= 0 {
+		return fmt.Errorf("分类不存在", nil)
+	}
+
+	// 更新or新增
+	if req.EdbCode != "" {
+		// 查询指标库指标
+		edbInfo, err := data_manage.GetEdbInfoByEdbCode(utils.DATA_SOURCE_RESIDUAL_ANALYSIS, req.EdbCode)
+		if err != nil {
+			return err
+		}
+		if edbInfo == nil {
+			return fmt.Errorf("指标不存在", nil)
+		}
+		// todo 须补充更新指标最大值,最小值,数据最新时间,
+		err = edbInfo.Update([]string{"min_value", "max_value", "latest_date", "latest_value"})
+		if err != nil {
+			return err
+		}
+
+		// 删除对应得指标数据
+		err = residual_analysis_model.DeleteResidualAnalysisDataByEdbCode(req.EdbCode)
+		if err != nil {
+			return fmt.Errorf("删除指标数据失败", nil)
+		}
+	}
+	// 新增指标
+	edbCode, err := utils.GenerateEdbCode(1, "")
+	if err != nil {
+		return err
+	}
+
+	_, err = data_manage.AddEdbInfo(&data_manage.EdbInfo{
+		EdbCode:    edbCode,
+		EdbName:    req.EdbName,
+		EdbNameEn:  req.EdbNameEn,
+		EdbType:    req.EdbType,
+		Unit:       req.Unit,
+		UnitEn:     req.UnitEn,
+		Frequency:  req.Frequency,
+		Source:     utils.DATA_SOURCE_RESIDUAL_ANALYSIS,
+		SourceName: "残差分析",
+	})
+	if err != nil {
+		return err
+	}
+
+	// 新增数据
+	edbInfoId, err := residual_analysis_model.AddResidualAnalysisData(req.DataList)
+	if err != nil {
+		return err
+	}
+
+	// 更新保存指标和配置的映射关系
+	mappingList, err := residual_analysis_model.GetConfigMappingListByConfigId(req.ConfigId)
+	if err != nil {
+		return err
+	}
+	flag := true
+	for _, mapping := range mappingList {
+		if mapping.CalculateResidualAnalysisConfigId == req.ConfigId {
+			flag = false
+		}
+	}
+
+	if flag {
+		_, err = residual_analysis_model.SaveConfigMapping(residual_analysis_model.CalculateResidualAnalysisConfigMapping{
+			CalculateResidualAnalysisConfigId: req.ConfigId,
+			EdbInfoId:                         edbInfoId,
+			ResidualType:                      req.ResidualType,
+		})
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func ResidualAnalysisDetail(edbInfoId int) (residual_analysis_model.ResidualAnalysisDetailResp, error) {
+	// 通过指标配置映射表 拿到配置id,再获取关联的所有指标信息
+	var condition string
+	var pars []interface{}
+
+	condition += " and edb_info_id=?"
+	pars = append(pars, edbInfoId)
+
+	mappingList, err := residual_analysis_model.GetConfigMappingListByCondition(condition, pars)
+	if err != nil {
+		return residual_analysis_model.ResidualAnalysisDetailResp{}, err
+	}
+
+	if len(mappingList) <= 0 {
+		return residual_analysis_model.ResidualAnalysisDetailResp{}, fmt.Errorf("指标不存在", nil)
+	}
+
+	mapping := mappingList[0]
+
+	configMappingList, err := residual_analysis_model.GetConfigMappingListByConfigId(mapping.CalculateResidualAnalysisConfigId)
+	if err != nil {
+		return residual_analysis_model.ResidualAnalysisDetailResp{}, err
+	}
+
+	var edbInfoIdList []int64
+	for _, v := range configMappingList {
+		edbInfoIdList = append(edbInfoIdList, v.EdbInfoId)
+	}
+
+	condition = ""
+	pars = []interface{}{}
+
+	condition += ` and edb_info_id in(` + utils.GetOrmInReplace(len(edbInfoIdList)) + `)`
+	for _, id := range edbInfoIdList {
+		pars = append(pars, id)
+	}
+
+	edbInfoList, err := data_manage.GetEdbInfoListByCond(condition, pars)
+	if err != nil {
+		return residual_analysis_model.ResidualAnalysisDetailResp{}, err
+	}
+
+	// 获取配置
+	configInfo, err := residual_analysis_model.GetResidualAnalysisConfigById(mapping.CalculateResidualAnalysisConfigId)
+	if err != nil {
+		return residual_analysis_model.ResidualAnalysisDetailResp{}, err
+	}
+
+	resp := residual_analysis_model.ResidualAnalysisDetailResp{
+		ConfigInfo:  &configInfo,
+		EdbInfoList: edbInfoList,
+	}
+
+	return resp, nil
+}
+
+func SaveResidualAnalysisConfig(req residual_analysis_model.ResidualAnalysisReq, sysUser *system.Admin) error {
+
+	config := residual_analysis_model.ResidualAnalysisConfigVo{
+		DateType:         req.DateType,
+		StartDate:        req.StartDate,
+		EndDate:          req.EndDate,
+		IsOrder:          req.IsOrder,
+		IndexType:        req.IndexType,
+		LeadValue:        req.LeadValue,
+		LeadFrequency:    req.LeadFrequency,
+		LeftIndexMin:     req.LeftIndexMin,
+		LeftIndexMax:     req.LeftIndexMax,
+		RightIndexMin:    req.RightIndexMin,
+		RightIndexMax:    req.RightIndexMax,
+		ResidualIndexMin: req.ResidualIndexMin,
+		ResidualIndexMax: req.ResidualIndexMax,
+		ContrastIndexMin: req.ContrastIndexMin,
+		ContrastIndexMax: req.ContrastIndexMax,
+	}
+
+	// 转换为json格式
+	configJson, err := json.Marshal(config)
+	if err != nil {
+		return err
+	}
+
+	var condition string
+	var pars []interface{}
+	// 新增or更新
+	if req.EdbInfoId > 0 {
+		condition += " and edb_info_id=?"
+		pars = append(pars, req.EdbInfoId)
+		configMappings, err := residual_analysis_model.GetConfigMappingListByCondition(condition, pars)
+		if err != nil {
+			return err
+		}
+		if len(configMappings) > 0 {
+			mapping := configMappings[0]
+			configInfo, err := residual_analysis_model.GetResidualAnalysisConfigById(mapping.CalculateResidualAnalysisConfigId)
+			if err != nil {
+				return err
+			}
+			configInfo.Config = string(configJson)
+			err = residual_analysis_model.UpdateResidualAnalysisConfig(configInfo)
+			if err != nil {
+				return err
+			}
+		}
+	}
+
+	analysisConfig := residual_analysis_model.CalculateResidualAnalysisConfig{
+		Config:    string(configJson),
+		SysUserId: sysUser.AdminId,
+	}
+
+	_, err = residual_analysis_model.SaveResidualAnalysisConfig(analysisConfig)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}

+ 2 - 1
utils/constants.go

@@ -187,7 +187,8 @@ const (
 	DATA_SOURCE_TRADE_ANALYSIS                       = 92       // 持仓分析
 	DATA_SOURCE_USDA_FAS                             = 96       //美国农业部->96
 	DATA_SOURCE_RZD                                  = 97       // 睿姿得数据
-	DATA_SOURCE_CALCULATE_STL                        = 98       // STL趋势分解 -> 98
+	DATA_SOURCE_CALCULATE_STL                                  = 93       // 泛糖科技 -> 93
+	DATA_SOURCE_RESIDUAL_ANALYSIS                    = 99       // 残差分析
 )
 
 // 数据刷新频率

+ 120 - 0
utils/date_util.go

@@ -0,0 +1,120 @@
+package utils
+
+import (
+	"strconv"
+	"time"
+)
+
+// 定义时间格式常量
+const (
+	YearMonthDay     = "2006-01-02"                     // yyyy-MM-dd
+	YearMonthDayTime = "2006-01-02 15:04:05"            // yyyy-MM-dd HH:mm:ss
+	MonthDay         = "01-02"                          // MM-dd
+	DayMonthYear     = "02-01-2006"                     // dd-MM-yyyy
+	YearMonth        = "2006-01"                        // yyyy-MM
+	FullDate         = "Monday, 02-Jan-06 15:04:05 PST" // 完整日期:例如:Monday, 02-Jan-06 15:04:05 PST
+)
+
+// GetPreYear 获取当前时间 前n年的时间
+func GetPreYear(n int) string {
+	now := time.Now()
+	year := now.Year() - n
+	return strconv.Itoa(year)
+}
+
+// IsMoreThanOneDay 判断两个yyyy-MM-dd类型的时间,相差是否大于1天
+func IsMoreThanOneDay(startDate, endDate string) bool {
+	startTime, _ := time.Parse("2006-01-02", startDate)
+	endTime, _ := time.Parse("2006-01-02", endDate)
+	diff := endTime.Sub(startTime)
+	days := diff.Hours() / 24
+	return days > 1
+}
+
+// GetNextDay 获取 yyyy-MM-dd类型的时间的下一天
+func GetNextDay(date string) string {
+	t, _ := time.Parse("2006-01-02", date)
+	nextDay := t.AddDate(0, 0, 1)
+	return nextDay.Format("2006-01-02")
+}
+
+// GetNextDayN 获取 yyyy-MM-dd 类型的时间的下n天
+func GetNextDayN(date string, n int) string {
+	t, _ := time.Parse("2006-01-02", date)
+	nextDay := t.AddDate(0, 0, n)
+	return nextDay.Format("2006-01-02")
+}
+
+var daysOfMonth = [...]int{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
+
+// AddDate 解决 Go time 包 AddDate() 添加年份/月份溢出到下一个月的问题。
+// 例如:
+//
+//	2024-02-29 AddDate(1, 0, 0) 期望结果: 2025-02-28
+//	2024-08-31 AddDate(0, 1, 1) 期望结果: 2024-09-30
+func AddDate(t time.Time, years, months int) time.Time {
+	month := t.Month()
+
+	// 规范年份和月份
+	years, months = norm(years, months, 12)
+
+	// 计算目标月份
+	targetMonth := int(month) + months
+	if targetMonth <= 0 {
+		// 处理负值月份
+		targetMonth += 12 * ((-targetMonth)/12 + 1)
+	}
+	// 取余计算目标月份
+	targetMonth = (targetMonth-1)%12 + 1
+
+	// 计算目标年份
+	targetYear := t.Year() + years + (int(month)+months)/12
+
+	// 计算目标月份最大天数
+	maxDayOfTargetMonth := daysOfMonth[targetMonth-1]
+	if isLeap(targetYear) && targetMonth == 2 {
+		maxDayOfTargetMonth++ // 闰年2月多一天
+	}
+
+	// 计算目标日期
+	targetDay := t.Day()
+	if targetDay > maxDayOfTargetMonth {
+		// 如果目标日期超出该月的天数,设置为该月的最后一天
+		targetDay = maxDayOfTargetMonth
+	}
+
+	// 返回新的日期
+	return time.Date(targetYear, time.Month(targetMonth), targetDay, t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location())
+}
+
+// norm 规范化年份和月份
+func norm(hi, lo, base int) (nhi, nlo int) {
+	if lo < 0 {
+		n := (-lo-1)/base + 1
+		hi -= n
+		lo += n * base
+	}
+	if lo >= base {
+		n := lo / base
+		hi += n
+		lo -= n * base
+	}
+	return hi, lo
+}
+
+// isLeap 判断是否为闰年
+func isLeap(year int) bool {
+	return year%4 == 0 && (year%100 != 0 || year%400 == 0)
+}
+
+// StringToTime string 类型时间 转换为 time.Time 类型
+func StringToTime(date string) time.Time {
+	t, _ := time.Parse("2006-01-02", date)
+	return t
+}
+
+// TimeToString time.Time 类型时间 转换为 string 类型
+func TimeToString(t time.Time, format string) string {
+	formattedTime := t.Format(format)
+	return formattedTime
+}