2
0

14 Commits c0e0a5a432 ... 57b2b2e1f0

Autor SHA1 Nachricht Datum
  hsun 57b2b2e1f0 Merge branch 'master' of http://8.136.199.33:3000/eta_server/eta_api vor 1 Monat
  hsun 8029e100d3 Merge branch 'feature/eta_2.0.3' vor 1 Monat
  hsun 5efd85fb02 fix: 持仓分析指标刷新 vor 1 Monat
  hsun 2e2209bc0d fix: 期货公司名称 vor 2 Monaten
  hsun 7c6eea2994 fix: 建仓过程 vor 2 Monaten
  hsun ad8efae917 fix: 持仓分析图表郑商所 vor 2 Monaten
  hsun f629b765c5 fix: 持仓分析图表广期所 vor 2 Monaten
  hsun 0c356000e9 fix: 持仓分析图表顺序 vor 2 Monaten
  hsun b0996efe40 fix: 持仓分析图-另存为、删除 vor 2 Monaten
  hsun e6c7ca759a fix: 持仓分析图表 vor 2 Monaten
  hsun 773bb57d52 fix: 郑商所品种 vor 2 Monaten
  hsun 6d85ccc7be fix: 持仓分析指标 vor 2 Monaten
  hsun f2565ce8fc 持仓分析-建仓过程接口 vor 2 Monaten
  hsun c511137209 优化-通用分类 vor 3 Monaten
28 geänderte Dateien mit 6285 neuen und 10 gelöschten Zeilen
  1. 334 2
      controllers/trade_analysis/trade_analysis.go
  2. 1812 0
      controllers/trade_analysis/warehouse.go
  3. 683 0
      controllers/trade_analysis/warehouse_classify.go
  4. 80 0
      models/common_classify.go
  5. 14 2
      models/data_manage/chart_info.go
  6. 20 0
      models/data_manage/chart_series.go
  7. 2 0
      models/data_manage/edb_data_base.go
  8. 7 0
      models/data_manage/multiple_graph_config_chart_mapping.go
  9. 122 0
      models/data_manage/trade_analysis/base_from_trade_exchange.go
  10. 20 0
      models/data_manage/trade_analysis/request/warehouse.go
  11. 17 0
      models/data_manage/trade_analysis/response/warehouse.go
  12. 372 0
      models/data_manage/trade_analysis/trade_analysis.go
  13. 76 0
      models/data_manage/trade_analysis/trade_classify.go
  14. 129 0
      models/data_manage/trade_analysis/trade_futures_company.go
  15. 103 0
      models/data_manage/trade_analysis/warehouse.go
  16. 340 0
      models/data_manage/trade_analysis/warehouse_process_classify.go
  17. 207 0
      routers/commentsRouter.go
  18. 2 0
      routers/router.go
  19. 2 0
      services/data/base_edb_lib.go
  20. 485 0
      services/data/common_classify.go
  21. 106 0
      services/data/common_classify_ctx.go
  22. 1 0
      services/data/edb_info.go
  23. 13 6
      services/data/trade_analysis/trade_analysis.go
  24. 647 0
      services/data/trade_analysis/trade_analysis_data.go
  25. 181 0
      services/data/trade_analysis/trade_analysis_interface.go
  26. 418 0
      services/data/trade_analysis/warehouse.go
  27. 69 0
      utils/common.go
  28. 23 0
      utils/constants.go

+ 334 - 2
controllers/trade_analysis/trade_analysis.go

@@ -3,8 +3,11 @@ package trade_analysis
 import (
 	"eta/eta_api/controllers"
 	"eta/eta_api/models"
-	trade_analysisModel "eta/eta_api/models/data_manage/trade_analysis"
+	tradeAnalysisModel "eta/eta_api/models/data_manage/trade_analysis"
 	"eta/eta_api/services/data/trade_analysis"
+	"eta/eta_api/utils"
+	"fmt"
+	"strings"
 )
 
 // TradeAnalysisController 供应分析
@@ -61,7 +64,7 @@ func (c *TradeAnalysisController) GetPositionTop() {
 		c.ServeJSON()
 	}()
 
-	req := trade_analysisModel.GetPositionTopReq{
+	req := tradeAnalysisModel.GetPositionTopReq{
 		Exchange:     c.GetString("Exchange"),
 		ClassifyName: c.GetString("ClassifyName"),
 		ClassifyType: c.GetString("ClassifyType"),
@@ -92,3 +95,332 @@ func (c *TradeAnalysisController) GetPositionTop() {
 
 	return
 }
+
+// GetTradeExchangeList
+// @Title 获取交易所列表
+// @Description 获取交易所列表
+// @Param   IsTotal  query  bool  false  "是否显示全部"
+// @Success 200 {object} data_manage.BaseFromTradeExchangeItem
+// @router /exchange_list [get]
+func (this *TradeAnalysisController) GetTradeExchangeList() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	isTotal, _ := this.GetBool("IsTotal", false)
+
+	var cond string
+	var pars []interface{}
+	exchangeOb := new(tradeAnalysisModel.BaseFromTradeExchange)
+	if !isTotal {
+		cond += fmt.Sprintf(` AND %s = ?`, exchangeOb.Cols().AnalysisState)
+		pars = append(pars, 1)
+	}
+	list, e := exchangeOb.GetItemsByCondition(cond, pars, []string{}, "")
+	if e != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = fmt.Sprintf("获取交易所列表失败, %v", e)
+		return
+	}
+	resp := make([]*tradeAnalysisModel.BaseFromTradeExchangeItem, 0)
+	for _, v := range list {
+		resp = append(resp, v.Format2Item())
+	}
+
+	br.Data = resp
+	br.Msg = "获取成功"
+	br.Ret = 200
+	br.Success = true
+}
+
+// GetTradeClassifyList
+// @Title 获取品种列表
+// @Description 获取品种列表
+// @Param   Exchange  query  string  false  "交易所标识"
+// @Param   ClassifyName  query  string  false  "品种名称"
+// @Success 200 {object} tradeAnalysisModel.BaseFromTradeClassifyItem
+// @router /classify_list [get]
+func (this *TradeAnalysisController) GetTradeClassifyList() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	exchange := this.GetString("Exchange")
+	exchange = strings.TrimSpace(exchange)
+	classifyName := this.GetString("ClassifyName")
+	classifyName = strings.TrimSpace(classifyName)
+
+	// 交易所名称
+	exchangeName := make(map[string]string)
+	{
+		var cond string
+		var pars []interface{}
+		exchangeOb := new(tradeAnalysisModel.BaseFromTradeExchange)
+		if exchange != "" {
+			cond += fmt.Sprintf(` AND %s = ?`, exchangeOb.Cols().Exchange)
+			pars = append(pars, exchange)
+			item, e := exchangeOb.GetItemByCondition(cond, pars, "")
+			if e != nil {
+				br.Msg = "获取失败"
+				br.ErrMsg = fmt.Sprintf("获取交易所信息失败, %v", e)
+				return
+			}
+			exchangeName[item.Exchange] = item.ExchangeName
+		} else {
+			list, e := exchangeOb.GetItemsByCondition(cond, pars, []string{}, "")
+			if e != nil {
+				br.Msg = "获取失败"
+				br.ErrMsg = fmt.Sprintf("获取交易所列表失败, %v", e)
+				return
+			}
+			for _, v := range list {
+				exchangeName[v.Exchange] = v.ExchangeName
+			}
+		}
+	}
+
+	// 搜索品种
+	var cond string
+	var pars []interface{}
+	classifyOb := new(tradeAnalysisModel.BaseFromTradeClassify)
+	if exchange != "" {
+		cond += fmt.Sprintf(` AND %s = ?`, classifyOb.Cols().Exchange)
+		pars = append(pars, exchange)
+	}
+	if classifyName != "" {
+		cond += fmt.Sprintf(` AND %s LIKE ?`, classifyOb.Cols().ClassifyName)
+		pars = append(pars, fmt.Sprint("%", classifyName, "%"))
+	}
+	fields := []string{classifyOb.Cols().ClassifyName, classifyOb.Cols().Exchange}
+	list, e := classifyOb.GetClassifyItemsByCondition(cond, pars, fields, "id ASC")
+	if e != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = fmt.Sprintf("获取品种列表失败, %v", e)
+		return
+	}
+
+	resp := make([]*tradeAnalysisModel.BaseFromTradeClassifyItem, 0)
+	classifyExist := make(map[string]bool)
+	for _, v := range list {
+		// 郑商所
+		if v.Exchange == tradeAnalysisModel.TradeExchangeZhengzhou {
+			name := trade_analysis.GetZhengzhouClassifyName(v.ClassifyName)
+			if name == "" {
+				continue
+			}
+			if classifyExist[name] {
+				continue
+			}
+			classifyExist[name] = true
+			resp = append(resp, &tradeAnalysisModel.BaseFromTradeClassifyItem{
+				ClassifyName: name,
+				Exchange:     v.Exchange,
+				ExchangeName: exchangeName[v.Exchange],
+			})
+			continue
+		}
+		resp = append(resp, &tradeAnalysisModel.BaseFromTradeClassifyItem{
+			ClassifyName: v.ClassifyName,
+			Exchange:     v.Exchange,
+			ExchangeName: exchangeName[v.Exchange],
+		})
+	}
+
+	br.Data = resp
+	br.Msg = "获取成功"
+	br.Ret = 200
+	br.Success = true
+}
+
+// GetTradeContractList
+// @Title 获取合约列表
+// @Description 获取合约列表
+// @Param   Exchange  query  string  false  "交易所标识"
+// @Param   ClassifyName  query  string  false  "品种名称"
+// @Param   ContractName  query  string  false  "合约名称"
+// @Param   ExceptTop  query  bool  false  "排除TOP20: true-不包含; false-包含(默认)"
+// @Success 200 {object} tradeAnalysisModel.BaseFromTradeContractItem
+// @router /contract_list [get]
+func (this *TradeAnalysisController) GetTradeContractList() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	exchange := this.GetString("Exchange")
+	exchange = strings.TrimSpace(exchange)
+	classifyName := this.GetString("ClassifyName")
+	classifyName = strings.TrimSpace(classifyName)
+	contractName := this.GetString("ContractName")
+	contractName = strings.TrimSpace(contractName)
+	exceptTop, _ := this.GetBool("ExceptTop")
+
+	// 交易所名称
+	exchangeName := make(map[string]string)
+	{
+		var cond string
+		var pars []interface{}
+		exchangeOb := new(tradeAnalysisModel.BaseFromTradeExchange)
+		if exchange != "" {
+			cond += fmt.Sprintf(` AND %s = ?`, exchangeOb.Cols().Exchange)
+			pars = append(pars, exchange)
+			item, e := exchangeOb.GetItemByCondition(cond, pars, "")
+			if e != nil {
+				br.Msg = "获取失败"
+				br.ErrMsg = fmt.Sprintf("获取交易所信息失败, %v", e)
+				return
+			}
+			exchangeName[item.Exchange] = item.ExchangeName
+		} else {
+			list, e := exchangeOb.GetItemsByCondition(cond, pars, []string{}, "")
+			if e != nil {
+				br.Msg = "获取失败"
+				br.ErrMsg = fmt.Sprintf("获取交易所列表失败, %v", e)
+				return
+			}
+			for _, v := range list {
+				exchangeName[v.Exchange] = v.ExchangeName
+			}
+		}
+	}
+
+	// 搜索合约
+	var cond string
+	var pars []interface{}
+	classifyOb := new(tradeAnalysisModel.BaseFromTradeClassify)
+	if exchange != "" {
+		cond += fmt.Sprintf(` AND %s = ?`, classifyOb.Cols().Exchange)
+		pars = append(pars, exchange)
+	}
+	// 郑商所品种名称在列表中过滤
+	if classifyName != "" {
+		cond += fmt.Sprintf(` AND ((%s = ? AND %s <> ?) OR %s = ?)`, classifyOb.Cols().ClassifyName, classifyOb.Cols().Exchange, classifyOb.Cols().Exchange)
+		pars = append(pars, classifyName, tradeAnalysisModel.TradeExchangeZhengzhou, tradeAnalysisModel.TradeExchangeZhengzhou)
+	}
+	if contractName != "" {
+		cond += fmt.Sprintf(` AND %s LIKE ?`, classifyOb.Cols().ClassifyType)
+		pars = append(pars, fmt.Sprint("%", contractName, "%"))
+	}
+	list, e := classifyOb.GetItemsByCondition(cond, pars, []string{}, fmt.Sprintf("%s ASC, %s DESC", classifyOb.Cols().Exchange, classifyOb.Cols().LatestDate))
+	if e != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = fmt.Sprintf("获取合约列表失败, %v", e)
+		return
+	}
+
+	resp := make([]*tradeAnalysisModel.BaseFromTradeContractItem, 0)
+	if !exceptTop && contractName == "" {
+		resp = append(resp, &tradeAnalysisModel.BaseFromTradeContractItem{
+			ClassifyType: tradeAnalysisModel.TradeFuturesCompanyTop20,
+		})
+	}
+	for _, v := range list {
+		// 郑商所
+		classifyType := v.ClassifyType
+		if classifyName != "" && v.Exchange == tradeAnalysisModel.TradeExchangeZhengzhou {
+			name := trade_analysis.GetZhengzhouClassifyName(v.ClassifyName)
+			if classifyName != name {
+				continue
+			}
+			classifyType = v.ClassifyName
+		}
+		resp = append(resp, &tradeAnalysisModel.BaseFromTradeContractItem{
+			ClassifyName: v.ClassifyName,
+			ClassifyType: classifyType,
+			Exchange:     v.Exchange,
+			ExchangeName: exchangeName[v.Exchange],
+			LatestDate:   v.LatestDate.Format(utils.FormatDate),
+			CreateTime:   v.CreateTime.Format(utils.FormatDateTime),
+		})
+	}
+
+	br.Data = resp
+	br.Msg = "获取成功"
+	br.Ret = 200
+	br.Success = true
+}
+
+// GetTradeFuturesCompanyList
+// @Title 获取期货公司列表
+// @Description 获取期货公司列表
+// @Param   CompanyName  query  string  false  "公司名称"
+// @Success 200 {object} tradeAnalysisModel.BaseFromTradeContractItem
+// @router /company_list [get]
+func (this *TradeAnalysisController) GetTradeFuturesCompanyList() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	companyName := this.GetString("CompanyName")
+	companyName = strings.TrimSpace(companyName)
+
+	var cond string
+	var pars []interface{}
+	companyOb := new(tradeAnalysisModel.TradeFuturesCompany)
+	if companyName != "" {
+		cond += fmt.Sprintf(` AND %s LIKE ?`, companyOb.Cols().CompanyName)
+		pars = append(pars, fmt.Sprint("%", companyName, "%"))
+	}
+	list, e := companyOb.GetItemsByCondition(cond, pars, []string{}, fmt.Sprintf("%s ASC", companyOb.Cols().Sort))
+	if e != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = fmt.Sprintf("获取期货公司列表失败, %v", e)
+		return
+	}
+	resp := make([]*tradeAnalysisModel.TradeFuturesCompanyItem, 0)
+	for _, v := range list {
+		resp = append(resp, &tradeAnalysisModel.TradeFuturesCompanyItem{
+			CompanyId:   v.TradeFuturesCompanyId,
+			CompanyName: v.CompanyName,
+			Sort:        v.Sort,
+		})
+	}
+
+	br.Data = resp
+	br.Msg = "获取成功"
+	br.Ret = 200
+	br.Success = true
+}

+ 1812 - 0
controllers/trade_analysis/warehouse.go

