浏览代码

Merge branch 'master' into ETA_1.4.5

zwxi 1 年之前
父节点
当前提交
79330b7093

+ 89 - 0
controllers/base_common.go

@@ -0,0 +1,89 @@
+package controllers
+
+import (
+	"encoding/json"
+	"net/http"
+	"net/url"
+
+	"github.com/beego/beego/v2/server/web"
+
+	"eta/eta_index_lib/models"
+	"eta/eta_index_lib/utils"
+)
+
+type BaseCommonController struct {
+	web.Controller
+}
+
+func (c *BaseCommonController) ServeJSON(encoding ...bool) {
+	// 方法处理完后,需要后置处理的业务逻辑
+	//if handlerList, ok := AfterHandlerUrlMap[c.Ctx.Request.URL.Path]; ok {
+	//	for _, handler := range handlerList {
+	//		handler(c.Ctx.Input.RequestBody)
+	//	}
+	//}
+
+	var (
+		hasIndent   = false
+		hasEncoding = false
+	)
+	if web.BConfig.RunMode == web.PROD {
+		hasIndent = false
+	}
+	if len(encoding) > 0 && encoding[0] == true {
+		hasEncoding = true
+	}
+	if c.Data["json"] == nil {
+		go utils.SendEmail("异常提醒:", "接口:"+"URI:"+c.Ctx.Input.URI()+";无返回值", utils.EmailSendToUsers)
+		return
+	}
+
+	baseRes := c.Data["json"].(*models.BaseResponse)
+	if baseRes != nil && baseRes.Ret != 408 {
+		body, _ := json.Marshal(baseRes)
+		var requestBody string
+		method := c.Ctx.Input.Method()
+		if method == "GET" {
+			requestBody = c.Ctx.Request.RequestURI
+		} else {
+			requestBody, _ = url.QueryUnescape(string(c.Ctx.Input.RequestBody))
+		}
+		if baseRes.Ret != 200 && baseRes.IsSendEmail {
+			go utils.SendEmail(utils.APP_NAME_CN+"【"+utils.RunMode+"】"+"失败提醒", "URI:"+c.Ctx.Input.URI()+"<br/> "+"Params"+requestBody+" <br/>"+"ErrMsg:"+baseRes.ErrMsg+";<br/>Msg:"+baseRes.Msg+";<br/> Body:"+string(body)+"<br/>", utils.EmailSendToUsers)
+		}
+	}
+	c.JSON(c.Data["json"], hasIndent, hasEncoding)
+}
+
+func (c *BaseCommonController) JSON(data interface{}, hasIndent bool, coding bool) error {
+	c.Ctx.Output.Header("Content-Type", "application/json; charset=utf-8")
+	var content []byte
+	var err error
+	if hasIndent {
+		content, err = json.MarshalIndent(data, "", "  ")
+	} else {
+		content, err = json.Marshal(data)
+	}
+	if err != nil {
+		http.Error(c.Ctx.Output.Context.ResponseWriter, err.Error(), http.StatusInternalServerError)
+		return err
+	}
+	ip := c.Ctx.Input.IP()
+	requestBody, err := url.QueryUnescape(string(c.Ctx.Input.RequestBody))
+	if err != nil {
+		requestBody = string(c.Ctx.Input.RequestBody)
+	}
+	if requestBody == "" {
+		requestBody = c.Ctx.Input.URI()
+	}
+	authorization := c.Ctx.Input.Header("authorization")
+	if authorization == "" {
+		authorization = c.Ctx.Input.Header("Authorization")
+	}
+
+	utils.ApiLog.Info("uri:%s, authorization:%s, requestBody:%s, responseBody:%s, ip:%s", c.Ctx.Input.URI(), authorization, requestBody, content, ip)
+	if coding {
+		content = []byte(utils.StringsToJSON(string(content)))
+	}
+	return c.Ctx.Output.Body(content)
+}

+ 171 - 0
controllers/base_from_ths_ds.go

