فهرست منبع

Merge branch 'eta_2.2.9_residual_analysis_1104@guomengyuan' into debug

# Conflicts:
#	controllers/data_manage/multiple_graph_config.go
#	routers/router.go
#	utils/constants.go
gmy 5 ماه پیش
والد
کامیت
3a8ff3ea7c

+ 230 - 0
controllers/residual_analysis/residual_analysis.go

@@ -0,0 +1,230 @@
+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.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 = "添加成功"
+}
+
+// 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
+}

+ 121 - 0
models/residual_analysis_model/calculate_residual_analysis_config.go

@@ -0,0 +1,121 @@
+package residual_analysis_model
+
+import (
+	"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"`
+	DateType         int     `description:"时间类型 0-自定义时间 1-至今 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:"指标数据"`
+}

+ 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 - 0
routers/commentsRouter.go

@@ -8134,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)
 }

+ 434 - 0
services/residual_analysis_service/residual_analysis_service.go

@@ -0,0 +1,434 @@
+package residual_analysis_service
+
+import (
+	"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) ([]*data_manage.EdbInfoList, 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 []*data_manage.EdbInfoList{}, err
+	}
+
+	if len(mappingList) <= 0 {
+		return []*data_manage.EdbInfoList{}, fmt.Errorf("指标不存在", nil)
+	}
+
+	mapping := mappingList[0]
+
+	configMappingList, err := residual_analysis_model.GetConfigMappingListByConfigId(mapping.CalculateResidualAnalysisConfigId)
+	if err != nil {
+		return []*data_manage.EdbInfoList{}, 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 []*data_manage.EdbInfoList{}, err
+	}
+
+	return edbInfoList, 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
+}