@@ -0,0 +1,1812 @@
+package trade_analysis
+
+import (
+	"encoding/json"
+	"eta/eta_api/controllers"
+	"eta/eta_api/models"
+	"eta/eta_api/models/data_manage"
+	tradeAnalysisModel "eta/eta_api/models/data_manage/trade_analysis"
+	tradeAnalysisRequest "eta/eta_api/models/data_manage/trade_analysis/request"
+	tradeAnalysisResponse "eta/eta_api/models/data_manage/trade_analysis/response"
+	"eta/eta_api/services/alarm_msg"
+	"eta/eta_api/services/data"
+	tradeAnalysisService "eta/eta_api/services/data/trade_analysis"
+	"eta/eta_api/utils"
+	"fmt"
+	"github.com/rdlucklib/rdluck_tools/paging"
+	"sort"
+	"strconv"
+	"strings"
+	"time"
+)
+
+// WarehouseController 建仓过程
+type WarehouseController struct {
+	controllers.BaseAuthController
+}
+
+// Preview
+// @Title 图表预览/编辑页预览
+// @Description 图表预览/编辑页预览
+// @Param	request	body tradeAnalysisRequest.WarehousePreviewReq true "type json string"
+// @Success 200 {object} request.WarehousePreviewReq
+// @router /warehouse/chart/preview [post]
+func (this *WarehouseController) Preview() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		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 tradeAnalysisRequest.WarehousePreviewReq
+	if err := json.Unmarshal(this.Ctx.Input.RequestBody, &req); err != nil {
+		br.Msg = "参数解析异常!"
+		br.ErrMsg = "参数解析失败,Err:" + err.Error()
+		return
+	}
+	// 非编辑页预览时需要校验参数
+	if req.ChartInfoId <= 0 {
+		if req.ExtraConfig == nil {
+			br.Msg = "图表参数有误"
+			return
+		}
+		pass, tips := tradeAnalysisService.CheckWarehouseChartExtraConfig(*req.ExtraConfig)
+		if !pass {
+			br.Msg = tips
+			return
+		}
+	}
+	resp := new(tradeAnalysisResponse.WarehouseChartDetailResp)
+	resp.WarehouseCharts = make([]*data_manage.ChartInfoDetailResp, 0)
+
+	var (
+		multiConfigId                     int
+		extraConfig                       tradeAnalysisModel.WarehouseExtraConfig
+		configBuy, configSold, configPure tradeAnalysisModel.WarehouseChartPars
+		chartBuy, chartSold, chartPure    *data_manage.ChartInfoView
+	)
+	configBuy.WarehouseChartType = tradeAnalysisModel.WarehouseBuyChartType
+	configSold.WarehouseChartType = tradeAnalysisModel.WarehouseSoldChartType
+	configPure.WarehouseChartType = tradeAnalysisModel.WarehousePureBuyChartType
+
+	// 编辑页预览, 图表默认取自己的配置, 入参有值时则均取入参
+	if req.ChartInfoId > 0 {
+		chartInfoView, e := data_manage.GetChartInfoViewById(req.ChartInfoId)
+		if e != nil {
+			if e.Error() == utils.ErrNoRow() {
+				br.Msg = "图表已被删除, 请刷新页面"
+				return
+			}
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("获取图表信息失败, %v", e)
+			return
+		}
+		if chartInfoView.ExtraConfig == `` {
+			br.Msg = "图表配置信息错误"
+			return
+		}
+		if e = json.Unmarshal([]byte(chartInfoView.ExtraConfig), &extraConfig); e != nil {
+			br.Msg = "配置信息错误"
+			br.ErrMsg = fmt.Sprintf("图表配置信息错误, %v", e)
+			return
+		}
+		multiConfigId = extraConfig.MultipleGraphConfigId
+
+		// 图表的图例设置
+		seriesList, e := data_manage.GetChartSeriesByChartInfoId(req.ChartInfoId)
+		if e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("获取图例失败, %v", e)
+			return
+		}
+		saveItems := make([]*data_manage.ChartSaveItem, 0)
+		for _, v := range seriesList {
+			saveItems = append(saveItems, &data_manage.ChartSaveItem{
+				UniqueFlag:   v.UniqueFlag,
+				EdbAliasName: v.SeriesName,
+				IsAxis:       v.IsAxis,
+			})
+		}
+
+		// 图表取自己的日期配置
+		switch extraConfig.WarehouseChartType {
+		case tradeAnalysisModel.WarehouseBuyChartType:
+			chartBuy = chartInfoView
+			configBuy.WarehouseChartType = extraConfig.WarehouseChartType
+			configBuy.DateType = chartInfoView.DateType
+			configBuy.DateTypeNum = chartInfoView.DateTypeNum
+			configBuy.StartDate = chartInfoView.StartDate
+			configBuy.EndDate = chartInfoView.EndDate
+			configBuy.ChartEdbInfoList = saveItems
+		case tradeAnalysisModel.WarehouseSoldChartType:
+			chartSold = chartInfoView
+			configSold.WarehouseChartType = extraConfig.WarehouseChartType
+			configSold.DateType = chartInfoView.DateType
+			configSold.DateTypeNum = chartInfoView.DateTypeNum
+			configSold.StartDate = chartInfoView.StartDate
+			configSold.EndDate = chartInfoView.EndDate
+			configSold.ChartEdbInfoList = saveItems
+		case tradeAnalysisModel.WarehousePureBuyChartType:
+			chartPure = chartInfoView
+			configPure.WarehouseChartType = extraConfig.WarehouseChartType
+			configPure.DateType = chartInfoView.DateType
+			configPure.DateTypeNum = chartInfoView.DateTypeNum
+			configPure.StartDate = chartInfoView.StartDate
+			configPure.EndDate = chartInfoView.EndDate
+			configPure.ChartEdbInfoList = saveItems
+		}
+	}
+
+	// 入参有图表配置那么均取入参的
+	if req.ExtraConfig != nil {
+		extraConfig = *req.ExtraConfig
+	}
+	if len(req.ChartsConfig) > 0 {
+		for _, v := range req.ChartsConfig {
+			if v.WarehouseChartType == tradeAnalysisModel.WarehouseBuyChartType {
+				configBuy = v
+				continue
+			}
+			if v.WarehouseChartType == tradeAnalysisModel.WarehouseSoldChartType {
+				configSold = v
+				continue
+			}
+			if v.WarehouseChartType == tradeAnalysisModel.WarehousePureBuyChartType {
+				configPure = v
+			}
+		}
+	}
+
+	// 查询配置关联的指标
+	multiEdbBuy, multiEdbSold, multiEdbPure := make([]*tradeAnalysisModel.WarehouseEdbSaveItem, 0), make([]*tradeAnalysisModel.WarehouseEdbSaveItem, 0), make([]*tradeAnalysisModel.WarehouseEdbSaveItem, 0)
+	if multiConfigId > 0 {
+		multiEdbList, e := data_manage.GetMultipleGraphConfigEdbMappingListByIdAndSource(multiConfigId, utils.CHART_SOURCE_TRADE_ANALYSIS_PROCESS)
+		if e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("获取配置与图表指标关系失败, %v", e)
+			return
+		}
+		var edbIds []int
+		for _, v := range multiEdbList {
+			edbIds = append(edbIds, v.EdbInfoId)
+		}
+		if len(edbIds) > 0 {
+			edbList, e := data_manage.GetEdbInfoByIdList(edbIds)
+			if e != nil {
+				br.Msg = "获取失败"
+				br.ErrMsg = fmt.Sprintf("获取配置关联指标信息失败, %v", e)
+				return
+			}
+			for _, v := range edbList {
+				if v.CalculateFormula == "" {
+					utils.FileLog.Info(fmt.Sprintf("持仓分析图表详情-指标计算公式为空, EdbInfoId: %d", v.EdbInfoId))
+					continue
+				}
+				var conf tradeAnalysisModel.WarehouseExtraConfig
+				if e = json.Unmarshal([]byte(v.CalculateFormula), &conf); e != nil {
+					utils.FileLog.Info(fmt.Sprintf("持仓分析图表详情-指标计算公式解析失败, EdbInfoId: %d, err: %v", v.EdbInfoId, e))
+					continue
+				}
+				if len(conf.Companies) != 1 {
+					utils.FileLog.Info(fmt.Sprintf("持仓分析图表详情-指标计算公式信息有误, EdbInfoId: %d", v.EdbInfoId))
+					continue
+				}
+				item := new(tradeAnalysisModel.WarehouseEdbSaveItem)
+				item.EdbInfoId = v.EdbInfoId
+				item.EdbName = v.EdbName
+				item.Unit = v.Unit
+				item.Frequency = v.Frequency
+				item.ClassifyId = v.ClassifyId
+				item.UniqueFlag = conf.Companies[0]
+				switch conf.WarehouseChartType {
+				case tradeAnalysisModel.WarehouseBuyChartType:
+					multiEdbBuy = append(multiEdbBuy, item)
+				case tradeAnalysisModel.WarehouseSoldChartType:
+					multiEdbSold = append(multiEdbSold, item)
+				case tradeAnalysisModel.WarehousePureBuyChartType:
+					multiEdbPure = append(multiEdbPure, item)
+				}
+			}
+		}
+	}
+
+	// 获取指标数据, 该图表未用实际指标, 为了统一数据格式用ChartEdbInfoMapping
+	companyTradeData, e := tradeAnalysisService.GetOriginTradeData(extraConfig.Exchange, extraConfig.ClassifyName, extraConfig.Contracts, extraConfig.Companies, extraConfig.PredictRatio)
+	if e != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = fmt.Sprintf("获取期货公司持仓加总数据失败, %v", e)
+		return
+	}
+
+	// 获取三种图表
+	buyChartResp, e := tradeAnalysisService.GetWarehouseChartResp(chartBuy, companyTradeData, multiEdbBuy, extraConfig, configBuy)
+	if e != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = fmt.Sprintf("获取建仓过程多单图表数据失败, %v", e)
+		return
+	}
+	soldChartResp, e := tradeAnalysisService.GetWarehouseChartResp(chartSold, companyTradeData, multiEdbSold, extraConfig, configSold)
+	if e != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = fmt.Sprintf("获取建仓过程空单图表数据失败, %v", e)
+		return
+	}
+	pureChartResp, e := tradeAnalysisService.GetWarehouseChartResp(chartPure, companyTradeData, multiEdbPure, extraConfig, configPure)
+	if e != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = fmt.Sprintf("获取建仓过程净多单图表数据失败, %v", e)
+		return
+	}
+	resp.MultipleGraphConfigId = multiConfigId
+	resp.WarehouseCharts = append(resp.WarehouseCharts, buyChartResp, soldChartResp, pureChartResp)
+
+	// 多图配置
+	if multiConfigId == 0 {
+		multipleGraphConfig := &data_manage.MultipleGraphConfig{
+			SysUserId:       sysUser.AdminId,
+			SysUserRealName: sysUser.RealName,
+			ModifyTime:      time.Now(),
+			CreateTime:      time.Now(),
+		}
+		if e = data_manage.AddMultipleGraphConfig(multipleGraphConfig); e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("新增建仓过程多图配置失败, %v", e)
+			return
+		}
+		resp.MultipleGraphConfigId = multipleGraphConfig.MultipleGraphConfigId
+	}
+
+	br.Data = resp
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "操作成功"
+}
+
+// Add
+// @Title 新增图表接口
+// @Description 新增图表接口
+// @Param	request	body data_manage.AddChartInfoReq true "type json string"
+// @Success 200 {object} data_manage.AddChartInfoResp
+// @router /warehouse/chart/add [post]
+func (this *WarehouseController) Add() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	deleteCache := true
+	cacheKey := "CACHE_CHART_INFO_ADD_" + strconv.Itoa(sysUser.AdminId)
+	defer func() {
+		if deleteCache {
+			_ = utils.Rc.Delete(cacheKey)
+		}
+	}()
+	if !utils.Rc.SetNX(cacheKey, 1, 30*time.Second) {
+		deleteCache = false
+		br.Msg = "系统处理中,请稍后重试!"
+		br.ErrMsg = "系统处理中,请稍后重试!" + sysUser.RealName + ";data:" + string(this.Ctx.Input.RequestBody)
+		return
+	}
+	var req data_manage.AddChartInfoReq
+	if err := json.Unmarshal(this.Ctx.Input.RequestBody, &req); err != nil {
+		br.Msg = "参数解析异常!"
+		br.ErrMsg = "参数解析失败,Err:" + err.Error()
+		return
+	}
+
+	// 图表校验
+	req.ChartName = strings.Trim(req.ChartName, " ")
+	if req.ChartName == "" {
+		br.Msg = "请填写图表名称"
+		return
+	}
+	if req.ChartClassifyId <= 0 {
+		br.Msg = "请选择分类"
+		return
+	}
+	var extraConfig tradeAnalysisModel.WarehouseExtraConfig
+	if req.ExtraConfig == `` {
+		br.Msg = "配置有误"
+		br.ErrMsg = fmt.Sprintf("建仓图表配置有误, conf: %s", req.ExtraConfig)
+		return
+	}
+	if e := json.Unmarshal([]byte(req.ExtraConfig), &extraConfig); e != nil {
+		br.Msg = "配置有误"
+		br.ErrMsg = fmt.Sprintf("建仓图表配置解析失败, err: %v; conf: %s", e, req.ExtraConfig)
+		return
+	}
+	pass, tips := tradeAnalysisService.CheckWarehouseChartExtraConfig(extraConfig)
+	if !pass {
+		br.Msg = tips
+		return
+	}
+	// 校验图例
+	if len(req.ChartEdbInfoList) == 0 {
+		br.Msg = "请填写图例"
+		return
+	}
+	if len(req.ChartEdbInfoList) != len(extraConfig.Companies) {
+		br.Msg = "图例不一致"
+		br.ErrMsg = fmt.Sprintf("图例不一致, len: %d, companies: %d", len(req.ChartEdbInfoList), len(extraConfig.Companies))
+		return
+	}
+	flagExist := make(map[string]bool)
+	for _, v := range req.ChartEdbInfoList {
+		if v.UniqueFlag == "" {
+			br.Msg = ""
+		}
+		if flagExist[v.UniqueFlag] {
+			br.Msg = "图例异常"
+			br.ErrMsg = fmt.Sprintf("建仓图表图例异常, %s", v.UniqueFlag)
+			return
+		}
+	}
+
+	// 校验分类
+	_, e := data_manage.GetChartClassifyById(req.ChartClassifyId)
+	if e != nil {
+		if e.Error() == utils.ErrNoRow() {
+			br.Msg = "分类不存在, 请刷新页面"
+			return
+		}
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("获取分类失败, %v", e)
+		return
+	}
+
+	// 校验图表是否存在
+	{
+		var cond string
+		var pars []interface{}
+
+		switch this.Lang {
+		case utils.EnLangVersion:
+			cond += " AND chart_name_en = ? AND source = ? "
+		default:
+			cond += " AND chart_name = ? AND source = ? "
+		}
+		pars = append(pars, req.ChartName, utils.CHART_SOURCE_TRADE_ANALYSIS_PROCESS)
+		count, e := data_manage.GetChartInfoCountByCondition(cond, pars)
+		if e != nil {
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("获取同名图表计数失败, %v", e)
+			return
+		}
+		if count > 0 {
+			br.Msg = "图表名称已存在, 请重新填写"
+			return
+		}
+	}
+
+	// 新增图表
+	chartInfo, e := tradeAnalysisService.AddWarehouseChart(req, extraConfig, sysUser.AdminId, sysUser.RealName)
+	if e != nil {
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("新增持仓分析图表失败, %v", e)
+		return
+	}
+
+	// 新增操作日志
+	{
+		chartLog := new(data_manage.ChartInfoLog)
+		chartLog.ChartInfoId = chartInfo.ChartInfoId
+		chartLog.ChartName = req.ChartName
+		chartLog.ChartClassifyId = req.ChartClassifyId
+		chartLog.SysUserId = sysUser.AdminId
+		chartLog.SysUserRealName = sysUser.RealName
+		chartLog.UniqueCode = chartInfo.UniqueCode
+		chartLog.CreateTime = time.Now()
+		chartLog.Content = string(this.Ctx.Input.RequestBody)
+		chartLog.Status = "新增持仓分析图表"
+		chartLog.Method = this.Ctx.Input.URI()
+		go data_manage.AddChartInfoLog(chartLog)
+	}
+
+	resp := new(data_manage.AddChartInfoResp)
+	resp.ChartInfoId = chartInfo.ChartInfoId
+	resp.UniqueCode = chartInfo.UniqueCode
+	resp.ChartType = chartInfo.ChartType
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "保存成功"
+	br.Data = resp
+	br.IsAddLog = true
+}
+
+// Edit
+// @Title 编辑图表接口
+// @Description 编辑图表接口
+// @Param	request	body data_manage.EditChartInfoReq true "type json string"
+// @Success Ret=200 保存成功
+// @router /warehouse/chart/edit [post]
+func (this *WarehouseController) Edit() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		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 data_manage.EditChartInfoReq
+	if e := json.Unmarshal(this.Ctx.Input.RequestBody, &req); e != nil {
+		br.Msg = "参数解析失败"
+		br.ErrMsg = fmt.Sprintf("参数解析失败, %v", e)
+		return
+	}
+
+	// 图表校验
+	req.ChartName = strings.Trim(req.ChartName, " ")
+	if req.ChartName == "" {
+		br.Msg = "请填写图表名称"
+		return
+	}
+	if req.ChartClassifyId <= 0 {
+		br.Msg = "请选择分类"
+		return
+	}
+	var extraConfig tradeAnalysisModel.WarehouseExtraConfig
+	if req.ExtraConfig == `` {
+		br.Msg = "配置有误"
+		br.ErrMsg = fmt.Sprintf("建仓图表配置有误, conf: %s", req.ExtraConfig)
+		return
+	}
+	if e := json.Unmarshal([]byte(req.ExtraConfig), &extraConfig); e != nil {
+		br.Msg = "配置有误"
+		br.ErrMsg = fmt.Sprintf("建仓图表配置解析失败, err: %v; conf: %s", e, req.ExtraConfig)
+		return
+	}
+	pass, tips := tradeAnalysisService.CheckWarehouseChartExtraConfig(extraConfig)
+	if !pass {
+		br.Msg = tips
+		return
+	}
+
+	// 图表校验
+	chartItem, e := data_manage.GetChartInfoById(req.ChartInfoId)
+	if e != nil {
+		if e.Error() == utils.ErrNoRow() {
+			br.Msg = "图表不存在, 请刷新页面"
+			return
+		}
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("获取图表信息失败, %v", e)
+		return
+	}
+	if chartItem.Source != utils.CHART_SOURCE_TRADE_ANALYSIS_PROCESS {
+		br.Msg = "图表类型异常"
+		br.ErrMsg = fmt.Sprintf("图表类型异常: %d", chartItem.Source)
+		return
+	}
+
+	// 图表操作权限
+	ok := data.CheckOpChartPermission(sysUser, chartItem.SysUserId, true)
+	if !ok {
+		br.Msg = "没有该图表的操作权限"
+		return
+	}
+
+	// 校验图例
+	if len(req.ChartEdbInfoList) == 0 {
+		br.Msg = "请填写图例"
+		return
+	}
+	if len(req.ChartEdbInfoList) != len(extraConfig.Companies) {
+		br.Msg = "图例不一致"
+		br.ErrMsg = fmt.Sprintf("图例不一致, len: %d, companies: %d", len(req.ChartEdbInfoList), len(extraConfig.Companies))
+		return
+	}
+	flagExist := make(map[string]bool)
+	for _, v := range req.ChartEdbInfoList {
+		if v.UniqueFlag == "" {
+			br.Msg = ""
+		}
+		if flagExist[v.UniqueFlag] {
+			br.Msg = "图例异常"
+			br.ErrMsg = fmt.Sprintf("建仓图表图例异常, %s", v.UniqueFlag)
+			return
+		}
+	}
+
+	// 校验分类
+	_, e = data_manage.GetChartClassifyById(req.ChartClassifyId)
+	if e != nil {
+		if e.Error() == utils.ErrNoRow() {
+			br.Msg = "分类不存在, 请刷新页面"
+			return
+		}
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("获取分类失败, %v", e)
+		return
+	}
+
+	// 校验图表是否存在
+	{
+		var cond string
+		var pars []interface{}
+		cond += " AND chart_info_id <> ? "
+		pars = append(pars, req.ChartInfoId)
+
+		switch this.Lang {
+		case utils.EnLangVersion:
+			cond += " AND chart_name_en = ? AND source = ? "
+		default:
+			cond += " AND chart_name = ? AND source = ? "
+		}
+		pars = append(pars, req.ChartName, utils.CHART_SOURCE_TRADE_ANALYSIS_PROCESS)
+		count, e := data_manage.GetChartInfoCountByCondition(cond, pars)
+		if e != nil {
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("获取同名图表计数失败, %v", e)
+			return
+		}
+		if count > 0 {
+			br.Msg = "图表名称已存在, 请重新填写"
+			return
+		}
+	}
+
+	// 更新图表
+	chartItem, e = tradeAnalysisService.EditWarehouseChart(req)
+	if e != nil {
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("新增持仓分析图表失败, %v", e)
+		return
+	}
+	resp := new(data_manage.AddChartInfoResp)
+	resp.ChartInfoId = chartItem.ChartInfoId
+	resp.UniqueCode = chartItem.UniqueCode
+	resp.ChartType = req.ChartType
+
+	// 新增操作日志
+	{
+		chartLog := new(data_manage.ChartInfoLog)
+		chartLog.ChartName = chartItem.ChartName
+		chartLog.ChartInfoId = req.ChartInfoId
+		chartLog.ChartClassifyId = chartItem.ChartClassifyId
+		chartLog.SysUserId = sysUser.AdminId
+		chartLog.SysUserRealName = sysUser.RealName
+		chartLog.UniqueCode = chartItem.UniqueCode
+		chartLog.CreateTime = time.Now()
+		chartLog.Content = string(this.Ctx.Input.RequestBody)
+		chartLog.Status = "编辑持仓分析图表"
+		chartLog.Method = this.Ctx.Input.URL()
+		go data_manage.AddChartInfoLog(chartLog)
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "保存成功"
+	br.Data = resp
+	br.IsAddLog = true
+}
+
+// Detail
+// @Title 获取图表详情
+// @Description 获取图表详情接口
+// @Param   ChartInfoId   query   int  true       "图表id"
+// @Param   DateType   query   int  true       "日期类型:1:00年至今,2:10年至今,3:15年至今,4:年初至今,5:自定义时间"
+// @Param   StartDate   query   string  true       "自定义开始日期"
+// @Param   EndDate   query   string  true       "自定义结束日期"
+// @Param   Calendar   query   string  true       "公历/农历"
+// @Param   SeasonStartDate   query   string  true       "季节性图开始日期"
+// @Param   SeasonEndDate   query   string  true       "季节性图结束日期"
+// @Param   EdbInfoId   query   string  true       "指标ID,多个用英文逗号隔开"
+// @Param   ChartType   query   int  true       "生成样式:1:曲线图,2:季节性图"
+// @Success 200 {object} data_manage.ChartInfoDetailResp
+// @router /warehouse/chart/detail [get]
+func (this *WarehouseController) Detail() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	chartInfoId, _ := this.GetInt("ChartInfoId")
+	if chartInfoId <= 0 {
+		br.Msg = "参数有误"
+		return
+	}
+	dateType, _ := this.GetInt("DateType")
+	startDate := this.GetString("StartDate")
+	endDate := this.GetString("EndDate")
+	dateTypeNum, _ := this.GetInt("DateTypeNum")
+
+	var err error
+	chartInfo := new(data_manage.ChartInfoView)
+	chartInfo, err = data_manage.GetChartInfoViewById(chartInfoId)
+	if err != nil {
+		if err.Error() == utils.ErrNoRow() {
+			br.Msg = "图被删除,请刷新页面"
+			br.ErrMsg = "图被删除,请刷新页面,Err:" + err.Error()
+			return
+		}
+		br.Msg = "获取失败"
+		br.ErrMsg = "获取图表信息失败,Err:" + err.Error()
+		return
+	}
+	if dateType <= 0 {
+		dateType = chartInfo.DateType
+	}
+	if startDate == "" {
+		startDate = chartInfo.StartDate
+	}
+	if endDate == "" {
+		endDate = chartInfo.EndDate
+	}
+	if dateTypeNum <= 0 {
+		dateTypeNum = chartInfo.DateTypeNum
+	}
+
+	// 持仓分析图表配置校验
+	var extraConfig tradeAnalysisModel.WarehouseExtraConfig
+	if chartInfo.ExtraConfig == `` {
+		br.Msg = "配置信息错误"
+		return
+	}
+	if e := json.Unmarshal([]byte(chartInfo.ExtraConfig), &extraConfig); e != nil {
+		br.Msg = "配置信息错误"
+		br.ErrMsg = fmt.Sprintf("图表配置信息错误, %v", e)
+		return
+	}
+
+	// 获取图表数据
+	companyTradeData, e := tradeAnalysisService.GetOriginTradeData(extraConfig.Exchange, extraConfig.ClassifyName, extraConfig.Contracts, extraConfig.Companies, extraConfig.PredictRatio)
+	if e != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = fmt.Sprintf("获取期货公司持仓加总数据失败, %v", e)
+		return
+	}
+
+	var chartConfig tradeAnalysisModel.WarehouseChartPars
+	chartConfig.WarehouseChartType = extraConfig.WarehouseChartType
+	chartConfig.DateType = dateType
+	chartConfig.StartDate = startDate
+	chartConfig.EndDate = endDate
+	chartConfig.DateTypeNum = dateTypeNum
+	// 获取图表的图例设置
+	seriesList, e := data_manage.GetChartSeriesByChartInfoId(chartInfoId)
+	if e != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = fmt.Sprintf("获取图例失败, %v", e)
+		return
+	}
+	saveItems := make([]*data_manage.ChartSaveItem, 0)
+	for _, v := range seriesList {
+		saveItems = append(saveItems, &data_manage.ChartSaveItem{
+			UniqueFlag:   v.UniqueFlag,
+			EdbAliasName: v.SeriesName,
+			IsAxis:       v.IsAxis,
+		})
+	}
+	chartConfig.ChartEdbInfoList = saveItems
+
+	// 查询配置关联的指标
+	multiEdb := make([]*tradeAnalysisModel.WarehouseEdbSaveItem, 0)
+	if extraConfig.MultipleGraphConfigId > 0 {
+		multiEdbList, e := data_manage.GetMultipleGraphConfigEdbMappingListByIdAndSource(extraConfig.MultipleGraphConfigId, utils.CHART_SOURCE_TRADE_ANALYSIS_PROCESS)
+		if e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("获取配置与图表指标关系失败, %v", e)
+			return
+		}
+		var edbIds []int
+		for _, v := range multiEdbList {
+			edbIds = append(edbIds, v.EdbInfoId)
+		}
+		if len(edbIds) > 0 {
+			edbList, e := data_manage.GetEdbInfoByIdList(edbIds)
+			if e != nil {
+				br.Msg = "获取失败"
+				br.ErrMsg = fmt.Sprintf("获取配置关联指标信息失败, %v", e)
+				return
+			}
+			for _, v := range edbList {
+				if v.CalculateFormula == "" {
+					utils.FileLog.Info(fmt.Sprintf("持仓分析图表详情-指标计算公式为空, EdbInfoId: %d", v.EdbInfoId))
+					continue
+				}
+				var conf tradeAnalysisModel.WarehouseExtraConfig
+				if e = json.Unmarshal([]byte(v.CalculateFormula), &conf); e != nil {
+					utils.FileLog.Info(fmt.Sprintf("持仓分析图表详情-指标计算公式解析失败, EdbInfoId: %d, err: %v", v.EdbInfoId, e))
+					continue
+				}
+				if len(conf.Companies) != 1 {
+					utils.FileLog.Info(fmt.Sprintf("持仓分析图表详情-指标计算公式信息有误, EdbInfoId: %d", v.EdbInfoId))
+					continue
+				}
+				item := new(tradeAnalysisModel.WarehouseEdbSaveItem)
+				item.EdbInfoId = v.EdbInfoId
+				item.EdbName = v.EdbName
+				item.Unit = v.Unit
+				item.Frequency = v.Frequency
+				item.ClassifyId = v.ClassifyId
+				item.UniqueFlag = conf.Companies[0]
+				if conf.WarehouseChartType == extraConfig.WarehouseChartType {
+					multiEdb = append(multiEdb, item)
+				}
+			}
+		}
+	}
+
+	chartResp, e := tradeAnalysisService.GetWarehouseChartResp(chartInfo, companyTradeData, multiEdb, extraConfig, chartConfig)
+	if e != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = fmt.Sprintf("获取图表详情失败, %v", e)
+		return
+	}
+	resp := chartResp
+
+	// 判断是否加入我的图库
+	if chartInfoId > 0 && chartInfo != nil {
+		{
+			var myChartCondition string
+			var myChartPars []interface{}
+			myChartCondition += ` AND a.admin_id=? `
+			myChartPars = append(myChartPars, sysUser.AdminId)
+			myChartCondition += ` AND a.chart_info_id=? `
+			myChartPars = append(myChartPars, chartInfo.ChartInfoId)
+
+			myChartList, err := data_manage.GetMyChartByCondition(myChartCondition, myChartPars)
+			if err != nil && err.Error() != utils.ErrNoRow() {
+				br.Msg = "获取失败"
+				br.ErrMsg = "获取我的图表信息失败,GetMyChartByCondition,Err:" + err.Error()
+				return
+			}
+			if myChartList != nil && len(myChartList) > 0 {
+				chartInfo.IsAdd = true
+				chartInfo.MyChartId = myChartList[0].MyChartId
+				chartInfo.MyChartClassifyId = myChartList[0].MyChartClassifyId
+			}
+		}
+	}
+
+	// 图表操作权限
+	edbList := make([]*data_manage.ChartEdbInfoMapping, 0)
+	chartInfo.IsEdit = data.CheckOpChartPermission(sysUser, chartInfo.SysUserId, true)
+	// 判断是否需要展示英文标识
+	//chartInfo.IsEnChart = data.CheckIsEnChart(chartInfo.ChartNameEn, edbList, chartInfo.Source, chartInfo.ChartType)
+
+	// 数据来源
+	sourceNameList, sourceNameEnList := data.GetEdbSourceByEdbInfoIdList(edbList)
+	sourceNameList = append(sourceNameList, utils.SourceNameTradeAnalysis)
+	sourceNameEnList = append(sourceNameEnList, utils.SourceNameTradeAnalysis)
+	chartInfo.ChartSource = strings.Join(sourceNameList, ",")
+	chartInfo.ChartSourceEn = strings.Join(sourceNameEnList, ",")
+
+	// 另存为
+	chartInfo.Button = data_manage.ChartViewButton{
+		IsEdit:    chartInfo.IsEdit,
+		IsEnChart: chartInfo.IsEnChart,
+		IsAdd:     chartInfo.IsAdd,
+		IsCopy:    chartInfo.IsEdit,
+		IsSetName: chartInfo.IsSetName,
+	}
+
+	// 图表当前分类的分类树
+	classifyLevels := make([]string, 0)
+	{
+		list, e := data_manage.GetChartClassifyAllBySource(utils.CHART_SOURCE_TRADE_ANALYSIS_PROCESS)
+		if e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("获取图表分类失败, Err: %v", e)
+			return
+		}
+		parents := data.GetChartClassifyParentRecursive(list, chartInfo.ChartClassifyId)
+		sort.Slice(parents, func(i, j int) bool {
+			return parents[i].Level < parents[i].Level
+		})
+		for _, v := range parents {
+			classifyLevels = append(classifyLevels, v.UniqueCode)
+		}
+	}
+
+	resp.ClassifyLevels = classifyLevels
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+	br.Data = resp
+}
+
+// List
+// @Title 持仓分析图表列表接口
+// @Description 持仓分析图表列表接口
+// @Param   PageSize   query   int  true       "每页数据条数"
+// @Param   CurrentIndex   query   int  true       "当前页页码,从1开始"
+// @Param   ChartClassifyId   query   int  true       "分类id"
+// @Param   Keyword   query   string  true       "搜索关键词"
+// @Param   IsShowMe   query   bool  true       "是否只看我的,true、false"
+// @Success 200 {object} data_manage.ChartListResp
+// @router /warehouse/chart/list [get]
+func (this *WarehouseController) List() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+
+	chartClassifyId, _ := this.GetInt("ChartClassifyId")
+
+	pageSize, _ := this.GetInt("PageSize")
+	currentIndex, _ := this.GetInt("CurrentIndex")
+	keyword := this.GetString("KeyWord")
+
+	var total int
+	page := paging.GetPaging(currentIndex, pageSize, total)
+
+	var startSize int
+	if pageSize <= 0 {
+		pageSize = utils.PageSize20
+	}
+	if currentIndex <= 0 {
+		currentIndex = 1
+	}
+	startSize = paging.StartIndex(currentIndex, pageSize)
+
+	source := utils.CHART_SOURCE_TRADE_ANALYSIS_PROCESS
+
+	var condition string
+	var pars []interface{}
+
+	// 普通图表
+	condition += ` AND source = ? `
+	pars = append(pars, source)
+
+	if chartClassifyId > 0 {
+		classifyIds, err := data_manage.GetChartClassify(chartClassifyId)
+		if err != nil && err.Error() != utils.ErrNoRow() {
+			br.Msg = "获取图表信息失败"
+			br.ErrMsg = "获取信息失败,GetChartClassify,Err:" + err.Error()
+			return
+		}
+		if classifyIds == "" {
+			resp := new(data_manage.ChartListResp)
+			page = paging.GetPaging(currentIndex, pageSize, 0)
+			resp.Paging = page
+			resp.List = make([]*data_manage.ChartInfoView, 0)
+			br.Ret = 200
+			br.Success = true
+			br.Msg = "获取成功"
+			br.Data = resp
+			return
+		}
+		condition += " AND chart_classify_id IN(" + classifyIds + ") "
+	}
+	if keyword != "" {
+		//将关键词按照空格分割
+		keywords := strings.Split(keyword, " ")
+		condition += ` AND  ( chart_name LIKE '%` + keywords[0] + `%' `
+		for k, key := range keywords {
+			if k == 0 {
+				continue
+			}
+			condition += ` OR chart_name LIKE '%` + key + `%' `
+		}
+		condition += ` )`
+	}
+
+	//只看我的
+	isShowMe, _ := this.GetBool("IsShowMe")
+	if isShowMe {
+		condition += ` AND sys_user_id = ? `
+		pars = append(pars, sysUser.AdminId)
+	}
+
+	// 获取当前账号的不可见指标
+	noPermissionChartIdList := make([]int, 0)
+	{
+		obj := data_manage.EdbInfoNoPermissionAdmin{}
+		confList, err := obj.GetAllChartListByAdminId(this.SysUser.AdminId)
+		if err != nil && err.Error() != utils.ErrNoRow() {
+			br.Msg = "获取失败"
+			br.ErrMsg = "获取不可见指标配置数据失败,Err:" + err.Error()
+			return
+		}
+		for _, v := range confList {
+			noPermissionChartIdList = append(noPermissionChartIdList, v.ChartInfoId)
+		}
+	}
+
+	lenNoPermissionChartIdList := len(noPermissionChartIdList)
+	if lenNoPermissionChartIdList > 0 {
+		condition += ` AND chart_info_id not in (` + utils.GetOrmInReplace(lenNoPermissionChartIdList) + `) `
+		pars = append(pars, noPermissionChartIdList)
+	}
+
+	//获取图表信息
+	list, err := data_manage.GetChartListByCondition(condition, pars, startSize, pageSize)
+	if err != nil && err.Error() != utils.ErrNoRow() {
+		br.Success = true
+		br.Msg = "获取图表信息失败"
+		br.ErrMsg = "获取图表信息失败,Err:" + err.Error()
+		return
+	}
+
+	myChartList, err := data_manage.GetMyChartListByAdminId(sysUser.AdminId)
+	if err != nil && err.Error() != utils.ErrNoRow() {
+		br.Msg = "获取图表信息失败"
+		br.ErrMsg = "获取我的图表信息失败,Err:" + err.Error()
+		return
+	}
+	myChartMap := make(map[int]*data_manage.MyChartView)
+	for _, v := range myChartList {
+		myChartMap[v.ChartInfoId] = v
+	}
+	listLen := len(list)
+	chartEdbMap := make(map[int][]*data_manage.ChartEdbInfoMapping)
+	if listLen > 0 {
+		chartInfoIds := ""
+		for _, v := range list {
+			chartInfoIds += strconv.Itoa(v.ChartInfoId) + ","
+		}
+		if chartInfoIds != "" {
+			chartInfoIds = strings.Trim(chartInfoIds, ",")
+			//判断是否需要展示英文标识
+			edbList, e := data_manage.GetChartEdbMappingListByChartInfoIds(chartInfoIds)
+			if e != nil {
+				br.Msg = "获取失败"
+				br.ErrMsg = "获取图表,指标信息失败,Err:" + e.Error()
+				return
+			}
+			for _, v := range edbList {
+				chartEdbMap[v.ChartInfoId] = append(chartEdbMap[v.ChartInfoId], v)
+			}
+		}
+	}
+	for i := 0; i < listLen; i++ {
+		//判断是否需要展示英文标识
+		if edbTmpList, ok := chartEdbMap[list[i].ChartInfoId]; ok {
+			list[i].IsEnChart = data.CheckIsEnChart(list[i].ChartNameEn, edbTmpList, list[i].Source, list[i].ChartType)
+		}
+
+		if existItem, ok := myChartMap[list[i].ChartInfoId]; ok {
+			list[i].IsAdd = true
+			list[i].MyChartId = existItem.MyChartId
+			list[i].MyChartClassifyId = existItem.MyChartClassifyId
+		}
+	}
+
+	resp := new(data_manage.ChartListResp)
+	if list == nil || len(list) <= 0 || (err != nil && err.Error() == utils.ErrNoRow()) {
+		items := make([]*data_manage.ChartInfoView, 0)
+		resp.Paging = page
+		resp.List = items
+		br.Ret = 200
+		br.Success = true
+		br.Msg = "获取成功"
+		return
+	}
+
+	dataCount, err := data_manage.GetChartListCountByCondition(condition, pars)
+	if err != nil && err.Error() != utils.ErrNoRow() {
+		br.Msg = "获取指标信息失败"
+		br.ErrMsg = "获取指标数据总数失败,Err:" + err.Error()
+		return
+	}
+	page = paging.GetPaging(currentIndex, pageSize, dataCount)
+	resp.Paging = page
+	resp.List = list
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+	br.Data = resp
+}
+
+// Copy
+// @Title 复制并新增图表接口
+// @Description 新增图表接口
+// @Params	request	body data_manage.CopyAddChartInfoReq true "type json string"
+// @Success 200 {object} data_manage.AddChartInfoResp
+// @router /warehouse/chart/copy [post]
+func (this *WarehouseController) Copy() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		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 data_manage.CopyAddChartInfoReq
+	err := json.Unmarshal(this.Ctx.Input.RequestBody, &req)
+	if err != nil {
+		br.Msg = "参数解析异常!"
+		br.ErrMsg = "参数解析失败,Err:" + err.Error()
+		return
+	}
+	if req.ChartInfoId <= 0 {
+		br.Msg = "参数有误"
+		br.ErrMsg = fmt.Sprintf("参数有误, ChartInfoId: %d", req.ChartInfoId)
+		return
+	}
+	req.ChartName = strings.TrimSpace(req.ChartName)
+	if req.ChartName == "" {
+		br.Msg = "请输入图表名称"
+		return
+	}
+	if req.ChartClassifyId <= 0 {
+		br.Msg = "请选择图表分类"
+		return
+	}
+
+	deleteCache := true
+	cacheKey := "CACHE_CHART_INFO_ADD_" + strconv.Itoa(sysUser.AdminId)
+	defer func() {
+		if deleteCache {
+			utils.Rc.Delete(cacheKey)
+		}
+	}()
+	if !utils.Rc.SetNX(cacheKey, 1, 30*time.Second) {
+		deleteCache = false
+		br.Msg = "系统处理中,请稍后重试!"
+		br.ErrMsg = "系统处理中,请稍后重试!" + sysUser.RealName + ";data:" + string(this.Ctx.Input.RequestBody)
+		return
+	}
+
+	chartSource := utils.CHART_SOURCE_TRADE_ANALYSIS_PROCESS
+	// 校验分类、图表名称
+	{
+		var cond string
+		var pars []interface{}
+		switch this.Lang {
+		case utils.EnLangVersion:
+			cond += " AND chart_name_en = ? AND source = ? "
+		default:
+			cond += " AND chart_name = ? AND source = ? "
+		}
+		pars = append(pars, req.ChartName, chartSource)
+		count, e := data_manage.GetChartInfoCountByCondition(cond, pars)
+		if e != nil {
+			br.Msg = "保存失败"
+			br.ErrMsg = fmt.Sprintf("获取同名图表失败, Err: %v", e)
+			return
+		}
+		if count > 0 {
+			br.Msg = "图表名称已存在, 请重新填写"
+			return
+		}
+
+		_, e = data_manage.GetChartClassifyById(req.ChartClassifyId)
+		if e != nil {
+			if e.Error() == utils.ErrNoRow() {
+				br.Msg = "分类不存在"
+				return
+			}
+			br.Msg = "保存失败"
+			br.ErrMsg = fmt.Sprintf("获取图表分类失败, Err: %v", e)
+			return
+		}
+	}
+
+	// 原图表
+	originChart, e := data_manage.GetChartInfoById(req.ChartInfoId)
+	if e != nil {
+		if e.Error() == utils.ErrNoRow() {
+			br.Msg = "原图表不存在"
+			return
+		}
+		br.Msg = "保存失败"
+		br.ErrMsg = fmt.Sprintf("获取原图表信息失败, Err: %v", e)
+		return
+	}
+
+	chartInfo := new(data_manage.ChartInfo)
+	newChart, e := tradeAnalysisService.CopyWarehouseChart(req.ChartClassifyId, req.ChartName, originChart, sysUser.AdminId, sysUser.RealName)
+	if e != nil {
+		br.Msg = "保存失败"
+		br.ErrMsg = fmt.Sprintf("复制图表失败, %v", e)
+		return
+	}
+	chartInfo = newChart
+
+	// 新增操作日志
+	{
+		chartLog := new(data_manage.ChartInfoLog)
+		chartLog.ChartInfoId = chartInfo.ChartInfoId
+		chartLog.ChartName = req.ChartName
+		chartLog.ChartClassifyId = req.ChartClassifyId
+		chartLog.SysUserId = sysUser.AdminId
+		chartLog.SysUserRealName = sysUser.RealName
+		chartLog.UniqueCode = chartInfo.UniqueCode
+		chartLog.CreateTime = time.Now()
+		chartLog.Content = string(this.Ctx.Input.RequestBody)
+		chartLog.Status = "复制区间计算图表"
+		chartLog.Method = this.Ctx.Input.URI()
+		go data_manage.AddChartInfoLog(chartLog)
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "保存成功"
+	br.Data = data_manage.AddChartInfoResp{
+		ChartInfoId: chartInfo.ChartInfoId,
+		UniqueCode:  chartInfo.UniqueCode,
+		ChartType:   chartInfo.ChartType,
+	}
+	br.IsAddLog = true
+}
+
+// Refresh
+// @Title 图表刷新接口
+// @Description 图表刷新接口
+// @Param   ChartInfoId   query   int  true       "图表id"
+// @Param   UniqueCode   query   string  true       "唯一code"
+// @Success Ret=200 刷新成功
+// @router /warehouse/chart/refresh [get]
+func (this *WarehouseController) Refresh() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	chartInfoId, _ := this.GetInt("ChartInfoId")
+	uniqueCode := this.GetString("UniqueCode")
+	if chartInfoId <= 0 && uniqueCode == `` {
+		br.Msg = "参数错误"
+		br.ErrMsg = "参数错误:chartInfoId:" + strconv.Itoa(chartInfoId) + ",UniqueCode:" + uniqueCode
+		return
+	}
+
+	var chartInfo *data_manage.ChartInfo
+	var err error
+	if chartInfoId > 0 {
+		chartInfo, err = data_manage.GetChartInfoById(chartInfoId)
+	} else {
+		chartInfo, err = data_manage.GetChartInfoByUniqueCode(uniqueCode)
+	}
+	if err != nil {
+		if err.Error() == utils.ErrNoRow() {
+			br.Msg = "图表已被删除,无需刷新"
+			br.ErrMsg = "获取指标信息失败,Err:" + err.Error()
+			return
+		}
+		br.Msg = "刷新失败"
+		br.ErrMsg = "获取图表信息失败,Err:" + err.Error()
+		return
+	}
+
+	// 清除图表缓存
+	{
+		key := utils.HZ_CHART_LIB_DETAIL + chartInfo.UniqueCode
+		_ = utils.Rc.Delete(key)
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "刷新成功"
+}
+
+// SearchByEs
+// @Title 图表模糊搜索(从es获取)
+// @Description  图表模糊搜索(从es获取)
+// @Param   Keyword   query   string  true       "图表名称"
+// @Param   IsShowMe   query   bool  true       "是否只看我的,true、false"
+// @Param   Source   query   int  true       "来源,3:相关性,4:滚动相关性,默认0:全部"
+// @Success 200 {object} data_manage.ChartInfo
+// @router /warehouse/chart/search_by_es [get]
+func (this *WarehouseController) SearchByEs() {
+	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
+	}
+	pageSize, _ := this.GetInt("PageSize")
+	currentIndex, _ := this.GetInt("CurrentIndex")
+
+	var startSize int
+	if pageSize <= 0 {
+		pageSize = utils.PageSize20
+	}
+	if currentIndex <= 0 {
+		currentIndex = 1
+	}
+	startSize = paging.StartIndex(currentIndex, pageSize)
+
+	keyword := this.GetString("Keyword")
+
+	//只看我的
+	isShowMe, _ := this.GetBool("IsShowMe")
+	showSysId := 0
+	if isShowMe {
+		showSysId = sysUser.AdminId
+	}
+
+	sourceList := make([]int, 0)
+	sourceList = append(sourceList, utils.CHART_SOURCE_TRADE_ANALYSIS_PROCESS)
+
+	var searchList []*data_manage.ChartInfo
+	var total int64
+	var err error
+
+	// 获取当前账号的不可见指标
+	noPermissionChartIdList := make([]int, 0)
+	{
+		obj := data_manage.EdbInfoNoPermissionAdmin{}
+		confList, err := obj.GetAllChartListByAdminId(this.SysUser.AdminId)
+		if err != nil && err.Error() != utils.ErrNoRow() {
+			br.Msg = "获取失败"
+			br.ErrMsg = "获取不可见指标配置数据失败,Err:" + err.Error()
+			return
+		}
+		for _, v := range confList {
+			noPermissionChartIdList = append(noPermissionChartIdList, v.ChartInfoId)
+		}
+	}
+
+	if keyword != "" {
+		searchList, total, err = data.EsSearchChartInfo(keyword, showSysId, sourceList, noPermissionChartIdList, startSize, pageSize)
+	} else {
+		total, searchList, err = data_manage.ChartInfoSearchByEmptyKeyWord(showSysId, sourceList, noPermissionChartIdList, startSize, pageSize)
+		if err != nil && err.Error() != utils.ErrNoRow() {
+			br.Msg = "获取失败"
+			br.ErrMsg = "获取图表信息失败,Err:" + err.Error()
+			return
+		}
+	}
+
+	finalList := make([]*data_manage.ChartInfoMore, 0)
+	if len(searchList) > 0 {
+		chartInfoIds := ""
+		chartEdbMap := make(map[int][]*data_manage.ChartEdbInfoMapping)
+		for _, v := range searchList {
+			chartInfoIds += strconv.Itoa(v.ChartInfoId) + ","
+		}
+		if chartInfoIds != "" {
+			chartInfoIds = strings.Trim(chartInfoIds, ",")
+			//判断是否需要展示英文标识
+			edbList, e := data_manage.GetChartEdbMappingListByChartInfoIds(chartInfoIds)
+			if e != nil {
+				br.Msg = "获取失败"
+				br.ErrMsg = "获取图表,指标信息失败,Err:" + e.Error()
+				return
+			}
+			for _, v := range edbList {
+				chartEdbMap[v.ChartInfoId] = append(chartEdbMap[v.ChartInfoId], v)
+			}
+		}
+
+		for _, v := range searchList {
+			tmp := new(data_manage.ChartInfoMore)
+			tmp.ChartInfo = *v
+			// 图表数据权限
+			tmp.HaveOperaAuth = true
+			//判断是否需要展示英文标识
+			if edbTmpList, ok := chartEdbMap[v.ChartInfoId]; ok {
+				tmp.IsEnChart = data.CheckIsEnChart(v.ChartNameEn, edbTmpList, v.Source, v.ChartType)
+			}
+			finalList = append(finalList, tmp)
+		}
+	}
+	//新增搜索词记录
+	{
+		searchKeyword := new(data_manage.SearchKeyword)
+		searchKeyword.KeyWord = keyword
+		searchKeyword.CreateTime = time.Now()
+		go data_manage.AddSearchKeyword(searchKeyword)
+	}
+
+	page := paging.GetPaging(currentIndex, pageSize, int(total))
+	resp := data_manage.ChartInfoListByEsResp{
+		Paging: page,
+		List:   finalList,
+	}
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+	br.Data = resp
+}
+
+// ChartInfoSave
+// @Title 保存图表接口
+// @Description 保存图表接口
+// @Param	request	body data_manage.SaveChartInfoReq true "type json string"
+// @Success Ret=200 返回图表id
+// @router /warehouse/chart/save [post]
+func (this *WarehouseController) ChartInfoSave() {
+	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 data_manage.SaveChartInfoReq
+	err := json.Unmarshal(this.Ctx.Input.RequestBody, &req)
+	if err != nil {
+		br.Msg = "参数解析异常!"
+		br.ErrMsg = "参数解析失败,Err:" + err.Error()
+		return
+	}
+
+	if req.ChartInfoId <= 0 {
+		br.Msg = "参数错误!"
+		return
+	}
+
+	chartItem, err := data_manage.GetChartInfoById(req.ChartInfoId)
+	if err != nil {
+		if err.Error() == utils.ErrNoRow() {
+			br.Msg = "图表已被删除,请刷新页面!"
+			br.ErrMsg = "图表已被删除,请刷新页面,ChartInfoId:" + strconv.Itoa(req.ChartInfoId)
+			return
+		}
+		br.Msg = "获取图表信息失败!"
+		br.ErrMsg = "获取图表信息失败,Err:" + err.Error()
+		return
+	}
+
+	chartItem.StartDate = req.StartDate
+	chartItem.EndDate = req.EndDate
+	chartItem.DateType = req.DateType
+	chartItem.DateTypeNum = req.DateTypeNum
+	chartItem.LeftMin = req.LeftMin
+	chartItem.LeftMax = req.LeftMax
+	chartItem.RightMin = req.RightMin
+	chartItem.RightMax = req.RightMax
+	chartItem.Right2Min = req.Right2Min
+	chartItem.Right2Max = req.Right2Max
+	chartItem.MinMaxSave = req.MinMaxSave
+	updateCols := []string{"StartDate", "EndDate", "DateType", "DateTypeNum", "LeftMin", "LeftMax", "RightMin", "RightMax", "Right2Min", "Right2Max", "MinMaxSave"}
+	if e := chartItem.Update(updateCols); e != nil {
+		br.Msg = "保存失败"
+		br.ErrMsg = fmt.Sprintf("更新图表基础信息失败, %v", e)
+		return
+	}
+
+	key := utils.HZ_CHART_LIB_DETAIL + chartItem.UniqueCode
+	if utils.Re == nil && utils.Rc != nil {
+		_ = utils.Rc.Delete(key)
+	}
+
+	// 更新ES
+	go func() {
+		data.EsAddOrEditChartInfo(chartItem.ChartInfoId)
+		data.EsAddOrEditMyChartInfoByChartInfoId(chartItem.ChartInfoId)
+	}()
+
+	//新增操作日志
+	{
+		chartLog := new(data_manage.ChartInfoLog)
+		chartLog.ChartName = chartItem.ChartName
+		chartLog.ChartInfoId = req.ChartInfoId
+		chartLog.ChartClassifyId = chartItem.ChartClassifyId
+		chartLog.SysUserId = sysUser.AdminId
+		chartLog.SysUserRealName = sysUser.RealName
+		chartLog.UniqueCode = chartItem.UniqueCode
+		chartLog.CreateTime = time.Now()
+		chartLog.Content = string(this.Ctx.Input.RequestBody)
+		chartLog.Status = "修改配置项"
+		chartLog.Method = this.Ctx.Input.URI()
+		go data_manage.AddChartInfoLog(chartLog)
+	}
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "保存成功"
+	br.IsAddLog = true
+}
+
+// EdbSaveCheck
+// @Title 保存指标校验
+// @Description 保存指标校验
+// @Param	request	body request.WarehouseEdbSaveReq true "type json string"
+// @Success Ret=200 返回指标id
+// @router /warehouse/edb/save_check [post]
+func (this *WarehouseController) EdbSaveCheck() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		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 tradeAnalysisRequest.WarehouseEdbSaveReq
+	if e := json.Unmarshal(this.Ctx.Input.RequestBody, &req); e != nil {
+		br.Msg = "参数解析异常"
+		br.ErrMsg = fmt.Sprintf("参数解析异常, %v", e)
+		return
+	}
+
+	// 参数校验
+	if !req.IsSaveAs && req.MultipleGraphConfigId <= 0 {
+		br.Msg = "多图配置有误"
+		br.ErrMsg = fmt.Sprintf("配置ID有误, %d", req.MultipleGraphConfigId)
+		return
+	}
+	//var extraConfig tradeAnalysisModel.WarehouseExtraConfig
+	extraConfig := *req.ExtraConfig
+	//if req.ExtraConfig == `` {
+	//	br.Msg = "配置有误"
+	//	br.ErrMsg = fmt.Sprintf("建仓图表配置有误, conf: %s", req.ExtraConfig)
+	//	return
+	//}
+	//if e := json.Unmarshal([]byte(req.ExtraConfig), &extraConfig); e != nil {
+	//	br.Msg = "配置有误"
+	//	br.ErrMsg = fmt.Sprintf("建仓图表配置解析失败, err: %v; conf: %s", e, req.ExtraConfig)
+	//	return
+	//}
+	pass, tips := tradeAnalysisService.CheckWarehouseChartExtraConfig(extraConfig)
+	if !pass {
+		br.Msg = tips
+		return
+	}
+	if extraConfig.WarehouseChartType <= 0 {
+		br.Msg = "配置类型有误"
+		br.ErrMsg = fmt.Sprintf("配置类型有误, %d", extraConfig.WarehouseChartType)
+		return
+	}
+
+	// 根据extraConfig与已绑定关系的指标进行比对, 看是否需要新增指标
+	multiEdbList, e := data_manage.GetMultipleGraphConfigEdbMappingListByIdAndSource(req.MultipleGraphConfigId, utils.CHART_SOURCE_TRADE_ANALYSIS_PROCESS)
+	if e != nil {
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("获取配置与图表指标关系失败, %v", e)
+		return
+	}
+	resp := make([]*tradeAnalysisModel.WarehouseEdbSaveItem, 0)
+
+	// 校验是否需要新增指标
+	newEdbList, _, e := tradeAnalysisService.CheckEdbSave(extraConfig, multiEdbList, req.IsSaveAs)
+	if e != nil {
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("校验指标是否需要新增失败, %v", e)
+		return
+	}
+	if len(newEdbList) > 0 {
+		resp = append(resp, newEdbList...)
+	}
+
+	br.Data = resp
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "操作成功"
+}
+
+// EdbSave
+// @Title 保存指标接口
+// @Description 保存指标接口
+// @Param	request	body request.WarehouseEdbSaveReq true "type json string"
+// @Success Ret=200 返回指标id
+// @router /warehouse/edb/save [post]
+func (this *WarehouseController) EdbSave() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		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 tradeAnalysisRequest.WarehouseEdbSaveReq
+	if e := json.Unmarshal(this.Ctx.Input.RequestBody, &req); e != nil {
+		br.Msg = "参数解析异常"
+		br.ErrMsg = fmt.Sprintf("参数解析异常, %v", e)
+		return
+	}
+	cacheKey := "CACHE_CHART_EDB_INFO_ADD_" + strconv.Itoa(sysUser.AdminId)
+	if !utils.Rc.SetNX(cacheKey, 1, 30*time.Second) {
+		br.Msg = "系统处理中,请稍后重试!"
+		return
+	}
+	defer func() {
+		_ = utils.Rc.Delete(cacheKey)
+	}()
+
+	// 参数校验
+	if !req.IsSaveAs && req.MultipleGraphConfigId <= 0 {
+		br.Msg = "多图配置有误"
+		br.ErrMsg = fmt.Sprintf("配置ID有误, %d", req.MultipleGraphConfigId)
+		return
+	}
+	extraConfig := *req.ExtraConfig
+	pass, tips := tradeAnalysisService.CheckWarehouseChartExtraConfig(extraConfig)
+	if !pass {
+		br.Msg = tips
+		return
+	}
+	if len(req.EdbInfoList) == 0 {
+		br.Msg = "请选择指标"
+		return
+	}
+	edbNameExist := make(map[string]bool)
+	for _, v := range req.EdbInfoList {
+		if v.UniqueFlag == "" {
+			br.Msg = "指标标识有误"
+			br.ErrMsg = fmt.Sprintf("指标标识有误, %s", v.UniqueFlag)
+			return
+		}
+		// 仅新增的时候校验下面的参数
+		if v.EdbInfoId > 0 {
+			continue
+		}
+		edbName := strings.TrimSpace(v.EdbName)
+		if edbName == "" {
+			br.Msg = "请输入指标名称"
+			return
+		}
+		if edbNameExist[edbName] {
+			br.Msg = "指标名称重复, 请重新输入"
+			return
+		}
+		edbNameExist[edbName] = true
+		if v.Unit == "" {
+			br.Msg = "请输入单位"
+			return
+		}
+		if v.Frequency == "" {
+			br.Msg = "请选择频度"
+			return
+		}
+		if v.ClassifyId <= 0 {
+			br.Msg = "请选择分类"
+			return
+		}
+	}
+	resp := tradeAnalysisResponse.WarehouseEdbSaveResp{
+		Fail:    make([]*tradeAnalysisModel.WarehouseEdbSaveRespItem, 0),
+		Success: make([]*tradeAnalysisModel.WarehouseEdbSaveRespItem, 0),
+	}
+
+	// 获取需要移除关联的指标
+	multiEdbList, e := data_manage.GetMultipleGraphConfigEdbMappingListByIdAndSource(req.MultipleGraphConfigId, utils.CHART_SOURCE_TRADE_ANALYSIS_PROCESS)
+	if e != nil {
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("获取配置与图表指标关系失败, %v", e)
+		return
+	}
+	_, removeIds, e := tradeAnalysisService.CheckEdbSave(extraConfig, multiEdbList, req.IsSaveAs)
+	if e != nil {
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("校验指标是否需要新增失败, %v", e)
+		return
+	}
+
+	// 根据EdbInfoId判断是否需要更新指标
+	var newEdbIds []int
+	edbUpdateCols := []string{"CalculateFormula", "ModifyTime"}
+	newEdbRefresh := make([]*data_manage.EdbInfo, 0)
+	for _, v := range req.EdbInfoList {
+		// 图表配置-即指标计算公式
+		conf := extraConfig
+		conf.Companies = []string{v.UniqueFlag}
+		b, e := json.Marshal(conf)
+		if e != nil {
+			resp.Fail = append(resp.Fail, &tradeAnalysisModel.WarehouseEdbSaveRespItem{
+				WarehouseEdbSaveItem: *v,
+				Tips:                 "获取失败",
+				ErrMsg:               fmt.Sprintf("指标配置JSON格式化异常, %v", e),
+			})
+			continue
+		}
+		formula := string(b)
+		//edb.CalculateFormula = string(b)
+		// 更新指标
+		edbInfo := new(data_manage.EdbInfo)
+		if v.EdbInfoId > 0 {
+			edb, e := data_manage.GetEdbInfoById(v.EdbInfoId)
+			if e != nil {
+				resp.Fail = append(resp.Fail, &tradeAnalysisModel.WarehouseEdbSaveRespItem{
+					WarehouseEdbSaveItem: *v,
+					Tips:                 "获取失败",
+					ErrMsg:               fmt.Sprintf("获取失败, %v", e),
+				})
+				continue
+			}
+			edb.CalculateFormula = formula
+			edb.ModifyTime = time.Now().Local()
+			if e = edb.Update(edbUpdateCols); e != nil {
+				resp.Fail = append(resp.Fail, &tradeAnalysisModel.WarehouseEdbSaveRespItem{
+					WarehouseEdbSaveItem: *v,
+					Tips:                 "更新失败",
+					ErrMsg:               fmt.Sprintf("更新指标计算公式失败, %v", e),
+				})
+				continue
+			}
+			edbInfo = edb
+
+			// 刷新指标数据
+			res, e := data.RefreshEdbData(edbInfo.EdbInfoId, edbInfo.Source, edbInfo.SubSource, edbInfo.EdbCode, utils.BASE_START_DATE)
+			if e != nil {
+				resp.Fail = append(resp.Fail, &tradeAnalysisModel.WarehouseEdbSaveRespItem{
+					WarehouseEdbSaveItem: *v,
+					Tips:                 "刷新失败",
+					ErrMsg:               fmt.Sprintf("刷新失败, %v", e),
+				})
+				continue
+			}
+			if res.Ret != 200 {
+				resp.Fail = append(resp.Fail, &tradeAnalysisModel.WarehouseEdbSaveRespItem{
+					WarehouseEdbSaveItem: *v,
+					Tips:                 "刷新失败",
+					ErrMsg:               fmt.Sprintf("刷新失败, Ret: %d, err: %v", res.Ret, e),
+				})
+				continue
+			}
+		}
+
+		// 新增指标
+		if v.EdbInfoId == 0 {
+			edbCode, e := utils.GenerateEdbCode(1, "")
+			if e != nil {
+				br.Msg = "操作失败"
+				br.ErrMsg = fmt.Sprintf("生成指标编码失败, %v", e)
+				return
+			}
+			// PS:新增的开始结束日期为均为初始化日期
+			edb, e, errMsg, _ := data.EdbInfoAdd(utils.DATA_SOURCE_TRADE_ANALYSIS, utils.DATA_SUB_SOURCE_EDB, v.ClassifyId, edbCode, v.EdbName, v.Frequency, v.Unit, utils.BASE_START_DATE, utils.BASE_START_DATE, sysUser.AdminId, sysUser.RealName, this.Lang)
+			if e != nil {
+				resp.Fail = append(resp.Fail, &tradeAnalysisModel.WarehouseEdbSaveRespItem{
+					WarehouseEdbSaveItem: *v,
+					Tips:                 errMsg,
+					ErrMsg:               fmt.Sprintf("新增失败, %v", e),
+				})
+				continue
+			}
+			edb.CalculateFormula = formula
+			edb.ModifyTime = time.Now().Local()
+			if e = edb.Update(edbUpdateCols); e != nil {
+				resp.Fail = append(resp.Fail, &tradeAnalysisModel.WarehouseEdbSaveRespItem{
+					WarehouseEdbSaveItem: *v,
+					Tips:                 "更新失败",
+					ErrMsg:               fmt.Sprintf("更新指标计算公式失败, %v", e),
+				})
+				continue
+			}
+			newEdbIds = append(newEdbIds, edb.EdbInfoId)
+			edbInfo = edb
+
+			// 新增指标的刷新走异步吧, 指标如果没数据, 用户自己删了就行
+			newEdbRefresh = append(newEdbRefresh, edbInfo)
+		}
+
+		v.EdbInfoId = edbInfo.EdbInfoId
+		resp.Success = append(resp.Success, &tradeAnalysisModel.WarehouseEdbSaveRespItem{
+			WarehouseEdbSaveItem: *v,
+			Tips:                 "操作成功",
+		})
+	}
+
+	// 移除关联的指标
+	if len(removeIds) > 0 {
+		if e = data_manage.DeleteMultipleGraphConfigEdbMappingByEdbIds(req.MultipleGraphConfigId, utils.CHART_SOURCE_TRADE_ANALYSIS_PROCESS, removeIds); e != nil {
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("移除配置与指标关联失败, %v", e)
+			return
+		}
+	}
+
+	// 新增指标关联
+	if !req.IsSaveAs && len(newEdbIds) > 0 {
+		newMultiEdb := make([]*data_manage.MultipleGraphConfigEdbMapping, 0)
+		for _, v := range newEdbIds {
+			newMultiEdb = append(newMultiEdb, &data_manage.MultipleGraphConfigEdbMapping{
+				MultipleGraphConfigId: req.MultipleGraphConfigId,
+				EdbInfoId:             v,
+				Source:                utils.CHART_SOURCE_TRADE_ANALYSIS_PROCESS,
+				ModifyTime:            time.Now(),
+				CreateTime:            time.Now(),
+			})
+		}
+		if len(newMultiEdb) > 0 {
+			if e = data_manage.AddMultipleGraphConfigEdbMappingList(newMultiEdb); e != nil {
+				br.Msg = "操作失败"
+				br.ErrMsg = fmt.Sprintf("新增指标关联失败, %v", e)
+				return
+			}
+		}
+	}
+
+	// 异步刷新新增指标
+	if len(newEdbRefresh) > 0 {
+		go func() {
+			var refreshTips string
+			for _, edb := range newEdbRefresh {
+				_, e = data.RefreshEdbData(edb.EdbInfoId, edb.Source, edb.SubSource, edb.EdbCode, utils.BASE_START_DATE)
+				if e != nil {
+					refreshTips += fmt.Sprintf("刷新指标失败: %s, err: %v\n", edb.EdbCode, e)
+				}
+			}
+			if refreshTips != "" {
+				utils.FileLog.Info(refreshTips)
+				alarm_msg.SendAlarmMsg(refreshTips, 2)
+			}
+		}()
+	}
+
+	br.Data = resp
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "保存成功"
+	br.IsAddLog = true
+}

