Bläddra i källkod

Merge branch 'eta_2.2.9_residual_analysis_1104@guomengyuan'

# Conflicts:
#	controllers/data_manage/multiple_graph_config.go
#	models/data_manage/edb_info.go
#	routers/router.go
#	utils/constants.go
gmy 2 veckor sedan
förälder
incheckning
2495d270f3

+ 1 - 1
controllers/data_manage/chart_info.go

@@ -1606,7 +1606,7 @@ func (this *ChartInfoController) ChartInfoDetailV2() {
 	var dateMax time.Time
 	if dateType == utils.DateTypeNYears {
 		for _, v := range mappingList {
-			if v.LatestDate != "" {
+			if v.LatestDate != "" && v.LatestDate != "0000-00-00" {
 				lastDateT, tErr := time.Parse(utils.FormatDate, v.LatestDate)
 				if tErr != nil {
 					br.Msg = "获取失败"

+ 284 - 0
controllers/residual_analysis/residual_analysis_controller.go

@@ -0,0 +1,284 @@
+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
+	}
+
+	if req.EdbInfoIdA == req.EdbInfoIdB {
+		br.Msg = "自变量指标和因变量指标不能相同"
+		br.ErrMsg = "自变量指标和因变量指标不能相同"
+		return
+	}
+
+	resp, err := residual_analysis_service.ResidualAnalysisPreview(req)
+	if err != nil {
+		br.Ret = 403
+		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 = 403
+		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
+	}
+
+	if req.Source == 0 {
+		br.Msg = "来源不能为空"
+		br.ErrMsg = "来源不能为空"
+		return
+	}
+
+	err = residual_analysis_service.SaveResidualAnalysis(req, sysUser)
+	if err != nil {
+		br.Ret = 403
+		br.Msg = err.Error()
+		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
+	}
+
+	configId, err := residual_analysis_service.SaveResidualAnalysisConfig(req, sysUser)
+	if err != nil {
+		br.Ret = 403
+		br.Msg = "保存失败"
+		br.ErrMsg = err.Error()
+		return
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "添加成功"
+	br.Data = configId
+}
+
+// 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 {
+		br.Msg = "获取失败"
+		br.ErrMsg = err.Error()
+		br.Ret = 403
+		return
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+	br.Data = resp
+}
+
+// CheckResidualAnalysisExist
+// @Title 校验残差指标是否存在
+// @Description 校验残差指标是否存在
+// @Success 200 {object}
+// @router /check/residual/analysis/exist [get]
+func (this *ResidualAnalysisController) CheckResidualAnalysisExist() {
+	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
+	}
+
+	configId, err := this.GetInt("ConfigId")
+	if err != nil {
+		br.Msg = "EdbInfoId参数异常!"
+		br.ErrMsg = "EdbInfoId参数解析失败,Err:" + err.Error()
+	}
+	if configId <= 0 {
+		br.Msg = "ConfigId参数异常!"
+		br.ErrMsg = "ConfigId参数异常!"
+	}
+
+	resp, err := residual_analysis_service.CheckResidualAnalysisExist(configId)
+	if err != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = err.Error()
+		br.Ret = 403
+		return
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+	br.Data = resp
+}

+ 2 - 0
models/data_manage/edb_info.go

@@ -469,6 +469,8 @@ type EdbInfoList struct {
 	IsSupplierStop   int                     `description:"是否供应商停更:1:停更,0:未停更"`
 	MoveType         int                     `description:"移动方式:1:领先(默认),2:滞后"`
 	MoveFrequency    string                  `description:"移动频度"`
+	MinValue         float64                 `description:"最小值"`
+	MaxValue         float64                 `description:"最大值"`
 }
 
 type EdbDataInsertConfigItem struct {

+ 205 - 0
models/residual_analysis_model/calculate_residual_analysis_config.go

@@ -0,0 +1,205 @@
+package residual_analysis_model
+
+import (
+	"github.com/beego/beego/v2/client/orm"
+	"time"
+)
+
+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                        time.Time `orm:"column(create_time)" description:"创建时间"`
+	ModifyTime                        time.Time `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"`
+	ConfigId         int     `description:"配置id"`
+	QueryType        int     `description:"查询类型区分 避免查询过慢 1-查询第一个表格 2-查询需要计算的表格"`
+	ResidualType     int     `description:"残差类型: 1-映射残差 2-拟合残差"`
+	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:"残差图数据"`
+	A                 float64   `description:"斜率"`
+	B                 float64   `description:"截距"`
+	R                 float64   `description:"相关系数"`
+	R2                float64   `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 `description:"最小值"`
+	MaxValue            float64 `description:"最大值"`
+	DataList            interface{}
+}
+
+type ResidualAnalysisIndexSaveReq struct {
+	EdbInfoIdA   int                       `description:"指标A"`
+	EdbInfoIdB   int                       `description:"指标B"`
+	Source       int                       `description:"99-映射残差 100-拟合残差"`
+	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:"英文单位"`
+	Calendar     string                    `description:"公历/农历"`
+	ClassifyId   int                       `description:"分类id"`
+	ConfigId     int                       `description:"残差配置id"`
+	ResidualType int                       `orm:"column(residual_type)" description:"残差类型: 1-映射残差 2-拟合残差"`
+	IndexType    int                       `orm:"column(index_type)" description:"指标类型:1-映射指标 2-残差指标 3-因变量指标 4-自变量指标"`
+	DataList     []edbDataResidualAnalysis `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:"对比指标上限"`
+}
+
+type DetailEdbInfoList struct {
+	EdbInfoId   int    `orm:"column(edb_info_id);pk"`
+	EdbInfoType int    `description:"指标类型,0:普通指标,1:预测指标"`
+	IndexType   int    `orm:"column(index_type)" description:"指标类型:1-映射指标 2-残差指标 3-因变量指标 4-自变量指标"`
+	SourceName  string `description:"来源名称"`
+	Source      int    `description:"来源id"`
+	EdbCode     string `description:"指标编码"`
+	EdbNameEn   string `description:"英文指标名称"`
+	EdbName     string `description:"指标名称"`
+	Frequency   string `description:"频率"`
+	FrequencyEn string `description:"英文频率"`
+	Unit        string `description:"单位"`
+	UnitEn      string `description:"英文单位"`
+	ClassifyId  int    `description:"分类id"`
+}
+
+// ResidualAnalysisDetailResp 详情响应接口
+type ResidualAnalysisDetailResp struct {
+	ConfigInfo   *CalculateResidualAnalysisConfig
+	EdbInfoList  []*DetailEdbInfoList
+	ResidualType int `description:"残差类型: 1-映射残差 2-拟合残差"`
+}
+
+// GetResidualAnalysisConfigById 根据配置id查询配置信息
+func GetResidualAnalysisConfigById(configId int) (residualAnalysisConfig CalculateResidualAnalysisConfig, err error) {
+	o := orm.NewOrmUsingDB("data")
+	sql := "SELECT * FROM calculate_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.NewOrmUsingDB("data")
+	_, err = o.Update(&config)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+// SaveResidualAnalysisConfig 保存配置信息
+func SaveResidualAnalysisConfig(config CalculateResidualAnalysisConfig) (id int64, err error) {
+	o := orm.NewOrmUsingDB("data")
+	id, err = o.Insert(&config)
+	if err != nil {
+		return 0, err
+	}
+	return id, nil
+}

+ 50 - 0
models/residual_analysis_model/calculate_residual_analysis_config_mapping.go

@@ -0,0 +1,50 @@
+package residual_analysis_model
+
+import (
+	"github.com/beego/beego/v2/client/orm"
+	"time"
+)
+
+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-拟合残差"`
+	IndexType                                int       `orm:"column(index_type)" description:"指标类型:1-映射指标 2-残差指标 3-因变量指标 4-自变量指标"`
+	CreateTime                               time.Time `orm:"column(create_time)" description:"创建时间"`
+	ModifyTime                               time.Time `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.NewOrmUsingDB("data")
+	id, err = o.Insert(&mapping)
+	if err != nil {
+		return 0, err
+	}
+	return id, nil
+}

+ 40 - 0
models/residual_analysis_model/edb_data_residual_analysis.go

@@ -0,0 +1,40 @@
+package residual_analysis_model
+
+import (
+	"github.com/beego/beego/v2/client/orm"
+	"time"
+)
+
+type edbDataResidualAnalysis 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:"数据值"`
+	CreateTime    time.Time `orm:"column(create_time)" description:"创建时间"`
+	ModifyTime    time.Time `orm:"column(modify_time)" description:"修改时间"`
+	DataTimeStamp int64     `orm:"column(data_timestamp)"`
+}
+
+func init() {
+	orm.RegisterModel(new(edbDataResidualAnalysis))
+}
+
+// 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 []edbDataResidualAnalysis) (num int64, err error) {
+	o := orm.NewOrmUsingDB("data")
+	num, err = o.InsertMulti(len(dataList), dataList)
+	if err != nil {
+		return 0, err
+	}
+
+	return num, nil
+}

+ 54 - 0
routers/commentsRouter.go

@@ -8062,6 +8062,60 @@ 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: "CheckResidualAnalysisExist",
+            Router: `/check/residual/analysis/exist`,
+            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: "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 - 1
routers/router.go

@@ -30,13 +30,13 @@ import (
 	"eta/eta_api/controllers/eta_trial"
 	"eta/eta_api/controllers/fe_calendar"
 	"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"
 	"eta/eta_api/controllers/smart_report"
 	"eta/eta_api/controllers/speech_recognition"
 	"eta/eta_api/controllers/trade_analysis"
-
 	"github.com/beego/beego/v2/server/web"
 	"github.com/beego/beego/v2/server/web/filter/cors"
 )
@@ -425,6 +425,11 @@ func init() {
 				&ai_predict_model.AiPredictModelIndexController{},
 			),
 		),
+		web.NSNamespace("/residual_analysis",
+			web.NSInclude(
+				&residual_analysis.ResidualAnalysisController{},
+			),
+		),
 	)
 	web.AddNamespace(ns)
 }