@@ -0,0 +1,171 @@
+package controllers
+
+import (
+	"encoding/json"
+	"eta/eta_index_lib/logic"
+	"eta/eta_index_lib/models"
+	"eta/eta_index_lib/services"
+	"eta/eta_index_lib/utils"
+	"strconv"
+	"time"
+)
+
+// 同花顺
+type ThsDsController struct {
+	BaseAuthController
+}
+
+// @Title 新增同花顺指标接口
+// @Description 新增同花顺指标接口
+// @Success 200 {object} models.AddEdbInfoReq
+// @router /ds/add [post]
+func (this *ThsDsController) Add() {
+	br := new(models.BaseResponse).Init()
+	var cacheKey string
+	defer func() {
+		utils.Rc.Delete(cacheKey)
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	source := utils.DATA_SOURCE_THS
+	var req models.AddEdbInfoReq
+	err := json.Unmarshal(this.Ctx.Input.RequestBody, &req)
+	if err != nil {
+		br.Msg = "参数解析异常!"
+		br.ErrMsg = "参数解析失败,Err:" + err.Error()
+		return
+	}
+	if req.EdbCode == "" {
+		br.Msg = "请输入指标编码!"
+		br.ErrMsg = "请输入指标编码,指标编码为空"
+		return
+	}
+	//期货数据,就默认到今天,特殊处理下
+	endDate := time.Now().Format(utils.FormatDate)
+
+	cacheKey = utils.CACHE_EDB_DATA_ADD + strconv.Itoa(source) + "_" + req.StockCode + req.EdbCode
+	if !utils.Rc.IsExist(cacheKey) {
+		utils.Rc.SetNX(cacheKey, 1, 1*time.Minute)
+		dataItem, err := services.GetEdbDataFromThsDs(req.StockCode, req.EdbCode, utils.BASE_START_DATE, endDate, "")
+		if err != nil {
+			br.Msg = "获取指标信息失败!"
+			br.ErrMsg = "获取指标信息失败 GetEdbDataFromThsDs,Err:" + err.Error()
+			return
+		}
+		err = models.AddEdbDataFromThsDs(req.StockCode,req.EdbCode, dataItem)
+		if err != nil {
+			br.Msg = "获取指标信息失败!"
+			br.ErrMsg = "获取指标信息失败 AddEdbDataFromThs,Err:" + err.Error()
+			return
+		}
+		br.Ret = 200
+		br.Success = true
+		br.Msg = "获取成功"
+	} else {
+		br.Ret = 501
+		br.Success = true
+		br.Msg = "系统处理中,请稍后重试"
+	}
+}
+
+// @Title 刷新同花顺指标接口
+// @Description 刷新同花顺指标接口
+// @Success 200 {object} models.RefreshEdbInfoReq
+// @router /ds/refresh [post]
+func (this *ThsDsController) Refresh() {
+	br := new(models.BaseResponse).Init()
+	var cacheKey string
+	defer func() {
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	source := utils.DATA_SOURCE_THS
+	var req models.RefreshEdbInfoReq
+	err := json.Unmarshal(this.Ctx.Input.RequestBody, &req)
+	if err != nil {
+		br.Msg = "参数解析异常!"
+		br.ErrMsg = "参数解析失败,Err:" + err.Error()
+		return
+	}
+	if req.EdbCode == "" {
+		br.Msg = "请输入指标编码!"
+		br.ErrMsg = "请输入指标编码,指标编码为空"
+		return
+	}
+	if req.EdbInfoId <= 0 {
+		br.Msg = "请输入指标ID!"
+		br.ErrMsg = "请输入指标ID"
+		return
+	}
+	//期货数据,就默认到今天,特殊处理下
+	endDate := time.Now().Format(utils.FormatDate)
+	// 获取指标详情
+	edbInfo, err := models.GetEdbInfoByEdbCode(source, req.EdbCode)
+	if err != nil {
+		br.Msg = "指标不存在!"
+		br.ErrMsg = "指标不存在"
+		return
+	}
+	cacheKey = utils.CACHE_EDB_DATA_REFRESH + strconv.Itoa(source) + "_" + req.EdbCode
+	if utils.Rc.IsExist(cacheKey) {
+		br.Ret = 501
+		br.Success = true
+		br.Msg = "系统处理中,请稍后重试"
+		return
+	}
+
+	utils.Rc.SetNX(cacheKey, 1, 1*time.Minute)
+	defer func() {
+		utils.Rc.Delete(cacheKey)
+	}()
+	dataItem, err := services.GetEdbDataFromThsDs(edbInfo.StockCode, edbInfo.IndicatorCode, req.StartDate, endDate, edbInfo.TerminalCode)
+	if err != nil {
+		br.Msg = "获取指标信息失败!"
+		br.ErrMsg = "获取指标信息失败 GetEdbDataFromThsDs,Err:" + err.Error()
+		return
+	}
+	err = models.RefreshEdbDataFromThsDs(req.EdbInfoId, req.EdbCode, req.StartDate, dataItem)
+	if err != nil && err.Error() != utils.ErrNoRow() {
+		br.Msg = "刷新指标信息失败!"
+		br.ErrMsg = "刷新指标信息失败 RefreshEdbDataFromThsDs,Err:" + err.Error()
+		return
+	}
+	// 更新指标最大最小值
+	err, errMsg := models.UnifiedModifyEdbInfoMaxAndMinInfo(edbInfo)
+	if err != nil {
+		br.Msg = errMsg
+		br.ErrMsg = err.Error()
+		return
+	}
+
+	// 更新ES
+	go logic.UpdateEs(edbInfo.EdbInfoId)
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+}
+
+//func init() {
+//	//?EdbCode=s005696248&StartDate=2023-02-03&EndDate=2027-03-23
+//	//edbCode := `s005696248`
+//	//startDate := `2023-02-03`
+//	//endDate := `2027-03-23`
+//	//EdbCode=S011292460&StartDate=1993-03-23&EndDate=2027-03-23
+//	edbCode := `S011292460`
+//	startDate := `1993-03-23`
+//	endDate := `2027-03-23`
+//	//edbCode := `@CL0W.NMX`
+//	//startDate := `20221218`
+//	//endDate := `20230118`
+//	list, err := services.GetEdbDataFromThsHttp(edbCode, startDate, endDate, 0)
+//	//list, err := services.GetFutureGoodDataFromThsHttp(edbCode, startDate, endDate)
+//
+//	//token, err := services.GetAccessToken()
+//	if err != nil {
+//		fmt.Println("err:", err)
+//		return
+//	}
+//	fmt.Println(list)
+//	//fmt.Println(token)
+//}

+ 23 - 0
controllers/error.go

@@ -0,0 +1,23 @@
+package controllers
+
+import (
+	"eta/eta_index_lib/models"
+)
+
+// ErrorController
+// @Description: 该控制器处理页面错误请求
+type ErrorController struct {
+	BaseCommonController
+}
+
+func (c *ErrorController) Error404() {
+	c.Data["content"] = "很抱歉您访问的地址或者方法不存在"
+	//c.TplName = "error/404.html"
+
+	br := new(models.BaseResponse).Init()
+
+	br.Msg = "您访问的资源不存在"
+	br.Ret = 404
+	c.Data["json"] = br
+	c.ServeJSON()
+}

+ 2 - 0
logic/base_edb_info.go

@@ -27,6 +27,8 @@ func RefreshBaseEdbInfo(edbInfo *models.EdbInfo, startDate string) (isHandling b
 		err = models.RefreshEdbDataFromBaiinfo(edbInfo.EdbInfoId, edbInfo.EdbCode, startDate)
 	case utils.DATA_SOURCE_MYSTEEL_CHEMICAL:
 		err = models.RefreshEdbDataFromMysteelChemical(edbInfo.EdbInfoId, edbInfo.EdbCode, startDate)
+	case utils.DATA_SOURCE_YS:
+		err = models.RefreshEdbDataFromSmm(edbInfo.EdbInfoId, edbInfo.EdbCode, startDate)
 	default:
 		return
 	}

+ 4 - 0
main.go

@@ -1,6 +1,7 @@
 package main
 
 import (
+	"eta/eta_index_lib/controllers"
 	_ "eta/eta_index_lib/routers"
 	"eta/eta_index_lib/services/alarm_msg"
 	"eta/eta_index_lib/utils"
@@ -17,6 +18,9 @@ func main() {
 		web.BConfig.WebConfig.DirectoryIndex = true
 		web.BConfig.WebConfig.StaticDir["/swagger"] = "swagger"
 	}
+	// 异常处理
+	web.ErrorController(&controllers.ErrorController{})
+
 	web.BConfig.RecoverFunc = Recover
 	web.Run()
 }

+ 1 - 1
models/base_from_ths.go

@@ -64,7 +64,7 @@ func AddEdbDataFromThs(edbCode string, item EdbDataFromThs) (err error) {
 func RefreshEdbDataFromThs(edbInfoId int, edbCode, startDate string, item EdbDataFromThs) (err error) {
 	o := orm.NewOrm()
 	source := utils.DATA_SOURCE_THS
-	subSource := utils.DATA_SUB_SOURCE_DATE
+	subSource := utils.DATA_SUB_SOURCE_EDB
 
 	// 真实数据的最大日期  , 插入规则配置的日期
 	var realDataMaxDate, edbDataInsertConfigDate time.Time

+ 167 - 0
models/base_from_ths_ds.go

@@ -0,0 +1,167 @@
+package models
+
+import (
+	"eta/eta_index_lib/utils"
+	"fmt"
+	"github.com/beego/beego/v2/client/orm"
+	"strconv"
+	"strings"
+	"time"
+)
+
+var thsds = "thsds"
+
+// 新增同花顺指标数据
+func AddEdbDataFromThsDs(stockCode,edbCode string, item EdbDataFromThs) (err error) {
+	o := orm.NewOrm()
+
+	edbCodeList := strings.Split(edbCode, ",")
+	indexCodeMap := make(map[string]string)
+	if len(item.Tables) > 0 {
+		var isAdd bool
+		addSql := ` INSERT INTO edb_data_ths_ds(edb_info_id,edb_code,data_time,value,create_time,modify_time,data_timestamp) values `
+		for k, table := range item.Tables {
+			dataLen := len(table.Value)
+			for i := 0; i < dataLen; i++ {
+				eDate := table.Time[i]
+				var sValue float64
+				if len(table.Value) != 0 {
+					sValue = table.Value[i]
+				}
+				code := edbCodeList[k]
+				dataTime, err := time.ParseInLocation(utils.FormatDate, eDate, time.Local)
+				if err != nil {
+					return err
+				}
+				timestamp := dataTime.UnixNano() / 1e6
+				timeStr := fmt.Sprintf("%d", timestamp)
+				indexCode := thsds+stockCode+code
+				indexCodeMap[indexCode] = indexCode
+				addSql += GetAddSql("0", indexCode, eDate, timeStr, utils.SubFloatToString(sValue, 20))
+				isAdd = true
+			}
+		}
+		if isAdd {
+			for _, v := range indexCodeMap {
+				var count int
+				sql := ` SELECT COUNT(1) FROM edb_data_ths_ds WHERE edb_code=? `
+				err = o.Raw(sql, v).QueryRow(&count)
+				if err != nil {
+					return err
+				}
+				if count > 0 {
+					sql = ` DELETE FROM edb_data_ths_ds WHERE edb_code=? `
+					_, err = o.Raw(sql, v).Exec()
+					if err != nil {
+						return err
+					}
+				}
+			}
+
+			addSql = strings.TrimRight(addSql, ",")
+			_, err = o.Raw(addSql).Exec()
+			if err != nil {
+				return
+			}
+		}
+	}
+	return
+}
+
+// 刷新同花顺指标数据
+func RefreshEdbDataFromThsDs(edbInfoId int, edbCode, startDate string, item EdbDataFromThs) (err error) {
+	o := orm.NewOrm()
+	source := utils.DATA_SOURCE_THS
+	subSource := utils.DATA_SUB_SOURCE_DATE
+
+	// 真实数据的最大日期  , 插入规则配置的日期
+	var realDataMaxDate, edbDataInsertConfigDate time.Time
+	var edbDataInsertConfig *EdbDataInsertConfig
+	var isFindConfigDateRealData bool //是否找到配置日期的实际数据的值
+	{
+		edbDataInsertConfig, err = GetEdbDataInsertConfigByEdbId(edbInfoId)
+		if err != nil && err.Error() != utils.ErrNoRow() {
+			return
+		}
+		if edbDataInsertConfig != nil {
+			edbDataInsertConfigDate = edbDataInsertConfig.Date
+		}
+	}
+
+	if len(item.Tables) > 0 {
+		var condition string
+		var pars []interface{}
+
+		condition += " AND edb_info_id=? "
+		pars = append(pars, edbInfoId)
+		if startDate != "" {
+			condition += " AND data_time>=? "
+			pars = append(pars, startDate)
+		}
+
+		existList, err := GetEdbDataByCondition(source, subSource, condition, pars)
+		if err != nil {
+			return err
+		}
+
+		existMap := make(map[string]*EdbInfoSearchData)
+		for _, v := range existList {
+			existMap[v.DataTime] = v
+		}
+
+		table := item.Tables[0]
+		dataLen := len(table.Time)
+		addSql := ` INSERT INTO edb_data_ths_ds(edb_info_id,edb_code,data_time,value,create_time,modify_time,data_timestamp) values `
+		var isAdd bool
+		addMap := make(map[string]string)
+		edbInfoIdStr := strconv.Itoa(edbInfoId)
+		for i := 0; i < dataLen; i++ {
+			eDate := table.Time[i]
+			sValue := table.Value[i]
+			sValueStr := utils.SubFloatToString(sValue, 30)
+
+			dataTime, err := time.ParseInLocation(utils.FormatDate, eDate, time.Local)
+			if err != nil {
+				return err
+			}
+			if findItem, ok := existMap[eDate]; !ok {
+				if _, addOk := addMap[eDate]; !addOk {
+					timestamp := dataTime.UnixNano() / 1e6
+					timeStr := fmt.Sprintf("%d", timestamp)
+					addSql += GetAddSql(edbInfoIdStr, edbCode, eDate, timeStr, sValueStr)
+					isAdd = true
+					addMap[eDate] = sValueStr
+				}
+			} else {
+				if findItem != nil && utils.SubFloatToString(findItem.Value, 30) != utils.SubFloatToString(sValue, 30) {
+					err = ModifyEdbDataById(source, subSource, findItem.EdbDataId, sValueStr)
+					if err != nil {
+						return err
+					}
+				}
+			}
+
+			// 下面代码主要目的是处理掉手动插入的数据判断
+			{
+				if realDataMaxDate.IsZero() || dataTime.After(realDataMaxDate) {
+					realDataMaxDate = dataTime
+				}
+				if edbDataInsertConfigDate.IsZero() || dataTime.Equal(edbDataInsertConfigDate) {
+					isFindConfigDateRealData = true
+				}
+			}
+		}
+
+		// 处理手工数据补充的配置
+		HandleConfigInsertEdbData(realDataMaxDate, edbDataInsertConfig, edbInfoId, source, subSource, existMap, isFindConfigDateRealData)
+
+		if isAdd {
+			addSql = strings.TrimRight(addSql, ",")
+			_, err = o.Raw(addSql).Exec()
+			if err != nil {
+				return err
+			}
+		}
+	}
+	return
+}

+ 5 - 0
models/edb_data_table.go

@@ -9,6 +9,11 @@ func GetEdbDataTableName(source, subSource int) (tableName string) {
 	switch source {
 	case utils.DATA_SOURCE_THS:
 		tableName = "edb_data_ths"
+		if subSource == utils.DATA_SUB_SOURCE_DATE {
+			tableName = "edb_data_ths_ds"
+		} else {
+			tableName = "edb_data_ths"
+		}
 	case utils.DATA_SOURCE_WIND:
 		if subSource == utils.DATA_SUB_SOURCE_DATE {
 			tableName = "edb_data_wind_wsd"

+ 18 - 0
routers/commentsRouter.go

@@ -961,6 +961,24 @@ func init() {
             Filters: nil,
             Params: nil})
 
+    beego.GlobalControllerRouter["eta/eta_index_lib/controllers:ThsDsController"] = append(beego.GlobalControllerRouter["eta/eta_index_lib/controllers:ThsDsController"],
+        beego.ControllerComments{
+            Method: "Add",
+            Router: `/ds/add`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_index_lib/controllers:ThsDsController"] = append(beego.GlobalControllerRouter["eta/eta_index_lib/controllers:ThsDsController"],
+        beego.ControllerComments{
+            Method: "Refresh",
+            Router: `/ds/refresh`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
     beego.GlobalControllerRouter["eta/eta_index_lib/controllers:WindController"] = append(beego.GlobalControllerRouter["eta/eta_index_lib/controllers:WindController"],
         beego.ControllerComments{
             Method: "Add",

+ 1 - 0
routers/router.go

@@ -20,6 +20,7 @@ func init() {
 		beego.NSNamespace("/ths",
 			beego.NSInclude(
 				&controllers.ThsController{},
+				&controllers.ThsDsController{},
 			),
 		),
 		beego.NSNamespace("/pb",

+ 11 - 0
services/base_from_jiayue.go

@@ -246,6 +246,17 @@ func SyncJiaYueNewIndex(item models.BridgeJiaYueIndexAndData, menus []models.Bri
 		return
 	}
 
+	// 查询指标是否已存在, 存在则忽略
+	exist, e := models.GetEdbInfoByEdbCode(sourceId, indexCode)
+	if e != nil && e.Error() != utils.ErrNoRow() {
+		err = fmt.Errorf("查询指标是否存在失败, err: %s", e.Error())
+		return
+	}
+	if exist != nil && exist.EdbInfoId > 0 {
+		utils.FileLog.Info(fmt.Sprintf("指标%s已存在, 忽略同步", indexCode))
+		return
+	}
+
 	// 从桥接服务获取指标和数据
 	var params models.BridgeJiaYueIndexDataParams
 	params.IndexCode = indexCode

+ 1 - 1
services/base_from_mysteel_chemical.go

@@ -222,7 +222,7 @@ func handleIndex(indexItem *models.HandleMysteelIndex) (err error) {
 		dataUpdateFailedReason := "服务异常"
 		_, logErrMsg, logErr := logic.RefreshBaseEdbInfo(edbInfo, ``)
 		if logErr != nil {
-			lErr = AddEdbInfoUpdateLog(edbInfo.EdbInfoId, 2, logErrMsg+err.Error(), dataUpdateResult, dataUpdateFailedReason, 1)
+			lErr = AddEdbInfoUpdateLog(edbInfo.EdbInfoId, 2, logErrMsg+logErr.Error(), dataUpdateResult, dataUpdateFailedReason, 1)
 			return
 		}
 

+ 1 - 1
services/base_from_python.go

@@ -165,7 +165,7 @@ func getPythonFileAbsolutePath(edbCode string) (pythonFile string, err error) {
 // getPythonFrontStr 获取python前面的代码
 func getPythonFrontStr() string {
 	//return "#!/usr/bin/python\n# -*- coding: UTF-8 -*-\nimport json\n\nimport pymysql\nimport pandas as pd\n\nsql_config = {\n    'host': 'rm-uf67kg347rhjfep5c1o.mysql.rds.aliyuncs.com',\n    'port': 3306,#主机号\n    'user': 'hz_technology',#账户名\n    'passwd': 'hongze@2021',#密码\n    'db': 'test_hz_data',\n    'charset': 'utf8mb4',\n    'cursorclass': pymysql.cursors.DictCursor\n}\n\ndb = pymysql.connect(**sql_config)\ndb.autocommit(1)\ncursor = db.cursor()\npandas_fetch_all = pd.read_sql\n\n# 返回数据\nresult = {}\n\n# 格式化返回数据\ndef format_data(data: pd.DataFrame,\n                index_str: str = \"data_time\",\n                value_str: str = \"value\"\n                ) -> pd.DataFrame:\n    \"\"\"\n        Parameters\n        ----------\n        data : pandas的DataFrame数据结构.\n        index_str : 对象下标字符串,在pandas的DataFrame中的列名.\n        value_str : 对象值字符串,在pandas的DataFrame中的列名\n\n        Returns\n        -------\n        DataFrame or Iterator[DataFrame]\n        例子:{'2007-01-09': 3220.0, '2007-01-10': 3230.0}\n\n        \"\"\"\n    index_list = []  # 空列表\n    value_list = []  # 空列表\n\n    for num in range(1, data.index.size):  # 迭代 所有的指标\n        index_list.append(data[index_str][num])\n        value_list.append(data[value_str][num])\n\n    tmp_data = {\n        \"date\": index_list,\n        \"value\": value_list\n    }\n    pd_data = pd.DataFrame(tmp_data)\n    # print(pd_data)\n    return pd_data\n"
-	str := fmt.Sprintf("#!/usr/bin/python\n# -*- coding: UTF-8 -*-\nimport json\n\nimport pymysql\nimport pandas as pd\n\nsql_config = {\n    'host': '%s',\n    'port': 3306,#主机号\n    'user': '%s',#账户名\n    'passwd': '%s',#密码\n    'db': '%s',\n    'charset': 'utf8mb4',\n    'cursorclass': pymysql.cursors.DictCursor\n}\n\ndb = pymysql.connect(**sql_config)\ndb.autocommit(1)\ncursor = db.cursor()\npandas_fetch_all = pd.read_sql\n\n# 返回数据\nresult = {}\n\n# 格式化返回数据\ndef format_data(data: pd.DataFrame,\n                index_str: str = \"data_time\",\n                value_str: str = \"value\"\n                ) -> pd.DataFrame:\n    \"\"\"\n        Parameters\n        ----------\n        data : pandas的DataFrame数据结构.\n        index_str : 对象下标字符串,在pandas的DataFrame中的列名.\n        value_str : 对象值字符串,在pandas的DataFrame中的列名\n\n        Returns\n        -------\n        DataFrame or Iterator[DataFrame]\n        例子:{'2007-01-09': 3220.0, '2007-01-10': 3230.0}\n\n        \"\"\"\n    index_list = []  # 空列表\n    value_list = []  # 空列表\n\n    for num in range(0, data.index.size):  # 迭代 所有的指标\n        index_list.append(data[index_str][num])\n        value_list.append(data[value_str][num])\n\n    tmp_data = {\n        \"date\": index_list,\n        \"value\": value_list\n    }\n    pd_data = pd.DataFrame(tmp_data)\n    # print(pd_data)\n    return pd_data\n\n", utils.PYTHON_MYSQL_HOST, utils.PYTHON_MYSQL_USER, utils.PYTHON_MYSQL_PASSWD, utils.PYTHON_MYSQL_DB)
+	str := fmt.Sprintf("#!/usr/bin/python\n# -*- coding: UTF-8 -*-\nimport json\n\nimport pymysql\nimport pandas as pd\n\nsql_config = {\n    'host': '%s',\n    'port': 3306,#主机号\n    'user': '%s',#账户名\n    'passwd': '%s',#密码\n    'db': '%s',\n    'charset': 'utf8mb4',\n    'cursorclass': pymysql.cursors.DictCursor\n}\n\ndb = pymysql.connect(**sql_config)\ndb.autocommit(1)\ncursor = db.cursor()\npandas_fetch_all = pd.read_sql\n\n# 返回数据\nresult = {}\n\n# 格式化返回数据\ndef format_data(data: pd.DataFrame,\n                index_str: str = \"data_time\",\n                value_str: str = \"value\"\n                ) -> pd.DataFrame:\n    \"\"\"\n        Parameters\n        ----------\n        data : pandas的DataFrame数据结构.\n        index_str : 对象下标字符串,在pandas的DataFrame中的列名.\n        value_str : 对象值字符串,在pandas的DataFrame中的列名\n\n        Returns\n        -------\n        DataFrame or Iterator[DataFrame]\n        例子:{'2007-01-09': 3220.0, '2007-01-10': 3230.0}\n\n        \"\"\"\n    index_list = []  # 空列表\n    value_list = []  # 空列表\n\n    for num in range(0, data.index.size):  # 迭代 所有的指标\n        date=data[index_str][num]\n        if isinstance(date,str) is False:\n            index_list.append(data[index_str][num].strftime(\"%%Y-%%m-%%d\"))\n        else:\n            index_list.append(data[index_str][num])\n        value_list.append(data[value_str][num])\n\n    tmp_data = {\n        \"date\": index_list,\n        \"value\": value_list\n    }\n    pd_data = pd.DataFrame(tmp_data)\n    return pd_data\n\n", utils.PYTHON_MYSQL_HOST, utils.PYTHON_MYSQL_USER, utils.PYTHON_MYSQL_PASSWD, utils.PYTHON_MYSQL_DB)
 	return str
 }
 

+ 15 - 0
services/base_from_ths.go

@@ -269,3 +269,18 @@ func getFutureGoodDataFromThsApp(edbCode, startDate, endDate string, num int, se
 	}
 	return
 }
+
+type StockDatas struct {
+	Time    string `json:"time"`
+	ThsCode string `json:"thscode"`
+	//OpenPriceStock   *float64 `json:"ths_open_price_stock"`
+	//HighPriceStock   *float64 `json:"ths_high_price_stock"`
+	//LowStock         *float64 `json:"ths_low_stock,omitempty"`
+	Value *float64
+}
+
+type TerminalResponse struct {
+	ErrorCode int          `json:"errorcode"`
+	ErrMsg    string       `json:"errmsg"`
+	Data      []map[string]interface{} `json:"data"`
+}

+ 280 - 0
services/base_from_ths_ds.go

@@ -0,0 +1,280 @@
+package services
+
+import (
+	"encoding/json"
+	"errors"
+	"eta/eta_index_lib/models"
+	"eta/eta_index_lib/utils"
+	"fmt"
+	"github.com/rdlucklib/rdluck_tools/http"
+	"strings"
+)
+
+func GetEdbDataFromThsDs(stockCode, edbCode, startDate, endDate, edbTerminalCode string) (item models.EdbDataFromThs, err error) {
+	terminal, err := GetTerminal(utils.DATA_SOURCE_THS, edbTerminalCode)
+	if err != nil {
+		err = fmt.Errorf("获取同花顺接口配置出错 Err: %s", err)
+		return
+	}
+	if terminal.ServerUrl == "" {
+		err = fmt.Errorf("同花顺接口未配置")
+		return
+	}
+
+	if edbTerminalCode == "" {
+		// 设置指标与终端关系的缓存
+		terminalCodeCacheKey := utils.CACHE_EDB_TERMINAL_CODE_URL + stockCode
+		_ = utils.Rc.Put(terminalCodeCacheKey, terminal.TerminalCode, utils.GetTodayLastSecond())
+	}
+
+	// 如果没有配置,获取配置的方式是api,那么就走官方接口
+	if utils.ThsDataMethod == "" || utils.ThsDataMethod == "api" {
+		var token string
+		token, err = GetAccessToken(false, terminal.Value)
+		if err != nil {
+			return
+		}
+		return getEdbDataFromThsDsHttp(stockCode, edbCode, startDate, endDate, terminal.Value, token)
+	}
+
+	return getEdbDataFromThsDsApp(stockCode, edbCode, startDate, endDate, 0, terminal.ServerUrl)
+}
+
+type EdbDataFromThsSdInterface struct {
+	Errorcode   int         `json:"errorcode"`
+	Errmsg      string      `json:"errmsg"`
+	Tables      []Table     `json:"tables"`
+	Datatype    []Type      `json:"datatype"`
+	InputParams interface{} `json:"inputParams"`
+	DataVol     int         `json:"dataVol"`
+	Perf        int         `json:"perf"`
+}
+
+type Table struct {
+	Thscode string                 `json:"thscode"`
+	Time    []string               `json:"time"`
+	Table   map[string]interface{} `json:"table"`
+}
+
+type StockData struct {
+	THSOpenPriceStock     []float64 `json:"ths_open_price_stock"`
+	THSHighPriceStock     []float64 `json:"ths_high_price_stock"`
+	THSLowStock           []float64 `json:"ths_low_stock"`
+	THSClosePriceStock    []float64 `json:"ths_close_price_stock"`
+	THSChgRatioStock      []float64 `json:"ths_chg_ratio_stock"`
+	THSChgStock           []float64 `json:"ths_chg_stock"`
+	THSVolStock           []float64 `json:"ths_vol_stock"`
+	THSPreCloseStock      []float64 `json:"ths_pre_close_stock"`
+	THSSwingStock         []float64 `json:"ths_swing_stock"`
+	THSTurnoverRatioStock []float64 `json:"ths_turnover_ratio_stock"`
+	THSAmtStock           []float64 `json:"ths_amt_stock"`
+}
+
+type Type struct {
+	Itemid string `json:"itemid"`
+	Type   string `json:"type"`
+}
+
+//
+//type Params struct {
+//	Jsonrpc bool     `json:"jsonrpc"`
+//	Params  []Param  `json:"params"`
+//}
+//
+//type Param struct {
+//	Function string   `json:"function"`
+//	ID       string   `json:"id"`
+//	Params   []Param2 `json:"params"`
+//}
+//
+//type Param2 struct {
+//	Name   string `json:"name"`
+//	System string `json:"system"`
+//	Value  string `json:"value"`
+//}
+
+// getEdbDataFromThsDs 获取同花顺接口数据
+func getEdbDataFromThsDsApp(stockCode, edbCode, startDate, endDate string, num int, serverUrl string) (item models.EdbDataFromThs, err error) {
+	if serverUrl == `` {
+		err = errors.New("同花顺接口未配置")
+		return
+	}
+	thsUrl := serverUrl + `edbInfo/ths/ds?StockCode=%s&EdbCode=%s&StartDate=%s&EndDate=%s`
+	thsUrl = fmt.Sprintf(thsUrl, stockCode, edbCode, startDate, endDate)
+	utils.FileLog.Info("thsUrl:" + thsUrl)
+	body, err := http.Get(thsUrl)
+	utils.FileLog.Info("ths result:" + string(body))
+	if err != nil {
+		err = errors.New(" Err:" + err.Error() + ";result:" + string(body))
+		return
+	}
+	if string(body) == "null" {
+		err = errors.New("同花顺数据获取异常:" + err.Error() + ";result:" + string(body))
+		return
+	}
+	println(string(body))
+	tablesList := make([]models.Tables, 0)
+	var errCode int64
+	if strings.Contains(edbCode, ",") {
+		var jsonArray []string
+		if err = json.Unmarshal(body, &jsonArray); err != nil {
+			fmt.Println("json.Unmarshal Err:", err)
+			return
+		}
+		// 解码数组内的每个 JSON 字符串
+		//var responses []TerminalResponse
+
+		for _, data := range jsonArray {
+			data = strings.Replace(data, "NaN", "null", -1)
+			tableTimeList := make([]string, 0)
+			tableValueList := make([]float64, 0)
+			var response TerminalResponse
+			if err = json.Unmarshal([]byte(data), &response); err != nil {
+				fmt.Println("json.Unmarshal Err:", err)
+				return
+			}
+
+			errCode = int64(response.ErrorCode)
+			if response.ErrorCode != 0 {
+				//session has expired,please re-login after using the system
+				//如果是同花顺登录session失效了,那么就重新请求获取数据
+				if response.ErrorCode == -1020 && num == 0 {
+					return getEdbDataFromThsDsApp(stockCode, edbCode, startDate, endDate, 1, serverUrl)
+				}
+				err = errors.New(string(body))
+				return
+			}
+
+			for _, stockData := range response.Data {
+				time := stockData["time"].(string)
+				//thsCode := stockData["thscode"].(string)
+				//tableTimeList = append(tableTimeList, time)
+
+				for k, v := range stockData {
+					if k != "time" && k != "thscode" {
+						if v != nil {
+							tableTimeList = append(tableTimeList, time)
+							tableValueList = append(tableValueList, v.(float64))
+						}
+					}
+				}
+			}
+			tmpTable := models.Tables{
+				ID:    []string{},
+				Time:  tableTimeList,
+				Value: tableValueList,
+			}
+			tablesList = append(tablesList, tmpTable)
+		}
+	} else {
+		var data string
+		if err = json.Unmarshal(body, &data); err != nil {
+			fmt.Println("json.Unmarshal Err:", err)
+			return
+		}
+		// 解码数组内的每个 JSON 字符串
+		//var responses []TerminalResponse
+		data = strings.Replace(data, "NaN", "null", -1)
+		tableTimeList := make([]string, 0)
+		tableValueList := make([]float64, 0)
+		var response TerminalResponse
+		if err = json.Unmarshal([]byte(data), &response); err != nil {
+			fmt.Println("json.Unmarshal Err:", err)
+			return
+		}
+
+		errCode = int64(response.ErrorCode)
+		if response.ErrorCode != 0 {
+			//session has expired,please re-login after using the system
+			//如果是同花顺登录session失效了,那么就重新请求获取数据
+			if response.ErrorCode == -1020 && num == 0 {
+				return getEdbDataFromThsDsApp(stockCode, edbCode, startDate, endDate, 1, serverUrl)
+			}
+			err = errors.New(string(body))
+			return
+		}
+
+		for _, stockData := range response.Data {
+			time := stockData["time"].(string)
+			//thsCode := stockData["thscode"].(string)
+
+			for k, v := range stockData {
+				if k != "time" && k != "thscode" {
+					if v != nil {
+						tableTimeList = append(tableTimeList, time)
+						tableValueList = append(tableValueList, v.(float64))
+					}
+				}
+			}
+		}
+		tmpTable := models.Tables{
+			ID:    []string{},
+			Time:  tableTimeList,
+			Value: tableValueList,
+		}
+		tablesList = append(tablesList, tmpTable)
+
+	}
+
+	item = models.EdbDataFromThs{
+		DataVol:   0,
+		Errmsg:    "",
+		Errorcode: errCode,
+		Perf:      "",
+		Tables:    tablesList,
+	}
+	//tmpItems := new([]TerminalResponse)
+	//err = json.Unmarshal(body, &tmpItems)
+	//if err != nil {
+	//	err = errors.New("GetEdbDataFromThs json.Unmarshal Err:" + err.Error())
+	//	return
+	//}
+
+	//// 因为table里面的value有的时候返回的是string,有的是float64,所以需要用interface来反射取值
+	//tablesList := make([]models.Tables, 0)
+	//for _, table := range tmpItems.Tables {
+	//	tableIdList := make([]string, 0)
+	//	tableTimeList := make([]string, 0)
+	//	tableValueList := make([]float64, 0)
+	//
+	//	for _, tableId := range table.ID {
+	//		tableIdList = append(tableIdList, tableId)
+	//	}
+	//	for _, tableTime := range table.Time {
+	//		tableTimeList = append(tableTimeList, tableTime)
+	//	}
+	//
+	//	//指标数据
+	//	for _, tmpValue := range table.Value {
+	//		var tableValue float64
+	//		if reflect.TypeOf(tmpValue).Kind() == reflect.Float64 {
+	//			tableValue = reflect.ValueOf(tmpValue).Float()
+	//		} else if reflect.TypeOf(tmpValue).Kind() == reflect.String {
+	//			tmpTableValue, tmpErr := decimal.NewFromString(reflect.ValueOf(tmpValue).String())
+	//			if tmpErr != nil {
+	//				err = tmpErr
+	//				return
+	//			}
+	//			tableValue, _ = tmpTableValue.Truncate(4).Float64()
+	//		} else {
+	//			err = errors.New("错误的数据类型" + reflect.TypeOf(tmpValue).String())
+	//			return
+	//		}
+	//		tableValueList = append(tableValueList, tableValue)
+	//	}
+	//	tmpTable := models.Tables{
+	//		ID:    tableIdList,
+	//		Time:  tableTimeList,
+	//		Value: tableValueList,
+	//	}
+	//	tablesList = append(tablesList, tmpTable)
+	//}
+	//item = models.EdbDataFromThs{
+	//	DataVol:   tmpItems.DataVol,
+	//	Errmsg:    tmpItems.Errmsg,
+	//	Errorcode: tmpItems.Errorcode,
+	//	Perf:      tmpItems.Perf,
+	//	Tables:    tablesList,
+	//}
+	return item, nil
+}

+ 207 - 0
services/base_from_ths_ds_http.go

@@ -0,0 +1,207 @@
+package services
+
+import (
+	"encoding/json"
+	"errors"
+	"eta/eta_index_lib/models"
+	"fmt"
+	"reflect"
+	"strings"
+)
+
+// getEdbDataFromThsDsHttp 通过url获取同花顺的日期序列数据
+func getEdbDataFromThsDsHttp(stockCode, edbCode, startDate, endDate, thsRefreshToken, token string) (item models.EdbDataFromThs, err error) {
+	thsUrl := "https://quantapi.51ifind.com/api/v1/date_sequence"
+	//indicators 是 半角逗号分隔的所有指标,宏观指标过多,推荐使用Windows超级命令生成。 "indicators":"M001620326,M002822183"
+	//functionpara 否 key-value格式,省略时不进行更新时间筛选。两个时间控件更新起始时间(startrtime)和更新结束时间(endrtime),不勾选时省略见下方代码块
+	//startdate 是 开始日期,支持”YYYYMMDD"”YYYY-MM-DD"”YYYY/MM/DD"三种时间格式 "startdate":"2018-01-01"
+	//enddate 是 结束日期,支持”YYYYMMDD"”YYYY-MM-DD"”YYYY/MM/DD"三种日期格式 "enddate":"2018-01-01
+	//发送创建请求
+	// 分割字符串
+	paramList := strings.Split(edbCode, ",")
+
+	// 创建一个包含映射的切片
+	var indipara []map[string]interface{}
+
+	// 遍历分割后的参数列表
+	for _, param := range paramList {
+		// 创建一个映射
+		paramMap := map[string]interface{}{
+			"indicator": param,
+		}
+
+		// 将映射添加到切片中
+		indipara = append(indipara, paramMap)
+	}
+	dataMap := map[string]interface{}{
+		"codes":     stockCode,
+		"startdate": startDate,
+		"enddate":   endDate,
+		"indipara":  indipara,
+	}
+
+	body, err, _ := postCurl(thsUrl, dataMap, 0, thsRefreshToken, token)
+	if err != nil {
+		return
+	}
+
+	tmpItems := new(EdbDataFromThsSdInterface)
+	err = json.Unmarshal(body, &tmpItems)
+	if err != nil {
+		err = errors.New("GetEdbDataFromThs json.Unmarshal Err:" + err.Error())
+		return
+	}
+	if tmpItems.Errorcode != 0 {
+		err = errors.New(tmpItems.Errmsg)
+		return
+	}
+
+	// 请求的字段名
+	edbCodeList := strings.Split(edbCode, ",")
+
+	// 遍历请求的字段,并从 map 中获取对应的结构体字段
+	// 证券代码为多个时有多个table,目前只有一个
+	tablesList := make([]models.Tables, 0)
+	tableTimeList := make([]string, 0)
+	if len(tmpItems.Tables) > 0 {
+		//for _, time := range tmpItems.Tables[0].Time{
+		//	tableTimeList = append(tableTimeList, time)
+		//}
+		for _, field := range edbCodeList {
+			tableValueList := make([]float64, 0)
+
+			if values, ok := tmpItems.Tables[0].Table[field]; ok {
+				// 在这里,value 是一个接口类型,需要使用反射获取切片的值
+				sliceValue := reflect.ValueOf(values)
+				for i := 0; i < sliceValue.Len(); i++ {
+					fmt.Printf("%s: %v\n", field, sliceValue.Index(i).Interface())
+
+					var tableValue float64
+					value := reflect.ValueOf(sliceValue.Index(i).Interface())
+					if value.Kind() == reflect.Float64 {
+						tableValue = value.Float()
+						if tableValue != 0 {
+							tableTimeList = append(tableTimeList, tmpItems.Tables[0].Time[i])
+							tableValueList = append(tableValueList, tableValue)
+						}
+					}
+				}
+			} else {
+				fmt.Printf("Field '%s' not found\n", field)
+			}
+			tmpTable := models.Tables{
+				ID:  []string{},
+				Time:  tableTimeList,
+				Value: tableValueList,
+			}
+			tablesList = append(tablesList, tmpTable)
+		}
+
+		//// 因为table里面的value有的时候返回的是string,有的是float64,所以需要用interface来反射取值
+		//tablesList := make([]models.Tables, 0)
+		//for _, table := range tmpItems.Tables[0].Time {
+		//	//tableCodeList := make([]string, 0)
+		//	tableTimeList := make([]string, 0)
+		//	tableValueList := make([]float64, 0)
+		//
+		//	//for _, code := range table.Thscode {
+		//	//	tableCodeList = append(tableCodeList, code)
+		//	//}
+		//	for _, tableTime := range table {
+		//		tableTimeList = append(tableTimeList, tableTime)
+		//	}
+		//
+		//	//指标数据
+		//	for _, tmpValue := range table {
+		//		var tableValue float64
+		//		if reflect.TypeOf(tmpValue).Kind() == reflect.Float64 {
+		//			tableValue = reflect.ValueOf(tmpValue).Float()
+		//		} else if reflect.TypeOf(tmpValue).Kind() == reflect.String {
+		//			tmpTableValue, tmpErr := decimal.NewFromString(reflect.ValueOf(tmpValue).String())
+		//			if tmpErr != nil {
+		//				err = tmpErr
+		//				return
+		//			}
+		//			tableValue, _ = tmpTableValue.Truncate(4).Float64()
+		//		} else {
+		//			err = errors.New("错误的数据类型" + reflect.TypeOf(tmpValue).String())
+		//			return
+		//		}
+		//		tableValueList = append(tableValueList, tableValue)
+		//	}
+		//	tmpTable := models.Tables{
+		//		ID:    []string{},
+		//		Time:  tableTimeList,
+		//		Value: tableValueList,
+		//	}
+		//	tablesList = append(tablesList, tmpTable)
+		//}
+	}
+
+	item = models.EdbDataFromThs{
+		DataVol:   int64(tmpItems.DataVol),
+		Errmsg:    tmpItems.Errmsg,
+		Errorcode: int64(tmpItems.Errorcode),
+		Perf:      tmpItems.Perf,
+		Tables:    tablesList,
+	}
+	return
+}
+
+// getFutureGoodDataFromThsHttp 通过url获取同花顺的商品数据
+//func getFutureGoodDataFromThsHttp(edbCode, startDate, endDate, thsRefreshToken, token string) (item future_good.FutureGoodDataFromThs, err error) {
+//	thsUrl := `https://quantapi.51ifind.com/api/v1/cmd_history_quotation`
+//
+//	//codes 是 半角逗号分隔的所有代码 "codes":"300033.SZ,600030.SH"
+//	//indicators 是 半角逗号分隔的所有指标 "indicators":"preClose,open"
+//	//functionpara 否 /key-value格式。所有key均取默认时,functionpara省略。 见下方说明
+//	//startdate 是 开始日期,支持"YYYYMMDD""YYYY-MMDD""YYYY/MM/DD"三种日期格式
+//	//"startdate":"2018-01-01"
+//	//enddate 是 结束日期,支持"YYYYMMDD""YYYY-MMDD""YYYY/MM/DD"三种日期格式
+//	//发送创建请求
+//	dataMap := map[string]interface{}{
+//		"codes":      edbCode,
+//		"indicators": `lastclose,open,high,low,close,avgprice,change,changeper,volume,amount,hsl,lastsettlement,settlement,zdsettlement,zdfsettlement,ccl,ccbd,zf,zjlx,zjcd`,
+//		"startdate":  startDate,
+//		"enddate":    endDate,
+//	}
+//
+//	body, err, _ := postCurl(thsUrl, dataMap, 0, thsRefreshToken, token)
+//	if err != nil {
+//		return
+//	}
+//
+//	tmpItems := new(FutureGoodDataFromThsInterface)
+//	err = json.Unmarshal(body, &tmpItems)
+//	if err != nil {
+//		err = errors.New("GetEdbDataFromThs json.Unmarshal Err:" + err.Error())
+//		return
+//	}
+//	if tmpItems.Errorcode != 0 {
+//		err = errors.New(tmpItems.Errmsg)
+//		return
+//	}
+//
+//	if len(tmpItems.Tables) <= 0 {
+//		return
+//	}
+//	table := tmpItems.Tables[0]
+//	item = future_good.FutureGoodDataFromThs{
+//		DataVol:   tmpItems.DataVol,
+//		Errmsg:    tmpItems.Errmsg,
+//		Errorcode: tmpItems.Errorcode,
+//		Perf:      tmpItems.Perf,
+//		Tables: future_good.FutureGoodDataTables{
+//			Time:       table.Time,
+//			Open:       table.Table.Open,
+//			High:       table.Table.High,
+//			Low:        table.Table.Low,
+//			Close:      table.Table.Close,
+//			Volume:     table.Table.Volume,
+//			Amount:     table.Table.Amount,
+//			Ccl:        table.Table.Ccl,
+//			Settlement: table.Table.Settlement,
+//		},
+//	}
+//	return
+//}

+ 36 - 0
utils/common.go

@@ -24,6 +24,7 @@ import (
 	"strconv"
 	"strings"
 	"time"
+	"unicode"
 )
 
 // 随机数种子
@@ -1127,3 +1128,38 @@ func CheckFrequency(leftFrequency, rightFrequency string) int {
 
 	return frequencyMap[leftFrequency] - frequencyMap[rightFrequency]
 }
+
+// 将下划线命名转为驼峰命名
+func SnakeToCamel(s string) string {
+	var result string
+	upper := true
+	for _, c := range s {
+		if c == '_' {
+			upper = true
+			continue
+		}
+		if upper {
+			result += string(unicode.ToUpper(c))
+			upper = false
+		} else {
+			result += string(c)
+		}
+	}
+	return result
+}
+
+// 将驼峰命名转为下划线命名
+func CamelToSnake(s string) string {
+	var result string
+	for i, c := range s {
+		if unicode.IsUpper(c) {
+			if i > 0 {
+				result += "_"
+			}
+			result += string(unicode.ToLower(c))
+		} else {
+			result += string(c)
+		}
+	}
+	return result
+}