+ 683 - 0
controllers/trade_analysis/warehouse_classify.go

@@ -0,0 +1,683 @@
+package trade_analysis
+
+import (
+	"encoding/json"
+	"eta/eta_api/controllers"
+	"eta/eta_api/models"
+	"eta/eta_api/models/data_manage"
+	"eta/eta_api/models/system"
+	"eta/eta_api/services/data"
+	"eta/eta_api/services/data/data_manage_permission"
+	"eta/eta_api/utils"
+	"fmt"
+	"sort"
+	"time"
+)
+
+// WarehouseClassifyController 建仓过程-分类
+type WarehouseClassifyController struct {
+	controllers.BaseAuthController
+}
+
+//func (c *WarehouseClassifyController) ClassifyMove() {
+//	br := new(models.BaseResponse).Init()
+//	defer func() {
+//		if br.ErrMsg == "" {
+//			br.IsSendEmail = false
+//		}
+//		c.Data["json"] = br
+//		c.ServeJSON()
+//	}()
+//	var pars models.CommonClassifyMoveReq
+//
+//	classifyOb := new(trade_analysis.WareHouseProcessClassify)
+//	ctx := data.NewCommonClassifyCtx(classifyOb)
+//
+//	tips, e := data.CommonClassifyMove(pars, ctx)
+//	if tips != "" {
+//		br.Msg = tips
+//		return
+//	}
+//	if e != nil {
+//		br.Msg = "操作失败"
+//		br.ErrMsg = fmt.Sprintf("移动失败, %v", e)
+//		return
+//	}
+//
+//	br.Ret = 200
+//	br.Success = true
+//	br.Msg = "操作成功"
+//}
+
+// ChartClassifyList
+// @Title 分类列表
+// @Description 分类列表接口
+// @Param   IsShowMe   query   bool  false       "是否只看我的,true、false"
+// @Param   ParentId   query   bool  false       "父级ID"
+// @Param   Source   query   int  false       "图表类型,3:相关性,4:滚动相关性"
+// @Success 200 {object} data_manage.ChartClassifyListResp
+// @router /warehouse/classify/list [get]
+func (this *WarehouseClassifyController) ChartClassifyList() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	resp := new(data_manage.ChartClassifyListResp)
+
+	isShowMe, _ := this.GetBool("IsShowMe")
+	parentId, _ := this.GetInt("ParentId")
+	source, _ := this.GetInt("Source", utils.CHART_SOURCE_TRADE_ANALYSIS_PROCESS)
+
+	nodeAll := make([]*data_manage.ChartClassifyItems, 0)
+	// 查询分类节点
+	rootList, err := data_manage.GetChartClassifyByParentId(parentId, utils.CHART_SOURCE_TRADE_ANALYSIS_PROCESS)
+	if err != nil && err.Error() != utils.ErrNoRow() {
+		br.Msg = "获取失败"
+		br.ErrMsg = "获取数据失败,Err:" + err.Error()
+		return
+	}
+
+	if len(rootList) > 0 {
+		permissionClassifyIdList, e := data_manage_permission.GetUserChartClassifyPermissionList(this.SysUser.AdminId, 0)
+		if e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = "获取已授权分类id数据失败,Err:" + e.Error()
+			return
+		}
+
+		for _, v := range rootList {
+			// 操作按钮权限
+			v.HaveOperaAuth = data_manage_permission.CheckChartClassifyPermissionByPermissionIdList(v.IsJoinPermission, v.ChartClassifyId, permissionClassifyIdList)
+			button := data.GetChartClassifyOpButton(this.SysUser, v.SysUserId, v.HaveOperaAuth)
+			v.Button = button
+			v.ParentId = parentId
+			v.Children = make([]*data_manage.ChartClassifyItems, 0)
+
+			nodeAll = append(nodeAll, v)
+		}
+	}
+
+	// 查询图表节点, ParentId=0时说明仅查询一级目录节点
+	if parentId > 0 {
+		// 查询当前分类信息
+		currClassify, e := data_manage.GetChartClassifyById(parentId)
+		if e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = "获取当前分类信息失败,Err:" + e.Error()
+			return
+		}
+
+		// 获取所有有权限的指标和分类
+		permissionEdbIdList, permissionClassifyIdList, e := data_manage_permission.GetUserChartAndClassifyPermissionList(this.SysUser.AdminId, 0, 0)
+		if e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = "获取所有有权限的指标和分类失败,Err:" + e.Error()
+			return
+		}
+
+		var adminId int
+		if isShowMe {
+			adminId = this.SysUser.AdminId
+		}
+
+		charts, e := data_manage.GetChartInfoBySourceAndParentId(source, parentId, adminId)
+		if e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("获取图表信息失败, Err: %v", e)
+			return
+		}
+		for _, v := range charts {
+			// 操作按钮权限
+			v.HaveOperaAuth = data_manage_permission.CheckChartPermissionByPermissionIdList(v.IsJoinPermission, currClassify.IsJoinPermission, v.ChartInfoId, v.ChartClassifyId, permissionEdbIdList, permissionClassifyIdList)
+			button := data.GetChartOpButton(this.SysUser, v.SysUserId, v.HaveOperaAuth)
+			button.AddButton = false //不管有没有权限,图表都是没有添加按钮的
+			v.Button = button
+			v.ParentId = parentId
+			v.Children = make([]*data_manage.ChartClassifyItems, 0)
+
+			nodeAll = append(nodeAll, v)
+		}
+	}
+
+	// 整体排序
+	if len(nodeAll) > 0 {
+		sort.Slice(nodeAll, func(i, j int) bool {
+			return nodeAll[i].Sort < nodeAll[j].Sort
+		})
+	}
+
+	resp.AllNodes = nodeAll
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+	br.Data = resp
+}
+
+// ChartClassifyItems
+// @Title 获取所有分类接口-不包含图表
+// @Description 获取所有分类接口-不包含图表
+// @Success 200 {object} data_manage.ChartClassifyListResp
+// @router /warehouse/classify/items [get]
+func (this *WarehouseClassifyController) ChartClassifyItems() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	rootList, err := data_manage.GetChartClassifyByParentId(0, utils.CHART_SOURCE_TRADE_ANALYSIS_PROCESS)
+	if err != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = "获取数据失败,Err:" + err.Error()
+		return
+	}
+
+	nodeAll := make([]*data_manage.ChartClassifyItems, 0)
+	for k := range rootList {
+		rootNode := rootList[k]
+		nodeAll = append(nodeAll, rootNode)
+	}
+	resp := new(data_manage.ChartClassifyListResp)
+	resp.AllNodes = nodeAll
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+	br.Data = resp
+}
+
+// AddChartClassify
+// @Title 新增分类
+// @Description 新增分类接口
+// @Param	request	body data_manage.AddChartClassifyReq true "type json string"
+// @Success 200 Ret=200 保存成功
+// @router /warehouse/classify/add [post]
+func (this *WarehouseClassifyController) AddChartClassify() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	var req data_manage.AddChartClassifyReq
+	err := json.Unmarshal(this.Ctx.Input.RequestBody, &req)
+	if err != nil {
+		br.Msg = "参数解析异常!"
+		br.ErrMsg = "参数解析失败,Err:" + err.Error()
+		return
+	}
+	if req.ChartClassifyName == "" {
+		br.Msg = "请输入分类名称"
+		br.IsSendEmail = false
+		return
+	}
+	if req.ParentId < 0 {
+		br.Msg = "参数错误"
+		br.IsSendEmail = false
+		return
+	}
+
+	// 新增图表分类
+	_, err, errMsg, isSendEmail := data.AddChartClassify(req.ChartClassifyName, req.ParentId, req.Level, utils.CHART_SOURCE_TRADE_ANALYSIS_PROCESS, this.Lang, this.SysUser)
+	if err != nil {
+		br.Msg = errMsg
+		br.ErrMsg = "添加分类失败,Err:" + err.Error()
+		br.IsSendEmail = isSendEmail
+		return
+	}
+
+	br.Ret = 200
+	br.Msg = "添加成功"
+	br.Success = true
+}
+
+// EditChartClassify
+// @Title 修改分类
+// @Description 修改分类接口
+// @Param	request	body data_manage.EditChartClassifyReq true "type json string"
+// @Success 200 Ret=200 修改成功
+// @router /warehouse/classify/edit [post]
+func (this *WarehouseClassifyController) EditChartClassify() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	var req data_manage.EditChartClassifyReq
+	err := json.Unmarshal(this.Ctx.Input.RequestBody, &req)
+	if err != nil {
+		br.Msg = "参数解析异常!"
+		br.ErrMsg = "参数解析失败,Err:" + err.Error()
+		return
+	}
+	if req.ChartClassifyName == "" {
+		br.Msg = "请输入分类名称"
+		br.IsSendEmail = false
+		return
+	}
+
+	if req.ChartClassifyId <= 0 {
+		br.Msg = "参数错误"
+		br.IsSendEmail = false
+		return
+	}
+
+	// 编辑图表分类
+	_, err, errMsg, isSendEmail := data.EditChartClassify(req.ChartClassifyId, utils.CHART_SOURCE_TRADE_ANALYSIS_PROCESS, req.ChartClassifyName, this.Lang, this.SysUser)
+	if err != nil {
+		br.Msg = errMsg
+		br.ErrMsg = "保存分类失败,Err:" + err.Error()
+		br.IsSendEmail = isSendEmail
+		return
+	}
+
+	br.Ret = 200
+	br.Msg = "修改成功"
+	br.Success = true
+	br.IsAddLog = true
+}
+
+// DeleteChartClassifyCheck
+// @Title 删除图表检测接口
+// @Description 删除图表检测接口
+// @Param	request	body data_manage.ChartClassifyDeleteCheckResp true "type json string"
+// @Success 200 Ret=200 检测成功
+// @router /warehouse/classify/delete/check [post]
+func (this *WarehouseClassifyController) DeleteChartClassifyCheck() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	var req data_manage.ChartClassifyDeleteCheckReq
+	err := json.Unmarshal(this.Ctx.Input.RequestBody, &req)
+	if err != nil {
+		br.Msg = "参数解析异常!"
+		br.ErrMsg = "参数解析失败,Err:" + err.Error()
+		return
+	}
+
+	if req.ChartClassifyId < 0 && req.ChartInfoId <= 0 {
+		br.Msg = "参数错误"
+		br.IsSendEmail = false
+		return
+	}
+	var deleteStatus int
+	var tipsMsg string
+	//删除分类
+	if req.ChartClassifyId > 0 && req.ChartInfoId == 0 {
+		//判断分类下,是否含有图表
+		count, err := data_manage.GetChartInfoCountByClassifyId(req.ChartClassifyId)
+		if err != nil {
+			br.Msg = "删除失败"
+			br.ErrMsg = "分类下是否含有图表失败,Err:" + err.Error()
+			return
+		}
+
+		if count > 0 {
+			deleteStatus = 1
+			tipsMsg = "该分类下关联图表不可删除"
+		}
+	}
+
+	if deleteStatus != 1 && req.ChartInfoId == 0 {
+		classifyCount, err := data_manage.GetChartClassifyCountByClassifyId(req.ChartClassifyId)
+		if err != nil && err.Error() != utils.ErrNoRow() {
+			br.Msg = "删除失败"
+			br.ErrMsg = "分类下是否含有图表失败,Err:" + err.Error()
+			return
+		}
+		if classifyCount > 0 {
+			deleteStatus = 2
+			tipsMsg = "确认删除当前目录及包含的子目录吗"
+		}
+	}
+	if deleteStatus == 0 {
+		tipsMsg = "可删除,进行删除操作"
+	}
+
+	resp := new(data_manage.ChartClassifyDeleteCheckResp)
+	resp.DeleteStatus = deleteStatus
+	resp.TipsMsg = tipsMsg
+	br.Ret = 200
+	br.Msg = "检测成功"
+	br.Success = true
+	br.Data = resp
+}
+
+// DeleteChartClassify
+// @Title 删除分类/图表
+// @Description 删除分类/图表接口
+// @Param	request	body data_manage.DeleteChartClassifyReq true "type json string"
+// @Success 200 Ret=200 删除成功
+// @router /warehouse/classify/delete [post]
+func (this *WarehouseClassifyController) DeleteChartClassify() {
+	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 data_manage.DeleteChartClassifyReq
+	err := json.Unmarshal(this.Ctx.Input.RequestBody, &req)
+	if err != nil {
+		br.Msg = "参数解析异常!"
+		br.ErrMsg = "参数解析失败,Err:" + err.Error()
+		return
+	}
+
+	if req.ChartClassifyId < 0 && req.ChartInfoId <= 0 {
+		br.Msg = "参数错误"
+		br.IsSendEmail = false
+		return
+	}
+
+	//删除分类
+	if req.ChartClassifyId > 0 && req.ChartInfoId == 0 {
+		//判断是否含有指标
+		count, err := data_manage.GetChartInfoCountByClassifyId(req.ChartClassifyId)
+		if err != nil && err.Error() != utils.ErrNoRow() {
+			br.Msg = "删除失败"
+			br.ErrMsg = "判断名称是否已存在失败,Err:" + err.Error()
+			return
+		}
+
+		if count > 0 {
+			br.Msg = "该目录下存在关联指标,不可删除"
+			br.IsSendEmail = false
+			return
+		}
+
+		err = data_manage.DeleteChartClassify(req.ChartClassifyId)
+		if err != nil {
+			br.Msg = "删除失败"
+			br.ErrMsg = "删除失败,Err:" + err.Error()
+			return
+		}
+	}
+	resp := new(data_manage.AddChartInfoResp)
+	//删除图表
+	if req.ChartInfoId > 0 {
+		chartInfo, err := data_manage.GetChartInfoById(req.ChartInfoId)
+		if err != nil {
+			if err.Error() == utils.ErrNoRow() {
+				br.Msg = "图表已删除,请刷新页面"
+				br.ErrMsg = "指标不存在,Err:" + err.Error()
+				return
+			} else {
+				br.Msg = "删除失败"
+				br.ErrMsg = "删除失败,获取指标信息失败,Err:" + err.Error()
+				return
+			}
+		}
+		if chartInfo == nil {
+			br.Msg = "图表已删除,请刷新页面"
+			return
+		}
+		//图表操作权限
+		ok := data.CheckOpChartPermission(sysUser, chartInfo.SysUserId, true)
+		if !ok {
+			br.Msg = "没有该图表的操作权限"
+			br.ErrMsg = "没有该图表的操作权限"
+			return
+		}
+
+		// 获取引用该图表的MyCharts, 用于ES删除
+		var myCond string
+		var myPars []interface{}
+		myCond += ` AND a.chart_info_id = ? `
+		myPars = append(myPars, chartInfo.ChartInfoId)
+		myCharts, e := data_manage.GetMyChartListGroupByCharyInfoIdAndAdminIdByCondition(myCond, myPars)
+		if e != nil {
+			br.Msg = "删除失败"
+			br.ErrMsg = "获取引用图表的MyChats失败, Err: " + e.Error()
+			return
+		}
+		myIds := make([]int, 0)
+		for _, m := range myCharts {
+			myIds = append(myIds, m.MyChartId)
+		}
+
+		source := chartInfo.Source
+		//删除图表及关联指标
+		err = data_manage.DeleteChartInfoAndData(chartInfo.ChartInfoId)
+		if err != nil {
+			br.Msg = "删除失败"
+			br.ErrMsg = "删除失败,Err:" + err.Error()
+			return
+		}
+		//删除ES
+		{
+			go data.EsDeleteChartInfo(chartInfo.ChartInfoId)
+			// 删除MY ETA 图表 es数据
+			//go data.EsDeleteMyChartInfoByChartInfoId(chartInfo.ChartInfoId)
+			go data.EsDeleteMyChartInfoByMyChartIds(myIds)
+		}
+
+		// 删除图例和多图关联
+		seriesOb := new(data_manage.ChartSeries)
+		{
+			cond := fmt.Sprintf("chart_info_id = ?")
+			pars := make([]interface{}, 0)
+			pars = append(pars, chartInfo.ChartInfoId)
+			if e = seriesOb.RemoveByCondition(cond, pars); e != nil {
+				br.Msg = "删除失败"
+				br.ErrMsg = fmt.Sprintf("删除图表关联图例失败, %v", e)
+				return
+			}
+		}
+		if e = data_manage.RemoveMultiConfigChartMappingByChartInfoId(chartInfo.ChartInfoId); e != nil {
+			br.Msg = "删除失败"
+			br.ErrMsg = fmt.Sprintf("删除图表多图配置关联失败, %v", e)
+			return
+		}
+
+		var condition string
+		var pars []interface{}
+		condition += " AND chart_classify_id=? AND source = ? "
+		pars = append(pars, chartInfo.ChartClassifyId, source)
+
+		condition += " AND chart_info_id>? ORDER BY create_time ASC LIMIT 1 "
+		pars = append(pars, req.ChartInfoId)
+
+		nextItem, err := data_manage.GetChartInfoByCondition(condition, pars)
+		if err != nil && err.Error() != utils.ErrNoRow() {
+			br.Msg = "删除失败"
+			br.ErrMsg = "获取下一级图库信息失败,Err:" + err.Error()
+			return
+		}
+
+		if nextItem != nil {
+			resp.UniqueCode = nextItem.UniqueCode
+			resp.ChartInfoId = nextItem.ChartInfoId
+		} else {
+			var condition string
+			var pars []interface{}
+
+			condition += " AND level=1 "
+			//pars = append(pars, chartInfo.ChartClassifyId)
+
+			condition += " AND chart_classify_id>? ORDER BY chart_classify_id ASC LIMIT 1 "
+			pars = append(pars, chartInfo.ChartClassifyId)
+
+			classifyItem, err := data_manage.GetChartClassifyByCondition(condition, pars)
+			if err != nil && err.Error() != utils.ErrNoRow() {
+				br.Msg = "删除失败"
+				br.ErrMsg = "获取下一级图库分类信息失败,Err:" + err.Error()
+				return
+			}
+			if classifyItem != nil {
+				nextItem, err = data_manage.GetNextChartInfo(chartInfo.ChartClassifyId)
+				if err != nil && err.Error() != utils.ErrNoRow() {
+					br.Msg = "删除失败"
+					br.ErrMsg = "获取下一级图库信息失败,Err:" + err.Error()
+					return
+				}
+				if nextItem != nil {
+					resp.UniqueCode = nextItem.UniqueCode
+					resp.ChartInfoId = nextItem.ChartInfoId
+				}
+			}
+		}
+
+		//新增操作日志
+		{
+			chartLog := new(data_manage.ChartInfoLog)
+			chartLog.ChartName = chartInfo.ChartName
+			chartLog.ChartInfoId = req.ChartInfoId
+			chartLog.ChartClassifyId = chartInfo.ChartClassifyId
+			chartLog.SysUserId = sysUser.AdminId
+			chartLog.SysUserRealName = sysUser.RealName
+			chartLog.UniqueCode = chartInfo.UniqueCode
+			chartLog.CreateTime = time.Now()
+			chartLog.Content = string(this.Ctx.Input.RequestBody)
+			chartLog.Status = "删除图表"
+			chartLog.Method = this.Ctx.Input.URI()
+			go data_manage.AddChartInfoLog(chartLog)
+		}
+	}
+
+	br.Ret = 200
+	br.Msg = "删除成功"
+	br.Success = true
+	br.Data = resp
+	br.IsAddLog = true
+}
+
+// ChartClassifyMove
+// @Title 分类移动接口
+// @Description 分类移动接口
+// @Success 200 {object} data_manage.MoveChartClassifyReq
+// @router /warehouse/classify/move [post]
+func (this *WarehouseClassifyController) ChartClassifyMove() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		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 data_manage.MoveChartClassifyReq
+	err := json.Unmarshal(this.Ctx.Input.RequestBody, &req)
+	if err != nil {
+		br.Msg = "参数解析异常!"
+		br.ErrMsg = "参数解析失败,Err:" + err.Error()
+		return
+	}
+	if req.ClassifyId <= 0 && req.ChartInfoId <= 0 {
+		br.Msg = "参数错误"
+		br.ErrMsg = "请选择拖动目标,分类目录或者指标"
+		return
+	}
+
+	err, errMsg := data.MoveChartClassify(req, sysUser, utils.CHART_SOURCE_TRADE_ANALYSIS_PROCESS)
+	if errMsg != `` {
+		br.Msg = errMsg
+		br.ErrMsg = errMsg
+		if err != nil {
+			br.ErrMsg = err.Error()
+		} else {
+			br.IsSendEmail = false
+		}
+		return
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "移动成功"
+}
+
+// ClassifyTree
+// @Title 多层分类列表树
+// @Description 多层分类列表树
+// @Success 200 {object} data_manage.ChartClassifyListResp
+// @router /warehouse/classify/tree [get]
+func (this *WarehouseClassifyController) ClassifyTree() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+
+	allList, err := data_manage.GetChartClassifyAllBySource(utils.CHART_SOURCE_TRADE_ANALYSIS_PROCESS)
+	if err != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = "获取所有分类失败, Err:" + err.Error()
+		return
+	}
+	nodeAll := make([]*data_manage.ChartClassifyItems, 0)
+
+	if len(allList) > 0 {
+		// 已授权分类id
+		permissionClassifyIdList, e := data_manage_permission.GetUserChartClassifyPermissionList(this.SysUser.AdminId, 0)
+		if e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = "获取已授权分类id数据失败,Err:" + e.Error()
+			return
+		}
+
+		for k, v := range allList {
+			// 数据权限
+			v.HaveOperaAuth = data_manage_permission.CheckChartClassifyPermissionByPermissionIdList(v.IsJoinPermission, v.ChartClassifyId, permissionClassifyIdList)
+			// 按钮权限
+			button := data.GetChartClassifyOpButton(this.SysUser, v.SysUserId, v.HaveOperaAuth)
+			allList[k].Button = button
+		}
+
+		nodeAll = data.GetChartClassifyTreeRecursive(allList, 0)
+		//根据sort值排序
+		sort.Slice(nodeAll, func(i, j int) bool {
+			return nodeAll[i].Sort < nodeAll[j].Sort
+		})
+	}
+
+	language := `CN`
+	// 显示的语言
+	{
+		configDetail, _ := system.GetConfigDetailByCode(this.SysUser.AdminId, system.ChartLanguageVar)
+		if configDetail != nil {
+			language = configDetail.ConfigValue
+		} else {
+			configDetail, _ = system.GetDefaultConfigDetailByCode(system.ChartLanguageVar)
+			if configDetail != nil {
+				language = configDetail.ConfigValue
+			}
+		}
+	}
+
+	// 是否允许添加一级分类
+	canOpClassify := true
+	button := data.GetChartClassifyOpButton(this.SysUser, 0, true)
+	if !button.AddButton {
+		canOpClassify = false
+	}
+
+	resp := new(data_manage.ChartClassifyListResp)
+	resp.AllNodes = nodeAll
+	resp.Language = language
+	resp.CanOpClassify = canOpClassify
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+	br.Data = resp
+}