+ 1048 - 0
services/residual_analysis_service/residual_analysis_service.go

@@ -0,0 +1,1048 @@
+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"
+	"math"
+	"sort"
+	"strconv"
+	"time"
+)
+
+func ResidualAnalysisPreview(req residual_analysis_model.ResidualAnalysisReq) (residual_analysis_model.ResidualAnalysisResp, error) {
+
+	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.Unit == "无" {
+			v.Unit = ""
+		}
+		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不存在")
+	}
+	if edbInfoMappingB == nil {
+		return residual_analysis_model.ResidualAnalysisResp{}, fmt.Errorf("指标B不存在")
+	}
+
+	// 时间处理
+	var startDate, endDate string
+	switch req.DateType {
+	case 0:
+		startDate = req.StartDate
+		endDate = req.EndDate
+	case 1:
+		startDate = req.StartDate
+		endDate = ""
+	default:
+		startDate = utils.GetPreYearTime(req.DateType)
+		endDate = ""
+	}
+
+	resp := residual_analysis_model.ResidualAnalysisResp{}
+
+	// 原始图表信息
+	originalEdbList := make([]residual_analysis_model.ResidualAnalysisChartEdbInfoMapping, 0)
+
+	originalEdbList, fullADataList, fullBDataList, err := fillOriginalChart(req, mappingList, startDate, endDate, edbInfoMappingA, edbInfoMappingB, originalEdbList)
+	if err != nil {
+		return residual_analysis_model.ResidualAnalysisResp{}, err
+	}
+
+	originalChartInfo := createChartInfoResp(req, startDate, endDate, edbInfoMappingA.EdbName+"与"+edbInfoMappingB.EdbName)
+
+	resp.OriginalChartData = residual_analysis_model.ChartResp{
+		ChartInfo:   originalChartInfo,
+		EdbInfoList: originalEdbList,
+	}
+	// 如果只需要第一张图表的数据 直接返回,避免继续处理
+	if req.QueryType == 1 {
+		return resp, nil
+	}
+
+	dataAList, ok := edbInfoMappingA.DataList.([]*data_manage.EdbDataList)
+	if !ok {
+		return residual_analysis_model.ResidualAnalysisResp{}, fmt.Errorf("数据类型转换失败")
+	}
+	indexADataMap := map[string]*data_manage.EdbDataList{}
+	for _, indexData := range dataAList {
+		indexADataMap[indexData.DataTime] = indexData
+	}
+
+	// 映射图表信息
+	mappingEdbList, a, b, r, err := fillMappingChartInfo(req, edbInfoMappingA, edbInfoMappingB, originalEdbList, indexADataMap, startDate, endDate, fullADataList, fullBDataList)
+	if err != nil {
+		return residual_analysis_model.ResidualAnalysisResp{}, err
+	}
+
+	mappingChartInfo := createChartInfoResp(req, startDate, endDate, edbInfoMappingA.EdbName+"与"+edbInfoMappingB.EdbName+"映射"+edbInfoMappingA.EdbName)
+
+	resp.MappingChartData = residual_analysis_model.ChartResp{
+		ChartInfo:   mappingChartInfo,
+		EdbInfoList: mappingEdbList,
+	}
+
+	// 残差图表信息
+	residualEdbList, R2, err := fillResidualChartInfo(req, edbInfoMappingA, edbInfoMappingB, mappingEdbList)
+	if err != nil {
+		return residual_analysis_model.ResidualAnalysisResp{}, err
+	}
+
+	residualChartInfo := createChartInfoResp(req, startDate, endDate, edbInfoMappingA.EdbName+"与"+edbInfoMappingA.EdbName+"映射残差/"+edbInfoMappingB.EdbName)
+
+	resp.ResidualChartData = residual_analysis_model.ChartResp{
+		ChartInfo:   residualChartInfo,
+		EdbInfoList: residualEdbList,
+	}
+
+	if req.ResidualType == 2 {
+		resp.A = math.Round(a*10000) / 10000
+		resp.B = math.Round(b*10000) / 10000
+		resp.R = math.Round(r*10000) / 10000
+		resp.R2 = math.Round(R2*10000) / 10000
+	}
+
+	return resp, nil
+}
+
+func createChartInfoResp(req residual_analysis_model.ResidualAnalysisReq, startDate, endDate, chartName string) residual_analysis_model.ResidualAnalysisChartInfo {
+	return residual_analysis_model.ResidualAnalysisChartInfo{
+		Calendar:  `公历`,
+		Source:    utils.CHART_SOURCE_DEFAULT,
+		DateType:  req.DateType,
+		StartDate: startDate,
+		EndDate:   endDate,
+		ChartType: utils.CHART_TYPE_CURVE,
+		ChartName: chartName,
+	}
+}
+
+func fillResidualChartInfo(req residual_analysis_model.ResidualAnalysisReq, edbInfoMappingA *data_manage.ChartEdbInfoMapping, edbInfoMappingB *data_manage.ChartEdbInfoMapping, mappingEdbList []residual_analysis_model.ResidualAnalysisChartEdbInfoMapping) ([]residual_analysis_model.ResidualAnalysisChartEdbInfoMapping, float64, 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, 0, fmt.Errorf("数据类型转换失败")
+	}
+	edbData := make([]*data_manage.EdbDataList, len(dataAList))
+	for i, data := range dataAList {
+		edbData[i] = &data_manage.EdbDataList{
+			Value:         data.Value, // 确保为每个元素创建一个新的对象
+			DataTimestamp: data.DataTimestamp,
+			DataTime:      data.DataTime,
+			EdbInfoId:     data.EdbInfoId,
+			EdbDataId:     data.EdbDataId,
+		}
+	}
+
+	dataBList, ok := edbInfoB.DataList.([]*data_manage.EdbDataList)
+	if !ok {
+		return nil, 0, fmt.Errorf("数据类型转换失败")
+	}
+
+	// 映射指标开始时间
+	var startTime string
+	if len(dataBList) > 0 {
+		startTime = dataBList[0].DataTime
+	}
+
+	var indexDataBMap = make(map[string]*data_manage.EdbDataList)
+	for _, data := range dataBList {
+		indexDataBMap[data.DataTime] = data
+	}
+	// 求R2
+	var valueB, sumValueA, averageValueA, residualQuadraticSum, totalQuadraticSum, R2 float64
+
+	for _, indexData := range edbData {
+		// 因变量的值总和
+		sumValueA += indexData.Value
+	}
+	// 因变量平均值
+	averageValueA = sumValueA / float64(len(edbData))
+
+	var indexMax, indexMin float64
+
+	var edbDataResp []*data_manage.EdbDataList
+	if len(edbData) > 0 {
+		indexMax = edbData[0].Value
+		indexMin = edbData[0].Value
+		for _, indexData := range edbData {
+			if dataB, ok := indexDataBMap[indexData.DataTime]; ok {
+				valueB = dataB.Value
+			} else {
+				continue
+			}
+
+			// 总因变量平方和
+			totalQuadraticSum += math.Pow(indexData.Value-averageValueA, 2)
+
+			// 补全残差值
+
+			indexData.Value = math.Round((indexData.Value-valueB)*10000) / 10000
+
+			// 残差平方和
+			residualQuadraticSum += math.Pow(indexData.Value, 2)
+
+			if indexData.Value > indexMax {
+				indexMax = indexData.Value
+			}
+			if indexData.Value < indexMin {
+				indexMin = indexData.Value
+			}
+
+			// 获取映射指标之后的数据
+			if startTime != "" && utils.CompareDate(startTime, indexData.DataTime) {
+				edbDataResp = append(edbDataResp, indexData)
+			}
+		}
+	}
+
+	// 计算R2 公式:R2=1-SSE/SST R2越大,越符合线性  R2 = 1 - 残差平方和/总平方和
+	R2 = 1 - residualQuadraticSum/totalQuadraticSum
+
+	mappingEdb := make([]residual_analysis_model.ResidualAnalysisChartEdbInfoMapping, len(mappingEdbList))
+	copy(mappingEdb, mappingEdbList)
+
+	for i, mapping := range mappingEdb {
+		if mapping.EdbInfoId != edbInfoMappingA.EdbInfoId {
+			mappingEdb[i].DataList = edbDataResp
+			mappingEdb[i].EdbName = edbInfoMappingA.EdbName + "映射残差/" + edbInfoMappingB.EdbName
+			if req.IndexType == 2 {
+				if req.LeadValue > 0 {
+					mappingEdb[i].EdbName = edbInfoMappingA.EdbName + "映射残差/" + edbInfoMappingB.EdbName + "(领先" + strconv.Itoa(req.LeadValue) + req.LeadFrequency + ")"
+				}
+			}
+			mappingEdb[i].IsAxis = 1
+			mappingEdb[i].ChartColor = `#00F`
+			mappingEdb[i].IsOrder = false
+			mappingEdb[i].MinValue = indexMin
+			mappingEdb[i].MaxValue = indexMax
+		} else {
+			mappingEdb[i].IsAxis = 0
+			mappingEdb[i].ChartColor = `#F00`
+		}
+	}
+
+	return mappingEdb, R2, 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, startDate string, endDate string, fullADataList []*data_manage.EdbDataList, fullBDataList []*data_manage.EdbDataList) ([]residual_analysis_model.ResidualAnalysisChartEdbInfoMapping, float64, float64, float64, 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, r float64
+
+	// 映射残差 计算a,b
+	if req.ResidualType == 1 {
+		if req.IsOrder {
+			a = (req.LeftIndexMax - req.LeftIndexMin) / (req.RightIndexMin - req.RightIndexMax)
+			b = req.LeftIndexMax - req.RightIndexMin*a
+		} else {
+			a = (req.LeftIndexMax - req.LeftIndexMin) / (req.RightIndexMax - req.RightIndexMin)
+			b = req.LeftIndexMax - req.RightIndexMax*a
+		}
+	}
+
+	//dataAList := edbInfoMappingA.DataList.([]*data_manage.EdbDataList)
+	dataList, ok := edbInfoMappingB.DataList.([]*data_manage.EdbDataList)
+	if !ok {
+		return nil, a, b, r, fmt.Errorf("数据类型转换失败")
+	}
+
+	// 指标B数据补充
+	// 新建一个切片来保存补充的数据
+	var replenishDataList []*data_manage.EdbDataList
+
+	for index := 0; index < len(dataList)-1; index++ {
+		// 获取当前数据和下一个数据
+		beforeIndexData := dataList[index]
+		afterIndexData := dataList[index+1]
+
+		// 从最早时间开始,补充时间为自然日
+		for utils.IsMoreThanOneDay(beforeIndexData.DataTime, afterIndexData.DataTime) {
+			// 创建补充数据
+			nextDay := utils.GetNextDay(beforeIndexData.DataTime)
+			toTime := utils.StringToTime(nextDay)
+			replenishIndexData := data_manage.EdbDataList{
+				DataTime:      nextDay, // 计算下一个自然日
+				DataTimestamp: toTime.UnixMilli(),
+				Value:         beforeIndexData.Value, // 可以选择使用前一天的值,或者其他逻辑来计算值
+			}
+
+			// 将补充数据加入补充数据列表
+			replenishDataList = append(replenishDataList, &replenishIndexData)
+
+			// 更新 beforeIndexData 为新创建的补充数据
+			beforeIndexData = &replenishIndexData
+		}
+	}
+
+	// 将补充数据插入原始数据列表
+	dataList = append(dataList, replenishDataList...)
+
+	// 排序
+	sort.Sort(ByDataTime(dataList))
+
+	// 拟合残差 计算a,b
+	var coordinateList []utils.Coordinate
+	var replenishADataList []*data_manage.EdbDataList
+	var replenishBDataList []*data_manage.EdbDataList
+	if req.ResidualType == 2 {
+		//
+
+		// 因变量指标也转换为日度
+		for index := 0; index < len(fullADataList)-1; index++ {
+			// 获取当前数据和下一个数据
+			beforeIndexData := fullADataList[index]
+			afterIndexData := fullADataList[index+1]
+
+			replenishADataList = append(replenishADataList, beforeIndexData)
+			// 从最早时间开始,补充时间为自然日
+			if utils.IsMoreThanOneDay(beforeIndexData.DataTime, afterIndexData.DataTime) {
+				for {
+					// 创建补充数据
+					nextDay := utils.GetNextDay(beforeIndexData.DataTime)
+					toTime := utils.StringToTime(nextDay)
+					replenishIndexData := data_manage.EdbDataList{
+						DataTime:      nextDay, // 计算下一个自然日
+						DataTimestamp: toTime.UnixMilli(),
+						Value:         beforeIndexData.Value, // 可以选择使用前一天的值,或者其他逻辑来计算值
+					}
+
+					// 将补充数据加入补充数据列表
+					replenishADataList = append(replenishADataList, &replenishIndexData)
+
+					// 更新 beforeIndexData 为新创建的补充数据
+					beforeIndexData = &replenishIndexData
+
+					// 检查是否还需要继续补充数据
+					if !utils.IsMoreThanOneDay(beforeIndexData.DataTime, afterIndexData.DataTime) {
+						break
+					}
+				}
+			}
+		}
+		replenishADataList = append(replenishADataList, fullADataList[len(fullADataList)-1])
+
+		// 自变量指标也转换为日度
+		for index := 0; index < len(fullBDataList)-1; index++ {
+			// 获取当前数据和下一个数据
+			beforeIndexData := fullBDataList[index]
+			afterIndexData := fullBDataList[index+1]
+
+			replenishBDataList = append(replenishBDataList, beforeIndexData)
+			// 从最早时间开始,补充时间为自然日
+			if utils.IsMoreThanOneDay(beforeIndexData.DataTime, afterIndexData.DataTime) {
+				for {
+					// 创建补充数据
+					nextDay := utils.GetNextDay(beforeIndexData.DataTime)
+					toTime := utils.StringToTime(nextDay)
+					replenishIndexData := data_manage.EdbDataList{
+						DataTime:      nextDay, // 计算下一个自然日
+						DataTimestamp: toTime.UnixMilli(),
+						Value:         beforeIndexData.Value, // 可以选择使用前一天的值,或者其他逻辑来计算值
+					}
+
+					// 将补充数据加入补充数据列表
+					replenishBDataList = append(replenishBDataList, &replenishIndexData)
+
+					// 更新 beforeIndexData 为新创建的补充数据
+					beforeIndexData = &replenishIndexData
+
+					// 检查是否还需要继续补充数据
+					if !utils.IsMoreThanOneDay(beforeIndexData.DataTime, afterIndexData.DataTime) {
+						break
+					}
+				}
+			}
+		}
+		replenishBDataList = append(replenishBDataList, fullBDataList[len(fullBDataList)-1])
+
+		// replenishADataList --> map
+		replenishADataMap := make(map[string]*data_manage.EdbDataList)
+		for _, indexData := range replenishADataList {
+			if (utils.StringToTime(indexData.DataTime).After(utils.StringToTime(startDate)) || utils.StringToTime(indexData.DataTime).Equal(utils.StringToTime(startDate))) && (endDate == "" || utils.StringToTime(indexData.DataTime).Before(utils.StringToTime(endDate))) {
+				replenishADataMap[indexData.DataTime] = indexData
+			}
+		}
+
+		for _, indexData := range replenishBDataList {
+			if _, ok = replenishADataMap[indexData.DataTime]; ok {
+
+				coordinate := utils.Coordinate{
+					X: indexData.Value,
+					Y: replenishADataMap[indexData.DataTime].Value,
+				}
+				coordinateList = append(coordinateList, coordinate)
+			}
+		}
+		a, b = utils.GetLinearResult(coordinateList)
+		r = utils.ComputeCorrelation(coordinateList)
+	}
+
+	// 填充映射指标值 使得时间长度一致
+	dataList = FillDataBList(dataList, edbInfoMappingA)
+
+	// 根据指标A的时间key,在B的映射指标中筛选出对应的值
+	var dataBList []*data_manage.EdbDataList
+
+	var indexMax, indexMin float64
+
+	if len(dataList) > 0 {
+		indexMax = dataList[0].Value
+		indexMin = dataList[0].Value
+		for _, indexData := range dataList {
+			if _, ok := indexADataMap[indexData.DataTime]; ok {
+				indexDataCopy := *indexData
+
+				// 计算指标B映射值
+				indexDataCopy.Value = math.Round((a*indexData.Value+b)*10000) / 10000
+
+				// 比较最大值
+				if indexData.Value > indexMax {
+					indexMax = indexData.Value
+				}
+
+				// 比较最小值
+				if indexData.Value < indexMin {
+					indexMin = indexData.Value
+				}
+
+				// 将副本添加到 dataBList
+				dataBList = append(dataBList, &indexDataCopy)
+			}
+		}
+	}
+
+	mappingEdbList := make([]residual_analysis_model.ResidualAnalysisChartEdbInfoMapping, len(originalEdbList))
+	copy(mappingEdbList, originalEdbList)
+
+	for i, mapping := range mappingEdbList {
+		if mapping.EdbInfoId != req.EdbInfoIdA {
+			mappingEdbList[i].EdbInfoId = 0
+			mappingEdbList[i].EdbCode = ""
+			mappingEdbList[i].IsAxis = 1
+			mappingEdbList[i].EdbName = edbInfoMappingB.EdbName + "映射" + edbInfoMappingA.EdbName
+			if req.IndexType == 2 {
+				if req.LeadValue > 0 {
+					mappingEdbList[i].EdbName = edbInfoMappingB.EdbName + "映射" + edbInfoMappingA.EdbName + "(领先" + strconv.Itoa(req.LeadValue) + req.LeadFrequency + ")"
+				}
+			}
+			mappingEdbList[i].DataList = dataBList
+			mappingEdbList[i].MinValue = indexMin
+			mappingEdbList[i].MaxValue = indexMax
+		}
+	}
+	return mappingEdbList, a, b, r, nil
+}
+
+// FillDataBList 填充B的数据 使得与A的时间保持一致
+func FillDataBList(dataList []*data_manage.EdbDataList, edbInfoMappingA *data_manage.ChartEdbInfoMapping) []*data_manage.EdbDataList {
+	dataAList, ok := edbInfoMappingA.DataList.([]*data_manage.EdbDataList)
+	if !ok {
+		return nil
+	}
+
+	for utils.StringToTime(dataList[len(dataList)-1].DataTime).Before(utils.StringToTime(dataAList[len(dataAList)-1].DataTime)) {
+		// 使用A的时间填充时间差
+		timeDiff := utils.GetNextDayN(dataList[len(dataList)-1].DataTime, 1)
+
+		// 创建新的数据点并填充 前值填充
+		newDataPoint := &data_manage.EdbDataList{
+			DataTime:      timeDiff,
+			Value:         dataList[len(dataList)-1].Value,
+			DataTimestamp: utils.StringToTime(timeDiff).UnixMilli(),
+		}
+
+		// 将新数据点添加到dataList末尾
+		dataList = append(dataList, newDataPoint)
+	}
+
+	return dataList
+}
+
+type ByDataTime []*data_manage.EdbDataList
+
+func (a ByDataTime) Len() int {
+	return len(a)
+}
+
+func (a ByDataTime) Swap(i, j int) {
+	a[i], a[j] = a[j], a[i]
+}
+
+func (a ByDataTime) Less(i, j int) bool {
+	t1 := utils.StringToTime(a[i].DataTime)
+	t2 := utils.StringToTime(a[j].DataTime)
+	return t1.Before(t2)
+}
+
+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, []*data_manage.EdbDataList, []*data_manage.EdbDataList, error) {
+
+	var fullADataList, fullBDataList []*data_manage.EdbDataList
+	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
+		edbInfoMapping.EdbName = v.EdbName
+
+		// 获取图表中的指标数据
+		dataList, err := data_manage.GetEdbDataList(v.Source, v.SubSource, v.EdbInfoId, startDate, endDate)
+		if err != nil {
+			return nil, nil, nil, fmt.Errorf("获取指标数据失败,Err:%s", err.Error())
+		}
+		// 重新获取指标数据 产品要求需要和计算指标-拟合残差逻辑保持一致
+		fullDataList, err := data_manage.GetEdbDataList(v.Source, v.SubSource, v.EdbInfoId, "", "")
+		if err != nil {
+			return nil, nil, 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
+
+			// 领先指标 dataList进行数据处理
+			if req.IndexType == 2 {
+				if req.LeadValue < 0 {
+					return nil, nil, nil, fmt.Errorf("领先值不能小于0")
+				} else if req.LeadValue > 0 {
+					edbInfoMapping.EdbName = v.EdbName + "(领先" + strconv.Itoa(req.LeadValue) + req.LeadFrequency + ")"
+					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)
+						}
+						indexData.DataTimestamp = utils.StringToTime(indexData.DataTime).UnixMilli()
+					}
+				}
+			}
+
+			edbInfoMappingB.DataList = dataList
+			fullBDataList = fullDataList
+		} else {
+			edbInfoMappingA.DataList = dataList
+			fullADataList = fullDataList
+		}
+		edbInfoMapping.EdbInfoId = v.EdbInfoId
+		edbInfoMapping.EdbCode = v.EdbCode
+		edbInfoMapping.Unit = v.Unit
+		edbInfoMapping.Frequency = v.Frequency
+		edbInfoMapping.Source = v.Source
+		edbInfoMapping.SourceName = v.SourceName
+		edbInfoMapping.MinValue = v.MinValue
+		edbInfoMapping.MaxValue = v.MaxValue
+		edbInfoMapping.LatestDate = v.LatestDate
+		edbInfoMapping.LatestValue = v.LatestValue
+
+		edbInfoMapping.DataList = dataList
+		originalEdbList = append(originalEdbList, edbInfoMapping)
+	}
+	return originalEdbList, fullADataList, fullBDataList, 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.EdbCode = edbInfo.EdbCode
+	resp.ChartColor = "#F00"
+	resp.SourceName = edbInfo.SourceName
+	resp.EdbName = edbInfo.EdbName
+	resp.Unit = edbInfo.Unit
+	resp.Frequency = edbInfo.Frequency
+	resp.MinValue = edbInfo.MinValue
+	resp.MaxValue = edbInfo.MaxValue
+	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("分类不存在")
+	}
+
+	// 校验名称是否重复
+	var condition string
+	var pars []interface{}
+
+	condition += " and source = ? AND edb_name=?"
+	pars = append(pars, req.Source, req.EdbName)
+
+	edbInfoByCondition, err := data_manage.GetEdbInfoByCondition(condition, pars)
+	if err != nil && err.Error() != utils.ErrNoRow() {
+		return err
+	}
+
+	// 获取指标数据最大值 最小值 最后更新时间 最后更新时间对应的值
+	var indexMax, indexMin, indexLatestValue float64
+	var indexLatestDate string
+	if len(req.DataList) > 0 {
+		latestTime, _ := time.Parse(utils.YearMonthDay, req.DataList[0].DataTime)
+		for _, data := range req.DataList {
+			// 比较最大值
+			if data.Value > indexMax {
+				indexMax = data.Value
+			}
+
+			// 比较最小值
+			if data.Value < indexMin {
+				indexMin = data.Value
+			}
+
+			// 比较最新时间和对应值
+			currentTime, err := time.Parse(utils.YearMonthDay, data.DataTime)
+			if err != nil {
+				// 时间解析失败,跳过此项
+				continue
+			}
+
+			// 如果当前时间更晚
+			if currentTime.After(latestTime) {
+				latestTime = currentTime
+				indexLatestDate = data.DataTime
+				indexLatestValue = data.Value
+			}
+		}
+	}
+
+	// 更新保存指标和配置的映射关系
+	mappingList, err := residual_analysis_model.GetConfigMappingListByConfigId(req.ConfigId)
+	if err != nil {
+		return err
+	}
+
+	// 判断是更新还是修改 看指标配置映射中,是否存在对应指标 存在 则更新 不存在 则新增
+	var edbInfoMapping residual_analysis_model.CalculateResidualAnalysisConfigMapping
+	for _, mapping := range mappingList {
+		if req.IndexType == mapping.IndexType && req.IndexType != 0 {
+			edbInfoMapping = mapping
+		}
+	}
+
+	var edbInfoId int64
+	var edbCode string
+	// 更新or新增
+	if edbInfoMapping.EdbInfoId > 0 {
+		// 查询指标库指标
+		edbInfo, err := data_manage.GetEdbInfoById(int(edbInfoMapping.EdbInfoId))
+		if err != nil {
+			return err
+		}
+		if edbInfo == nil {
+			return fmt.Errorf("指标不存在")
+		}
+		edbInfoId = int64(edbInfo.EdbInfoId)
+		edbCode = edbInfo.EdbCode
+
+		if edbInfoByCondition != nil && edbInfoByCondition.EdbInfoId != edbInfo.EdbInfoId {
+			return fmt.Errorf("指标名称重复")
+		}
+
+		// 须补充更新指标最大值,最小值,数据最新时间,数据最新值
+		edbInfo.MaxValue = indexMax
+		edbInfo.MinValue = indexMin
+		edbInfo.LatestDate = indexLatestDate
+		edbInfo.LatestValue = indexLatestValue
+		edbInfo.Unit = req.Unit
+		edbInfo.Frequency = req.Frequency
+		edbInfo.ClassifyId = req.ClassifyId
+		edbInfo.EdbName = req.EdbName
+		err = edbInfo.Update([]string{"min_value", "max_value", "latest_date", "latest_value", "unit", "frequency", "classify_id", "edb_name"})
+		if err != nil {
+			return err
+		}
+
+		// 删除对应得指标数据
+		err = residual_analysis_model.DeleteResidualAnalysisDataByEdbCode(edbInfo.EdbCode)
+		if err != nil {
+			return fmt.Errorf("删除指标数据失败")
+		}
+	} else {
+		if edbInfoByCondition != nil {
+			return fmt.Errorf("指标名称重复")
+		}
+
+		// 新增指标
+		edbCode, err = utils.GenerateEdbCode(1, "")
+		if err != nil {
+			return err
+		}
+
+		timestamp := strconv.FormatInt(time.Now().UnixNano(), 10)
+
+		edbInfoId, err = data_manage.AddEdbInfo(&data_manage.EdbInfo{
+			EdbCode:         edbCode,
+			UniqueCode:      utils.MD5(utils.CHART_PREFIX + "_" + timestamp),
+			EdbName:         req.EdbName,
+			EdbNameEn:       req.EdbNameEn,
+			ClassifyId:      req.ClassifyId,
+			EdbType:         req.EdbType,
+			Unit:            req.Unit,
+			UnitEn:          req.UnitEn,
+			Frequency:       req.Frequency,
+			Source:          req.Source,
+			SourceName:      "残差分析",
+			Calendar:        req.Calendar,
+			SysUserRealName: sysUser.RealName,
+			SysUserId:       sysUser.AdminId,
+			LatestDate:      indexLatestDate,
+			LatestValue:     indexLatestValue,
+			MinValue:        indexMin,
+			MaxValue:        indexMax,
+			CreateTime:      time.Now(),
+			ModifyTime:      time.Now(),
+		})
+		if err != nil {
+			return err
+		}
+
+		// 新增指标配置关系
+		_, err = residual_analysis_model.SaveConfigMapping(residual_analysis_model.CalculateResidualAnalysisConfigMapping{
+			CalculateResidualAnalysisConfigId: req.ConfigId,
+			EdbInfoId:                         edbInfoId,
+			ResidualType:                      req.ResidualType,
+			IndexType:                         req.IndexType,
+			CreateTime:                        time.Now(),
+			ModifyTime:                        time.Now(),
+		})
+		if err != nil {
+			return err
+		}
+	}
+
+	// 新增数据
+	for i := range req.DataList {
+		req.DataList[i].EdbDataId = 0
+		req.DataList[i].EdbInfoId = int(edbInfoId)
+		req.DataList[i].EdbCode = edbCode
+	}
+	_, err = residual_analysis_model.AddResidualAnalysisData(req.DataList)
+	if err != nil {
+		return err
+	}
+
+	// 新增自变量 因变量与配置得关系 配置中不存在该指标,则新增
+	var indexMap = make(map[int64]residual_analysis_model.CalculateResidualAnalysisConfigMapping)
+	for _, mapping := range mappingList {
+		indexMap[mapping.EdbInfoId] = mapping
+	}
+
+	if _, ok := indexMap[int64(req.EdbInfoIdA)]; !ok {
+		_, err = residual_analysis_model.SaveConfigMapping(residual_analysis_model.CalculateResidualAnalysisConfigMapping{
+			CalculateResidualAnalysisConfigId: req.ConfigId,
+			EdbInfoId:                         int64(req.EdbInfoIdA),
+			ResidualType:                      req.ResidualType,
+			IndexType:                         3,
+			CreateTime:                        time.Now(),
+			ModifyTime:                        time.Now(),
+		})
+		if err != nil {
+			return err
+		}
+	}
+	if _, ok := indexMap[int64(req.EdbInfoIdB)]; !ok {
+		_, err = residual_analysis_model.SaveConfigMapping(residual_analysis_model.CalculateResidualAnalysisConfigMapping{
+			CalculateResidualAnalysisConfigId: req.ConfigId,
+			EdbInfoId:                         int64(req.EdbInfoIdB),
+			ResidualType:                      req.ResidualType,
+			IndexType:                         4,
+			CreateTime:                        time.Now(),
+			ModifyTime:                        time.Now(),
+		})
+		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("指标不存在")
+	}
+
+	mapping := mappingList[0]
+
+	configMappingList, err := residual_analysis_model.GetConfigMappingListByConfigId(mapping.CalculateResidualAnalysisConfigId)
+	if err != nil {
+		return residual_analysis_model.ResidualAnalysisDetailResp{}, err
+	}
+
+	var edbInfoIdList []int64
+	var edbInfoMap = make(map[int64]residual_analysis_model.CalculateResidualAnalysisConfigMapping)
+	var mappgingFlag = false
+	var residualFlag = false
+	for _, v := range configMappingList {
+		edbInfoIdList = append(edbInfoIdList, v.EdbInfoId)
+
+		edbInfoMap[v.EdbInfoId] = v
+
+		if v.IndexType == 1 {
+			mappgingFlag = true
+		} else if v.IndexType == 2 {
+			residualFlag = true
+		}
+	}
+
+	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
+	}
+
+	var edbInfoListResp []*residual_analysis_model.DetailEdbInfoList
+	var dependentEdbInfo residual_analysis_model.DetailEdbInfoList
+	var independentEdbInfo residual_analysis_model.DetailEdbInfoList
+	for _, edbInfo := range edbInfoList {
+		var indexType int
+		if _, ok := edbInfoMap[int64(edbInfo.EdbInfoId)]; ok {
+			indexType = edbInfoMap[int64(edbInfo.EdbInfoId)].IndexType
+		}
+
+		info := residual_analysis_model.DetailEdbInfoList{
+			EdbInfoId:   edbInfo.EdbInfoId,
+			EdbInfoType: edbInfo.EdbInfoType,
+			IndexType:   indexType,
+			SourceName:  edbInfo.SourceName,
+			Source:      edbInfo.Source,
+			EdbCode:     edbInfo.EdbCode,
+			EdbName:     edbInfo.EdbName,
+			EdbNameEn:   edbInfo.EdbNameEn,
+			Unit:        edbInfo.Unit,
+			UnitEn:      edbInfo.UnitEn,
+			Frequency:   edbInfo.Frequency,
+			FrequencyEn: edbInfo.FrequencyEn,
+			ClassifyId:  edbInfo.ClassifyId,
+		}
+		edbInfoListResp = append(edbInfoListResp, &info)
+
+		if indexType == 3 {
+			dependentEdbInfo = info
+		} else if indexType == 4 {
+			independentEdbInfo = info
+		}
+	}
+
+	// 补充表格中 映射指标或者残差指标
+	if mappgingFlag && !residualFlag {
+		info := residual_analysis_model.DetailEdbInfoList{
+			IndexType:   2,
+			EdbName:     independentEdbInfo.EdbName + "映射残差/" + dependentEdbInfo.EdbName,
+			EdbNameEn:   dependentEdbInfo.EdbNameEn,
+			Unit:        dependentEdbInfo.Unit,
+			UnitEn:      dependentEdbInfo.UnitEn,
+			Frequency:   dependentEdbInfo.Frequency,
+			FrequencyEn: dependentEdbInfo.FrequencyEn,
+			ClassifyId:  dependentEdbInfo.ClassifyId,
+		}
+		edbInfoListResp = append(edbInfoListResp, &info)
+	} else if !mappgingFlag && residualFlag {
+		info := residual_analysis_model.DetailEdbInfoList{
+			IndexType:   1,
+			EdbName:     dependentEdbInfo.EdbName + "映射" + independentEdbInfo.EdbName,
+			EdbNameEn:   dependentEdbInfo.EdbNameEn,
+			Unit:        dependentEdbInfo.Unit,
+			UnitEn:      dependentEdbInfo.UnitEn,
+			Frequency:   dependentEdbInfo.Frequency,
+			FrequencyEn: dependentEdbInfo.FrequencyEn,
+			ClassifyId:  dependentEdbInfo.ClassifyId,
+		}
+		edbInfoListResp = append(edbInfoListResp, &info)
+	}
+
+	resp := residual_analysis_model.ResidualAnalysisDetailResp{
+		ConfigInfo:   &configInfo,
+		EdbInfoList:  edbInfoListResp,
+		ResidualType: mapping.ResidualType,
+	}
+
+	return resp, nil
+}
+
+func SaveResidualAnalysisConfig(req residual_analysis_model.ResidualAnalysisReq, sysUser *system.Admin) (int64, 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 0, err
+	}
+
+	// 新增or更新
+	/*
+		var condition string
+			var pars []interface{}
+		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 0, err
+			}
+			if len(configMappings) > 0 {
+				mapping := configMappings[0]
+				configInfo, err := residual_analysis_model.GetResidualAnalysisConfigById(mapping.CalculateResidualAnalysisConfigId)
+				if err != nil {
+					return 0, err
+				}
+				configInfo.Config = string(configJson)
+				err = residual_analysis_model.UpdateResidualAnalysisConfig(configInfo)
+				if err != nil {
+					return 0, err
+				}
+			}
+		}*/
+	var configId int64
+	if req.ConfigId > 0 {
+		configInfo, err := residual_analysis_model.GetResidualAnalysisConfigById(req.ConfigId)
+		if err != nil {
+			return 0, err
+		}
+		if configInfo.CalculateResidualAnalysisConfigId == 0 {
+			return 0, fmt.Errorf("未找到配置信息")
+		}
+
+		configId = int64(configInfo.CalculateResidualAnalysisConfigId)
+		configInfo.Config = string(configJson)
+
+		err = residual_analysis_model.UpdateResidualAnalysisConfig(configInfo)
+		if err != nil {
+			return 0, err
+		}
+	} else {
+		analysisConfig := residual_analysis_model.CalculateResidualAnalysisConfig{
+			Config:     string(configJson),
+			SysUserId:  sysUser.AdminId,
+			CreateTime: time.Now(),
+			ModifyTime: time.Now(),
+		}
+
+		configId, err = residual_analysis_model.SaveResidualAnalysisConfig(analysisConfig)
+		if err != nil {
+			return 0, err
+		}
+	}
+
+	return configId, nil
+}
+
+func CheckResidualAnalysisExist(configId int) (int64, error) {
+
+	configMappingList, err := residual_analysis_model.GetConfigMappingListByConfigId(configId)
+	if err != nil {
+		return 0, err
+	}
+
+	var configMapping residual_analysis_model.CalculateResidualAnalysisConfigMapping
+	for _, mapping := range configMappingList {
+		if mapping.IndexType == 2 {
+			configMapping = mapping
+		}
+	}
+
+	return configMapping.EdbInfoId, nil
+}