+ 80 - 0
models/common_classify.go

@@ -0,0 +1,80 @@
+package models
+
+import "time"
+
+// CommonClassify 通用分类
+type CommonClassify struct {
+	ClassifyId   int       `description:"分类ID"`
+	ClassifyName string    `description:"分类名称"`
+	ParentId     int       `description:"父级ID"`
+	RootId       int       `description:"顶级ID"`
+	Level        int       `description:"层级"`
+	LevelPath    string    `description:"层级路径"`
+	Sort         int       `description:"排序"`
+	CreateTime   time.Time `description:"创建时间"`
+	ModifyTime   time.Time `description:"修改时间"`
+}
+
+// CommonClassifyCols 通用分类基本字段
+type CommonClassifyCols struct {
+	ClassifyId   string `description:"分类ID"`
+	ClassifyName string `description:"分类名称"`
+	ParentId     string `description:"父级id"`
+	RootId       string `description:"顶级id"`
+	Level        string `description:"层级"`
+	LevelPath    string `description:"层级路径"`
+	Sort         string `description:"排序字段,越小越靠前,默认值:10"`
+	CreateTime   string `description:"创建时间"`
+	ModifyTime   string `description:"修改时间"`
+}
+
+// CommonClassifyObj 通用分类对象
+type CommonClassifyObj struct {
+	ObjectId   int       `description:"对象ID"`
+	ClassifyId int       `description:"分类ID"`
+	Sort       int       `description:"排序"`
+	CreateTime time.Time `description:"创建时间"`
+	ModifyTime time.Time `description:"修改时间"`
+}
+
+// CommonClassifyObjCols 通用分类对象基本字段
+type CommonClassifyObjCols struct {
+	ObjectId   string `description:"对象ID"`
+	ClassifyId string `description:"分类ID"`
+	Sort       string `description:"排序"`
+	CreateTime string `description:"创建时间"`
+	ModifyTime string `description:"修改时间"`
+}
+
+// CommonClassifyMoveReq 移动分类
+type CommonClassifyMoveReq struct {
+	ClassifyId       int `description:"分类ID"`
+	ParentClassifyId int `description:"父级分类ID"`
+	PrevClassifyId   int `description:"上一个兄弟节点分类ID"`
+	NextClassifyId   int `description:"下一个兄弟节点分类ID"`
+	ObjectId         int `description:"对象ID(指标/图表..), 如果对象ID>0则移动对象, 否则认为移动分类"`
+	PrevObjectId     int `description:"上一个对象ID"`
+	NextObjectId     int `description:"下一个对象ID"`
+}
+
+// ExtraPermissionClassifyStrategy 是一个带有额外权限校验的装饰器
+//type ExtraPermissionClassifyStrategy struct {
+//	BaseClassifyStrategy
+//}
+
+// UpdateCommonClassify 覆盖基础策略的UpdateClassify方法,并添加额外的权限校验
+//func (s *ExtraPermissionClassifyStrategy) UpdateCommonClassify(classify *CommonClassify) error {
+//	// 额外的权限校验
+//	if !checkExtraPermission(classify) {
+//		return fmt.Errorf("无操作权限")
+//	}
+//
+//	// 调用基础策略的UpdateClassify方法
+//	return s.BaseClassifyStrategy.UpdateCommonClassify(classify)
+//}
+//
+//// checkExtraPermission 进行额外的权限校验
+//func checkExtraPermission(classify *CommonClassify) bool {
+//	// 实现额外权限校验逻辑
+//	return true
+//}

+ 14 - 2
models/data_manage/chart_info.go

@@ -59,6 +59,7 @@ type ChartInfo struct {
 	IsJoinPermission  int    `description:"是否加入权限管控,0:不加入;1:加入;默认:0"`
 	ForumChartInfoId  int    `description:"社区的图表ID"`
 	ChartAlias        string `description:"图表别名"`
+	DateTypeNum       int    `description:"date_type=25(N月前)时的N值,其他N值可复用此字段"`
 }
 
 type ChartInfoMore struct {
@@ -70,6 +71,10 @@ type ChartInfoMore struct {
 func AddChartInfo(item *ChartInfo) (lastId int64, err error) {
 	o := orm.NewOrmUsingDB("data")
 	lastId, err = o.Insert(item)
+	if err != nil {
+		return
+	}
+	item.ChartInfoId = int(lastId)
 	return
 }
 
@@ -174,6 +179,7 @@ type SaveChartInfoReq struct {
 	MinMaxSave       int              `description:"是否手动保存过上下限:0-否;1-是"`
 	ExtraConfig      string           `description:"图表额外配置,json数据"`
 	StartYear        int              `description:"当选择的日期类型为最近N年类型时,即date_type=20, 用start_year表示N"`
+	DateTypeNum      int              `description:"date_type=25(N月前)时的N值,其他N值可复用此字段"`
 }
 
 type ChartSaveItem struct {
@@ -196,6 +202,7 @@ type ChartSaveItem struct {
 	ConvertValue      float64 `description:"数据转换值"`
 	ConvertUnit       string  `description:"数据转换单位"`
 	ConvertEnUnit     string  `description:"数据转换单位"`
+	UniqueFlag        string  `description:"唯一标识"`
 }
 
 func DeleteChartInfoAndData(chartInfoId int) (err error) {
@@ -263,6 +270,7 @@ type EditChartInfoReq struct {
 	Unit                 string                  `description:"中文单位名称"`
 	UnitEn               string                  `description:"英文单位名称"`
 	ChartAlias           string                  `description:"图表别名"`
+	DateTypeNum          int                     `description:"date_type=25(N月前)时的N值,其他N值可复用此字段"`
 }
 
 type EditFutureGoodChartInfoReq struct {
@@ -728,6 +736,7 @@ type ChartEdbInfoMapping struct {
 	ConvertEnUnit       string  `description:"数据转换单位"`
 	IsJoinPermission    int     `description:"是否加入权限管控,0:不加入;1:加入;默认:0"`
 	HaveOperaAuth       bool    `description:"是否有数据权限,默认:false"`
+	UniqueFlag          string  `description:"唯一标识(与唯一编码不是一个东西)"`
 }
 
 type QuarterData struct {
@@ -1066,6 +1075,9 @@ func EditChartInfoAndMapping(req *EditChartInfoReq, edbInfoIdStr string, calenda
 	sql += `,chart_alias = ? `
 	pars = append(pars, req.ChartAlias)
 
+	sql += `,date_type_num = ? `
+	pars = append(pars, req.DateTypeNum)
+
 	sql += `WHERE chart_info_id = ?`
 
 	pars = append(pars, req.ChartInfoId)
@@ -1417,8 +1429,6 @@ func EditChartBaseInfoAndEdbEnInfo(req *EditChartInfoBaseReq, chartItem *ChartIn
 		updateChartCols = append(updateChartCols, "ExtraConfig")
 	}
 
-
-
 	chartItem.ModifyTime = time.Now()
 	updateChartCols = append(updateChartCols, "ModifyTime")
 	_, err = to.Update(chartItem, updateChartCols...)
@@ -1502,6 +1512,7 @@ type AddChartInfoReq struct {
 	Unit                 string                  `description:"中文单位名称"`
 	UnitEn               string                  `description:"英文单位名称"`
 	ChartAlias           string                  `description:"图表别名"`
+	DateTypeNum          int                     `description:"date_type=25(N月前)时的N值,其他类型N值可复用此字段"`
 }
 
 type AddFutureGoodChartInfoReq struct {
@@ -1958,6 +1969,7 @@ type ChartInfoView struct {
 	HaveOperaAuth     bool            `description:"是否有数据权限,默认:false"`
 	ForumChartInfoId  int             `description:"社区的图表ID"`
 	ChartAlias        string          `description:"图表别名"`
+	DateTypeNum       int             `description:"date_type=25(N月前)时的N值,其他类型N值可复用此字段"`
 }
 
 type ChartViewButton struct {

+ 20 - 0
models/data_manage/chart_series.go

@@ -24,6 +24,7 @@ type ChartSeries struct {
 	IsOrder       bool      `description:"true:正序,false:逆序"`
 	CreateTime    time.Time `description:"创建时间"`
 	ModifyTime    time.Time `description:"修改时间"`
+	UniqueFlag    string    `description:"唯一标识"`
 }
 
 func (c *ChartSeries) TableName() string {
@@ -372,3 +373,22 @@ func (s ChartSectionSeriesValSortDesc) Less(i, j int) bool {
 func (s ChartSectionSeriesValSortDesc) Swap(i, j int) {
 	s[i], s[j] = s[j], s[i]
 }
+
+func (c *ChartSeries) CreateMulti(items []*ChartSeries) (err error) {
+	if len(items) == 0 {
+		return
+	}
+	o := orm.NewOrmUsingDB("data")
+	_, err = o.InsertMulti(utils.MultiAddNum, items)
+	return
+}
+
+func (m *ChartSeries) RemoveByCondition(condition string, pars []interface{}) (err error) {
+	if condition == "" {
+		return
+	}
+	o := orm.NewOrmUsingDB("data")
+	sql := fmt.Sprintf(`DELETE FROM %s WHERE %s`, m.TableName(), condition)
+	_, err = o.Raw(sql, pars).Exec()
+	return
+}

+ 2 - 0
models/data_manage/edb_data_base.go

@@ -181,6 +181,8 @@ func GetEdbDataTableName(source, subSource int) (tableName string) {
 		tableName = "edb_data_sci_hq"
 	case utils.DATA_SOURCE_LY: // 粮油商务网->86
 		tableName = "edb_data_ly"
+	case utils.DATA_SOURCE_TRADE_ANALYSIS: // 持仓分析->92
+		tableName = "edb_data_trade_analysis"
 	default:
 		edbSource := EdbSourceIdMap[source]
 		if edbSource != nil {

+ 7 - 0
models/data_manage/multiple_graph_config_chart_mapping.go

@@ -196,3 +196,10 @@ func ReplaceMultipleGraphConfigChartEdb(oldEdbInfo, newEdbInfo *EdbInfo) (replac
 	}
 	return
 }
+
+func RemoveMultiConfigChartMappingByChartInfoId(chartInfoId int) (err error) {
+	o := orm.NewOrmUsingDB("data")
+	sql := `DELETE FROM multiple_graph_config_chart_mapping WHERE chart_info_id = ?`
+	_, err = o.Raw(sql, chartInfoId).Exec()
+	return
+}

+ 122 - 0
models/data_manage/trade_analysis/base_from_trade_exchange.go

@@ -0,0 +1,122 @@
+package trade_analysis
+
+import (
+	"fmt"
+	"github.com/beego/beego/v2/client/orm"
+	"strings"
+	"time"
+)
+
+// BaseFromTradeExchange 交易所表
+type BaseFromTradeExchange struct {
+	BaseFromTradeExchangeId int       `orm:"column(base_from_trade_exchange_id);pk"`
+	Exchange                string    `description:"交易所标识"`
+	ExchangeName            string    `description:"交易所名称"`
+	ExchangeNameEn          string    `description:"交易所英文名称"`
+	Sort                    int       `description:"排序"`
+	AnalysisState           int       `description:"持仓分析状态:0-隐藏;1-显示"`
+	CreateTime              time.Time `description:"创建时间"`
+	ModifyTime              time.Time `description:"修改时间"`
+}
+
+func (m *BaseFromTradeExchange) TableName() string {
+	return "base_from_trade_exchange"
+}
+
+type BaseFromTradeExchangeCols struct {
+	PrimaryId      string
+	Exchange       string
+	ExchangeName   string
+	ExchangeNameEn string
+	Sort           string
+	AnalysisState  string
+	CreateTime     string
+	ModifyTime     string
+}
+
+func (m *BaseFromTradeExchange) Cols() BaseFromTradeExchangeCols {
+	return BaseFromTradeExchangeCols{
+		PrimaryId:      "base_from_trade_exchange_id",
+		Exchange:       "exchange",
+		ExchangeName:   "exchange_name",
+		ExchangeNameEn: "exchange_name_en",
+		Sort:           "sort",
+		AnalysisState:  "analysis_state",
+		CreateTime:     "create_time",
+		ModifyTime:     "modify_time",
+	}
+}
+
+func (m *BaseFromTradeExchange) GetItemById(id int) (item *BaseFromTradeExchange, err error) {
+	o := orm.NewOrmUsingDB("data")
+	sql := fmt.Sprintf(`SELECT * FROM %s WHERE %s = ? LIMIT 1`, m.TableName(), m.Cols().PrimaryId)
+	err = o.Raw(sql, id).QueryRow(&item)
+	return
+}
+
+func (m *BaseFromTradeExchange) GetItemByCondition(condition string, pars []interface{}, orderRule string) (item *BaseFromTradeExchange, err error) {
+	o := orm.NewOrmUsingDB("data")
+	order := ``
+	if orderRule != "" {
+		order = ` ORDER BY ` + orderRule
+	}
+	sql := fmt.Sprintf(`SELECT * FROM %s WHERE 1=1 %s %s LIMIT 1`, m.TableName(), condition, order)
+	err = o.Raw(sql, pars).QueryRow(&item)
+	return
+}
+
+func (m *BaseFromTradeExchange) GetCountByCondition(condition string, pars []interface{}) (count int, err error) {
+	o := orm.NewOrmUsingDB("data")
+	sql := fmt.Sprintf(`SELECT COUNT(1) FROM %s WHERE 1=1 %s`, m.TableName(), condition)
+	err = o.Raw(sql, pars).QueryRow(&count)
+	return
+}
+
+func (m *BaseFromTradeExchange) GetItemsByCondition(condition string, pars []interface{}, fieldArr []string, orderRule string) (items []*BaseFromTradeExchange, err error) {
+	o := orm.NewOrmUsingDB("data")
+	fields := strings.Join(fieldArr, ",")
+	if len(fieldArr) == 0 {
+		fields = `*`
+	}
+	order := fmt.Sprintf(`ORDER BY %s DESC`, m.Cols().CreateTime)
+	if orderRule != "" {
+		order = ` ORDER BY ` + orderRule
+	}
+	sql := fmt.Sprintf(`SELECT %s FROM %s WHERE 1=1 %s %s`, fields, m.TableName(), condition, order)
+	_, err = o.Raw(sql, pars).QueryRows(&items)
+	return
+}
+
+func (m *BaseFromTradeExchange) GetPageItemsByCondition(condition string, pars []interface{}, fieldArr []string, orderRule string, startSize, pageSize int) (items []*BaseFromTradeExchange, err error) {
+	o := orm.NewOrmUsingDB("data")
+	fields := strings.Join(fieldArr, ",")
+	if len(fieldArr) == 0 {
+		fields = `*`
+	}
+	order := fmt.Sprintf(`ORDER BY %s DESC`, m.Cols().CreateTime)
+	if orderRule != "" {
+		order = ` ORDER BY ` + orderRule
+	}
+	sql := fmt.Sprintf(`SELECT %s FROM %s WHERE 1=1 %s %s LIMIT ?,?`, fields, m.TableName(), condition, order)
+	_, err = o.Raw(sql, pars, startSize, pageSize).QueryRows(&items)
+	return
+}
+
+// BaseFromTradeExchangeItem 交易所信息
+type BaseFromTradeExchangeItem struct {
+	ExchangeId     int    `description:"交易所ID"`
+	Exchange       string `description:"交易所标识"`
+	ExchangeName   string `description:"交易所名称"`
+	ExchangeNameEn string `description:"交易所英文名称"`
+	Sort           int    `description:"排序"`
+}
+
+func (m *BaseFromTradeExchange) Format2Item() (item *BaseFromTradeExchangeItem) {
+	item = new(BaseFromTradeExchangeItem)
+	item.ExchangeId = m.BaseFromTradeExchangeId
+	item.Exchange = m.Exchange
+	item.ExchangeName = m.ExchangeName
+	item.ExchangeNameEn = m.ExchangeNameEn
+	item.Sort = m.Sort
+	return
+}

+ 20 - 0
models/data_manage/trade_analysis/request/warehouse.go

@@ -0,0 +1,20 @@
+package request
+
+import (
+	tradeAnalysisModel "eta/eta_api/models/data_manage/trade_analysis"
+)
+
+// WarehousePreviewReq 建仓图表预览
+type WarehousePreviewReq struct {
+	ChartInfoId  int                                      `description:"图表ID"`
+	ExtraConfig  *tradeAnalysisModel.WarehouseExtraConfig `description:"建仓过程参数"`
+	ChartsConfig []tradeAnalysisModel.WarehouseChartPars  `description:"图表配置(日期配置等)"`
+}
+
+// WarehouseEdbSaveReq 建仓指标保存
+type WarehouseEdbSaveReq struct {
+	MultipleGraphConfigId int                                        `description:"配置ID"`
+	ExtraConfig           *tradeAnalysisModel.WarehouseExtraConfig   `description:"图表配置"`
+	IsSaveAs              bool                                       `description:"是否另存为, true表示另存为, 不会建立与配置的关系"`
+	EdbInfoList           []*tradeAnalysisModel.WarehouseEdbSaveItem `description:"指标列表"`
+}

+ 17 - 0
models/data_manage/trade_analysis/response/warehouse.go

@@ -0,0 +1,17 @@
+package response
+
+import (
+	"eta/eta_api/models/data_manage"
+	tradeAnalysisModel "eta/eta_api/models/data_manage/trade_analysis"
+)
+
+type WarehouseChartDetailResp struct {
+	MultipleGraphConfigId int                                `description:"多图配置ID"`
+	WarehouseCharts       []*data_manage.ChartInfoDetailResp `description:"建仓多图信息"`
+}
+
+// WarehouseEdbSaveResp 建仓指标保存响应
+type WarehouseEdbSaveResp struct {
+	Success []*tradeAnalysisModel.WarehouseEdbSaveRespItem `description:"操作成功的指标"`
+	Fail    []*tradeAnalysisModel.WarehouseEdbSaveRespItem `description:"操作失败的指标"`
+}

+ 372 - 0
models/data_manage/trade_analysis/trade_analysis.go

@@ -1,6 +1,8 @@
 package trade_analysis
 
 import (
+	"eta/eta_api/utils"
+	"fmt"
 	"github.com/beego/beego/v2/client/orm"
 	"time"
 )
@@ -159,3 +161,373 @@ func GetTradePositionTop(exchange string, classifyName, classifyType, dataTime s
 
 	return
 }
+
+type OriginTradeData struct {
+	Rank         int       `description:"排名"`
+	CompanyName  string    `description:"期货公司名称"`
+	Val          int       `description:"持仓量"`
+	ValChange    int       `description:"持仓增减"`
+	DataTime     time.Time `description:"数据日期"`
+	ClassifyName string    `description:"品种名称"`
+	ClassifyType string    `description:"合约代码"`
+	ValType      int       `description:"数据类型: 1-多单; 2-空单"`
+}
+
+// GetTradeDataByClassifyAndCompany 根据品种和公司名称获取持仓数据
+func GetTradeDataByClassifyAndCompany(exchange, classifyName string, contracts, companies []string) (items []*OriginTradeData, err error) {
+	if exchange == "" {
+		err = fmt.Errorf("数据表名称有误")
+		return
+	}
+	if len(contracts) == 0 || len(companies) == 0 {
+		return
+	}
+	tableName := fmt.Sprintf("base_from_trade_%s_index", exchange)
+	sql := `SELECT
+			rank,
+			buy_short_name AS company_name,
+			buy_value AS val,
+			buy_change AS val_change,
+			classify_name,
+			classify_type,
+			data_time,
+			1 AS val_type 
+		FROM
+			%s 
+		WHERE
+			classify_name = ? AND classify_type IN (%s) AND buy_short_name IN (%s)
+		UNION ALL
+		(
+		SELECT
+			rank,
+			sold_short_name,
+			sold_value,
+			sold_change,
+			classify_name,
+			classify_type,
+			data_time,
+			2 AS val_type 
+		FROM
+			%s 
+		WHERE
+			classify_name = ? AND classify_type IN (%s) AND sold_short_name IN (%s)
+		)`
+	sql = fmt.Sprintf(sql, tableName, utils.GetOrmInReplace(len(contracts)), utils.GetOrmInReplace(len(companies)), tableName, utils.GetOrmInReplace(len(contracts)), utils.GetOrmInReplace(len(companies)))
+	o := orm.NewOrmUsingDB("data")
+	_, err = o.Raw(sql, classifyName, contracts, companies, classifyName, contracts, companies).QueryRows(&items)
+	return
+}
+
+// GetTradeZhengzhouDataByClassifyAndCompany 郑商所-根据品种和公司名称获取持仓数据
+func GetTradeZhengzhouDataByClassifyAndCompany(exchange string, contracts, companies []string) (items []*OriginTradeData, err error) {
+	if exchange == "" {
+		err = fmt.Errorf("数据表名称有误")
+		return
+	}
+	if len(contracts) == 0 || len(companies) == 0 {
+		return
+	}
+	tableName := fmt.Sprintf("base_from_trade_%s_index", exchange)
+	sql := `SELECT
+			rank,
+			buy_short_name AS company_name,
+			buy_value AS val,
+			buy_change AS val_change,
+			classify_name AS classify_type,
+			data_time,
+			1 AS val_type 
+		FROM
+			%s 
+		WHERE
+			classify_name IN (%s) AND buy_short_name IN (%s)
+		UNION ALL
+		(
+		SELECT
+			rank,
+			sold_short_name,
+			sold_value,
+			sold_change,
+			classify_name AS classify_type,
+			data_time,
+			2 AS val_type 
+		FROM
+			%s 
+		WHERE
+			classify_name IN (%s) AND sold_short_name IN (%s)
+		)`
+	sql = fmt.Sprintf(sql, tableName, utils.GetOrmInReplace(len(contracts)), utils.GetOrmInReplace(len(companies)), tableName, utils.GetOrmInReplace(len(contracts)), utils.GetOrmInReplace(len(companies)))
+	o := orm.NewOrmUsingDB("data")
+	_, err = o.Raw(sql, contracts, companies, contracts, companies).QueryRows(&items)
+	return
+}
+
+// ContractCompanyTradeData [合约-期货公司]持仓数据
+type ContractCompanyTradeData struct {
+	CompanyName  string                          `description:"期货公司名称"`
+	ClassifyType string                          `description:"合约代码"`
+	StartDate    time.Time                       `description:"数据开始日期"`
+	EndDate      time.Time                       `description:"数据结束日期"`
+	DataList     []*ContractCompanyTradeDataList `description:"数据序列"`
+}
+
+const (
+	TradeDataTypeNull      = 0 // 无值
+	TradeDataTypeOrigin    = 1 // 原始值
+	TradeDataTypeCalculate = 2 // 推算值
+
+	WarehouseBuyChartType     = 1 // 多单图
+	WarehouseSoldChartType    = 2 // 空单图
+	WarehousePureBuyChartType = 3 // 净多单图
+
+	WarehouseDefaultUnit      = "手"
+	WarehouseDefaultFrequency = "日度"
+
+	GuangZhouTopCompanyAliasName = "日成交持仓排名" // 广期所TOP20对应的公司名称
+	GuangZhouSeatNameBuy         = "持买单量"    // 广期所指标名称中的多单名称
+	GuangZhouSeatNameSold        = "持卖单量"    // 广期所指标名称中的空单名称
+	GuangZhouTopSeatNameBuy      = "持买单量总计"  // 广期所指标名称中的TOP20多单名称
+	GuangZhouTopSeatNameSold     = "持卖单量总计"  // 广期所指标名称中的TOP20空单名称
+)
+
+const (
+	TradeExchangeZhengzhou = "zhengzhou"
+	TradeExchangeGuangzhou = "guangzhou"
+)
+
+var WarehouseTypeSuffixNames = map[int]string{
+	WarehouseBuyChartType:     "席位多单",
+	WarehouseSoldChartType:    "席位空单",
+	WarehousePureBuyChartType: "席位净多单",
+}
+
+// GuangzhouSeatNameValType 广期所数据名称对应的席位方向
+var GuangzhouSeatNameValType = map[string]int{
+	GuangZhouSeatNameBuy:     1,
+	GuangZhouSeatNameSold:    2,
+	GuangZhouTopSeatNameBuy:  1,
+	GuangZhouTopSeatNameSold: 2,
+}
+
+// ContractCompanyTradeDataList [合约-期货公司]持仓数据详情
+type ContractCompanyTradeDataList struct {
+	Date              time.Time `description:"数据日期"`
+	BuyVal            int       `description:"多单持仓量"`
+	BuyValType        int       `description:"多单数据类型: 0-无值; 1-原始值; 2-推算值"`
+	BuyChange         int       `description:"多单持仓增减"`
+	BuyChangeType     int       `description:"多单持仓增减类型: 0-无值; 1-原始值; 2-推算值"`
+	SoldVal           int       `description:"空单持仓量"`
+	SoldValType       int       `description:"空单数据类型: 0-无值; 1-原始值; 2-推算值"`
+	SoldChange        int       `description:"空单持仓增减"`
+	SoldChangeType    int       `description:"空单持仓增减类型: 0-无值; 1-原始值; 2-推算值"`
+	PureBuyVal        int       `description:"净多单持仓量"`
+	PureBuyValType    int       `description:"净多单数据类型: 0-无值; 1-原始值; 2-推算值"`
+	PureBuyChange     int       `description:"净多单持仓增减"`
+	PureBuyChangeType int       `description:"净多单持仓增减类型: 0-无值; 1-原始值; 2-推算值"`
+}
+
+// GetLastTradeDataByClassify 获取[合约]末位多空单数据
+func GetLastTradeDataByClassify(exchange, classifyName string, contracts []string) (items []*OriginTradeData, err error) {
+	if exchange == "" {
+		err = fmt.Errorf("数据表名称有误")
+		return
+	}
+	if len(contracts) == 0 {
+		return
+	}
+	contractReplacer := utils.GetOrmInReplace(len(contracts))
+
+	tableName := fmt.Sprintf("base_from_trade_%s_index", exchange)
+	sql := `SELECT 
+			tpt.rank,
+			tpt.buy_short_name AS company_name,
+			tpt.buy_value AS val,
+			tpt.buy_change AS val_change,
+			tpt.classify_name,
+			tpt.classify_type,
+			tpt.data_time,
+			1 AS val_type
+		FROM 
+			%s tpt
+		JOIN 
+			(
+				SELECT
+					data_time, classify_type, MAX(rank) AS max_rank
+				FROM 
+					%s
+				WHERE 
+					classify_name = ? AND classify_type IN (%s) AND buy_short_name <> ''
+				GROUP BY 
+					data_time,
+					classify_type
+			) sub
+		ON
+			tpt.data_time = sub.data_time AND tpt.classify_type = sub.classify_type AND tpt.rank = sub.max_rank
+		WHERE 
+			tpt.classify_name = ? AND tpt.classify_type IN (%s)
+		UNION ALL
+		(
+		SELECT 
+			tpt.rank, tpt.sold_short_name, tpt.sold_value, tpt.sold_change, tpt.classify_name, tpt.classify_type, tpt.data_time, 2 AS val_type
+		FROM 
+			%s tpt
+		JOIN 
+			(
+				SELECT 
+					data_time, classify_type, MAX(rank) AS max_rank
+				FROM 
+					%s
+				WHERE 
+					classify_name = ? AND classify_type IN (%s) AND sold_short_name <> ''
+				GROUP BY 
+					data_time, classify_type
+			) sub
+		ON 
+			tpt.data_time = sub.data_time AND tpt.classify_type = sub.classify_type AND tpt.rank = sub.max_rank
+		WHERE 
+			tpt.classify_name = ? AND tpt.classify_type IN (%s)
+		)`
+	sql = fmt.Sprintf(sql, tableName, tableName, contractReplacer, contractReplacer, tableName, tableName, contractReplacer, contractReplacer)
+	o := orm.NewOrmUsingDB("data")
+	_, err = o.Raw(sql, classifyName, contracts, classifyName, contracts, classifyName, contracts, classifyName, contracts).QueryRows(&items)
+	return
+}
+
+// GetLastTradeZhengzhouDataByClassify 郑商所-获取[合约]末位多空单数据
+func GetLastTradeZhengzhouDataByClassify(exchange string, contracts []string) (items []*OriginTradeData, err error) {
+	if exchange == "" {
+		err = fmt.Errorf("数据表名称有误")
+		return
+	}
+	if len(contracts) == 0 {
+		return
+	}
+	contractReplacer := utils.GetOrmInReplace(len(contracts))
+
+	tableName := fmt.Sprintf("base_from_trade_%s_index", exchange)
+	sql := `SELECT 
+			tpt.rank,
+			tpt.buy_short_name AS company_name,
+			tpt.buy_value AS val,
+			tpt.buy_change AS val_change,
+  			tpt.classify_name AS classify_type,
+			tpt.data_time,
+			1 AS val_type
+		FROM 
+			%s tpt
+		JOIN 
+			(
+				SELECT
+					data_time, classify_name, MAX(rank) AS max_rank
+				FROM 
+					%s
+				WHERE 
+					classify_name IN (%s) AND buy_short_name <> ''
+				GROUP BY 
+					data_time,
+					classify_name
+			) sub
+		ON
+			tpt.data_time = sub.data_time AND tpt.classify_name = sub.classify_name AND tpt.rank = sub.max_rank
+		WHERE 
+			tpt.classify_name IN (%s)
+		UNION ALL
+		(
+		SELECT 
+			tpt.rank, tpt.sold_short_name, tpt.sold_value, tpt.sold_change, tpt.classify_name AS classify_type, tpt.data_time, 2 AS val_type
+		FROM 
+			%s tpt
+		JOIN 
+			(
+				SELECT 
+					data_time, classify_name, MAX(rank) AS max_rank
+				FROM 
+					%s
+				WHERE 
+					classify_name IN (%s) AND sold_short_name <> ''
+				GROUP BY 
+					data_time, classify_name
+			) sub
+		ON 
+			tpt.data_time = sub.data_time AND tpt.classify_name = sub.classify_name AND tpt.rank = sub.max_rank
+		WHERE 
+			tpt.classify_name IN (%s)
+		)`
+	sql = fmt.Sprintf(sql, tableName, tableName, contractReplacer, contractReplacer, tableName, tableName, contractReplacer, contractReplacer)
+	o := orm.NewOrmUsingDB("data")
+	_, err = o.Raw(sql, contracts, contracts, contracts, contracts).QueryRows(&items)
+	return
+}
+
+type BaseFromTradeGuangzhouIndex struct {
+	BaseFromTradeGuangzhouIndexId    int       `orm:"column(base_from_trade_guangzhou_index_id);pk"`
+	BaseFromTradeGuangzhouClassifyId int       `description:"分类id"`
+	IndexCode                        string    `description:"指标编码"`
+	IndexName                        string    `description:"指标名称"`
+	Frequency                        string    `description:"频率"`
+	Unit                             string    `description:"单位"`
+	StartDate                        string    `description:"开始日期"`
+	EndDate                          string    `description:"结束日期"`
+	CreateTime                       time.Time `description:"创建日期"`
+	ModifyTime                       time.Time `description:"修改日期"`
+}
+
+func GetBaseFromTradeGuangzhouIndexByClassifyId(classifyId int) (list []*BaseFromTradeGuangzhouIndex, err error) {
+	o := orm.NewOrmUsingDB("data")
+	sql := `SELECT * FROM base_from_trade_guangzhou_index WHERE base_from_trade_guangzhou_classify_id = ?`
+	_, err = o.Raw(sql, classifyId).QueryRows(&list)
+	return
+}
+
+type BaseFromTradeGuangzhouData struct {
+	BaseFromTradeGuangzhouDataId  int       `orm:"column(base_from_trade_guangzhou_data_id);pk"`
+	BaseFromTradeGuangzhouIndexId int       `description:"指标id"`
+	IndexCode                     string    `description:"指标编码"`
+	DataTime                      time.Time `description:"数据日期"`
+	Value                         float64   `description:"数据值"`
+	QtySub                        float64   `description:"增减"`
+	CreateTime                    time.Time `description:"创建日期"`
+	ModifyTime                    time.Time `description:"修改日期"`
+}
+
+// GetBaseFromTradeGuangzhouDataByIndexIds 获取指标数据
+func GetBaseFromTradeGuangzhouDataByIndexIds(indexIds []int) (list []*BaseFromTradeGuangzhouData, err error) {
+	if len(indexIds) == 0 {
+		return
+	}
+	o := orm.NewOrmUsingDB("data")
+	sql := fmt.Sprintf(`SELECT * FROM base_from_trade_guangzhou_data WHERE base_from_trade_guangzhou_index_id IN (%s) ORDER BY base_from_trade_guangzhou_index_id`, utils.GetOrmInReplace(len(indexIds)))
+	_, err = o.Raw(sql, indexIds).QueryRows(&list)
+	return
+}
+
+// GetBaseFromTradeGuangzhouMinDataByIndexIds 获取指标中的末位数据
+func GetBaseFromTradeGuangzhouMinDataByIndexIds(indexIds []int) (list []*BaseFromTradeGuangzhouData, err error) {
+	indexLen := len(indexIds)
+	if indexLen == 0 {
+		return
+	}
+	o := orm.NewOrmUsingDB("data")
+	sql := fmt.Sprintf(`SELECT 
+			t1.data_time,
+			t1.min_value AS value
+		FROM 
+			(
+				SELECT 
+					data_time,
+					MIN(value) AS min_value
+				FROM 
+					base_from_trade_guangzhou_data
+				WHERE 
+					base_from_trade_guangzhou_index_id IN (%s)
+				GROUP BY 
+					data_time
+			) t1
+		JOIN 
+			base_from_trade_guangzhou_data t2
+		ON 
+			t1.data_time = t2.data_time AND t1.min_value = t2.value AND t2.base_from_trade_guangzhou_index_id IN (%s)
+		GROUP BY 
+			t1.data_time`, utils.GetOrmInReplace(indexLen), utils.GetOrmInReplace(indexLen))
+	_, err = o.Raw(sql, indexIds, indexIds).QueryRows(&list)
+	return
+}

+ 76 - 0
models/data_manage/trade_analysis/trade_classify.go

@@ -1,7 +1,9 @@
 package trade_analysis
 
 import (
+	"fmt"
 	"github.com/beego/beego/v2/client/orm"
+	"strings"
 	"time"
 )
 
@@ -53,3 +55,77 @@ func GetClassifyTypeByClassifyName(exchange, classifyName string) (item *TradeCl
 
 	return
 }
+
+func (m *BaseFromTradeClassify) TableName() string {
+	return "base_from_trade_classify"
+}
+
+type BaseFromTradeClassifyCols struct {
+	PrimaryId    string
+	Exchange     string
+	ClassifyName string
+	ClassifyType string
+	LatestDate   string
+	CreateTime   string
+	ModifyTime   string
+}
+
+func (m *BaseFromTradeClassify) Cols() BaseFromTradeClassifyCols {
+	return BaseFromTradeClassifyCols{
+		PrimaryId:    "id",
+		Exchange:     "exchange",
+		ClassifyName: "classify_name",
+		ClassifyType: "classify_type",
+		LatestDate:   "latest_date",
+		CreateTime:   "create_time",
+		ModifyTime:   "modify_time",
+	}
+}
+
+func (m *BaseFromTradeClassify) GetItemsByCondition(condition string, pars []interface{}, fieldArr []string, orderRule string) (items []*BaseFromTradeClassify, err error) {
+	o := orm.NewOrmUsingDB("data")
+	fields := strings.Join(fieldArr, ",")
+	if len(fieldArr) == 0 {
+		fields = `*`
+	}
+	order := fmt.Sprintf(`ORDER BY %s DESC`, m.Cols().CreateTime)
+	if orderRule != "" {
+		order = ` ORDER BY ` + orderRule
+	}
+	sql := fmt.Sprintf(`SELECT %s FROM %s WHERE 1=1 %s %s`, fields, m.TableName(), condition, order)
+	_, err = o.Raw(sql, pars).QueryRows(&items)
+	return
+}
+
+// GetClassifyItemsByCondition 获取品种信息
+func (m *BaseFromTradeClassify) GetClassifyItemsByCondition(condition string, pars []interface{}, fieldArr []string, orderRule string) (items []*BaseFromTradeClassify, err error) {
+	o := orm.NewOrmUsingDB("data")
+	fields := strings.Join(fieldArr, ",")
+	if len(fieldArr) == 0 {
+		fields = `*`
+	}
+	order := fmt.Sprintf(`ORDER BY %s DESC`, m.Cols().CreateTime)
+	if orderRule != "" {
+		order = ` ORDER BY ` + orderRule
+	}
+	sql := fmt.Sprintf(`SELECT %s FROM %s WHERE 1=1 %s GROUP BY %s %s`, fields, m.TableName(), condition, m.Cols().ClassifyName, order)
+	_, err = o.Raw(sql, pars).QueryRows(&items)
+	return
+}
+
+// BaseFromTradeClassifyItem 交易所品种信息
+type BaseFromTradeClassifyItem struct {
+	ClassifyName string `description:"品种"`
+	Exchange     string `description:"交易所标识"`
+	ExchangeName string `description:"交易所名称"`
+}
+
+// BaseFromTradeContractItem 交易所合约信息
+type BaseFromTradeContractItem struct {
+	ClassifyName string `description:"品种"`
+	Exchange     string `description:"交易所标识"`
+	ExchangeName string `description:"交易所名称"`
+	ClassifyType string `description:"合约"`
+	LatestDate   string `description:"最近数据的日期"`
+	CreateTime   string `description:"创建时间"`
+}

+ 129 - 0
models/data_manage/trade_analysis/trade_futures_company.go

@@ -0,0 +1,129 @@
+package trade_analysis
+
+import (
+	"fmt"
+	"github.com/beego/beego/v2/client/orm"
+	"strings"
+	"time"
+)
+
+const TradeFuturesCompanyTop20 = "TOP20"
+
+// TradeFuturesCompany 期货公司表
+type TradeFuturesCompany struct {
+	TradeFuturesCompanyId int       `orm:"column(trade_futures_company_id);pk"`
+	CompanyName           string    `description:"标准公司名称"`
+	ZhengzhouName         string    `description:"郑商所下的名称"`
+	DalianName            string    `description:"大商所下的名称"`
+	ShanghaiName          string    `description:"上期所下的名称"`
+	IneName               string    `description:"上期能源下的名称"`
+	GuangzhouName         string    `description:"广期所下的名称"`
+	CffexName             string    `description:"中金所下的名称"`
+	Sort                  int       `description:"排序"`
+	CreateTime            time.Time `description:"创建时间"`
+	ModifyTime            time.Time `description:"修改时间"`
+}
+
+func (m *TradeFuturesCompany) TableName() string {
+	return "trade_futures_company"
+}
+
+type TradeFuturesCompanyCols struct {
+	PrimaryId     string
+	CompanyName   string
+	ZhengzhouName string
+	DalianName    string
+	ShanghaiName  string
+	IneName       string
+	GuangzhouName string
+	CffexName     string
+	Sort          string
+	CreateTime    string
+	ModifyTime    string
+}
+
+func (m *TradeFuturesCompany) Cols() TradeFuturesCompanyCols {
+	return TradeFuturesCompanyCols{
+		PrimaryId:     "trade_futures_company_id",
+		CompanyName:   "company_name",
+		ZhengzhouName: "zhengzhou_name",
+		DalianName:    "dalian_name",
+		ShanghaiName:  "shanghai_name",
+		IneName:       "ine_name",
+		GuangzhouName: "guangzhou_name",
+		CffexName:     "cffex_name",
+		Sort:          "sort",
+		CreateTime:    "create_time",
+		ModifyTime:    "modify_time",
+	}
+}
+
+func (m *TradeFuturesCompany) GetItemById(id int) (item *TradeFuturesCompany, err error) {
+	o := orm.NewOrmUsingDB("data")
+	sql := fmt.Sprintf(`SELECT * FROM %s WHERE %s = ? LIMIT 1`, m.TableName(), m.Cols().PrimaryId)
+	err = o.Raw(sql, id).QueryRow(&item)
+	return
+}
+
+func (m *TradeFuturesCompany) GetItemByCondition(condition string, pars []interface{}, orderRule string) (item *TradeFuturesCompany, err error) {
+	o := orm.NewOrmUsingDB("data")
+	order := ``
+	if orderRule != "" {
+		order = ` ORDER BY ` + orderRule
+	}
+	sql := fmt.Sprintf(`SELECT * FROM %s WHERE 1=1 %s %s LIMIT 1`, m.TableName(), condition, order)
+	err = o.Raw(sql, pars).QueryRow(&item)
+	return
+}
+
+func (m *TradeFuturesCompany) GetCountByCondition(condition string, pars []interface{}) (count int, err error) {
+	o := orm.NewOrmUsingDB("data")
+	sql := fmt.Sprintf(`SELECT COUNT(1) FROM %s WHERE 1=1 %s`, m.TableName(), condition)
+	err = o.Raw(sql, pars).QueryRow(&count)
+	return
+}
+
+func (m *TradeFuturesCompany) GetItemsByCondition(condition string, pars []interface{}, fieldArr []string, orderRule string) (items []*TradeFuturesCompany, err error) {
+	o := orm.NewOrmUsingDB("data")
+	fields := strings.Join(fieldArr, ",")
+	if len(fieldArr) == 0 {
+		fields = `*`
+	}
+	order := fmt.Sprintf(`ORDER BY %s DESC`, m.Cols().CreateTime)
+	if orderRule != "" {
+		order = ` ORDER BY ` + orderRule
+	}
+	sql := fmt.Sprintf(`SELECT %s FROM %s WHERE 1=1 %s %s`, fields, m.TableName(), condition, order)
+	_, err = o.Raw(sql, pars).QueryRows(&items)
+	return
+}
+
+func (m *TradeFuturesCompany) GetPageItemsByCondition(condition string, pars []interface{}, fieldArr []string, orderRule string, startSize, pageSize int) (items []*TradeFuturesCompany, err error) {
+	o := orm.NewOrmUsingDB("data")
+	fields := strings.Join(fieldArr, ",")
+	if len(fieldArr) == 0 {
+		fields = `*`
+	}
+	order := fmt.Sprintf(`ORDER BY %s DESC`, m.Cols().CreateTime)
+	if orderRule != "" {
+		order = ` ORDER BY ` + orderRule
+	}
+	sql := fmt.Sprintf(`SELECT %s FROM %s WHERE 1=1 %s %s LIMIT ?,?`, fields, m.TableName(), condition, order)
+	_, err = o.Raw(sql, pars, startSize, pageSize).QueryRows(&items)
+	return
+}
+
+// TradeFuturesCompanyItem 期货公司信息
+type TradeFuturesCompanyItem struct {
+	CompanyId   int    `description:"期货公司ID"`
+	CompanyName string `description:"标准公司名称"`
+	Sort        int    `description:"排序"`
+}
+
+func (m *TradeFuturesCompany) Format2Item() (item *TradeFuturesCompanyItem) {
+	item = new(TradeFuturesCompanyItem)
+	item.CompanyId = m.TradeFuturesCompanyId
+	item.CompanyName = m.CompanyName
+	item.Sort = m.Sort
+	return
+}

+ 103 - 0
models/data_manage/trade_analysis/warehouse.go

@@ -0,0 +1,103 @@
+package trade_analysis
+
+import (
+	"eta/eta_api/models/data_manage"
+	"eta/eta_api/utils"
+	"fmt"
+	"github.com/beego/beego/v2/client/orm"
+)
+
+// WarehouseExtraConfig 建仓图表配置
+type WarehouseExtraConfig struct {
+	MultipleGraphConfigId int      `description:"多图配置ID"`
+	WarehouseChartType    int      `description:"图表类型: 1-多单图; 2-空单图; 3-净多单图"`
+	Exchange              string   `description:"交易所标识"`
+	ClassifyName          string   `description:"品种名称"`
+	Contracts             []string `description:"合约代码"`
+	Companies             []string `description:"期货公司, 不超过5个"`
+	PredictRatio          float64  `description:"预估参数, 0-1之间"`
+}
+
+// WarehouseChartPars 建仓单表配置
+type WarehouseChartPars struct {
+	WarehouseChartType int    `description:"图表类型: 1-多单图; 2-空单图; 3-净多单图"`
+	DateType           int    `description:"日期类型"`
+	DateTypeNum        int    `description:"日期类型=25(N月)时的N值"`
+	StartDate          string `description:"自定义开始日期"`
+	EndDate            string `description:"自定义结束日期"`
+	//ChartThemeId       int                               `description:"图表主题ID"`
+	ChartEdbInfoList []*data_manage.ChartSaveItem `description:"指标及配置信息"`
+	//SourcesFrom        *data_manage.ChartInfoSourcesFrom `description:"图表来源"`
+}
+
+// WarehouseChartDataResp 图表详情返回信息
+type WarehouseChartDataResp struct {
+	WarehouseExtraConfig
+	MultiEdbMappings []*WarehouseEdbSaveItem
+}
+
+// WarehouseEdbSaveItem 建仓指标保存
+type WarehouseEdbSaveItem struct {
+	EdbInfoId  int    `description:"指标ID"`
+	EdbName    string `description:"指标名称"`
+	Unit       string `description:"单位"`
+	Frequency  string `description:"频度"`
+	ClassifyId int    `description:"指标库分类ID"`
+	UniqueFlag string `description:"唯一标识"`
+	//ExtraConfig string `description:"配置信息-JSON"`
+}
+
+type WarehouseEdbSaveRespItem struct {
+	WarehouseEdbSaveItem
+	Tips   string `description:"提示信息"`
+	ErrMsg string `description:"错误信息"`
+}
+
+// CreateWarehouseChart 新增建仓图表
+func CreateWarehouseChart(chartInfo *data_manage.ChartInfo, seriesList []*data_manage.ChartSeries, multiChartMapping *data_manage.MultipleGraphConfigChartMapping) (err error) {
+	o := orm.NewOrmUsingDB("data")
+	tx, e := o.Begin()
+	if e != nil {
+		err = fmt.Errorf("orm begin err: %s", e.Error())
+		return
+	}
+	defer func() {
+		if err != nil {
+			_ = tx.Rollback()
+			return
+		}
+		_ = tx.Commit()
+	}()
+
+	// 新增图表
+	id, e := tx.Insert(chartInfo)
+	if e != nil {
+		err = fmt.Errorf("insert chart err: %v", e)
+		return
+	}
+	newId := int(id)
+	chartInfo.ChartInfoId = newId
+
+	// 新增图例
+	if len(seriesList) > 0 {
+		for _, s := range seriesList {
+			s.ChartInfoId = newId
+		}
+		_, e = tx.InsertMulti(utils.MultiAddNum, seriesList)
+		if e != nil {
+			err = fmt.Errorf("insert multi series err: %v", e)
+			return
+		}
+	}
+
+	// 图表关联
+	if multiChartMapping != nil {
+		multiChartMapping.ChartInfoId = newId
+		_, e = tx.Insert(multiChartMapping)
+		if e != nil {
+			err = fmt.Errorf("insert multi chart mapping err: %v", e)
+			return
+		}
+	}
+	return
+}

+ 340 - 0
models/data_manage/trade_analysis/warehouse_process_classify.go

@@ -0,0 +1,340 @@
+package trade_analysis
+
+import (
+	"eta/eta_api/models"
+	"eta/eta_api/utils"
+	"fmt"
+	"github.com/beego/beego/v2/client/orm"
+	"strings"
+	"time"
+)
+
+// WareHouseProcessClassify 建仓过程分类表
+type WareHouseProcessClassify struct {
+	WareHouseProcessClassifyId int       `orm:"column(warehouse_process_classify_id);pk"`
+	ClassifyName               string    `description:"分类名称"`
+	ClassifyNameEn             string    `description:"英文分类名称"`
+	ParentId                   int       `description:"父级ID"`
+	SysUserId                  int       `description:"创建人ID"`
+	SysUserRealName            string    `description:"创建人姓名"`
+	Level                      int       `description:"层级"`
+	Sort                       int       `description:"排序"`
+	RootId                     int       `description:"顶级分类ID"`
+	LevelPath                  string    `description:"层级路径"`
+	UniqueCode                 string    `description:"唯一编码"`
+	CreateTime                 time.Time `description:"创建时间"`
+	ModifyTime                 time.Time `description:"修改时间"`
+}
+
+func (m *WareHouseProcessClassify) TableName() string {
+	return "warehouse_process_classify"
+}
+
+type WareHouseProcessClassifyCols struct {
+	PrimaryId       string
+	ClassifyName    string
+	ClassifyNameEn  string
+	ParentId        string
+	SysUserId       string
+	SysUserRealName string
+	Level           string
+	Sort            string
+	RootId          string
+	LevelPath       string
+	UniqueCode      string
+	CreateTime      string
+	ModifyTime      string
+}
+
+func (m *WareHouseProcessClassify) Cols() WareHouseProcessClassifyCols {
+	return WareHouseProcessClassifyCols{
+		PrimaryId:       "warehouse_process_classify_id",
+		ClassifyName:    "classify_name",
+		ClassifyNameEn:  "classify_name_en",
+		ParentId:        "parent_id",
+		SysUserId:       "sys_user_id",
+		SysUserRealName: "sys_user_real_name",
+		Level:           "level",
+		Sort:            "sort",
+		RootId:          "root_id",
+		LevelPath:       "level_path",
+		UniqueCode:      "unique_code",
+		CreateTime:      "create_time",
+		ModifyTime:      "modify_time",
+	}
+}
+
+func (m *WareHouseProcessClassify) Create() (err error) {
+	o := orm.NewOrmUsingDB("data")
+	id, err := o.Insert(m)
+	if err != nil {
+		return
+	}
+	m.WareHouseProcessClassifyId = int(id)
+	return
+}
+
+func (m *WareHouseProcessClassify) CreateMulti(items []*WareHouseProcessClassify) (err error) {
+	if len(items) == 0 {
+		return
+	}
+	o := orm.NewOrmUsingDB("data")
+	_, err = o.InsertMulti(len(items), items)
+	return
+}
+
+func (m *WareHouseProcessClassify) Update(cols []string) (err error) {
+	o := orm.NewOrmUsingDB("data")
+	_, err = o.Update(m, cols...)
+	return
+}
+
+func (m *WareHouseProcessClassify) Remove() (err error) {
+	o := orm.NewOrmUsingDB("data")
+	sql := fmt.Sprintf(`DELETE FROM %s WHERE %s = ? LIMIT 1`, m.TableName(), m.Cols().PrimaryId)
+	_, err = o.Raw(sql, m.WareHouseProcessClassifyId).Exec()
+	return
+}
+
+func (m *WareHouseProcessClassify) MultiRemove(ids []int) (err error) {
+	if len(ids) == 0 {
+		return
+	}
+	o := orm.NewOrmUsingDB("data")
+	sql := fmt.Sprintf(`DELETE FROM %s WHERE %s IN (%s)`, m.TableName(), m.Cols().PrimaryId, utils.GetOrmInReplace(len(ids)))
+	_, err = o.Raw(sql, ids).Exec()
+	return
+}
+
+func (m *WareHouseProcessClassify) RemoveByCondition(condition string, pars []interface{}) (err error) {
+	if condition == "" {
+		return
+	}
+	o := orm.NewOrmUsingDB("data")
+	sql := fmt.Sprintf(`DELETE FROM %s WHERE %s`, m.TableName(), condition)
+	_, err = o.Raw(sql, pars).Exec()
+	return
+}
+
+func (m *WareHouseProcessClassify) GetItemById(id int) (item *WareHouseProcessClassify, err error) {
+	o := orm.NewOrmUsingDB("data")
+	sql := fmt.Sprintf(`SELECT * FROM %s WHERE %s = ? LIMIT 1`, m.TableName(), m.Cols().PrimaryId)
+	err = o.Raw(sql, id).QueryRow(&item)
+	return
+}
+
+func (m *WareHouseProcessClassify) GetItemByCondition(condition string, pars []interface{}, orderRule string) (item *WareHouseProcessClassify, err error) {
+	o := orm.NewOrmUsingDB("data")
+	order := ``
+	if orderRule != "" {
+		order = ` ORDER BY ` + orderRule
+	}
+	sql := fmt.Sprintf(`SELECT * FROM %s WHERE 1=1 %s %s LIMIT 1`, m.TableName(), condition, order)
+	err = o.Raw(sql, pars).QueryRow(&item)
+	return
+}
+
+func (m *WareHouseProcessClassify) GetCountByCondition(condition string, pars []interface{}) (count int, err error) {
+	o := orm.NewOrmUsingDB("data")
+	sql := fmt.Sprintf(`SELECT COUNT(1) FROM %s WHERE 1=1 %s`, m.TableName(), condition)
+	err = o.Raw(sql, pars).QueryRow(&count)
+	return
+}
+
+func (m *WareHouseProcessClassify) GetItemsByCondition(condition string, pars []interface{}, fieldArr []string, orderRule string) (items []*WareHouseProcessClassify, err error) {
+	o := orm.NewOrmUsingDB("data")
+	fields := strings.Join(fieldArr, ",")
+	if len(fieldArr) == 0 {
+		fields = `*`
+	}
+	order := fmt.Sprintf(`ORDER BY %s DESC`, m.Cols().CreateTime)
+	if orderRule != "" {
+		order = ` ORDER BY ` + orderRule
+	}
+	sql := fmt.Sprintf(`SELECT %s FROM %s WHERE 1=1 %s %s`, fields, m.TableName(), condition, order)
+	_, err = o.Raw(sql, pars).QueryRows(&items)
+	return
+}
+
+func (m *WareHouseProcessClassify) GetPageItemsByCondition(condition string, pars []interface{}, fieldArr []string, orderRule string, startSize, pageSize int) (items []*WareHouseProcessClassify, err error) {
+	o := orm.NewOrmUsingDB("data")
+	fields := strings.Join(fieldArr, ",")
+	if len(fieldArr) == 0 {
+		fields = `*`
+	}
+	order := fmt.Sprintf(`ORDER BY %s DESC`, m.Cols().CreateTime)
+	if orderRule != "" {
+		order = ` ORDER BY ` + orderRule
+	}
+	sql := fmt.Sprintf(`SELECT %s FROM %s WHERE 1=1 %s %s LIMIT ?,?`, fields, m.TableName(), condition, order)
+	_, err = o.Raw(sql, pars, startSize, pageSize).QueryRows(&items)
+	return
+}
+
+// WareHouseProcessClassifyItem 建仓过程分类信息
+type WareHouseProcessClassifyItem struct {
+	ClassifyId     int                             `description:"分类ID"`
+	ClassifyName   string                          `description:"分类名称"`
+	ClassifyNameEn string                          `description:"英文分类名称"`
+	ParentId       int                             `description:"父级ID"`
+	Level          int                             `description:"层级"`
+	Sort           int                             `description:"排序"`
+	LevelPath      string                          `description:"层级路径"`
+	UniqueCode     string                          `description:"唯一编码"`
+	Children       []*WareHouseProcessClassifyItem `description:"子分类"`
+}
+
+func (m *WareHouseProcessClassify) Format2Item() (item *WareHouseProcessClassifyItem) {
+	item = new(WareHouseProcessClassifyItem)
+	item.ClassifyId = m.WareHouseProcessClassifyId
+	item.ClassifyName = m.ClassifyName
+	item.ClassifyNameEn = m.ClassifyNameEn
+	item.ParentId = m.ParentId
+	item.Level = m.Level
+	item.Sort = m.Sort
+	item.LevelPath = m.LevelPath
+	item.UniqueCode = m.UniqueCode
+	item.Children = make([]*WareHouseProcessClassifyItem, 0)
+	return
+}
+
+// ------------------------------------------------ 通用分类 ------------------------------------------------
+
+// GetCommonClassifyCols 通用分类字段映射
+func (m *WareHouseProcessClassify) GetCommonClassifyCols() models.CommonClassifyCols {
+	return models.CommonClassifyCols{
+		ClassifyId:   m.Cols().PrimaryId,
+		ClassifyName: m.Cols().ClassifyName,
+		ParentId:     m.Cols().ParentId,
+		Sort:         m.Cols().ParentId,
+		RootId:       m.Cols().RootId,
+		Level:        m.Cols().Level,
+		LevelPath:    m.Cols().LevelPath,
+		CreateTime:   m.Cols().CreateTime,
+		ModifyTime:   m.Cols().ModifyTime,
+	}
+}
+
+// GetCommonClassifyById 获取通用分类
+func (m *WareHouseProcessClassify) GetCommonClassifyById(classifyId int) (commonClassify *models.CommonClassify, err error) {
+	item, e := m.GetItemById(classifyId)
+	if e != nil {
+		err = e
+		return
+	}
+	commonClassify = new(models.CommonClassify)
+	commonClassify.ClassifyId = item.WareHouseProcessClassifyId
+	commonClassify.ClassifyName = item.ClassifyName
+	commonClassify.ParentId = item.ParentId
+	commonClassify.RootId = item.RootId
+	commonClassify.Level = item.Level
+	commonClassify.LevelPath = item.LevelPath
+	commonClassify.Sort = item.Sort
+	commonClassify.CreateTime = item.CreateTime
+	commonClassify.ModifyTime = item.ModifyTime
+	return
+}
+
+// GetClassifyByParentIdAndName 实现获取分类信息的方法
+func (m *WareHouseProcessClassify) GetClassifyByParentIdAndName(parentId int, name string, excludeId int) (*models.CommonClassify, error) {
+	// 实现获取分类信息的逻辑
+	return nil, nil
+}
+
+// UpdateCommonClassify 实现更新分类信息的方法
+func (m *WareHouseProcessClassify) UpdateCommonClassify(classify *models.CommonClassify, updateCols []string) (err error) {
+	return
+}
+
+func (m *WareHouseProcessClassify) UpdateClassifyChildByParentId(classifyIds []int, rootId int, stepLevel int) (err error) {
+	//	o := orm.NewOrmUsingDB("data")
+	//	var pars []interface{}
+	//	pars = append(pars, rootId, levelStep)
+	//	pars = append(pars, classifyIds)
+	//	// 更新相关联的二级分类的parentId,和classify_name_second
+	//	sql := `update edb_classify
+	//SET root_id = ?, level = level+?
+	//where classify_id IN (` + utils.GetOrmInReplace(len(classifyIds)) + `)`
+	//	_, err = o.Raw(sql, pars).Exec()
+	//	if err != nil {
+	//		return
+	//	}
+	return
+}
+
+// GetClassifySortMaxByParentId 实现获取分类排序的方法
+func (m *WareHouseProcessClassify) GetClassifySortMaxByParentId(parentId int) (sortMax int, err error) {
+	//	o := orm.NewOrmUsingDB("data")
+	//	sql := `SELECT Max(sort) AS sort FROM edb_classify WHERE parent_id=? AND classify_type=? `
+	//	err = o.Raw(sql, parentId, classifyType).QueryRow(&sort)
+	//	return
+	return
+}
+
+func (m *WareHouseProcessClassify) GetFirstClassifyByParentId(parentId int) (item *models.CommonClassify, err error) {
+	//o := orm.NewOrmUsingDB("data")
+	//sql := ` SELECT * FROM edb_classify WHERE parent_id=? order by sort asc,classify_id asc limit 1`
+	//err = o.Raw(sql, parentId).QueryRow(&item)
+	return
+}
+
+// SetClassifySortByParentId 实现设置分类排序的方法
+func (m *WareHouseProcessClassify) SetClassifySortByParentId(parentId, classifyId, sort int, sortUpdate string) (err error) {
+	//o := orm.NewOrmUsingDB("data")
+	//sql := ` update edb_classify set sort = ` + updateSort + ` WHERE parent_id=? AND sort > ? AND classify_type = ? `
+	//if classifyId > 0 {
+	//	sql += ` or ( classify_id > ` + fmt.Sprint(classifyId) + ` and sort = ` + fmt.Sprint(nowSort) + `)`
+	//}
+	//_, err = o.Raw(sql, parentId, nowSort, classifyType).Exec()
+	return
+}
+
+// GetCommonClassifyObjCols 通用分类对象字段映射
+func (m *WareHouseProcessClassify) GetCommonClassifyObjCols() models.CommonClassifyObjCols {
+	// TODO: 完善
+	return models.CommonClassifyObjCols{
+		ObjectId:   m.Cols().ClassifyName,
+		ClassifyId: m.Cols().PrimaryId,
+		Sort:       m.Cols().ParentId,
+	}
+}
+
+func (m *WareHouseProcessClassify) GetObjectById(objectId int) (*models.CommonClassifyObj, error) {
+	// 实现获取分类信息的逻辑
+	return nil, nil
+}
+
+// GetObjectSortMaxByClassifyId 获取分类下最大排序
+func (m *WareHouseProcessClassify) GetObjectSortMaxByClassifyId(classifyId int) (sortMax int, err error) {
+	//	o := orm.NewOrmUsingDB("data")
+	//	sql := `SELECT Max(sort) AS sort FROM edb_info WHERE classify_id=? `
+	//	err = o.Raw(sql, classifyId).QueryRow(&sort)
+	//	return
+	return
+}
+
+func (m *WareHouseProcessClassify) GetFirstObjectByClassifyId(classifyId int) (item *models.CommonClassifyObj, err error) {
+	//o := orm.NewOrmUsingDB("data")
+	//sql := ` SELECT * FROM edb_info WHERE classify_id=? order by sort asc,edb_info_id asc limit 1`
+	//err = o.Raw(sql, classifyId).QueryRow(&item)
+	return
+}
+
+func (m *WareHouseProcessClassify) SetObjectSortByClassifyId(classifyId, sort, prevObjectId int, sortUpdate string) (err error) {
+	//o := orm.NewOrmUsingDB("data")
+	//sql := ` update edb_info set sort = ` + updateSort + ` WHERE classify_id=?`
+	//if prevEdbInfoId > 0 {
+	//	sql += ` AND ( sort > ? or ( edb_info_id > ` + fmt.Sprint(prevEdbInfoId) + ` and sort=` + fmt.Sprint(nowSort) + ` )) `
+	//} else {
+	//	sql += ` AND ( sort > ? )`
+	//}
+	//_, err = o.Raw(sql, classifyId, nowSort).Exec()
+	return
+}
+
+// UpdateCommonClassifyObj 更新通用分类对象
+func (m *WareHouseProcessClassify) UpdateCommonClassifyObj(object *models.CommonClassifyObj, updateCols []string) (err error) {
+	return
+}
+
+// ------------------------------------------------ 通用分类 ------------------------------------------------

+ 207 - 0
routers/commentsRouter.go

@@ -8188,6 +8188,42 @@ func init() {
             Filters: nil,
             Params: nil})
 
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisController"],
+        beego.ControllerComments{
+            Method: "GetTradeClassifyList",
+            Router: `/classify_list`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisController"],
+        beego.ControllerComments{
+            Method: "GetTradeFuturesCompanyList",
+            Router: `/company_list`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisController"],
+        beego.ControllerComments{
+            Method: "GetTradeContractList",
+            Router: `/contract_list`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisController"],
+        beego.ControllerComments{
+            Method: "GetTradeExchangeList",
+            Router: `/exchange_list`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
     beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisController"],
         beego.ControllerComments{
             Method: "GetPositionTop",
@@ -8197,6 +8233,177 @@ func init() {
             Filters: nil,
             Params: nil})
 
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseClassifyController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseClassifyController"],
+        beego.ControllerComments{
+            Method: "AddChartClassify",
+            Router: `/warehouse/classify/add`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseClassifyController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseClassifyController"],
+        beego.ControllerComments{
+            Method: "DeleteChartClassify",
+            Router: `/warehouse/classify/delete`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseClassifyController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseClassifyController"],
+        beego.ControllerComments{
+            Method: "DeleteChartClassifyCheck",
+            Router: `/warehouse/classify/delete/check`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseClassifyController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseClassifyController"],
+        beego.ControllerComments{
+            Method: "EditChartClassify",
+            Router: `/warehouse/classify/edit`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseClassifyController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseClassifyController"],
+        beego.ControllerComments{
+            Method: "ChartClassifyItems",
+            Router: `/warehouse/classify/items`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseClassifyController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseClassifyController"],
+        beego.ControllerComments{
+            Method: "ChartClassifyList",
+            Router: `/warehouse/classify/list`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseClassifyController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseClassifyController"],
+        beego.ControllerComments{
+            Method: "ChartClassifyMove",
+            Router: `/warehouse/classify/move`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseClassifyController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseClassifyController"],
+        beego.ControllerComments{
+            Method: "ClassifyTree",
+            Router: `/warehouse/classify/tree`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseController"],
+        beego.ControllerComments{
+            Method: "Add",
+            Router: `/warehouse/chart/add`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseController"],
+        beego.ControllerComments{
+            Method: "Copy",
+            Router: `/warehouse/chart/copy`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseController"],
+        beego.ControllerComments{
+            Method: "Detail",
+            Router: `/warehouse/chart/detail`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseController"],
+        beego.ControllerComments{
+            Method: "Edit",
+            Router: `/warehouse/chart/edit`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseController"],
+        beego.ControllerComments{
+            Method: "List",
+            Router: `/warehouse/chart/list`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseController"],
+        beego.ControllerComments{
+            Method: "Preview",
+            Router: `/warehouse/chart/preview`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseController"],
+        beego.ControllerComments{
+            Method: "Refresh",
+            Router: `/warehouse/chart/refresh`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseController"],
+        beego.ControllerComments{
+            Method: "ChartInfoSave",
+            Router: `/warehouse/chart/save`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseController"],
+        beego.ControllerComments{
+            Method: "SearchByEs",
+            Router: `/warehouse/chart/search_by_es`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseController"],
+        beego.ControllerComments{
+            Method: "EdbSave",
+            Router: `/warehouse/edb/save`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseController"],
+        beego.ControllerComments{
+            Method: "EdbSaveCheck",
+            Router: `/warehouse/edb/save_check`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
     beego.GlobalControllerRouter["eta/eta_api/controllers:BannerController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers:BannerController"],
         beego.ControllerComments{
             Method: "Upload",

+ 2 - 0
routers/router.go

@@ -289,6 +289,8 @@ func init() {
 		web.NSNamespace("/trade_analysis",
 			web.NSInclude(
 				&trade_analysis.TradeAnalysisController{},
+				&trade_analysis.WarehouseClassifyController{},
+				&trade_analysis.WarehouseController{},
 			),
 		),
 		web.NSNamespace("/custom_analysis",

+ 2 - 0
services/data/base_edb_lib.go

@@ -278,6 +278,8 @@ func RefreshEdbData(edbInfoId, source, subSource int, edbCode, startDate string)
 		urlStr = "sci_hq/refresh"
 	case utils.DATA_SOURCE_LY:
 		urlStr = "ly/refresh"
+	case utils.DATA_SOURCE_TRADE_ANALYSIS:
+		urlStr = "trade_analysis/edb/refresh"
 	default:
 		edbSource := data_manage.EdbSourceIdMap[source]
 		if edbSource != nil {

+ 485 - 0
services/data/common_classify.go

@@ -0,0 +1,485 @@
+package data
+
+import (
+	"eta/eta_api/models"
+	"eta/eta_api/utils"
+	"fmt"
+	"strconv"
+	"strings"
+	"time"
+)
+
+func CommonClassifyMove(req models.CommonClassifyMoveReq, strategy CommonClassifyStrategy) (tips string, err error) {
+	ctx := NewCommonClassifyCtx(strategy)
+
+	var (
+		classify       *models.CommonClassify
+		parentClassify *models.CommonClassify
+		prevClassify   *models.CommonClassify
+		nextClassify   *models.CommonClassify
+		object         *models.CommonClassifyObj
+		prevObject     *models.CommonClassifyObj
+		nextObject     *models.CommonClassifyObj
+		sortPrev       int
+		sortNext       int
+	)
+
+	// 获取父级分类
+	if req.ParentClassifyId > 0 {
+		c, e := ctx.GetCommonClassifyById(req.ParentClassifyId)
+		if e != nil {
+			err = fmt.Errorf("获取上级分类失败, %v", e)
+			return
+		}
+		parentClassify = c
+	}
+
+	// 兄弟节点
+	if req.PrevClassifyId > 0 {
+		c, e := ctx.GetCommonClassifyById(req.PrevClassifyId)
+		if e != nil {
+			if e.Error() == utils.ErrNoRow() {
+				tips = "上一个分类不存在, 请刷新页面"
+				return
+			}
+			err = fmt.Errorf("获取上一个分类失败, %v", e)
+			return
+		}
+		prevClassify = c
+		sortPrev = prevClassify.Sort
+	} else if req.PrevObjectId > 0 {
+		obj, e := ctx.GetObjectById(req.PrevObjectId)
+		if e != nil {
+			if e.Error() == utils.ErrNoRow() {
+				tips = "上一个移动对象不存在, 请刷新页面"
+				return
+			}
+			err = fmt.Errorf("获取上一个移动对象失败, %v", e)
+			return
+		}
+		prevObject = obj
+		sortPrev = prevObject.Sort
+	}
+	if req.NextClassifyId > 0 {
+		c, e := ctx.GetCommonClassifyById(req.NextClassifyId)
+		if e != nil {
+			if e.Error() == utils.ErrNoRow() {
+				tips = "下一个分类不存在, 请刷新页面"
+				return
+			}
+			err = fmt.Errorf("获取下一个分类失败, %v", e)
+			return
+		}
+		nextClassify = c
+		sortNext = nextClassify.Sort
+	} else if req.NextObjectId > 0 {
+		obj, e := ctx.GetObjectById(req.NextObjectId)
+		if e != nil {
+			if e.Error() == utils.ErrNoRow() {
+				tips = "下一个移动对象不存在, 请刷新页面"
+				return
+			}
+			err = fmt.Errorf("获取下一个移动对象失败, %v", e)
+			return
+		}
+		nextObject = obj
+		sortNext = nextObject.Sort
+	}
+
+	// 移动分类
+	if req.ObjectId == 0 {
+		c, e := ctx.GetCommonClassifyById(req.ClassifyId)
+		if e != nil {
+			if e.Error() == utils.ErrNoRow() {
+				tips = "当前分类不存在, 请刷新页面"
+				return
+			}
+			err = fmt.Errorf("获取当前分类失败, %v", e)
+			return
+		}
+		classify = c
+		return moveCommonClassify(ctx, req, classify, parentClassify, prevClassify, nextClassify, prevObject, nextObject, sortPrev, sortNext)
+	}
+
+	// 移动对象
+	if req.ObjectId > 0 {
+		obj, e := ctx.GetObjectById(req.ObjectId)
+		if e != nil {
+			if e.Error() == utils.ErrNoRow() {
+				tips = "移动对象不存在, 请刷新页面"
+				return
+			}
+			err = fmt.Errorf("获取移动对象失败, %v", e)
+			return
+		}
+		if req.ParentClassifyId <= 0 {
+			tips = "移动对象必须挂在分类下"
+			return
+		}
+		object = obj
+
+		// TODO:对象的不同实现, 如指标校验权限
+
+		return moveCommonClassifyObj(ctx, req, prevClassify, nextClassify, object, prevObject, nextObject, sortPrev, sortNext)
+	}
+	return
+}
+
+func moveCommonClassify(ctx *CommonClassifyCtx, req models.CommonClassifyMoveReq, classify, parentClassify, prevClassify, nextClassify *models.CommonClassify, prevObject, nextObject *models.CommonClassifyObj, sortPrev, sortNext int) (tips string, err error) {
+	// 校验层级以及父级分类下同名分类
+	if req.ParentClassifyId > 0 && parentClassify.Level == 6 {
+		tips = "最高只支持添加6级分类"
+		return
+	}
+	exists, e := ctx.GetClassifyByParentIdAndName(req.ParentClassifyId, classify.ClassifyName, classify.ClassifyId)
+	if e != nil && e.Error() != utils.ErrNoRow() {
+		err = fmt.Errorf("获取父级分类下的同名分类失败, %v", e)
+		return
+	}
+	if exists != nil {
+		tips = "当前父级分类下存在相同名称"
+		return
+	}
+
+	// TODO:分类的非通用实现, 如指标分类需校验权限, 可以采用适配器模式兼容进来
+
+	var classifyChildIds []int
+	var classifyUpdateCols []string
+	originParentId := classify.ParentId
+	originLevel := classify.Level
+
+	// 需更新子层级分类ID
+	if originParentId != req.ParentClassifyId {
+		// TODO:此处如果要兼容以前的分类表, 那么要提出来处理
+		levelPathArr := strings.Split(classify.LevelPath, ",")
+		for _, p := range levelPathArr {
+			d, _ := strconv.Atoi(p)
+			if d > 0 {
+				classifyChildIds = append(classifyChildIds, d)
+			}
+		}
+	}
+
+	colsMapping := ctx.GetCommonClassifyCols() // 分类表实际的字段映射
+
+	// 判断上级ID是否一致, 不一致的话需要移动该分类层级
+	if classify.ParentId != req.ParentClassifyId && req.ParentClassifyId != 0 {
+		if classify.Level != parentClassify.Level+1 { //禁止层级调整
+			tips = "不支持目录层级变更"
+			return
+		}
+		classify.ParentId = parentClassify.ClassifyId
+		classify.RootId = parentClassify.RootId
+		classify.Level = parentClassify.Level + 1
+		classify.ModifyTime = time.Now()
+		classifyUpdateCols = append(classifyUpdateCols, colsMapping.ParentId, colsMapping.RootId, colsMapping.Level, colsMapping.ModifyTime)
+	}
+	if classify.ParentId != req.ParentClassifyId && req.ParentClassifyId == 0 {
+		tips = "不支持目录层级变更"
+		return
+	}
+
+	if sortPrev > 0 {
+		// 移动至两个兄弟之间
+		if sortNext > 0 {
+			// 如果上一个兄弟与下一个兄弟的排序权重是一致的, 那么需要将下一个兄弟(以及下个兄弟的同样排序权重)的排序权重+2, 自己变成上一个兄弟的排序权重+1
+			if sortPrev == sortNext || sortPrev == classify.Sort {
+				sortUpdate := `sort + 2`
+				if prevClassify != nil {
+					if e = ctx.SetClassifySortByParentId(req.ParentClassifyId, prevClassify.ClassifyId, prevClassify.Sort, sortUpdate); e != nil {
+						err = fmt.Errorf("SetClassifySortByParentId, parentId: %d, classifyId: %d, sort: %d, sortUpdate: %s, err: %v", req.ParentClassifyId, prevClassify.ClassifyId, prevClassify.Sort, sortUpdate, e)
+						return
+					}
+				} else {
+					if e = ctx.SetClassifySortByParentId(req.ParentClassifyId, 0, sortPrev, sortUpdate); e != nil {
+						err = fmt.Errorf("SetClassifySortByParentId, parentId: %d, classifyId: %d, sort: %d, sortUpdate: %s, err: %v", req.ParentClassifyId, 0, sortPrev, sortUpdate, e)
+						return
+					}
+				}
+
+				if prevObject != nil {
+					if e = ctx.SetObjectSortByClassifyId(req.ParentClassifyId, sortPrev, prevObject.ObjectId, sortUpdate); e != nil {
+						err = fmt.Errorf("SetObjectSortByClassifyId, classifyId: %d, sort: %d, objectId: %d, sortUpdate: %s, err: %v", req.ParentClassifyId, sortPrev, prevObject.ObjectId, sortUpdate, e)
+						return
+					}
+				} else {
+					if e = ctx.SetObjectSortByClassifyId(req.ParentClassifyId, sortPrev, 0, sortUpdate); e != nil {
+						err = fmt.Errorf("SetObjectSortByClassifyId, classifyId: %d, sort: %d, objectId: %d, sortUpdate: %s, err: %v", req.ParentClassifyId, sortPrev, 0, sortUpdate, e)
+						return
+					}
+				}
+			} else {
+				// 如果下一个兄弟的排序权重正好是上个兄弟节点的下一层, 那么需要再加一层
+				if sortNext-sortPrev == 1 {
+					sortUpdate := `sort + 1`
+					if prevClassify != nil {
+						if e = ctx.SetClassifySortByParentId(req.ParentClassifyId, prevClassify.ClassifyId, sortPrev, sortUpdate); e != nil {
+							err = fmt.Errorf("SetClassifySortByParentId, parentId: %d, classifyId: %d, sort: %d, sortUpdate: %s, err: %v", req.ParentClassifyId, prevClassify.ClassifyId, sortPrev, sortUpdate, e)
+							return
+						}
+					} else {
+						if e = ctx.SetClassifySortByParentId(req.ParentClassifyId, 0, sortPrev, sortUpdate); e != nil {
+							err = fmt.Errorf("SetClassifySortByParentId, parentId: %d, classifyId: %d, sort: %d, sortUpdate: %s, err: %v", req.ParentClassifyId, 0, sortPrev, sortUpdate, e)
+							return
+						}
+					}
+
+					if prevObject != nil {
+						if e = ctx.SetObjectSortByClassifyId(req.ParentClassifyId, sortPrev, prevObject.ObjectId, sortUpdate); e != nil {
+							err = fmt.Errorf("SetObjectSortByClassifyId, classifyId: %d, sort: %d, objectId: %d, sortUpdate: %s, err: %v", req.ParentClassifyId, sortPrev, prevObject.ObjectId, sortUpdate, e)
+							return
+						}
+					} else {
+						if e = ctx.SetObjectSortByClassifyId(req.ParentClassifyId, sortPrev, 0, sortUpdate); e != nil {
+							err = fmt.Errorf("SetObjectSortByClassifyId, classifyId: %d, sort: %d, objectId: %d, sortUpdate: %s, err: %v", req.ParentClassifyId, sortPrev, 0, sortUpdate, e)
+							return
+						}
+					}
+				}
+			}
+		}
+
+		classify.Sort = sortPrev + 1
+		classify.ModifyTime = time.Now()
+		classifyUpdateCols = append(classifyUpdateCols, colsMapping.Sort, colsMapping.ModifyTime)
+	} else if prevClassify == nil && nextClassify == nil && prevObject == nil && nextObject == nil && req.ParentClassifyId > 0 {
+		// 处理只拖动到目录里, 默认放到目录底部的情况
+		m, e := GetCommonClassifySortMaxByParentId(req.ParentClassifyId, ctx)
+		if e != nil {
+			err = fmt.Errorf("GetCommonClassifySortMaxByParentId, %v", e)
+			return
+		}
+		classify.Sort = m + 1
+		classify.ModifyTime = time.Now()
+		classifyUpdateCols = append(classifyUpdateCols, colsMapping.Sort, colsMapping.ModifyTime)
+	} else {
+		// 拖动到父级分类的第一位
+		firstClassify, e := ctx.GetFirstClassifyByParentId(req.ParentClassifyId)
+		if e != nil && e.Error() != utils.ErrNoRow() {
+			//tips = "移动失败"
+			err = fmt.Errorf("GetFirstClassifyByParentId, %v", e)
+			return
+		}
+
+		// 如果该分类下存在其他分类, 且第一个其他分类的排序等于0, 那么需要调整排序
+		if firstClassify != nil && firstClassify.Sort == 0 {
+			sortUpdate := ` sort + 1 `
+			if e = ctx.SetClassifySortByParentId(req.ParentClassifyId, firstClassify.ClassifyId-1, 0, sortUpdate); e != nil {
+				err = fmt.Errorf("SetClassifySortByParentId, parentId: %d, classifyId: %d, sort: %d, sortUpdate: %s, err: %v", req.ParentClassifyId, firstClassify.ClassifyId-1, 0, sortUpdate, e)
+				return
+			}
+			// 该分类下的所有指标也需要+1
+			if e = ctx.SetObjectSortByClassifyId(req.ParentClassifyId, 0, 0, sortUpdate); e != nil {
+				err = fmt.Errorf("SetObjectSortByClassifyId, classifyId: %d, sort: %d, objectId: %d, sortUpdate: %s, err: %v", req.ParentClassifyId, 0, 0, sortUpdate, e)
+				return
+			}
+		} else {
+			// 如果该分类下存在指标, 且第一个指标的排序等于0, 那么需要调整排序
+			firstObject, e := ctx.GetFirstObjectByClassifyId(req.ParentClassifyId)
+			if e != nil && e.Error() != utils.ErrNoRow() {
+				err = fmt.Errorf("GetFirstObjectByClassifyId, %v", e)
+				return
+			}
+
+			//如果该分类下存在其他分类,且第一个其他分类的排序等于0,那么需要调整排序
+			if firstObject != nil && firstObject.Sort == 0 {
+				sortUpdate := ` sort + 1 `
+				if e = ctx.SetObjectSortByClassifyId(req.ParentClassifyId, 0, firstObject.ObjectId-1, sortUpdate); e != nil {
+					err = fmt.Errorf("SetObjectSortByClassifyId, classifyId: %d, sort: %d, objectId: %d, sortUpdate: %s, err: %v", req.ParentClassifyId, 0, firstObject.ObjectId-1, sortUpdate, e)
+					return
+				}
+				if e = ctx.SetClassifySortByParentId(req.ParentClassifyId, 0, 0, sortUpdate); e != nil {
+					err = fmt.Errorf("SetClassifySortByParentId, parentId: %d, classifyId: %d, sort: %d, sortUpdate: %s, err: %v", req.ParentClassifyId, 0, 0, sortUpdate, e)
+					return
+				}
+			}
+		}
+
+		classify.Sort = 0 // 那就是排在第一位
+		classify.ModifyTime = time.Now()
+		classifyUpdateCols = append(classifyUpdateCols, colsMapping.Sort, colsMapping.ModifyTime)
+	}
+
+	// 更新分类
+	if len(classifyUpdateCols) > 0 {
+		if e = ctx.UpdateCommonClassify(classify, classifyUpdateCols); e != nil {
+			err = fmt.Errorf("UpdateCommonClassify, %v", e)
+			return
+		}
+
+		// 更新对应分类的root_id和层级
+		if originParentId != req.ParentClassifyId {
+			if len(classifyChildIds) > 0 {
+				stepLevel := classify.Level - originLevel
+				if e = ctx.UpdateClassifyChildByParentId(classifyChildIds, classify.RootId, stepLevel); e != nil {
+					err = fmt.Errorf("UpdateClassifyChildByParentId, parentId: %d, classifyId: %d, stepLevel: %d, err: %v", req.ParentClassifyId, classify.ClassifyId, stepLevel, e)
+					return
+				}
+			}
+		}
+	}
+	return
+}
+
+func moveCommonClassifyObj(ctx *CommonClassifyCtx, req models.CommonClassifyMoveReq, prevClassify, nextClassify *models.CommonClassify, object, prevObject, nextObject *models.CommonClassifyObj, sortPrev, sortNext int) (tips string, err error) {
+	var objUpdateCols []string
+	colsMapping := ctx.GetCommonClassifyObjCols() // 分类对象表实际的字段映射
+
+	// 分类变更的情况
+	if object.ClassifyId != req.ParentClassifyId {
+		object.ClassifyId = req.ParentClassifyId
+		object.ModifyTime = time.Now()
+		objUpdateCols = append(objUpdateCols, colsMapping.ClassifyId, colsMapping.ModifyTime)
+	}
+
+	if sortPrev > 0 {
+		// 移动至两个兄弟之间
+		if sortNext > 0 {
+			// 如果上一个兄弟与下一个兄弟的排序权重是一致的, 那么需要将下一个兄弟(以及下个兄弟的同样排序权重)的排序权重+2, 自己变成上一个兄弟的排序权重+1
+			if sortPrev == sortNext || sortPrev == object.Sort {
+				sortUpdate := `sort + 2`
+
+				if prevClassify != nil {
+					if e := ctx.SetClassifySortByParentId(req.ParentClassifyId, prevClassify.ClassifyId, prevClassify.Sort, sortUpdate); e != nil {
+						err = fmt.Errorf("SetClassifySortByParentId, parentId: %d, classifyId: %d, sort: %d, sortUpdate: %s, err: %v", req.ParentClassifyId, prevClassify.ClassifyId, prevClassify.Sort, sortUpdate, e)
+						return
+					}
+				} else {
+					if e := ctx.SetClassifySortByParentId(req.ParentClassifyId, 0, sortPrev, sortUpdate); e != nil {
+						err = fmt.Errorf("SetClassifySortByParentId, parentId: %d, classifyId: %d, sort: %d, sortUpdate: %s, err: %v", req.ParentClassifyId, 0, sortPrev, sortUpdate, e)
+						return
+					}
+				}
+
+				if prevObject != nil {
+					if e := ctx.SetObjectSortByClassifyId(req.ParentClassifyId, sortPrev, prevObject.ObjectId, sortUpdate); e != nil {
+						err = fmt.Errorf("SetObjectSortByClassifyId, classifyId: %d, sort: %d, objectId: %d, sortUpdate: %s, err: %v", req.ParentClassifyId, sortPrev, prevObject.ObjectId, sortUpdate, e)
+						return
+					}
+				} else {
+					if e := ctx.SetObjectSortByClassifyId(req.ParentClassifyId, sortPrev, 0, sortUpdate); e != nil {
+						err = fmt.Errorf("SetObjectSortByClassifyId, classifyId: %d, sort: %d, objectId: %d, sortUpdate: %s, err: %v", req.ParentClassifyId, sortPrev, 0, sortUpdate, e)
+						return
+					}
+				}
+			} else {
+				// 如果下一个兄弟的排序权重正好是上个兄弟节点的下一层, 那么需要再加一层
+				if sortNext-sortPrev == 1 {
+					sortUpdate := `sort + 1`
+
+					if prevClassify != nil {
+						if e := ctx.SetClassifySortByParentId(req.ParentClassifyId, prevClassify.ClassifyId, sortPrev, sortUpdate); e != nil {
+							err = fmt.Errorf("SetClassifySortByParentId, parentId: %d, classifyId: %d, sort: %d, sortUpdate: %s, err: %v", req.ParentClassifyId, prevClassify.ClassifyId, sortPrev, sortUpdate, e)
+							return
+						}
+					} else {
+						if e := ctx.SetClassifySortByParentId(req.ParentClassifyId, 0, sortPrev, sortUpdate); e != nil {
+							err = fmt.Errorf("SetClassifySortByParentId, parentId: %d, classifyId: %d, sort: %d, sortUpdate: %s, err: %v", req.ParentClassifyId, 0, sortPrev, sortUpdate, e)
+							return
+						}
+					}
+
+					if prevObject != nil {
+						if e := ctx.SetObjectSortByClassifyId(req.ParentClassifyId, sortPrev, prevObject.ObjectId, sortUpdate); e != nil {
+							err = fmt.Errorf("SetObjectSortByClassifyId, classifyId: %d, sort: %d, objectId: %d, sortUpdate: %s, err: %v", req.ParentClassifyId, sortPrev, prevObject.ObjectId, sortUpdate, e)
+							return
+						}
+					} else {
+						if e := ctx.SetObjectSortByClassifyId(req.ParentClassifyId, sortPrev, 0, sortUpdate); e != nil {
+							err = fmt.Errorf("SetObjectSortByClassifyId, classifyId: %d, sort: %d, objectId: %d, sortUpdate: %s, err: %v", req.ParentClassifyId, sortPrev, 0, sortUpdate, e)
+							return
+						}
+					}
+				}
+			}
+		}
+
+		object.Sort = sortPrev + 1
+		object.ModifyTime = time.Now()
+		objUpdateCols = append(objUpdateCols, colsMapping.Sort, colsMapping.ModifyTime)
+	} else if prevClassify == nil && nextClassify == nil && prevObject == nil && nextObject == nil && req.ParentClassifyId > 0 {
+		// 处理只拖动到目录里, 默认放到目录底部的情况
+		m, e := GetCommonClassifySortMaxByParentId(req.ParentClassifyId, ctx)
+		if e != nil {
+			err = fmt.Errorf("GetCommonClassifySortMaxByParentId, %v", e)
+			return
+		}
+		object.Sort = m + 1 //那就是排在组内最后一位
+		object.ModifyTime = time.Now()
+		objUpdateCols = append(objUpdateCols, colsMapping.Sort, colsMapping.ModifyTime)
+	} else {
+		// 拖动到父级分类的第一位
+		firstClassify, e := ctx.GetFirstClassifyByParentId(req.ParentClassifyId)
+		if e != nil && e.Error() != utils.ErrNoRow() {
+			err = fmt.Errorf("GetFirstClassifyByParentId, %v", e)
+			return
+		}
+
+		// 如果该分类下存在其他分类, 且第一个其他分类的排序等于0, 那么需要调整排序
+		if firstClassify != nil && firstClassify.Sort == 0 {
+			sortUpdate := ` sort + 1 `
+			if e = ctx.SetClassifySortByParentId(req.ParentClassifyId, firstClassify.ClassifyId-1, 0, sortUpdate); e != nil {
+				err = fmt.Errorf("SetClassifySortByParentId, parentId: %d, classifyId: %d, sort: %d, sortUpdate: %s, err: %v", req.ParentClassifyId, firstClassify.ClassifyId-1, 0, sortUpdate, e)
+				return
+			}
+			// 该分类下的所有指标也需要+1
+			if e = ctx.SetObjectSortByClassifyId(req.ParentClassifyId, 0, 0, sortUpdate); e != nil {
+				err = fmt.Errorf("SetObjectSortByClassifyId, classifyId: %d, sort: %d, objectId: %d, sortUpdate: %s, err: %v", req.ParentClassifyId, 0, 0, sortUpdate, e)
+				return
+			}
+		} else {
+			// 如果该分类下存在对象, 且第一个对象的排序等于0, 那么需要调整排序
+			firstObject, e := ctx.GetFirstObjectByClassifyId(req.ParentClassifyId)
+			if e != nil && e.Error() != utils.ErrNoRow() {
+				err = fmt.Errorf("GetFirstObjectByClassifyId, %v", e)
+				return
+			}
+
+			// 如果该分类下存在其他分类, 且第一个其他分类的排序等于0, 那么需要调整排序
+			if firstObject != nil && firstObject.Sort == 0 {
+				sortUpdate := ` sort + 1 `
+				if e = ctx.SetObjectSortByClassifyId(req.ParentClassifyId, 0, firstObject.ObjectId-1, sortUpdate); e != nil {
+					err = fmt.Errorf("SetObjectSortByClassifyId, classifyId: %d, sort: %d, objectId: %d, sortUpdate: %s, err: %v", req.ParentClassifyId, 0, firstObject.ObjectId-1, sortUpdate, e)
+					return
+				}
+				if e = ctx.SetClassifySortByParentId(req.ParentClassifyId, 0, 0, sortUpdate); e != nil {
+					err = fmt.Errorf("SetClassifySortByParentId, parentId: %d, classifyId: %d, sort: %d, sortUpdate: %s, err: %v", req.ParentClassifyId, 0, 0, sortUpdate, e)
+					return
+				}
+			}
+		}
+
+		object.Sort = 0 // 那就是排在第一位
+		object.ModifyTime = time.Now()
+		objUpdateCols = append(objUpdateCols, colsMapping.Sort, colsMapping.ModifyTime)
+	}
+
+	// 更新分类对象
+	if len(objUpdateCols) > 0 {
+		if e := ctx.UpdateCommonClassifyObj(object, objUpdateCols); e != nil {
+			err = fmt.Errorf("UpdateCommonClassifyObj, %v", e)
+			return
+		}
+	}
+	return
+}
+
+// GetCommonClassifySortMaxByParentId 获取分类下最大排序
+func GetCommonClassifySortMaxByParentId(parentId int, ctx *CommonClassifyCtx) (sortMax int, err error) {
+	// 比对分类和对象的最大排序
+	classifyMax, e := ctx.GetClassifySortMaxByParentId(parentId)
+	if e != nil {
+		err = fmt.Errorf("GetClassifySortMaxByParentId, %v", e)
+		return
+	}
+	sortMax = classifyMax
+	objectMax, e := ctx.GetObjectSortMaxByClassifyId(parentId)
+	if e != nil {
+		err = fmt.Errorf("GetObjectSortMaxByClassifyId, %v", e)
+		return
+	}
+	if sortMax < objectMax {
+		sortMax = objectMax
+	}
+	return
+}

+ 106 - 0
services/data/common_classify_ctx.go

@@ -0,0 +1,106 @@
+package data
+
+import (
+	"eta/eta_api/models"
+)
+
+// CommonClassifyStrategy 通用分类策略接口
+type CommonClassifyStrategy interface {
+	GetCommonClassifyCols() models.CommonClassifyCols
+	GetCommonClassifyById(classifyId int) (*models.CommonClassify, error)
+	GetClassifyByParentIdAndName(parentId int, name string, excludeId int) (*models.CommonClassify, error)
+	GetClassifySortMaxByParentId(parentId int) (int, error)
+	GetFirstClassifyByParentId(parentId int) (*models.CommonClassify, error)
+
+	SetClassifySortByParentId(parentId, classifyId, sort int, sortUpdate string) error
+	UpdateCommonClassify(classify *models.CommonClassify, updateCols []string) error
+	UpdateClassifyChildByParentId(classifyIds []int, rootId int, stepLevel int) error
+
+	GetCommonClassifyObjCols() models.CommonClassifyObjCols
+	GetObjectById(objectId int) (*models.CommonClassifyObj, error)
+	GetObjectSortMaxByClassifyId(classifyId int) (int, error)
+	GetFirstObjectByClassifyId(classifyId int) (*models.CommonClassifyObj, error)
+
+	SetObjectSortByClassifyId(classifyId, sort, prevObjectId int, sortUpdate string) error
+	UpdateCommonClassifyObj(object *models.CommonClassifyObj, updateCols []string) (err error)
+}
+
+// CommonClassifyCtx 通用分类上下文
+type CommonClassifyCtx struct {
+	strategy CommonClassifyStrategy
+}
+
+// NewCommonClassifyCtx New一个通用分类上下文
+func NewCommonClassifyCtx(strategy CommonClassifyStrategy) *CommonClassifyCtx {
+	return &CommonClassifyCtx{strategy: strategy}
+}
+
+// GetCommonClassifyCols 通用分类字段映射
+func (c *CommonClassifyCtx) GetCommonClassifyCols() models.CommonClassifyCols {
+	return c.strategy.GetCommonClassifyCols()
+}
+
+// GetCommonClassifyById 通过策略获取分类信息
+func (c *CommonClassifyCtx) GetCommonClassifyById(classifyId int) (*models.CommonClassify, error) {
+	return c.strategy.GetCommonClassifyById(classifyId)
+}
+
+// GetClassifyByParentIdAndName 通过策略获取分类信息
+func (c *CommonClassifyCtx) GetClassifyByParentIdAndName(parentId int, name string, excludeId int) (*models.CommonClassify, error) {
+	return c.strategy.GetClassifyByParentIdAndName(parentId, name, excludeId)
+}
+
+// GetClassifySortMaxByParentId 获取父级分类下最大排序
+func (c *CommonClassifyCtx) GetClassifySortMaxByParentId(parentId int) (int, error) {
+	return c.strategy.GetClassifySortMaxByParentId(parentId)
+}
+
+// GetFirstClassifyByParentId 获取父级分类下首个分类
+func (c *CommonClassifyCtx) GetFirstClassifyByParentId(parentId int) (*models.CommonClassify, error) {
+	return c.strategy.GetFirstClassifyByParentId(parentId)
+}
+
+// SetClassifySortByParentId 通过父级ID更新分类排序
+func (c *CommonClassifyCtx) SetClassifySortByParentId(parentId, classifyId, sort int, sortUpdate string) error {
+	return c.strategy.SetClassifySortByParentId(parentId, classifyId, sort, sortUpdate)
+}
+
+// UpdateCommonClassify 更新通用分类
+func (c *CommonClassifyCtx) UpdateCommonClassify(classify *models.CommonClassify, updateCols []string) error {
+	return c.strategy.UpdateCommonClassify(classify, updateCols)
+}
+
+// UpdateClassifyChildByParentId 更新分类子节点RootId
+func (c *CommonClassifyCtx) UpdateClassifyChildByParentId(classifyIds []int, rootId int, stepLevel int) error {
+	return c.strategy.UpdateClassifyChildByParentId(classifyIds, rootId, stepLevel)
+}
+
+// GetCommonClassifyObjCols 通用分类对象字段映射
+func (c *CommonClassifyCtx) GetCommonClassifyObjCols() models.CommonClassifyObjCols {
+	return c.strategy.GetCommonClassifyObjCols()
+}
+
+// GetObjectById 获取分类对象
+func (c *CommonClassifyCtx) GetObjectById(objectId int) (*models.CommonClassifyObj, error) {
+	return c.strategy.GetObjectById(objectId)
+}
+
+// GetObjectSortMaxByClassifyId 获取分类下最大排序
+func (c *CommonClassifyCtx) GetObjectSortMaxByClassifyId(classifyId int) (int, error) {
+	return c.strategy.GetObjectSortMaxByClassifyId(classifyId)
+}
+
+// GetFirstObjectByClassifyId 获取分类下首个对象
+func (c *CommonClassifyCtx) GetFirstObjectByClassifyId(classifyId int) (*models.CommonClassifyObj, error) {
+	return c.strategy.GetFirstObjectByClassifyId(classifyId)
+}
+
+// SetObjectSortByClassifyId 通过分类ID更新对象排序
+func (c *CommonClassifyCtx) SetObjectSortByClassifyId(classifyId, sort, prevObjectId int, sortUpdate string) error {
+	return c.strategy.SetObjectSortByClassifyId(classifyId, sort, prevObjectId, sortUpdate)
+}
+
+// UpdateCommonClassifyObj 更新分类对象
+func (c *CommonClassifyCtx) UpdateCommonClassifyObj(object *models.CommonClassifyObj, updateCols []string) (err error) {
+	return c.strategy.UpdateCommonClassifyObj(object, updateCols)
+}

+ 1 - 0
services/data/edb_info.go

@@ -1834,6 +1834,7 @@ func EdbInfoAdd(source, subSource, classifyId int, edbCode, edbName, frequency,
 		utils.DATA_SOURCE_GFEX:                "广期所",
 		utils.DATA_SOURCE_SCI_HQ:              "卓创红期",
 		utils.DATA_SOURCE_LY:                  "粮油商务网",
+		utils.DATA_SOURCE_TRADE_ANALYSIS:      "持仓分析",
 	}
 
 	sourceName, ok := sourceNameMap[source]

+ 13 - 6
services/data/trade_analysis/trade_analysis.go

@@ -75,11 +75,11 @@ func GetClassifyName(lang string) (list trade_analysis.TradeClassifyNameListSort
 	currDate := time.Now().Format(utils.FormatDate)
 	for k, v := range exchanges {
 		tmp := trade_analysis.TradeClassifyNameList{
-			Exchange: v,
+			Exchange:   v,
 			ExchangeEn: exchangesEn[v],
-			Items:    nil,
-			Sort:     exchangesSortMap[k],
-			CurrDate: currDate,
+			Items:      nil,
+			Sort:       exchangesSortMap[k],
+			CurrDate:   currDate,
 		}
 		nameList, ok := classifyExchangeMap[k]
 		if !ok {
@@ -101,7 +101,7 @@ func GetClassifyName(lang string) (list trade_analysis.TradeClassifyNameListSort
 		if len(nameList) > 0 {
 			if k == "zhengzhou" {
 				for _, item := range nameList {
-					classifyName := getZhengzhouClassifyName(item.ClassifyName)
+					classifyName := GetZhengzhouClassifyName(item.ClassifyName)
 					tmpItemItem := trade_analysis.TradeClassifyNameListItemItem{
 						ClassifyType: item.ClassifyName,
 					}
@@ -135,7 +135,7 @@ func GetClassifyName(lang string) (list trade_analysis.TradeClassifyNameListSort
 	return
 }
 
-func getZhengzhouClassifyName(code string) (name string) {
+func GetZhengzhouClassifyName(code string) (name string) {
 	if strings.HasPrefix(code, "PTA") {
 		name = "PTA"
 		return
@@ -240,6 +240,13 @@ func getZhengzhouClassifyName(code string) (name string) {
 		name = "烧碱"
 		return
 	}
+	if strings.HasPrefix(code, "PR") {
+		name = "瓶片"
+		return
+	}
+	if name == "" {
+		utils.FileLog.Info(fmt.Sprintf("郑商所-合约暂未归类: %s", code))
+	}
 	return
 }
 

+ 647 - 0
services/data/trade_analysis/trade_analysis_data.go

@@ -0,0 +1,647 @@
+package trade_analysis
+
+import (
+	"eta/eta_api/models/data_manage"
+	tradeAnalysisModel "eta/eta_api/models/data_manage/trade_analysis"
+	"eta/eta_api/services/data"
+	"eta/eta_api/utils"
+	"fmt"
+	"sort"
+	"strings"
+	"time"
+)
+
+// FormatCompanyTradeData2EdbMappings [公司-合约加总]转为指标数据
+func FormatCompanyTradeData2EdbMappings(companyTradeData []*tradeAnalysisModel.ContractCompanyTradeData, tradeType, dateType, dateTypeNum int, startDate, endDate string, chartEdbList []*data_manage.ChartSaveItem) (edbMappings []*data_manage.ChartEdbInfoMapping, chartName string, err error) {
+	edbMappings = make([]*data_manage.ChartEdbInfoMapping, 0)
+	if dateType <= 0 {
+		dateType = utils.DateTypeOneMonth
+	}
+
+	// 期货公司名称作为标识进行匹配
+	edbMap := make(map[string]*data_manage.ChartSaveItem)
+	if len(chartEdbList) > 0 {
+		for _, v := range chartEdbList {
+			edbMap[v.UniqueFlag] = v
+		}
+	}
+
+	for k, v := range companyTradeData {
+		mapping := new(data_manage.ChartEdbInfoMapping)
+		mapping.EdbName = v.CompanyName
+		mapping.EdbNameEn = v.CompanyName
+		mapping.EdbAliasName = v.CompanyName
+		mapping.EdbAliasNameEn = v.CompanyName
+		mapping.Frequency = "日度"
+		mapping.FrequencyEn = data.GetFrequencyEn(mapping.Frequency)
+		mapping.SourceName = utils.SourceNameTradeAnalysis
+		mapping.Source = utils.CHART_SOURCE_TRADE_ANALYSIS_PROCESS
+		mapping.IsAxis = 1
+		mapping.EdbInfoType = 1
+		mapping.StartDate = v.StartDate.Format(utils.FormatDate)
+		mapping.EndDate = v.EndDate.Format(utils.FormatDate)
+		mapping.ConvertUnit = tradeAnalysisModel.WarehouseDefaultUnit // 固定单位
+		mapping.UniqueFlag = v.CompanyName                            // 期货公司名称作为每条曲线的唯一标识
+
+		// 有配置那么取配置中的图例名称和左右轴
+		edbConf := edbMap[mapping.UniqueFlag]
+		if edbConf != nil {
+			mapping.EdbName = edbConf.EdbAliasName
+			mapping.EdbNameEn = edbConf.EdbAliasName
+			mapping.EdbAliasName = edbConf.EdbAliasName
+			mapping.EdbAliasNameEn = edbConf.EdbAliasName
+			mapping.IsAxis = edbConf.IsAxis
+		}
+
+		// 根据参数取日期范围
+		var startTime, endTime time.Time
+		if dateType > 0 {
+			st, ed := utils.GetDateByDateTypeV2(dateType, startDate, endDate, dateTypeNum, 0)
+			if st != "" {
+				startTime, _ = time.ParseInLocation(utils.FormatDate, st, time.Local)
+			}
+			if startTime.IsZero() {
+				startTime = v.StartDate
+			}
+			if ed != "" {
+				endTime, _ = time.ParseInLocation(utils.FormatDate, ed, time.Local)
+			}
+			if endTime.IsZero() {
+				endTime = v.EndDate
+			}
+		}
+
+		// 指标数据和最值
+		edbData := make([]*data_manage.EdbDataList, 0)
+		var minData, maxData float64
+		var setMinMax bool
+		for _, dv := range v.DataList {
+			if dv.Date.Before(startTime) || dv.Date.After(endTime) {
+				continue
+			}
+
+			// 交易方向
+			var (
+				val    float64
+				hasVal bool
+			)
+			if tradeType == tradeAnalysisModel.WarehouseBuyChartType {
+				if dv.BuyValType == tradeAnalysisModel.TradeDataTypeNull {
+					continue
+				}
+				hasVal = true
+				val = float64(dv.BuyVal)
+			}
+			if tradeType == tradeAnalysisModel.WarehouseSoldChartType {
+				if dv.SoldValType == tradeAnalysisModel.TradeDataTypeNull {
+					continue
+				}
+				hasVal = true
+				val = float64(dv.SoldVal)
+			}
+			if tradeType == tradeAnalysisModel.WarehousePureBuyChartType {
+				if dv.PureBuyValType == tradeAnalysisModel.TradeDataTypeNull {
+					continue
+				}
+				hasVal = true
+				val = float64(dv.PureBuyVal)
+			}
+			if !hasVal {
+				continue
+			}
+
+			if !setMinMax {
+				minData = val
+				maxData = val
+				setMinMax = true
+			}
+			if val < minData {
+				minData = val
+			}
+			if val > maxData {
+				maxData = val
+			}
+			edbData = append(edbData, &data_manage.EdbDataList{
+				DataTime:      dv.Date.Format(utils.FormatDate),
+				DataTimestamp: dv.Date.UnixNano() / 1e6,
+				Value:         val,
+			})
+		}
+		mapping.MinData = minData
+		mapping.MaxData = maxData
+		mapping.DataList = edbData
+		edbMappings = append(edbMappings, mapping)
+
+		// 图表默认名称
+		if k == 0 {
+			chartName += strings.ReplaceAll(v.ClassifyType, ",", "")
+		}
+		chartName += v.CompanyName
+	}
+
+	// 图表名称后缀
+	chartName += tradeAnalysisModel.WarehouseTypeSuffixNames[tradeType]
+	return
+}
+
+func GetOriginTradeData(exchange, classifyName string, contracts, companies []string, predictRatio float64) (companyTradeData []*tradeAnalysisModel.ContractCompanyTradeData, err error) {
+	// 各原始数据表期货公司名称不一致
+	companyMap := make(map[string]string)
+	{
+		ob := new(tradeAnalysisModel.TradeFuturesCompany)
+		list, e := ob.GetItemsByCondition(``, make([]interface{}, 0), []string{}, "")
+		if e != nil {
+			err = fmt.Errorf("获取期货公司名称失败: %v", e)
+			return
+		}
+		switch exchange {
+		case "zhengzhou":
+			for _, v := range list {
+				companyMap[v.CompanyName] = v.ZhengzhouName
+			}
+		case "dalian":
+			for _, v := range list {
+				companyMap[v.CompanyName] = v.DalianName
+			}
+		case "shanghai":
+			for _, v := range list {
+				companyMap[v.CompanyName] = v.ShanghaiName
+			}
+		case "cffex":
+			for _, v := range list {
+				companyMap[v.CompanyName] = v.CffexName
+			}
+		case "ine":
+			for _, v := range list {
+				companyMap[v.CompanyName] = v.IneName
+			}
+		case "guangzhou":
+			for _, v := range list {
+				companyMap[v.CompanyName] = v.GuangzhouName
+			}
+		}
+	}
+	var queryCompanies []string
+	for _, v := range companies {
+		// TOP20用空名称去查询
+		if v == tradeAnalysisModel.TradeFuturesCompanyTop20 {
+			queryCompanies = append(queryCompanies, "")
+			continue
+		}
+		companyName, ok := companyMap[v]
+		if !ok {
+			utils.FileLog.Info(fmt.Sprintf("交易所%s公司名称映射不存在: %s", exchange, v))
+			continue
+		}
+		queryCompanies = append(queryCompanies, companyName)
+	}
+
+	// 郑商所/广期所查询方式不一样
+	var tradeAnalysis TradeAnalysisInterface
+	switch exchange {
+	case tradeAnalysisModel.TradeExchangeZhengzhou:
+		tradeAnalysis = &ZhengzhouTradeAnalysis{}
+	case tradeAnalysisModel.TradeExchangeGuangzhou:
+		tradeAnalysis = &GuangzhouTradeAnalysis{}
+	default:
+		tradeAnalysis = &BaseTradeAnalysis{}
+	}
+
+	// 获取多单/空单原始数据
+	originList, e := tradeAnalysis.GetTradeDataByClassifyAndCompany(exchange, classifyName, contracts, queryCompanies)
+	if e != nil {
+		err = fmt.Errorf("获取多空单原始数据失败, %v", e)
+		return
+	}
+
+	keyItems := make(map[string]*tradeAnalysisModel.ContractCompanyTradeData)
+	keyDateData := make(map[string]*tradeAnalysisModel.ContractCompanyTradeDataList)
+	keyDateDataExist := make(map[string]bool)
+	for _, v := range originList {
+		// TOP20对应数据库中的空名称
+		companyName := v.CompanyName
+		if companyName == "" {
+			companyName = tradeAnalysisModel.TradeFuturesCompanyTop20
+		}
+
+		k := fmt.Sprintf("%s-%s", v.ClassifyType, companyName)
+		if keyItems[k] == nil {
+			keyItems[k] = new(tradeAnalysisModel.ContractCompanyTradeData)
+			keyItems[k].CompanyName = companyName
+			keyItems[k].ClassifyType = v.ClassifyType
+			keyItems[k].DataList = make([]*tradeAnalysisModel.ContractCompanyTradeDataList, 0)
+		}
+
+		kd := fmt.Sprintf("%s-%s", k, v.DataTime.Format(utils.FormatDate))
+		if keyDateData[kd] == nil {
+			keyDateData[kd] = new(tradeAnalysisModel.ContractCompanyTradeDataList)
+			keyDateData[kd].Date = v.DataTime
+		}
+		if v.ValType == 1 {
+			keyDateData[kd].BuyVal = v.Val
+			keyDateData[kd].BuyValType = tradeAnalysisModel.TradeDataTypeOrigin
+			keyDateData[kd].BuyChange = v.ValChange
+			keyDateData[kd].BuyChangeType = tradeAnalysisModel.TradeDataTypeOrigin
+		}
+		if v.ValType == 2 {
+			keyDateData[kd].SoldVal = v.Val
+			keyDateData[kd].SoldValType = tradeAnalysisModel.TradeDataTypeOrigin
+			keyDateData[kd].SoldChange = v.ValChange
+			keyDateData[kd].SoldChangeType = tradeAnalysisModel.TradeDataTypeOrigin
+		}
+		if !keyDateDataExist[kd] {
+			keyItems[k].DataList = append(keyItems[k].DataList, keyDateData[kd])
+			keyDateDataExist[kd] = true
+		}
+	}
+
+	// 获取[合约]每日的末位多空单
+	contractLastBuyDateVal := make(map[string]map[time.Time]int)
+	contractLastSoldDateVal := make(map[string]map[time.Time]int)
+	{
+		lastOriginList, e := tradeAnalysis.GetLastTradeDataByClassify(exchange, classifyName, contracts)
+		if e != nil {
+			err = fmt.Errorf("获取末位多空单原始数据失败, %v", e)
+			return
+		}
+		for _, v := range lastOriginList {
+			if v.ValType == 1 {
+				if contractLastBuyDateVal[v.ClassifyType] == nil {
+					contractLastBuyDateVal[v.ClassifyType] = make(map[time.Time]int)
+				}
+				contractLastBuyDateVal[v.ClassifyType][v.DataTime] = v.Val
+				continue
+			}
+			if contractLastSoldDateVal[v.ClassifyType] == nil {
+				contractLastSoldDateVal[v.ClassifyType] = make(map[time.Time]int)
+			}
+			contractLastSoldDateVal[v.ClassifyType][v.DataTime] = v.Val
+		}
+	}
+
+	// 填充[合约-公司]预估数据, 并根据[公司-多合约]分组, [公司]算作一个指标, 指标值为[多个合约]的计算加总
+	companyContracts := make(map[string][]*tradeAnalysisModel.ContractCompanyTradeData)
+	for _, v := range keyItems {
+		td, fd, ed, e := PredictingTradeData(v.DataList, contractLastBuyDateVal[v.ClassifyType], contractLastSoldDateVal[v.ClassifyType], predictRatio)
+		if e != nil {
+			err = fmt.Errorf("数据补全失败, %v", e)
+			return
+		}
+		v.DataList = td
+		v.StartDate = fd
+		v.EndDate = ed
+
+		if companyContracts[v.CompanyName] == nil {
+			companyContracts[v.CompanyName] = make([]*tradeAnalysisModel.ContractCompanyTradeData, 0)
+		}
+		companyContracts[v.CompanyName] = append(companyContracts[v.CompanyName], v)
+	}
+
+	// 以[公司]为组, 计算合约加总
+	mussyTradeData := make(map[string]*tradeAnalysisModel.ContractCompanyTradeData)
+	for k, v := range companyContracts {
+		companyData := new(tradeAnalysisModel.ContractCompanyTradeData)
+		companyData.CompanyName = k
+		companyData.DataList = make([]*tradeAnalysisModel.ContractCompanyTradeDataList, 0)
+		contractArr := make([]string, 0)
+
+		// 合约加总
+		sumDateData := make(map[time.Time]*tradeAnalysisModel.ContractCompanyTradeDataList)
+		for _, vv := range v {
+			contractArr = append(contractArr, vv.ClassifyType)
+			for _, dv := range vv.DataList {
+				if sumDateData[dv.Date] == nil {
+					sumDateData[dv.Date] = new(tradeAnalysisModel.ContractCompanyTradeDataList)
+					sumDateData[dv.Date].Date = dv.Date
+				}
+				// 数据类型以第一个非零值为准, 只处理多空和净多, 变化就不管了
+				if sumDateData[dv.Date].BuyValType == tradeAnalysisModel.TradeDataTypeNull && dv.BuyValType > tradeAnalysisModel.TradeDataTypeNull {
+					sumDateData[dv.Date].BuyValType = dv.BuyValType
+				}
+				if sumDateData[dv.Date].BuyValType == tradeAnalysisModel.TradeDataTypeOrigin && dv.BuyValType == tradeAnalysisModel.TradeDataTypeCalculate {
+					sumDateData[dv.Date].BuyValType = dv.BuyValType
+				}
+				if dv.BuyValType > tradeAnalysisModel.TradeDataTypeNull {
+					sumDateData[dv.Date].BuyVal += dv.BuyVal
+				}
+				// 空单
+				if sumDateData[dv.Date].SoldValType == tradeAnalysisModel.TradeDataTypeNull && dv.SoldValType > tradeAnalysisModel.TradeDataTypeNull {
+					sumDateData[dv.Date].SoldValType = dv.SoldValType
+				}
+				if sumDateData[dv.Date].SoldValType == tradeAnalysisModel.TradeDataTypeOrigin && dv.SoldValType == tradeAnalysisModel.TradeDataTypeCalculate {
+					sumDateData[dv.Date].SoldValType = dv.SoldValType
+				}
+				if dv.SoldValType > tradeAnalysisModel.TradeDataTypeNull {
+					sumDateData[dv.Date].SoldVal += dv.SoldVal
+				}
+				// 净多单
+				if sumDateData[dv.Date].PureBuyValType == tradeAnalysisModel.TradeDataTypeNull && dv.PureBuyValType > tradeAnalysisModel.TradeDataTypeNull {
+					sumDateData[dv.Date].PureBuyValType = dv.PureBuyValType
+				}
+				if sumDateData[dv.Date].PureBuyValType == tradeAnalysisModel.TradeDataTypeOrigin && dv.PureBuyValType == tradeAnalysisModel.TradeDataTypeCalculate {
+					sumDateData[dv.Date].PureBuyValType = dv.PureBuyValType
+				}
+				if dv.PureBuyValType > tradeAnalysisModel.TradeDataTypeNull {
+					sumDateData[dv.Date].PureBuyVal += dv.PureBuyVal
+				}
+			}
+
+			// 多个合约比对开始结束时间
+			if companyData.StartDate.IsZero() {
+				companyData.StartDate = vv.StartDate
+			}
+			if vv.StartDate.Before(companyData.StartDate) {
+				companyData.StartDate = vv.StartDate
+			}
+			if companyData.EndDate.IsZero() {
+				companyData.EndDate = vv.EndDate
+			}
+			if vv.EndDate.Before(companyData.EndDate) {
+				companyData.EndDate = vv.EndDate
+			}
+		}
+		for _, sv := range sumDateData {
+			companyData.DataList = append(companyData.DataList, sv)
+		}
+		sort.Slice(companyData.DataList, func(i, j int) bool {
+			return companyData.DataList[i].Date.Before(companyData.DataList[j].Date)
+		})
+		companyData.ClassifyType = strings.Join(contractArr, ",")
+		mussyTradeData[k] = companyData
+	}
+
+	// 数据根据公司排序, 不然会随机乱
+	companyTradeData = make([]*tradeAnalysisModel.ContractCompanyTradeData, 0)
+	for _, v := range companies {
+		// 没数据也需要加进去, 不然edbList会少
+		if mussyTradeData[v] == nil {
+			companyData := new(tradeAnalysisModel.ContractCompanyTradeData)
+			companyData.CompanyName = v
+			companyData.DataList = make([]*tradeAnalysisModel.ContractCompanyTradeDataList, 0)
+			companyTradeData = append(companyTradeData, companyData)
+			continue
+		}
+		companyTradeData = append(companyTradeData, mussyTradeData[v])
+	}
+	return
+}
+
+// PredictingTradeData 根据数据库中的多空数据填充预估数据
+func PredictingTradeData(originData []*tradeAnalysisModel.ContractCompanyTradeDataList, lastBuyDateVal, lastSoldDateVal map[time.Time]int, predictRatio float64) (newData []*tradeAnalysisModel.ContractCompanyTradeDataList, firstDate, endDate time.Time, err error) {
+	// 测试用的验证数据
+	//lastBuyDateVal, lastSoldDateVal = make(map[time.Time]int), make(map[time.Time]int)
+	//lastBuyDateVal[time.Date(2024, 7, 16, 0, 0, 0, 0, time.Local)] = 4602
+	//lastBuyDateVal[time.Date(2024, 7, 17, 0, 0, 0, 0, time.Local)] = 5116
+	//lastBuyDateVal[time.Date(2024, 7, 18, 0, 0, 0, 0, time.Local)] = 5130
+	//lastBuyDateVal[time.Date(2024, 7, 19, 0, 0, 0, 0, time.Local)] = 5354
+	//lastBuyDateVal[time.Date(2024, 7, 22, 0, 0, 0, 0, time.Local)] = 5916
+	//lastBuyDateVal[time.Date(2024, 7, 23, 0, 0, 0, 0, time.Local)] = 6524
+	//lastBuyDateVal[time.Date(2024, 7, 26, 0, 0, 0, 0, time.Local)] = 6575
+	//lastBuyDateVal[time.Date(2024, 7, 29, 0, 0, 0, 0, time.Local)] = 7461
+	//lastBuyDateVal[time.Date(2024, 7, 30, 0, 0, 0, 0, time.Local)] = 8488
+	//
+	//lastSoldDateVal[time.Date(2024, 7, 11, 0, 0, 0, 0, time.Local)] = 5467
+	//lastSoldDateVal[time.Date(2024, 7, 12, 0, 0, 0, 0, time.Local)] = 5248
+	//lastSoldDateVal[time.Date(2024, 7, 15, 0, 0, 0, 0, time.Local)] = 5102
+	//lastSoldDateVal[time.Date(2024, 7, 16, 0, 0, 0, 0, time.Local)] = 4771
+	//lastSoldDateVal[time.Date(2024, 7, 23, 0, 0, 0, 0, time.Local)] = 5989
+	//lastSoldDateVal[time.Date(2024, 7, 26, 0, 0, 0, 0, time.Local)] = 6745
+	//lastSoldDateVal[time.Date(2024, 7, 30, 0, 0, 0, 0, time.Local)] = 7272
+	//
+	//originData = make([]*tradeAnalysisModel.ContractCompanyTradeDataList, 0)
+	//originData = append(originData, &tradeAnalysisModel.ContractCompanyTradeDataList{
+	//	Date:           time.Date(2024, 7, 10, 0, 0, 0, 0, time.Local),
+	//	BuyVal:         14324,
+	//	BuyValType:     tradeAnalysisModel.TradeDataTypeOrigin,
+	//	BuyChange:      -1107,
+	//	BuyChangeType:  tradeAnalysisModel.TradeDataTypeOrigin,
+	//	SoldVal:        0,
+	//	SoldValType:    tradeAnalysisModel.TradeDataTypeNull,
+	//	SoldChange:     0,
+	//	SoldChangeType: tradeAnalysisModel.TradeDataTypeNull,
+	//}, &tradeAnalysisModel.ContractCompanyTradeDataList{
+	//	Date:          time.Date(2024, 7, 11, 0, 0, 0, 0, time.Local),
+	//	BuyVal:        14280,
+	//	BuyValType:    tradeAnalysisModel.TradeDataTypeOrigin,
+	//	BuyChange:     -44,
+	//	BuyChangeType: tradeAnalysisModel.TradeDataTypeOrigin,
+	//}, &tradeAnalysisModel.ContractCompanyTradeDataList{
+	//	Date:          time.Date(2024, 7, 12, 0, 0, 0, 0, time.Local),
+	//	BuyVal:        14214,
+	//	BuyValType:    tradeAnalysisModel.TradeDataTypeOrigin,
+	//	BuyChange:     -66,
+	//	BuyChangeType: tradeAnalysisModel.TradeDataTypeOrigin,
+	//}, &tradeAnalysisModel.ContractCompanyTradeDataList{
+	//	Date:          time.Date(2024, 7, 15, 0, 0, 0, 0, time.Local),
+	//	BuyVal:        14269,
+	//	BuyValType:    tradeAnalysisModel.TradeDataTypeOrigin,
+	//	BuyChange:     55,
+	//	BuyChangeType: tradeAnalysisModel.TradeDataTypeOrigin,
+	//}, &tradeAnalysisModel.ContractCompanyTradeDataList{
+	//	Date:           time.Date(2024, 7, 17, 0, 0, 0, 0, time.Local),
+	//	SoldVal:        5254,
+	//	SoldValType:    tradeAnalysisModel.TradeDataTypeOrigin,
+	//	SoldChange:     708,
+	//	SoldChangeType: tradeAnalysisModel.TradeDataTypeOrigin,
+	//}, &tradeAnalysisModel.ContractCompanyTradeDataList{
+	//	Date:           time.Date(2024, 7, 18, 0, 0, 0, 0, time.Local),
+	//	SoldVal:        6595,
+	//	SoldValType:    tradeAnalysisModel.TradeDataTypeOrigin,
+	//	SoldChange:     1341,
+	//	SoldChangeType: tradeAnalysisModel.TradeDataTypeOrigin,
+	//}, &tradeAnalysisModel.ContractCompanyTradeDataList{
+	//	Date:           time.Date(2024, 7, 19, 0, 0, 0, 0, time.Local),
+	//	SoldVal:        5938,
+	//	SoldValType:    tradeAnalysisModel.TradeDataTypeOrigin,
+	//	SoldChange:     -657,
+	//	SoldChangeType: tradeAnalysisModel.TradeDataTypeOrigin,
+	//}, &tradeAnalysisModel.ContractCompanyTradeDataList{
+	//	Date:           time.Date(2024, 7, 22, 0, 0, 0, 0, time.Local),
+	//	SoldVal:        6131,
+	//	SoldValType:    tradeAnalysisModel.TradeDataTypeOrigin,
+	//	SoldChange:     193,
+	//	SoldChangeType: tradeAnalysisModel.TradeDataTypeOrigin,
+	//}, &tradeAnalysisModel.ContractCompanyTradeDataList{
+	//	Date:           time.Date(2024, 7, 29, 0, 0, 0, 0, time.Local),
+	//	SoldVal:        6679,
+	//	SoldValType:    tradeAnalysisModel.TradeDataTypeOrigin,
+	//	SoldChange:     312,
+	//	SoldChangeType: tradeAnalysisModel.TradeDataTypeOrigin,
+	//})
+
+	if len(originData) == 0 {
+		return
+	}
+	if predictRatio < 0 || predictRatio > 1 {
+		err = fmt.Errorf("估计参数不在0-1之间")
+		return
+	}
+	sort.Slice(originData, func(i, j int) bool {
+		return originData[i].Date.Before(originData[j].Date)
+	})
+	dateVal := make(map[time.Time]*tradeAnalysisModel.ContractCompanyTradeDataList)
+	for _, v := range originData {
+		dateVal[v.Date] = v
+	}
+
+	// 生成开始日期-1d(可能会往前面推算一天)至结束日期间的交易日, 以交易日为时间序列遍历
+	tradeDays := utils.GetTradingDays(originData[0].Date.AddDate(0, 0, -1), originData[len(originData)-1].Date)
+	for k, v := range tradeDays {
+		// T日多空均无的情况
+		//bothLast := false
+		if dateVal[v] == nil {
+			// T-1和T+1[原始数据]均无值, 那么T日无数据
+			hasPrev, hasNext := false, false
+			if k-1 >= 0 {
+				hasPrev = true
+			}
+			if k+1 <= len(tradeDays)-1 {
+				hasNext = true
+			}
+			if !hasPrev && !hasNext {
+				continue
+			}
+
+			// T+1有值, 优先从T+1推, 然后继续走下面计算净多单的逻辑
+			if hasNext {
+				nextDay := tradeDays[k+1]
+				if dateVal[nextDay] != nil {
+					// T+1有多/空及多空变化, 且是原始数据, 那么推出数据并在map中新加一日数据
+					if dateVal[nextDay].BuyValType == tradeAnalysisModel.TradeDataTypeOrigin && dateVal[nextDay].BuyChangeType == tradeAnalysisModel.TradeDataTypeOrigin {
+						if _, ok := dateVal[v]; !ok {
+							dateVal[v] = new(tradeAnalysisModel.ContractCompanyTradeDataList)
+							dateVal[v].Date = v
+						}
+						dateVal[v].BuyVal = dateVal[nextDay].BuyVal - dateVal[nextDay].BuyChange
+						dateVal[v].BuyValType = tradeAnalysisModel.TradeDataTypeOrigin
+					}
+					if dateVal[nextDay].SoldValType == tradeAnalysisModel.TradeDataTypeOrigin && dateVal[nextDay].SoldChangeType == tradeAnalysisModel.TradeDataTypeOrigin {
+						if _, ok := dateVal[v]; !ok {
+							dateVal[v] = new(tradeAnalysisModel.ContractCompanyTradeDataList)
+							dateVal[v].Date = v
+						}
+						dateVal[v].SoldVal = dateVal[nextDay].SoldVal - dateVal[nextDay].SoldChange
+						dateVal[v].SoldValType = tradeAnalysisModel.TradeDataTypeOrigin
+					}
+				}
+			}
+
+			// T+1没推出来而T-1有值, 那么T多空均取末位, 计算净多单
+			_, has := dateVal[v]
+			if hasPrev && !has {
+				sv, sok := lastSoldDateVal[v]
+				bv, bok := lastBuyDateVal[v]
+				if !sok && !bok {
+					continue
+				}
+				dateVal[v] = new(tradeAnalysisModel.ContractCompanyTradeDataList)
+				dateVal[v].Date = v
+				if sok {
+					dateVal[v].SoldVal = int(predictRatio*float64(sv) + 0.5)
+					dateVal[v].SoldValType = tradeAnalysisModel.TradeDataTypeCalculate
+				}
+				if bok {
+					dateVal[v].BuyVal = int(predictRatio*float64(bv) + 0.5)
+					dateVal[v].BuyValType = tradeAnalysisModel.TradeDataTypeCalculate
+				}
+				if dateVal[v].BuyValType > tradeAnalysisModel.TradeDataTypeNull && dateVal[v].SoldValType > tradeAnalysisModel.TradeDataTypeNull {
+					dateVal[v].PureBuyVal = dateVal[v].BuyVal - dateVal[v].SoldVal
+					dateVal[v].PureBuyValType = tradeAnalysisModel.TradeDataTypeCalculate
+				}
+				continue
+			}
+		}
+
+		// 多空均有的情况下计算净多单
+		if dateVal[v].BuyValType == tradeAnalysisModel.TradeDataTypeOrigin && dateVal[v].SoldValType == tradeAnalysisModel.TradeDataTypeOrigin {
+			dateVal[v].PureBuyVal = dateVal[v].BuyVal - dateVal[v].SoldVal
+			dateVal[v].PureBuyValType = tradeAnalysisModel.TradeDataTypeOrigin // 原始值算出来的也作原始值
+		}
+
+		// 仅有多单, 空单取末位, 计算净多单
+		if dateVal[v].BuyValType == tradeAnalysisModel.TradeDataTypeOrigin && dateVal[v].SoldValType == tradeAnalysisModel.TradeDataTypeNull {
+			if sv, ok := lastSoldDateVal[v]; ok {
+				dateVal[v].SoldVal = int(predictRatio*float64(sv) + 0.5) // 估计参数*末位值, 向上取整
+				dateVal[v].SoldValType = tradeAnalysisModel.TradeDataTypeCalculate
+				dateVal[v].PureBuyVal = dateVal[v].BuyVal - dateVal[v].SoldVal
+				dateVal[v].PureBuyValType = tradeAnalysisModel.TradeDataTypeCalculate
+			}
+		}
+
+		// 仅有空单, 多单取末位, 计算净多单
+		if dateVal[v].SoldValType == tradeAnalysisModel.TradeDataTypeOrigin && dateVal[v].BuyValType == tradeAnalysisModel.TradeDataTypeNull {
+			if sv, ok := lastBuyDateVal[v]; ok {
+				dateVal[v].BuyVal = int(predictRatio*float64(sv) + 0.5)
+				dateVal[v].BuyValType = tradeAnalysisModel.TradeDataTypeCalculate
+				dateVal[v].PureBuyVal = dateVal[v].BuyVal - dateVal[v].SoldVal
+				dateVal[v].PureBuyValType = tradeAnalysisModel.TradeDataTypeCalculate
+			}
+		}
+	}
+
+	// 二次遍历, 计算与T-1的变化值
+	for k, v := range tradeDays {
+		// 无T/T-1数据, 忽略
+		if dateVal[v] == nil {
+			continue
+		}
+		if k-1 < 0 {
+			continue
+		}
+		beforeDay := tradeDays[k-1]
+		if dateVal[beforeDay] == nil {
+			continue
+		}
+
+		// 多单变化
+		if dateVal[v].BuyChangeType == tradeAnalysisModel.TradeDataTypeNull {
+			if dateVal[v].BuyValType > tradeAnalysisModel.TradeDataTypeNull && dateVal[beforeDay].BuyValType > tradeAnalysisModel.TradeDataTypeNull {
+				dateVal[v].BuyChange = dateVal[v].BuyVal - dateVal[beforeDay].BuyVal
+				// 如果当日多单或者前日多单是估计值, 那么多单变化也为估计值
+				if dateVal[v].BuyValType == tradeAnalysisModel.TradeDataTypeCalculate || dateVal[beforeDay].BuyValType == tradeAnalysisModel.TradeDataTypeCalculate {
+					dateVal[v].BuyChangeType = tradeAnalysisModel.TradeDataTypeCalculate
+				}
+			}
+		}
+
+		// 空单变化
+		if dateVal[v].SoldChangeType == tradeAnalysisModel.TradeDataTypeNull {
+			if dateVal[v].SoldValType > tradeAnalysisModel.TradeDataTypeNull && dateVal[beforeDay].SoldValType > tradeAnalysisModel.TradeDataTypeNull {
+				dateVal[v].SoldChange = dateVal[v].SoldVal - dateVal[beforeDay].SoldVal
+				// 如果当日空单或者前日空单是估计值, 那么空单变化也为估计值
+				if dateVal[v].SoldValType == tradeAnalysisModel.TradeDataTypeCalculate || dateVal[beforeDay].SoldValType == tradeAnalysisModel.TradeDataTypeCalculate {
+					dateVal[v].SoldChangeType = tradeAnalysisModel.TradeDataTypeCalculate
+				}
+			}
+		}
+
+		// 净多变化
+		if dateVal[v].PureBuyChangeType == tradeAnalysisModel.TradeDataTypeNull {
+			if dateVal[v].PureBuyValType > tradeAnalysisModel.TradeDataTypeNull && dateVal[beforeDay].PureBuyValType > tradeAnalysisModel.TradeDataTypeNull {
+				dateVal[v].PureBuyChange = dateVal[v].PureBuyVal - dateVal[beforeDay].PureBuyVal
+				dateVal[v].PureBuyChangeType = tradeAnalysisModel.TradeDataTypeOrigin
+				// 如果当日净多单或者前日净多单是估计值, 那么净多单变化也为估计值
+				if dateVal[v].PureBuyValType == tradeAnalysisModel.TradeDataTypeCalculate || dateVal[beforeDay].PureBuyValType == tradeAnalysisModel.TradeDataTypeCalculate {
+					dateVal[v].PureBuyChangeType = tradeAnalysisModel.TradeDataTypeCalculate
+				}
+			}
+		}
+	}
+
+	// 重新遍历map, 生成数据序列并排序
+	newData = make([]*tradeAnalysisModel.ContractCompanyTradeDataList, 0)
+	for _, v := range dateVal {
+		if v.BuyValType == tradeAnalysisModel.TradeDataTypeNull && v.SoldValType == tradeAnalysisModel.TradeDataTypeNull {
+			continue
+		}
+		newData = append(newData, v)
+	}
+	sort.Slice(newData, func(i, j int) bool {
+		return newData[i].Date.Before(newData[j].Date)
+	})
+	if len(newData) > 0 {
+		firstDate = newData[0].Date
+		endDate = newData[len(newData)-1].Date
+	}
+	return
+}

+ 181 - 0
services/data/trade_analysis/trade_analysis_interface.go

@@ -0,0 +1,181 @@
+package trade_analysis
+
+import (
+	tradeAnalysisModel "eta/eta_api/models/data_manage/trade_analysis"
+	"eta/eta_api/utils"
+	"fmt"
+	"strconv"
+	"strings"
+)
+
+// TradeAnalysisInterface 持仓分析查询接口
+type TradeAnalysisInterface interface {
+	GetTradeDataByClassifyAndCompany(exchange, classifyName string, contracts, queryCompanies []string) (items []*tradeAnalysisModel.OriginTradeData, err error) // 根据品种和公司获取原始数据
+	GetLastTradeDataByClassify(exchange, classifyName string, contracts []string) (items []*tradeAnalysisModel.OriginTradeData, err error)                       // 获取品种末位数据
+}
+
+// BaseTradeAnalysis 通用交易所
+type BaseTradeAnalysis struct{}
+
+func (b *BaseTradeAnalysis) GetTradeDataByClassifyAndCompany(exchange, classifyName string, contracts, queryCompanies []string) (items []*tradeAnalysisModel.OriginTradeData, err error) {
+	return tradeAnalysisModel.GetTradeDataByClassifyAndCompany(exchange, classifyName, contracts, queryCompanies)
+}
+
+func (b *BaseTradeAnalysis) GetLastTradeDataByClassify(exchange, classifyName string, contracts []string) (items []*tradeAnalysisModel.OriginTradeData, err error) {
+	return tradeAnalysisModel.GetLastTradeDataByClassify(exchange, classifyName, contracts)
+}
+
+// ZhengzhouTradeAnalysis 郑商所
+type ZhengzhouTradeAnalysis struct{}
+
+func (z *ZhengzhouTradeAnalysis) GetTradeDataByClassifyAndCompany(exchange, classifyName string, contracts, queryCompanies []string) (items []*tradeAnalysisModel.OriginTradeData, err error) {
+	return tradeAnalysisModel.GetTradeZhengzhouDataByClassifyAndCompany(exchange, contracts, queryCompanies)
+}
+
+func (z *ZhengzhouTradeAnalysis) GetLastTradeDataByClassify(exchange, classifyName string, contracts []string) (items []*tradeAnalysisModel.OriginTradeData, err error) {
+	return tradeAnalysisModel.GetLastTradeZhengzhouDataByClassify(exchange, contracts)
+}
+
+// GuangzhouTradeAnalysis 广期所
+type GuangzhouTradeAnalysis struct{}
+
+func (g *GuangzhouTradeAnalysis) GetTradeDataByClassifyAndCompany(exchange, classifyName string, contracts, queryCompanies []string) (items []*tradeAnalysisModel.OriginTradeData, err error) {
+	classifyIdMap := map[string]int{"si": 7, "lc": 8}
+	classifyId := classifyIdMap[classifyName]
+	if classifyId == 0 {
+		err = fmt.Errorf("品种有误")
+		return
+	}
+
+	// TOP20
+	seatNameArr := []string{tradeAnalysisModel.GuangZhouSeatNameBuy, tradeAnalysisModel.GuangZhouSeatNameSold}
+	if utils.InArrayByStr(queryCompanies, tradeAnalysisModel.TradeFuturesCompanyTop20) {
+		seatNameArr = append(seatNameArr, tradeAnalysisModel.GuangZhouTopSeatNameBuy, tradeAnalysisModel.GuangZhouTopSeatNameSold)
+	}
+
+	// 查询品种下所有指标
+	indexes, e := tradeAnalysisModel.GetBaseFromTradeGuangzhouIndexByClassifyId(classifyId)
+	if e != nil {
+		err = fmt.Errorf("获取广期所指标失败, %v", e)
+		return
+	}
+	var indexIds []int
+	indexInfo := make(map[int]*tradeAnalysisModel.OriginTradeData)
+	for _, v := range indexes {
+		// eg.永安期货_si2401_持买单量
+		nameArr := strings.Split(v.IndexName, "_")
+		if len(nameArr) != 3 {
+			continue
+		}
+		companyName := nameArr[0]
+		if nameArr[0] == tradeAnalysisModel.GuangZhouTopCompanyAliasName {
+			companyName = tradeAnalysisModel.TradeFuturesCompanyTop20
+		}
+		if !utils.InArrayByStr(seatNameArr, nameArr[2]) {
+			continue
+		}
+		if !utils.InArrayByStr(queryCompanies, companyName) {
+			continue
+		}
+		if !utils.InArrayByStr(contracts, nameArr[1]) {
+			continue
+		}
+		indexIds = append(indexIds, v.BaseFromTradeGuangzhouIndexId)
+		if indexInfo[v.BaseFromTradeGuangzhouIndexId] == nil {
+			if tradeAnalysisModel.GuangzhouSeatNameValType[nameArr[2]] == 0 {
+				continue
+			}
+			indexInfo[v.BaseFromTradeGuangzhouIndexId] = new(tradeAnalysisModel.OriginTradeData)
+			indexInfo[v.BaseFromTradeGuangzhouIndexId].CompanyName = companyName
+			indexInfo[v.BaseFromTradeGuangzhouIndexId].ClassifyName = classifyName
+			indexInfo[v.BaseFromTradeGuangzhouIndexId].ClassifyType = nameArr[1]
+			indexInfo[v.BaseFromTradeGuangzhouIndexId].ValType = tradeAnalysisModel.GuangzhouSeatNameValType[nameArr[2]]
+		}
+	}
+	if len(indexIds) == 0 {
+		return
+	}
+
+	// 查询指标数据
+	indexesData, e := tradeAnalysisModel.GetBaseFromTradeGuangzhouDataByIndexIds(indexIds)
+	if e != nil {
+		err = fmt.Errorf("获取广期所指标数据失败, %v", e)
+		return
+	}
+	items = make([]*tradeAnalysisModel.OriginTradeData, 0)
+	for _, v := range indexesData {
+		info, ok := indexInfo[v.BaseFromTradeGuangzhouIndexId]
+		if !ok {
+			continue
+		}
+		items = append(items, &tradeAnalysisModel.OriginTradeData{
+			CompanyName:  info.CompanyName,
+			Val:          int(v.Value),
+			ValChange:    int(v.QtySub),
+			DataTime:     v.DataTime,
+			ClassifyName: info.ClassifyName,
+			ClassifyType: info.ClassifyType,
+			ValType:      info.ValType,
+		})
+	}
+	return
+}
+
+func (g *GuangzhouTradeAnalysis) GetLastTradeDataByClassify(exchange, classifyName string, contracts []string) (items []*tradeAnalysisModel.OriginTradeData, err error) {
+	classifyIdMap := map[string]int{"si": 7, "lc": 8}
+	classifyId := classifyIdMap[classifyName]
+	if classifyId == 0 {
+		err = fmt.Errorf("品种有误")
+		return
+	}
+	seatNameArr := []string{tradeAnalysisModel.GuangZhouSeatNameBuy, tradeAnalysisModel.GuangZhouSeatNameSold}
+
+	// 查询品种下所有指标
+	indexes, e := tradeAnalysisModel.GetBaseFromTradeGuangzhouIndexByClassifyId(classifyId)
+	if e != nil {
+		err = fmt.Errorf("获取广期所指标失败, %v", e)
+		return
+	}
+
+	// 获取各合约下的指标
+	contractIndexIds := make(map[string][]int)
+	for _, v := range indexes {
+		// eg.永安期货_si2401_持买单量
+		nameArr := strings.Split(v.IndexName, "_")
+		if len(nameArr) != 3 {
+			continue
+		}
+		if !utils.InArrayByStr(contracts, nameArr[1]) {
+			continue
+		}
+		if !utils.InArrayByStr(seatNameArr, nameArr[2]) {
+			continue
+		}
+		if tradeAnalysisModel.GuangzhouSeatNameValType[nameArr[2]] == 0 {
+			continue
+		}
+		k := fmt.Sprintf("%s-%d", nameArr[1], tradeAnalysisModel.GuangzhouSeatNameValType[nameArr[2]])
+		contractIndexIds[k] = append(contractIndexIds[k], v.BaseFromTradeGuangzhouIndexId)
+	}
+
+	// ps.如果后面如果有空可以优化一下这里, 把末位数据每天写进一张表里面
+	for k, v := range contractIndexIds {
+		keyArr := strings.Split(k, "-")
+		contract := keyArr[0]
+		valType, _ := strconv.Atoi(keyArr[1])
+		lastVales, e := tradeAnalysisModel.GetBaseFromTradeGuangzhouMinDataByIndexIds(v)
+		if e != nil {
+			err = fmt.Errorf("获取合约末位数据失败, %v", e)
+			return
+		}
+		for _, vv := range lastVales {
+			items = append(items, &tradeAnalysisModel.OriginTradeData{
+				Val:          int(vv.Value),
+				DataTime:     vv.DataTime,
+				ClassifyType: contract,
+				ValType:      valType,
+			})
+		}
+	}
+	return
+}

+ 418 - 0
services/data/trade_analysis/warehouse.go

@@ -0,0 +1,418 @@
+package trade_analysis
+
+import (
+	"encoding/json"
+	"eta/eta_api/models/data_manage"
+	"eta/eta_api/models/data_manage/chart_theme"
+	tradeAnalysisModel "eta/eta_api/models/data_manage/trade_analysis"
+	"eta/eta_api/services/data"
+	"eta/eta_api/utils"
+	"fmt"
+	"strconv"
+	"strings"
+	"time"
+)
+
+// CheckWarehouseChartExtraConfig 校验持仓分析图表参数
+func CheckWarehouseChartExtraConfig(extraConfig tradeAnalysisModel.WarehouseExtraConfig) (pass bool, tips string) {
+	if extraConfig.Exchange == "" {
+		tips = "请选择交易所"
+		return
+	}
+	if extraConfig.ClassifyName == "" {
+		tips = "请选择品种"
+		return
+	}
+	if len(extraConfig.Contracts) == 0 {
+		tips = "请选择合约"
+		return
+	}
+	if len(extraConfig.Companies) == 0 {
+		tips = "请选择期货公司"
+		return
+	}
+	if len(extraConfig.Companies) > 5 {
+		tips = "最多可选5个期货公司"
+		return
+	}
+	if extraConfig.PredictRatio < 0 || extraConfig.PredictRatio > 1 {
+		tips = "请输入正确的估计参数"
+		return
+	}
+	pass = true
+	return
+}
+
+func GetWarehouseChartResp(chartView *data_manage.ChartInfoView, companyTradeData []*tradeAnalysisModel.ContractCompanyTradeData, multiEdb []*tradeAnalysisModel.WarehouseEdbSaveItem, extraConfig tradeAnalysisModel.WarehouseExtraConfig, chartConfig tradeAnalysisModel.WarehouseChartPars) (chartResp *data_manage.ChartInfoDetailResp, err error) {
+	edbMappings, defaultChartName, e := FormatCompanyTradeData2EdbMappings(companyTradeData, chartConfig.WarehouseChartType, chartConfig.DateType, chartConfig.DateTypeNum, chartConfig.StartDate, chartConfig.EndDate, chartConfig.ChartEdbInfoList)
+	if e != nil {
+		err = fmt.Errorf("多单数据转为指标失败, %v", e)
+		return
+	}
+	// chartView为空表示为预览图, 有则表示为详情图
+	var chartThemeId int
+	chartType := utils.CHART_TYPE_CURVE // 曲线图
+	if chartView == nil {
+		// 图表样式/主题
+		chartThemeType, e := chart_theme.GetChartThemeTypeByChartTypeAndSource(chartType, utils.CHART_SOURCE_DEFAULT)
+		if e != nil {
+			err = fmt.Errorf("获取图表类型失败, %v", e)
+			return
+		}
+		chartThemeId = chartThemeType.DefaultChartThemeId
+
+		chartView = new(data_manage.ChartInfoView)
+		chartView.ChartType = chartType
+		chartView.Source = utils.CHART_SOURCE_TRADE_ANALYSIS_PROCESS
+		chartView.ChartName = defaultChartName
+		chartView.ChartNameEn = defaultChartName
+	} else {
+		chartThemeId = chartView.ChartThemeId
+	}
+	chartView.DateType = chartConfig.DateType
+	chartView.DateTypeNum = chartConfig.DateTypeNum
+	chartView.StartDate = chartConfig.StartDate
+	chartView.EndDate = chartConfig.EndDate
+	chartTheme, e := data.GetChartThemeConfig(chartThemeId, utils.CHART_SOURCE_DEFAULT, chartType)
+	if e != nil {
+		err = fmt.Errorf("获取图表主题失败, %v", e)
+		return
+	}
+	chartView.ChartThemeId = chartTheme.ChartThemeId
+	chartView.ChartThemeStyle = chartTheme.Config
+
+	chartResp = new(data_manage.ChartInfoDetailResp)
+	chartResp.ChartInfo = chartView
+	chartResp.EdbInfoList = edbMappings
+	dataResp := tradeAnalysisModel.WarehouseChartDataResp{WarehouseExtraConfig: extraConfig, MultiEdbMappings: multiEdb}
+	dataResp.WarehouseChartType = chartConfig.WarehouseChartType
+	chartResp.DataResp = dataResp
+	return
+}
+
+// AddWarehouseChart 添加持仓分析图表
+func AddWarehouseChart(req data_manage.AddChartInfoReq, extraConfig tradeAnalysisModel.WarehouseExtraConfig, adminId int, adminRealName string) (chartInfo *data_manage.ChartInfo, err error) {
+	// 图表信息
+	chartInfo = new(data_manage.ChartInfo)
+	chartInfo.ChartName = req.ChartName
+	chartInfo.ChartNameEn = req.ChartName
+	chartInfo.ChartClassifyId = req.ChartClassifyId
+	chartInfo.SysUserId = adminId
+	chartInfo.SysUserRealName = adminRealName
+	chartInfo.CreateTime = time.Now()
+	chartInfo.ModifyTime = time.Now()
+	chartInfo.IsSetName = 0
+	timestamp := strconv.FormatInt(time.Now().UnixNano(), 10)
+	chartInfo.UniqueCode = utils.MD5(utils.CHART_PREFIX + "_" + timestamp)
+	chartInfo.ChartType = utils.CHART_TYPE_CURVE
+	chartInfo.Calendar = "公历"
+	chartInfo.DateType = req.DateType
+	chartInfo.StartDate = req.StartDate
+	chartInfo.EndDate = req.EndDate
+	chartInfo.SeasonStartDate = req.StartDate
+	chartInfo.SeasonEndDate = req.EndDate
+	chartInfo.LeftMin = req.LeftMin
+	chartInfo.LeftMax = req.LeftMax
+	chartInfo.RightMin = req.RightMin
+	chartInfo.RightMax = req.RightMax
+	chartInfo.Right2Min = req.Right2Min
+	chartInfo.Right2Max = req.Right2Max
+	chartInfo.Source = utils.CHART_SOURCE_TRADE_ANALYSIS_PROCESS
+	chartInfo.ChartThemeId = req.ChartThemeId
+	chartInfo.SourcesFrom = req.SourcesFrom
+	chartInfo.Instructions = req.Instructions
+	chartInfo.MarkersLines = req.MarkersLines
+	chartInfo.MarkersAreas = req.MarkersAreas
+	chartInfo.ExtraConfig = req.ExtraConfig
+	chartInfo.DateTypeNum = req.DateTypeNum
+	chartInfo.MinMaxSave = req.MinMaxSave
+
+	// 图例信息-由于持仓分析图表无指标, 图例信息就不存在chart_edb_mapping里了, 而是chart_series
+	seriesList := make([]*data_manage.ChartSeries, 0)
+	for _, v := range req.ChartEdbInfoList {
+		t := new(data_manage.ChartSeries)
+		t.SeriesName = v.EdbAliasName
+		t.SeriesNameEn = v.EdbAliasName
+		t.IsAxis = v.IsAxis
+		t.UniqueFlag = v.UniqueFlag
+		t.CreateTime = time.Now().Local()
+		t.ModifyTime = time.Now().Local()
+		seriesList = append(seriesList, t)
+	}
+
+	// 图表关联多图配置
+	multiChartMapping := new(data_manage.MultipleGraphConfigChartMapping)
+	if extraConfig.MultipleGraphConfigId > 0 {
+		multiChartMapping.MultipleGraphConfigId = extraConfig.MultipleGraphConfigId
+		multiChartMapping.Source = utils.CHART_SOURCE_TRADE_ANALYSIS_PROCESS
+		multiChartMapping.CreateTime = time.Now().Local()
+		multiChartMapping.ModifyTime = time.Now().Local()
+	}
+
+	// 新增
+	if e := tradeAnalysisModel.CreateWarehouseChart(chartInfo, seriesList, multiChartMapping); e != nil {
+		err = fmt.Errorf("新增图表失败, %v", e)
+		return
+	}
+
+	// 添加es数据
+	go data.EsAddOrEditChartInfo(chartInfo.ChartInfoId)
+	return
+}
+
+// EditWarehouseChart 编辑持仓分析图表
+func EditWarehouseChart(req data_manage.EditChartInfoReq) (chartItem *data_manage.ChartInfo, err error) {
+	// 更新图表
+	e := data_manage.EditChartInfoAndMapping(&req, "", "公历", req.DateType, 0, ``, make([]*data_manage.ChartSaveItem, 0), "")
+	if e != nil {
+		err = fmt.Errorf("更新图表失败, %v", e)
+		return
+	}
+	chartItem, e = data_manage.GetChartInfoById(req.ChartInfoId)
+	if e != nil {
+		err = fmt.Errorf("获取更新后的图表失败, %v", e)
+		return
+	}
+
+	// 替换原图例
+	if e = data_manage.DeleteChartSeriesAndEdbMapping(req.ChartInfoId); e != nil {
+		err = fmt.Errorf("删除原图例失败, %v", e)
+		return
+	}
+	seriesList := make([]*data_manage.ChartSeries, 0)
+	for _, v := range req.ChartEdbInfoList {
+		t := new(data_manage.ChartSeries)
+		t.SeriesName = v.EdbAliasName
+		t.SeriesNameEn = v.EdbAliasName
+		t.ChartInfoId = chartItem.ChartInfoId
+		t.IsAxis = v.IsAxis
+		t.UniqueFlag = v.UniqueFlag
+		t.CreateTime = time.Now().Local()
+		t.ModifyTime = time.Now().Local()
+		seriesList = append(seriesList, t)
+	}
+	if len(seriesList) > 0 {
+		seriesOb := new(data_manage.ChartSeries)
+		if e = seriesOb.CreateMulti(seriesList); e != nil {
+			err = fmt.Errorf("新增图例失败, %v", e)
+			return
+		}
+	}
+
+	// 更新ES
+	go func() {
+		data.EsAddOrEditChartInfo(chartItem.ChartInfoId)
+		data.EsAddOrEditMyChartInfoByChartInfoId(chartItem.ChartInfoId)
+	}()
+	return
+}
+
+// CopyWarehouseChart 复制持仓分析图表
+func CopyWarehouseChart(classifyId int, chartName string, originChart *data_manage.ChartInfo, adminId int, adminRealName string) (chartInfo *data_manage.ChartInfo, err error) {
+	var extraConfig tradeAnalysisModel.WarehouseExtraConfig
+	if e := json.Unmarshal([]byte(originChart.ExtraConfig), &extraConfig); e != nil {
+		err = fmt.Errorf("图表配置有误, %v", e)
+		return
+	}
+
+	// 新增配置并绑定图表
+	multiConfigCopy := &data_manage.MultipleGraphConfig{
+		SysUserId:       adminId,
+		SysUserRealName: adminRealName,
+		ModifyTime:      time.Now(),
+		CreateTime:      time.Now(),
+	}
+	if e := data_manage.AddMultipleGraphConfig(multiConfigCopy); e != nil {
+		err = fmt.Errorf("新增持仓分析多图配置失败, %v", e)
+		return
+	}
+	extraConfig.MultipleGraphConfigId = multiConfigCopy.MultipleGraphConfigId
+	configByte, e := json.Marshal(extraConfig)
+	if e != nil {
+		err = fmt.Errorf("图表配置格式化失败, %v", e)
+		return
+	}
+
+	// 新增图表
+	chartInfo = &data_manage.ChartInfo{
+		ChartName:         chartName,
+		ChartClassifyId:   classifyId,
+		SysUserId:         adminId,
+		SysUserRealName:   adminRealName,
+		UniqueCode:        utils.MD5(utils.CHART_PREFIX + "_" + strconv.FormatInt(time.Now().UnixNano(), 10)),
+		CreateTime:        time.Now(),
+		ModifyTime:        time.Now(),
+		DateType:          originChart.DateType,
+		StartDate:         originChart.StartDate,
+		EndDate:           originChart.EndDate,
+		IsSetName:         originChart.IsSetName,
+		EdbInfoIds:        originChart.EdbInfoIds,
+		ChartType:         originChart.ChartType,
+		Calendar:          originChart.Calendar,
+		SeasonStartDate:   originChart.SeasonStartDate,
+		SeasonEndDate:     originChart.SeasonEndDate,
+		ChartImage:        originChart.ChartImage,
+		BarConfig:         originChart.BarConfig,
+		LeftMin:           originChart.LeftMin,
+		LeftMax:           originChart.LeftMax,
+		RightMin:          originChart.RightMin,
+		RightMax:          originChart.RightMax,
+		Right2Min:         originChart.Right2Min,
+		Right2Max:         originChart.Right2Max,
+		Disabled:          originChart.Disabled,
+		Source:            originChart.Source,
+		ExtraConfig:       string(configByte),
+		SeasonExtraConfig: originChart.SeasonExtraConfig,
+		StartYear:         originChart.StartYear,
+		Unit:              originChart.Unit,
+		UnitEn:            originChart.UnitEn,
+		ChartThemeId:      originChart.ChartThemeId,
+		SourcesFrom:       originChart.SourcesFrom,
+		Instructions:      originChart.Instructions,
+		MarkersLines:      originChart.MarkersLines,
+		MarkersAreas:      originChart.MarkersAreas,
+		DateTypeNum:       originChart.DateTypeNum,
+	}
+
+	// 图例信息
+	seriesList, e := data_manage.GetChartSeriesByChartInfoId(originChart.ChartInfoId)
+	if e != nil {
+		err = fmt.Errorf("获取图例信息失败, %v", e)
+		return
+	}
+	seriesCopy := make([]*data_manage.ChartSeries, 0)
+	for _, v := range seriesList {
+		t := new(data_manage.ChartSeries)
+		t.SeriesName = v.SeriesName
+		t.SeriesNameEn = v.SeriesNameEn
+		t.IsAxis = v.IsAxis
+		t.UniqueFlag = v.UniqueFlag
+		t.CreateTime = time.Now().Local()
+		t.ModifyTime = time.Now().Local()
+		seriesCopy = append(seriesCopy, t)
+	}
+
+	// 新增图表-多图配置关联
+	configChartMapping := &data_manage.MultipleGraphConfigChartMapping{
+		MultipleGraphConfigId: multiConfigCopy.MultipleGraphConfigId,
+		Source:                utils.CHART_SOURCE_TRADE_ANALYSIS_PROCESS,
+		ModifyTime:            time.Now().Local(),
+		CreateTime:            time.Now().Local(),
+	}
+
+	// 新增
+	if e = tradeAnalysisModel.CreateWarehouseChart(chartInfo, seriesCopy, configChartMapping); e != nil {
+		err = fmt.Errorf("新增图表失败, %v", e)
+		return
+	}
+
+	// 新增ES
+	go data.EsAddOrEditChartInfo(chartInfo.ChartInfoId)
+	return
+}
+
+// CheckEdbSave 校验指标新增
+func CheckEdbSave(extraConfig tradeAnalysisModel.WarehouseExtraConfig, multiEdbList []*data_manage.MultipleGraphConfigEdbMapping, IsSaveAs bool) (newEdbList []*tradeAnalysisModel.WarehouseEdbSaveItem, removeEdbIds []int, err error) {
+	// 另存为或无关联指标时, 返回应当新增的全部指标列表
+	newEdbList = make([]*tradeAnalysisModel.WarehouseEdbSaveItem, 0)
+	suffixNames := tradeAnalysisModel.WarehouseTypeSuffixNames
+	prefix := strings.Join(extraConfig.Contracts, "")
+	if len(multiEdbList) == 0 || IsSaveAs {
+		for _, v := range extraConfig.Companies {
+			edb := new(tradeAnalysisModel.WarehouseEdbSaveItem)
+			edb.EdbName = fmt.Sprintf("%s%s%s", prefix, v, suffixNames[extraConfig.WarehouseChartType])
+			edb.Unit = tradeAnalysisModel.WarehouseDefaultUnit
+			edb.Frequency = tradeAnalysisModel.WarehouseDefaultFrequency
+			edb.UniqueFlag = v
+
+			//conf := extraConfig
+			//conf.Companies = []string{v}
+			//b, e := json.Marshal(conf)
+			//if e != nil {
+			//	err = fmt.Errorf("指标配置JSON格式化异常, %v", e)
+			//	return
+			//}
+			//edb.ExtraConfig = string(b)
+			newEdbList = append(newEdbList, edb)
+		}
+		return
+	}
+
+	// 已有关联指标
+	var edbIds []int
+	for _, v := range multiEdbList {
+		edbIds = append(edbIds, v.EdbInfoId)
+	}
+	if len(edbIds) == 0 {
+		err = fmt.Errorf("关联指标IDs异常")
+		return
+	}
+	warehouseType := extraConfig.WarehouseChartType
+
+	// 获取已关联的指标信息
+	edbList, e := data_manage.GetEdbInfoByIdList(edbIds)
+	if e != nil {
+		err = fmt.Errorf("获取指标信息失败, %v", e)
+		return
+	}
+
+	// 只需要匹配期货公司即可, 合约数不匹配那么是不需要新增指标的
+	existsMap := make(map[string]int) // [期货公司]:[指标ID]
+	for _, v := range edbList {
+		// 解析计算公式中的配置信息, 计算公式为空、解析失败的为异常需要移除绑定关系
+		if v.CalculateFormula == "" {
+			removeEdbIds = append(removeEdbIds, v.EdbInfoId)
+			continue
+		}
+		var conf tradeAnalysisModel.WarehouseExtraConfig
+		if e = json.Unmarshal([]byte(v.CalculateFormula), &conf); e != nil {
+			utils.FileLog.Info("持仓分析图表-解析指标计算公式失败, EdbInfoId: %d, Conf: %s", v.EdbInfoId, v.CalculateFormula)
+			removeEdbIds = append(removeEdbIds, v.EdbInfoId)
+			continue
+		}
+		if len(conf.Companies) != 1 {
+			utils.FileLog.Info("持仓分析图表-指标计算公式异常, EdbInfoId: %d, Conf: %s", v.EdbInfoId, v.CalculateFormula)
+			removeEdbIds = append(removeEdbIds, v.EdbInfoId)
+			continue
+		}
+		// 方向与配置中的方向不一致, 那么忽略
+		if conf.WarehouseChartType != warehouseType {
+			continue
+		}
+		existsMap[conf.Companies[0]] = v.EdbInfoId
+	}
+
+	// 配置中的, 不在已绑定中的需要新增
+	confMap := make(map[string]bool) // [期货公司]:True
+	for _, v := range extraConfig.Companies {
+		confMap[v] = true
+		if _, ok := existsMap[v]; !ok {
+			// 需要新增的指标
+			edb := new(tradeAnalysisModel.WarehouseEdbSaveItem)
+			edb.EdbName = fmt.Sprintf("%s%s%s", prefix, v, suffixNames[extraConfig.WarehouseChartType])
+			edb.Unit = tradeAnalysisModel.WarehouseDefaultUnit
+			edb.Frequency = tradeAnalysisModel.WarehouseDefaultFrequency
+			edb.UniqueFlag = v
+
+			//conf := extraConfig
+			//conf.Companies = []string{v}
+			//b, e := json.Marshal(conf)
+			//if e != nil {
+			//	err = fmt.Errorf("指标配置JSON格式化异常, %v", e)
+			//	return
+			//}
+			//edb.ExtraConfig = string(b)
+			newEdbList = append(newEdbList, edb)
+			continue
+		}
+	}
+
+	// 已绑定的, 不在配置中的需要移除绑定
+	for k, v := range existsMap {
+		if _, ok := existsMap[k]; !ok {
+			removeEdbIds = append(removeEdbIds, v)
+			continue
+		}
+	}
+	return
+}

+ 69 - 0
utils/common.go

@@ -1764,6 +1764,32 @@ func GetDateByDateTypeV2(dateType int, tmpStartDate, tmpEndDate string, startYea
 		baseDate, _ := time.Parse(FormatDate, fmt.Sprintf("%d-01-01", yearMax))
 		startDate = baseDate.AddDate(-startYear, 0, 0).Format(FormatDate)
 		endDate = ""
+	case DateTypeOneWeek:
+		//if startDate != "" {
+		//	st, e := time.ParseInLocation(FormatDate, startDate, time.Local)
+		//	if e != nil {
+		//		FileLog.Info(fmt.Sprintf("日期格式解析失败, %s, %v", startDate, e))
+		//		return
+		//	}
+		//	startDate = st.AddDate(0, 0, -7).Format(FormatDate)
+		//}
+		startDate = time.Now().AddDate(0, 0, -7).Format(FormatDate)
+		endDate = ""
+	case DateTypeOneMonth:
+		startDate = time.Now().AddDate(0, -1, 0).Format(FormatDate)
+		endDate = ""
+	case DateTypeTwoMonth:
+		startDate = time.Now().AddDate(0, -2, 0).Format(FormatDate)
+		endDate = ""
+	case DateTypeThreeMonth:
+		startDate = time.Now().AddDate(0, -3, 0).Format(FormatDate)
+		endDate = ""
+	case DateTypeNMonth:
+		if startYear == 0 {
+			startYear = 6
+		}
+		startDate = time.Now().AddDate(0, -startYear, 0).Format(FormatDate)
+		endDate = ""
 	}
 
 	// 兼容日期错误
@@ -2678,3 +2704,46 @@ func GetDuration(filePath string) (duration string, err error) {
 
 	return duration, nil
 }
+
+// GetTradingDays 获取开始时间至结束时间之间的交易日期(日度)
+func GetTradingDays(startDate, endDate time.Time) []time.Time {
+	var tradingDays []time.Time
+	for curr := startDate; !curr.After(endDate); curr = curr.AddDate(0, 0, 1) {
+		if curr.Weekday() >= time.Monday && curr.Weekday() <= time.Friday {
+			tradingDays = append(tradingDays, curr)
+		}
+	}
+	return tradingDays
+}
+
+// GenerateEdbCodeMap 当前已经生成的指标编码map(暂时不做定时数据清理了,因为数据不大,我们至少每个月会重启一次,所以暂时不做定时数据清理)
+var GenerateEdbCodeMap = map[string]bool{}
+
+// GenerateEdbCode
+// @Description:  生成指标编码
+// @author: Roc
+// @datetime 2024-06-05 09:49:53
+// @param num int
+// @param pre string 前缀
+// @return edbCode string
+// @return err error
+func GenerateEdbCode(num int, pre string) (edbCode string, err error) {
+	if num >= 10 {
+		err = errors.New("指标编码生成失败,请重新生成")
+		return
+	}
+
+	// 4位随机数
+	randStr := GetRandDigit(4)
+	// 年月日时分秒+4位随机数
+	edbCode = `C` + pre + time.Now().Format(FormatShortDateTimeUnSpace) + randStr
+
+	if _, ok := GenerateEdbCodeMap[edbCode]; ok {
+		num++
+		edbCode, err = GenerateEdbCode(num, pre)
+	}
+
+	GenerateEdbCodeMap[edbCode] = true
+
+	return
+}

+ 23 - 0
utils/constants.go

@@ -2,6 +2,7 @@ package utils
 
 import (
 	"io/fs"
+	"time"
 )
 
 // 常量定义
@@ -180,6 +181,7 @@ const (
 	DATA_SOURCE_OILCHEM                              = 89       // 隆众资讯 -> 89
 	DATA_SOURCE_FENWEI                               = 77       // 汾渭数据->92
 	DATA_SOURCE_LY                                   = 91       // 粮油商务网
+	DATA_SOURCE_TRADE_ANALYSIS                       = 92       // 持仓分析
 )
 
 // 数据刷新频率
@@ -290,6 +292,7 @@ const (
 	CHART_SOURCE_CROSS_HEDGING                   = 10 // 跨品种分析图表
 	CHART_SOURCE_BALANCE_EXCEL                   = 11 // 平衡表图表
 	CHART_SOURCE_RANGE_ANALYSIS                  = 12 // 	区间分析图表
+	CHART_SOURCE_TRADE_ANALYSIS_PROCESS          = 13 // 持仓分析-建仓过程图表
 )
 
 // 批量配置图表的位置来源
@@ -484,3 +487,23 @@ const (
 
 // MultiAddNum 批量插入的数据量
 const MultiAddNum = 500
+
+const (
+	DateTypeOneWeek    = 21 // 时间类型-近1周
+	DateTypeOneMonth   = 22 // 时间类型-近1月
+	DateTypeTwoMonth   = 23 // 时间类型-近2月
+	DateTypeThreeMonth = 24 // 时间类型-近3月
+	DateTypeNMonth     = 25 // 时间类型-近N月
+)
+
+const (
+	SourceNameTradeAnalysis = "持仓分析"
+)
+
+// 基础数据初始化日期
+var (
+	BASE_START_DATE         = `1900-01-01`                                          //基础数据开始日期
+	BASE_END_DATE           = time.Now().AddDate(4, 0, 0).Format(FormatDate)        //基础数据结束日期
+	BASE_START_DATE_UnSpace = "19000101"                                            //基础数据开始日期
+	BASE_END_DATE_UnSpace   = time.Now().AddDate(4, 0, 0).Format(FormatDateUnSpace) //基础数据结束日期
+)