+ 2 - 0
utils/constants.go

@@ -188,6 +188,8 @@ const (
 	DATA_SOURCE_USDA_FAS                             = 96       //美国农业部
 	DATA_SOURCE_CALCULATE_STL                        = 98       // STL趋势分解 -> 98
 	DATA_SOURCE_RZD                                  = 97       // 睿姿得数据
+	DATA_SOURCE_MAPPING_RESIDUAL                     = 99       // 映射残差
+	DATA_SOURCE_FIT_RESIDUAL                         = 100      // 拟合残差
 )
 
 // 数据刷新频率

+ 129 - 0
utils/date_util.go

@@ -0,0 +1,129 @@
+package utils
+
+import (
+	"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
+)
+
+// GetPreYearTime 获取当前时间 前n年的时间 返回yyyy-MM-dd 格式的时间
+func GetPreYearTime(n int) string {
+	// 获取当前时间
+	now := time.Now()
+	// 计算前n年的时间
+	preYearTime := now.AddDate(-n, 0, 0)
+	// 格式化时间
+	return preYearTime.Format("2006-01-02")
+}
+
+// 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-1)/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
+}
+
+// CompareDate 判断传入的两个字符串时间的前后顺序
+func CompareDate(data1, data2 string) bool {
+	t1, _ := time.Parse("2006-01-02", data1)
+	t2, _ := time.Parse("2006-01-02", data2)
+	return !t1.After(t2)
+}