pf 2 years ago
parent
commit
d89165b2b8

File diff suppressed because it is too large
+ 1 - 0
.agollo


+ 9 - 0
.idea/goiot-cronapi.iml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="WEB_MODULE" version="4">
+  <component name="Go" enabled="true" />
+  <component name="NewModuleRootManager">
+    <content url="file://$MODULE_DIR$" />
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+</module>

+ 8 - 0
.idea/modules.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/goiot-cronapi.iml" filepath="$PROJECT_DIR$/.idea/goiot-cronapi.iml" />
+    </modules>
+  </component>
+</project>

+ 61 - 0
.idea/workspace.xml

@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ChangeListManager">
+    <list default="true" id="d2dc8b4c-4ed5-48e2-a519-676a1d7dbb24" name="Default Changelist" comment="" />
+    <option name="SHOW_DIALOG" value="false" />
+    <option name="HIGHLIGHT_CONFLICTS" value="true" />
+    <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
+    <option name="LAST_RESOLUTION" value="IGNORE" />
+  </component>
+  <component name="GOROOT" path="F:\go1.16.1" />
+  <component name="ProjectId" id="2K4PmQBLpUatdNosnOrRDehsIQz" />
+  <component name="ProjectViewState">
+    <option name="hideEmptyMiddlePackages" value="true" />
+    <option name="showLibraryContents" value="true" />
+  </component>
+  <component name="PropertiesComponent">
+    <property name="RunOnceActivity.OpenProjectViewOnStart" value="true" />
+    <property name="WebServerToolWindowFactoryState" value="false" />
+    <property name="go.import.settings.migrated" value="true" />
+    <property name="go.sdk.automatically.set" value="true" />
+    <property name="last_opened_file_path" value="$PROJECT_DIR$" />
+  </component>
+  <component name="RunManager">
+    <configuration name="go build main.go" type="GoApplicationRunConfiguration" factoryName="Go Application" nameIsGenerated="true">
+      <module name="goiot-cronapi" />
+      <working_directory value="$PROJECT_DIR$" />
+      <go_parameters value="-i" />
+      <kind value="FILE" />
+      <filePath value="$PROJECT_DIR$/main.go" />
+      <package value="goiot-cronapi" />
+      <directory value="$PROJECT_DIR$" />
+      <method v="2" />
+    </configuration>
+  </component>
+  <component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
+  <component name="TypeScriptGeneratedFilesManager">
+    <option name="version" value="3" />
+  </component>
+  <component name="WindowStateProjectService">
+    <state x="223" y="67" width="1089" height="714" key="#com.intellij.execution.impl.EditConfigurationsDialog" timestamp="1673231198358">
+      <screen x="0" y="0" width="1536" height="824" />
+    </state>
+    <state x="223" y="67" width="1089" height="714" key="#com.intellij.execution.impl.EditConfigurationsDialog/0.0.1536.824@0.0.1536.824" timestamp="1673231198358" />
+    <state width="1493" height="207" key="GridCell.Tab.0.bottom" timestamp="1673232220053">
+      <screen x="0" y="0" width="1536" height="824" />
+    </state>
+    <state width="1493" height="207" key="GridCell.Tab.0.bottom/0.0.1536.824@0.0.1536.824" timestamp="1673232220053" />
+    <state width="1493" height="207" key="GridCell.Tab.0.center" timestamp="1673232220053">
+      <screen x="0" y="0" width="1536" height="824" />
+    </state>
+    <state width="1493" height="207" key="GridCell.Tab.0.center/0.0.1536.824@0.0.1536.824" timestamp="1673232220053" />
+    <state width="1493" height="207" key="GridCell.Tab.0.left" timestamp="1673232220053">
+      <screen x="0" y="0" width="1536" height="824" />
+    </state>
+    <state width="1493" height="207" key="GridCell.Tab.0.left/0.0.1536.824@0.0.1536.824" timestamp="1673232220053" />
+    <state width="1493" height="207" key="GridCell.Tab.0.right" timestamp="1673232220053">
+      <screen x="0" y="0" width="1536" height="824" />
+    </state>
+    <state width="1493" height="207" key="GridCell.Tab.0.right/0.0.1536.824@0.0.1536.824" timestamp="1673232220053" />
+  </component>
+</project>

+ 6 - 0
app.properties

@@ -0,0 +1,6 @@
+{
+    "appId": "LHR210118",
+    "cluster": "default",
+    "namespaceNames": ["application", "LH202101041547.mysql-config", "LH202101041547.redis-config", "LH202101041547.image-config"],
+    "ip": "apollo.l-worldchina.com:8080"
+}

+ 50 - 0
common/common.go

@@ -0,0 +1,50 @@
+package common
+
+import (
+	"github.com/gin-gonic/gin"
+	"net/http"
+)
+
+type ResponseJson struct {
+	Code    uint        `yaml:"code"    json:"code"`           // response code
+	Message string      `yaml:"message" json:"message"`        // response message
+	Data    interface{} `yaml:"data"    json:"data,omitempty"` // response data
+}
+
+/**
+返回成功Response
+*/
+func ReturnSuccess(c *gin.Context, data interface{}) {
+	if data != nil {
+		c.JSON(http.StatusOK, gin.H{
+			"code":    200,
+			"message": "SUCCESS",
+			"data":    data,
+		})
+	} else {
+		c.JSON(http.StatusOK, gin.H{
+			"code":    200,
+			"message": "SUCCESS",
+		})
+	}
+}
+
+/**
+返回失败Response
+*/
+func ReturnFailure(c *gin.Context, code int, msg string) {
+	c.JSON(http.StatusOK, gin.H{
+		"code":    code,
+		"message": msg,
+	})
+}
+
+/**
+返回错误Response
+*/
+func ReturnError(c *gin.Context, code int, msg string) {
+	c.JSON(code, gin.H{
+		"code":    code,
+		"message": msg,
+	})
+}

+ 110 - 0
common/config.go

@@ -0,0 +1,110 @@
+package common
+
+import (
+	"fmt"
+	"github.com/gin-gonic/gin"
+	"goiot-pkg/tools"
+	"gopkg.in/yaml.v2"
+	"io/ioutil"
+	"os"
+	"path"
+	"runtime"
+)
+
+var ConfigInfo *configModel
+
+//serverModel get server information from config.yml
+type serverModel struct {
+	Mode        string `yaml:"mode"`         // run mode
+	Host        string `yaml:"host"`         // server host
+	Port        string `yaml:"port"`         // server port
+	TokenExpire int64  `yaml:"token_expire"` // token expire seconds
+}
+
+//logModel get log information from config.yml
+type logModel struct {
+	Compress      int    `yaml:"compress"`
+	ConsoleStdout int    `yaml:"console_stdout"`
+	FileStdout    int    `yaml:"file_stdout"`
+	Level         string `yaml:"level"`
+	LocalTime     int    `yaml:"local_time"`
+	MaxAge        int    `yaml:"max_age"`
+	MaxBackups    int    `yaml:"max_backups"`
+	MaxSize       int    `yaml:"max_size"`
+	Path          string `yaml:"path"`
+}
+
+type configModel struct {
+	Server      *serverModel `yaml:"server"`
+	Log         *logModel    `yaml:"log"`
+	FileServUrl string
+}
+
+// LoadConfigInformation load config information for application
+func LoadConfigInformation() error {
+
+	var err error
+
+	// 先读取config.yml配置文件
+	// 获取当前是debug模式还是release模式, 根据模式读取不同的Apollo配置
+	var filePath string
+	wr, _ := os.Getwd()
+	if runtime.GOOS == "windows" {
+		filePath = wr + "\\conf\\config.yml"
+	} else {
+		filePath = path.Join(path.Join(wr, "conf"), "config.yml")
+	}
+
+	var configData []byte
+	configData, err = ioutil.ReadFile(filePath)
+	if err != nil {
+		fmt.Printf(" config file read failed: %s", err)
+		return err
+	}
+
+	err = yaml.Unmarshal(configData, &ConfigInfo)
+	if err != nil {
+		fmt.Printf(" config parse failed: %s", err)
+		return err
+	}
+
+	SetGinMode(ConfigInfo.Server.Mode)
+
+	return err
+}
+
+func LoadApolloConfig() (*tools.ApolloConfig, error) {
+	var err error
+
+	// 连接Apollo
+	err = tools.NewApollo()
+	if err != nil {
+		return nil, err
+	}
+
+	// 获取Apollo配置
+	var config *tools.ApolloConfig
+	config, err = tools.GetApolloConfig()
+	if err != nil {
+		return nil, err
+	}
+
+	// 按需加载应用配置信息
+	loadAppConfig(tools.GetApollo().GetNameSpace("application"))
+
+	return config, nil
+}
+
+func SetGinMode(mode string) {
+	if mode == "debug" {
+		gin.SetMode(gin.DebugMode)
+	} else if mode == "release" {
+		gin.SetMode(gin.ReleaseMode)
+	}
+}
+
+func loadAppConfig(m map[string]interface{}) {
+	if fileServ, ok := m["fileserv_url"]; ok {
+		ConfigInfo.FileServUrl = fileServ.(string)
+	}
+}

+ 20 - 0
common/const.go

@@ -0,0 +1,20 @@
+package common
+
+/// 定义错误码
+const (
+	Success              = 200
+	InvalidArgumentError = 1001
+	InternalError        = 1002
+	RecordNotFound       = 1003
+)
+
+const (
+	CtrlNameSystem       = "mainCtrl"
+	CtrlNameHost         = "hostCtrl"
+	CtrlNameNewTrend     = "newTrendCtrl"
+	CtrlNameHeatExchange = "heatExchangeCtrl"
+	CtrlNameEnd          = "endCtrl"
+)
+
+// MJAppCode 墨迹天气APPCODE
+const MJAppCode = "APPCODE 0053e6eb28264e6c90bfbdb1eb201800"

+ 49 - 0
common/icon.go

@@ -0,0 +1,49 @@
+package common
+
+/// WeatherIconMap 天气图标对照表
+var WeatherIconMap = map[string]string{
+	"晴":       "W0",
+	"大部晴朗":    "W0",
+	"多云":      "W1",
+	"少云":      "W1",
+	"阴":       "W2",
+	"阵雨":      "W3",
+	"局部阵雨":    "W3",
+	"小阵雨":     "W3",
+	"强阵雨":     "W3",
+	"雷阵雨":     "W4",
+	"雷电":      "W4",
+	"雷暴":      "W4",
+	"雷阵雨伴有冰雹": "W5",
+	"冰雹":      "W5",
+	"冰针":      "W5",
+	"冰粒":      "W5",
+	"雨夹雪":     "W6",
+	"小雨":      "W7",
+	"小到中雨":    "W7",
+	"中雨":      "W8",
+	"雨":       "W8",
+	"大雨":      "W9",
+	"中到大雨":    "W9",
+	"暴雨":      "W10",
+	"大到暴雨":    "W10",
+	"大暴雨":     "W10",
+	"特大暴雨":    "W10",
+	"阵雪":      "W13",
+	"小阵雪":     "W13",
+	"小雪":      "W14",
+	"中雪":      "W15",
+	"雪":       "W15",
+	"小到中雪":    "W15",
+	"大雪":      "W16",
+	"中到大雪":    "W16",
+	"暴雪":      "W17",
+	"雾":       "W18",
+	"冻雾":      "W18",
+	"沙尘暴":     "W20",
+	"强沙尘暴":    "W20",
+	"浮尘":      "W29",
+	"尘卷风":     "W29",
+	"扬沙":      "W29",
+	"霾":       "W45",
+}

+ 20 - 0
conf/config.yml

@@ -0,0 +1,20 @@
+# config information
+
+# server config information
+server:
+  mode: debug # debug,release
+  host: 0.0.0.0
+  port: 4004
+  token_expire_second: 360000
+
+# log config information
+log:
+  compress: 0
+  console_stdout: 1
+  file_stdout: 1
+  level: debug
+  local_time: 1
+  max_age: 30
+  max_backups: 300
+  max_size: 10240
+  path: ./logs/goiot-cronapi.log # log file path container log file name

+ 46 - 0
controllers/consume.go

@@ -0,0 +1,46 @@
+package controllers
+
+import (
+	"fmt"
+	"github.com/gin-gonic/gin"
+	"goiot-cronapi/common"
+	"goiot-cronapi/global/orm"
+	"goiot-cronapi/pkg/logger"
+	"goiot-cronapi/services"
+	"math"
+)
+
+// MaxCountOnce 定义每次计算多少条数据
+const MaxCountOnce = 100
+
+// Consume 每天23:00计算一次所有耗材的消耗率
+func Consume(c *gin.Context) {
+
+	// 先查询出总共有多少条记录
+	var totalCount int64
+	sql := `SELECT COUNT(id) FROM btk_consumables_management WHERE LENGTH(box_id) > 0 AND utilization_rate > 0 AND consumption_cycle > 0 AND is_delete = 0`
+	orm.Eloquent.Raw(sql).Count(&totalCount)
+
+	pageCount := int(math.Ceil(float64(totalCount) / float64(MaxCountOnce)))
+	logger.Info(fmt.Sprintf("查询到有%d条记录, 每页处理%d条, 共需处理%d次", totalCount, MaxCountOnce, pageCount))
+
+	// 获取每个盒子对应的运行总时长
+	timeMap := services.GetBoxRunTime(orm.Eloquent)
+
+	// 开始同步消耗率
+	for page := 0; page < pageCount; page++ {
+		datas := services.GetConsumeDatas(orm.Eloquent, page, MaxCountOnce)
+		if err := services.UpdateConsumeRate(orm.Eloquent, datas, timeMap); err != nil {
+			logger.Info(fmt.Sprintf("第%d页数据同步失败, 已回滚!!!", page+1))
+			continue
+		}
+		if err := services.SaveMessage(orm.Eloquent, datas); err != nil {
+			logger.Info(fmt.Sprintf("第%d页数据同步失败, 已回滚!!!", page+1))
+			continue
+		}
+		logger.Info(fmt.Sprintf("第%d页数据同步成功", page+1))
+	}
+
+	logger.Info("同步完成...")
+	common.ReturnSuccess(c, nil)
+}

+ 17 - 0
controllers/demo.go

@@ -0,0 +1,17 @@
+package controllers
+
+import (
+	"github.com/gin-gonic/gin"
+	"goiot-cronapi/common"
+	"net/http"
+	"strconv"
+)
+
+func Demo(c *gin.Context) {
+	isErr, _ := strconv.Atoi(c.Query("isErr"))
+	if isErr == 1 {
+		common.ReturnError(c, http.StatusBadRequest, "错误")
+	} else {
+		common.ReturnSuccess(c, nil)
+	}
+}

+ 81 - 0
controllers/weather.go

@@ -0,0 +1,81 @@
+package controllers
+
+import (
+	"github.com/gin-gonic/gin"
+	"go.uber.org/zap"
+	"goiot-cronapi/common"
+	"goiot-cronapi/global/orm"
+	"goiot-cronapi/global/redis"
+	"goiot-cronapi/models"
+	"goiot-cronapi/pkg/logger"
+	"goiot-cronapi/services"
+	"time"
+)
+
+// WeatherSample
+// 每15分钟采样一次天气接口数据
+// 从Redis中取出所有盒子对应的项目地区的天气信息
+// 如果取到了直接存入数据库中
+// 如果没取到则从接口拉取再同步到Redis, 最后再存到数据库中
+func WeatherSample(c *gin.Context) {
+
+	var err error
+
+	// 获取所有在线的盒子列表
+	areaList := services.GetAreaList(orm.Eloquent)
+	logger.Info("采样城市数", zap.Int("NUM", len(areaList)))
+
+	for _, area := range areaList {
+		weather := services.GetWeatherFromRedis(redis.Cacher, area.CityId)
+		if weather.CityId <= 0 || len(weather.Temp) <= 0 || len(weather.Humidity) <= 0 {
+			go saveWeatherData(area)
+			continue
+		}
+		// 直接存到数据库中
+		if err = saveWeatherToDB(weather); err != nil {
+			continue
+		}
+	}
+
+	common.ReturnSuccess(c, nil)
+}
+
+// 请求天气数据并存入到Redis和数据库中
+func saveWeatherData(area *models.AreaModel) {
+	// 从墨迹接口拉取天气数据
+	condition, err := services.RequestCondition(area.CityId)
+	if err == nil {
+		if aqi, aqiErr := services.RequestAqi(area.CityId); aqiErr == nil {
+			weather := models.Weather{
+				CityId:    area.CityId,
+				Temp:      condition.Data.Condition.Temp,
+				Humidity:  condition.Data.Condition.Humidity,
+				Condition: condition.Data.Condition.Condition,
+				Aqi:       aqi.Data.Aqi.Value,
+				AqiLevel:  services.GetAirLevel(aqi.Data.Aqi.Value),
+				PM25:      aqi.Data.Aqi.PM25,
+				Province:  area.Province,
+				City:      area.City,
+				Area:      area.District,
+				Icon:      services.GetWeatherIcon(condition.Data.Condition.Condition),
+				Created:   time.Now().Format("2006-01-02 15:04:05"),
+			}
+			if len(weather.Area) <= 0 {
+				weather.Area = area.Area
+			}
+			// 保存到Redis中
+			if err = weather.SaveToRedis(redis.Cacher); err != nil {
+				logger.Info("更新Redis失败", zap.String("Error", err.Error()))
+			}
+			// 同时保存到数据库中
+			if err = saveWeatherToDB(weather); err != nil {
+				logger.Info("更新DB失败", zap.String("Error", err.Error()))
+			}
+		}
+	}
+}
+
+func saveWeatherToDB(w models.Weather) error {
+	sql := "INSERT INTO btk_runstat_weather(cityId, temp, humidity, pm25) VALUES(?, ?, ?, ?)"
+	return orm.Eloquent.Exec(sql, w.CityId, w.Temp, w.Humidity, w.PM25).Error
+}

+ 5 - 0
global/orm/db.go

@@ -0,0 +1,5 @@
+package orm
+
+import "github.com/jinzhu/gorm"
+
+var Eloquent *gorm.DB

+ 5 - 0
global/redis/redis.go

@@ -0,0 +1,5 @@
+package redis
+
+import "github.com/aiscrm/redisgo"
+
+var Cacher *redisgo.Cacher

BIN
goiot-cronapi


File diff suppressed because it is too large
+ 129816 - 0
logs/goiot-cronapi.log


+ 62 - 0
main.go

@@ -0,0 +1,62 @@
+package main
+
+import (
+	"go.uber.org/zap"
+	"goiot-cronapi/common"
+	"goiot-cronapi/global/orm"
+	"goiot-cronapi/global/redis"
+	"goiot-cronapi/pkg/logger"
+	"goiot-cronapi/router"
+	"goiot-pkg/tools"
+	"net/http"
+)
+
+// @title LiHua Cloud Platform Server Api
+// @version 1.0
+// @description This is lihua cloud platform api
+func main() {
+
+	engine := router.InitRouter()
+
+	server := common.ConfigInfo.Server
+	serverInfo := server.Host + ":" + server.Port
+	logger.Info("Server Start Info: " + serverInfo)
+
+	if err := engine.Run(serverInfo); err != nil && err != http.ErrServerClosed {
+		logger.Fatal("Listen", zap.String("[FATAL]", err.Error()))
+	}
+}
+
+func init() {
+
+	var err error
+
+	// 加载本地配置
+	err = common.LoadConfigInformation()
+	if err != nil {
+		return
+	}
+
+	// 加载Apollo配置
+	var apolloConf *tools.ApolloConfig
+	apolloConf, err = common.LoadApolloConfig()
+	if err != nil {
+		return
+	}
+
+	orm.Eloquent = apolloConf.DB
+	redis.Cacher = apolloConf.Redis
+
+	// 日志初始化
+	logger.Init(&logger.Config{
+		Compress:      common.ConfigInfo.Log.Compress == 1,
+		ConsoleStdout: common.ConfigInfo.Log.ConsoleStdout == 1,
+		FileStdout:    common.ConfigInfo.Log.FileStdout == 1,
+		Level:         common.ConfigInfo.Log.Level,
+		LocalTime:     common.ConfigInfo.Log.LocalTime == 1,
+		MaxAge:        common.ConfigInfo.Log.MaxAge,
+		MaxBackups:    common.ConfigInfo.Log.MaxBackups,
+		MaxSize:       common.ConfigInfo.Log.MaxSize,
+		Path:          common.ConfigInfo.Log.Path,
+	})
+}

+ 30 - 0
models/air_product.go

@@ -0,0 +1,30 @@
+package models
+
+type AirProduct struct {
+	BoxId        string      `gorm:"column:boxId"`
+	StandaloneId int         `gorm:"column:standaloneId"`
+	MsLocalTemp  interface{} `gorm:"column:msLocalTemp"` // 温度
+	MsLocalHumi  interface{} `gorm:"column:msLocalHumi"` // 湿度
+	MsLocalCo2   interface{} `gorm:"column:msLocalCo2"`  // CO2浓度
+	MsLocalVoc   interface{} `gorm:"column:msLocalVoc"`  // TVOC浓度
+	MsLocalPm25  interface{} `gorm:"column:msLocalPm25"` // PM2.5浓度
+	MsLocalMeth  interface{} `gorm:"column:msLocalMeth"` // 甲醛浓度
+}
+
+func (AirProduct) TableName() string {
+	return "btk_runstat_system"
+}
+
+type SampleAirProduct struct {
+	BoxId        string      `gorm:"column:box_id;primary_key;"`
+	StandaloneId int         `gorm:"column:standalone_id;primary_key;"`
+	MsLocalTemp  interface{} `gorm:"column:msLocalTemp"`
+	MsLocalHumi  interface{} `gorm:"column:msLocalHumi"`
+	MsLocalCo2   interface{} `gorm:"column:msLocalCo2"`
+	MsLocalVoc   interface{} `gorm:"column:msLocalVoc"`
+	MsLocalPm25  interface{} `gorm:"column:msLocalPm25"`
+}
+
+func (SampleAirProduct) TableName() string {
+	return "t_statistics_airprod"
+}

+ 11 - 0
models/ammeter.go

@@ -0,0 +1,11 @@
+package models
+
+type Ammeter struct {
+	BoxId        string      `gorm:"column:boxId"`
+	StandaloneId int         `gorm:"column:standaloneId"`
+	MsTotalElec  interface{} `gorm:"column:msTotalElec"`
+}
+
+func (Ammeter) TableName() string {
+	return "btk_runstat_ammeter"
+}

+ 22 - 0
models/box.go

@@ -0,0 +1,22 @@
+package models
+
+type BoxBase struct {
+	BoxId      string `json:"box_id"      gorm:"column:box_id"`
+	BoxName    string `json:"box_name"    gorm:"column:box_name"`
+	BoxVersion string `json:"box_version" gorm:"column:box_version"`
+	BoxOutput  string `json:"box_output"  gorm:"column:box_output"`
+}
+
+type AreaModel struct {
+	Province string `gorm:"column:province"`
+	City     string `gorm:"column:city"`
+	Area     string `gorm:"column:area"`
+	District string `gorm:"column:district"`
+	CityId   int64  `gorm:"column:city_id"`
+}
+type AreaList []*AreaModel
+
+type BoxTime struct {
+	BoxId string `gorm:"column:box_id"`
+	Time  int64  `gorm:"column:time"`
+}

+ 14 - 0
models/consume.go

@@ -0,0 +1,14 @@
+package models
+
+type ConsumeData struct {
+	ID           int64
+	Name         string  `gorm:"column:name"`
+	BoxId        string  `gorm:"column:box_id"`
+	Rate         float64 `gorm:"column:utilization_rate"`
+	Cycle        int     `gorm:"column:consumption_cycle"`
+	Ratio        float64
+	ResetRuntime float64 `gorm:"column:reset_runtime"`
+	ProjectName  string
+	Ascription   string
+	UserId       string
+}

+ 33 - 0
models/end.go

@@ -0,0 +1,33 @@
+package models
+
+type CbStandalone struct {
+	BoxId            string      `gorm:"column:boxId"`
+	StandaloneId     int         `gorm:"column:standaloneId"`
+	CbTempSetStep    interface{} `gorm:"column:cbTempSetStep"`    // 温控步进值
+	CbCoTempSetULim  interface{} `gorm:"column:cbCoTempSetULim"`  // 制冷温度设定上限
+	CbCoTempSetLLim  interface{} `gorm:"column:cbCoTempSetLLim"`  // 制冷温度设定下限
+	CbHeTempSetLLim  interface{} `gorm:"column:cbHeTempSetLLim"`  // 采暖温度设定下限
+	CbHeTempSetULim  interface{} `gorm:"column:cbHeTempSetULim"`  // 采暖温度设定上限
+	CbCoEquipStat    interface{} `gorm:"column:cbCoEquipStat"`    // 制冷设备状态
+	CbHeEquipStat    interface{} `gorm:"column:cbHeEquipStat"`    // 制热设备状态
+	CbCoEquipCntrDev interface{} `gorm:"column:cbCoEquipCntrDev"` // 制冷装置控制偏移量
+	CbHeEquipCntrDev interface{} `gorm:"column:cbHeEquipCntrDev"` // 制热设备控制偏移量
+	CbSetTemp        interface{} `gorm:"column:cbSetTemp"`        // 设定温度
+	CbLastSetTemp    interface{} `gorm:"column:cbLastSetTemp"`    // 上次设定温度
+	CbLastFanSp      interface{} `gorm:"column:cbLastFanSp"`      // 上次设定风速(仅对流)
+	CbCoSetFanSp     interface{} `gorm:"column:cbCoSetFanSp"`     // 制冷设备风速 0~3 自动	低挡	中档	高档
+	CbHeSetFanSp     interface{} `gorm:"column:cbHeSetFanSp"`     // 制热设备风速
+	CbLocalTemp      interface{} `gorm:"column:cbLocalTemp"`      // 面板温度
+	CbLocalHumi      interface{} `gorm:"column:cbLocalHumi"`      // 面板湿度
+	CbLocalDewP      interface{} `gorm:"column:cbLocalDewP"`      // 面板露点
+	CbBoardTemp      interface{} `gorm:"column:cbBoardTemp"`      // 辐射板面温度
+	CbBoardHumi      interface{} `gorm:"column:cbBoardHumi"`      // 辐射板面湿度
+	CbBoardDewP      interface{} `gorm:"column:cbBoardDewP"`      // 辐射板面露点
+	CbLocalCo2       interface{} `gorm:"column:cbLocalCo2"`       // 二氧化碳浓度
+	CbLocalVoc       interface{} `gorm:"column:cbLocalVoc"`       // TVOC浓度
+	CbLocalPm25      interface{} `gorm:"column:cbLocalPm25"`      // PM2.5浓度
+}
+
+func (CbStandalone) TableName() string {
+	return "btk_runstat_cbstandalone"
+}

+ 21 - 0
models/heat_exchange.go

@@ -0,0 +1,21 @@
+package models
+
+type HexStandalone struct {
+	BoxId           string      `gorm:"column:boxId"`
+	StandaloneId    int         `gorm:"column:standaloneId"`
+	HexSuTemp       interface{} `gorm:"column:hexSuTemp"`       // 供水温度
+	HexReTemp       interface{} `gorm:"column:hexReTemp"`       // 回水温度
+	Hex3wRateFdbk   interface{} `gorm:"column:hex3wRateFdbk"`   // 三通阀开度反馈
+	Hex3wStep       interface{} `gorm:"column:hex3wStep"`       // 三通阀调整步进值
+	Hex3wAdjPer     interface{} `gorm:"column:hex3wAdjPer"`     // 三通阀调节周期
+	HexPumpRateFdbk interface{} `gorm:"column:hexPumpRateFdbk"` // 水泵运行频率反馈
+	HexDewPMax      interface{} `gorm:"column:hexDewPMax"`      // 域内露点高值
+	HexSuTarTemp    interface{} `gorm:"column:hexSuTarTemp"`    // 供水目标温度
+	HexReTarTemp    interface{} `gorm:"column:hexReTarTemp"`    // 回水目标温度
+	Hex3wSetRate    interface{} `gorm:"column:hex3wSetRate"`    // 三通阀设定开度
+	HexPumpSetRate  interface{} `gorm:"column:hexPumpSetRate"`  // 循环泵频率
+}
+
+func (HexStandalone) TableName() string {
+	return "btk_runstat_hexstandalone"
+}

+ 32 - 0
models/host.go

@@ -0,0 +1,32 @@
+package models
+
+type HostCtrl struct {
+	BoxId        string      `gorm:"column:boxId"`
+	HpSuIndiTemp interface{} `gorm:"column:hpSuIndiTemp"` // 主机指征供水温度,采集数据
+	HpReIndiTemp interface{} `gorm:"column:hpReIndiTemp"` // 主机指征回水温度,采集数据
+}
+
+type HpStandalone struct {
+	BoxId             string      `gorm:"column:boxId"`
+	StandaloneId      int         `gorm:"column:standaloneId"`
+	HpSuTemp          interface{} `gorm:"column:hpSuTemp"`          // 主机供水温度
+	HpReTemp          interface{} `gorm:"column:hpReTemp"`          // 主机回水温度
+	HpComp1Rate       interface{} `gorm:"column:hpComp1Rate"`       // 1#压缩机频率
+	HpComp2Rate       interface{} `gorm:"column:hpComp2Rate"`       // 2#压缩机频率
+	HpAttPump1Fdbk    interface{} `gorm:"column:hpAttPump1Fdbk"`    // 源侧水泵反馈
+	HpAttPump2Fdbk    interface{} `gorm:"column:hpAttPump2Fdbk"`    // 空调侧水泵反馈
+	HpSuTarTemp       interface{} `gorm:"column:hpSuTarTemp"`       // 供水目标温度
+	HpReTarTemp       interface{} `gorm:"column:hpReTarTemp"`       // 回水目标温度
+	HpAttPump1SetRate interface{} `gorm:"column:hpAttPump1SetRate"` // 源侧水泵设置值
+	HpAttPump2SetRate interface{} `gorm:"column:hpAttPump2SetRate"` // 空调水泵设置值
+	HpEnviTemp        interface{} `gorm:"column:hpEnviTemp"`        // 环境温度
+	HpStatFdbk        interface{} `gorm:"column:hpStatFdbk"`        // 主机开机反馈
+}
+
+func (HostCtrl) TableName() string {
+	return "btk_runstat_host"
+}
+
+func (HpStandalone) TableName() string {
+	return "btk_runstat_hpstandalone"
+}

+ 51 - 0
models/message.go

@@ -0,0 +1,51 @@
+package models
+
+import "github.com/jinzhu/gorm"
+
+type Message struct {
+	ProjectName string
+	ConsumeName string
+	SourceId    int64
+	UserId      string
+}
+
+type MessageInfo struct {
+	ID       int64  `gorm:"column:id;primary_key;auto_increment;"`
+	Title    string `gorm:"column:title"`
+	Content  string `gorm:"column:content"`
+	SourceId int64  `gorm:"column:source_id"`
+	Type     int    `gorm:"column:type"`
+	Subtype  int    `gorm:"column:subtype"`
+}
+
+func (MessageInfo) TableName() string {
+	return "btk_message_info"
+}
+
+func (m Message) GetMessageTitle() string {
+	return "【励华科技】提醒您, “" + m.ProjectName + "”" + m.ConsumeName + "建议更换, 请及时处理。"
+}
+
+func (m Message) GetMessageContent() string {
+	return "“" + m.ProjectName + "”" + m.ConsumeName + "建议更换, 您可以进入 服务-服务管理 模块进行预约更换, " +
+		"我们在收到你的预约后将与您联系确认上门时间, 请保证联系方式正确并保持通讯正常。\n感谢您的支持!"
+}
+
+func (m Message) Save(db *gorm.DB) error {
+	var err error
+	var msgInfo = MessageInfo{
+		Title:    m.GetMessageTitle(),
+		Content:  m.GetMessageContent(),
+		SourceId: m.SourceId,
+		Type:     1,
+		Subtype:  2,
+	}
+	if err = db.Save(&msgInfo).Error; err != nil {
+		return err
+	}
+	sql := "INSERT INTO btk_message_user_relation(message_id, message_type, receiver_id) VALUES(?,?,?)"
+	if err = db.Exec(sql, msgInfo.ID, 1, m.UserId).Error; err != nil {
+		return err
+	}
+	return nil
+}

+ 28 - 0
models/new_trend.go

@@ -0,0 +1,28 @@
+package models
+
+type DhStandalone struct {
+	BoxId               string      `gorm:"column:boxId"`
+	StandaloneId        int         `gorm:"column:standaloneId"`
+	DhSuAirTemp         interface{} `gorm:"column:dhSuAirTemp"`         // 送风温度
+	DhSuAirHumi         interface{} `gorm:"column:dhSuAirHumi"`         // 送风湿度
+	DhSuAirDewP         interface{} `gorm:"column:dhSuAirDewP"`         // 送风露点
+	DhFrAirTemp         interface{} `gorm:"column:dhFrAirTemp"`         // 新风温度
+	DhFrAirHumi         interface{} `gorm:"column:dhFrAirHumi"`         // 新风湿度
+	DhFrAirDewP         interface{} `gorm:"column:dhFrAirDewP"`         // 新风露点
+	DhReAirTemp         interface{} `gorm:"column:dhReAirTemp"`         // 回风温度
+	DhReAirHumi         interface{} `gorm:"column:dhReAirHumi"`         // 回风湿度
+	DhReAirDewP         interface{} `gorm:"column:dhReAirDewP"`         // 回风露点
+	DhSuAirDhRateFdbk   interface{} `gorm:"column:dhSuAirDhRateFdbk"`   // 温控装置开度反馈
+	DhSuAirRhSwitchFdbk interface{} `gorm:"column:dhSuAirRhSwitchFdbk"` // 再热装置状态反馈
+	DhSuAirAhSwitchFdbk interface{} `gorm:"column:dhSuAirAhSwitchFdbk"` // 加湿装置状态反馈
+	DhSuAirFanSp        interface{} `gorm:"column:dhSuAirFanSp"`        // 送风风速
+	DhExAirFanSp        interface{} `gorm:"column:dhExAirFanSp"`        // 排风风速
+	DhSuAirTarTemp      interface{} `gorm:"column:dhSuAirTarTemp"`      // 送风设定温度
+	DhSuAirTarDewP      interface{} `gorm:"column:dhSuAirTarDewP"`      // 送风设定露点
+	DhReAirTarTemp      interface{} `gorm:"column:dhReAirTarTemp"`      // 回风设定温度
+	DhReAirTarDewP      interface{} `gorm:"column:dhReAirTarDewP"`      // 回风设定露点
+}
+
+func (DhStandalone) TableName() string {
+	return "btk_runstat_dhstandalone"
+}

+ 22 - 0
models/runstat.go

@@ -0,0 +1,22 @@
+package models
+
+import pkg "goiot-pkg/models"
+
+type AllRunstat struct {
+	MsRunStat  *pkg.MsRunStat  `json:"mainCtrl"`
+	HpRunStat  *pkg.HpRunStat  `json:"hostCtrl"`
+	DhRunStat  *pkg.DhRunStat  `json:"newTrendCtrl"`
+	HexRunStat *pkg.HexRunStat `json:"heatExchangeCtrl"`
+	CbRunStat  *pkg.CbRunStat  `json:"endCtrl"`
+}
+
+type Runstat struct {
+	ID               string `gorm:"column:id;primary_key;auto_increment;" redis:"-"`
+	BoxId            string `gorm:"column:box_id"             redis:"-"`
+	MainCtrl         string `gorm:"column:main_ctrl"          redis:"mainCtrl"         json:"mainCtrl"`
+	HostCtrl         string `gorm:"column:host_ctrl"          redis:"hostCtrl"         json:"hostCtrl"`
+	NewTrendCtrl     string `gorm:"column:new_trend_ctrl"     redis:"newTrendCtrl"     json:"newTrendCtrl"`
+	HeatExchangeCtrl string `gorm:"column:heat_exchange_ctrl" redis:"heatExchangeCtrl" json:"heatExchangeCtrl"`
+	EndCtrl          string `gorm:"column:end_ctrl"           redis:"endCtrl"          json:"endCtrl"`
+	IndependTaskCtrl string `gorm:"column:independ_task_ctrl" redis:"independTaskCtrl" json:"independTaskCtrl"`
+}

+ 19 - 0
models/system.go

@@ -0,0 +1,19 @@
+package models
+
+type SampleMode struct {
+	BoxId string `gorm:"column:box_id;primary_key;"`
+	Mode  int    `gorm:"column:mode"`
+}
+
+type SampleScene struct {
+	BoxId string `gorm:"column:box_id;primary_key;"`
+	Scene int    `gorm:"column:scene"`
+}
+
+func (SampleMode) TableName() string {
+	return "t_statistics_mode"
+}
+
+func (SampleScene) TableName() string {
+	return "t_statistics_scene"
+}

+ 58 - 0
models/weather.go

@@ -0,0 +1,58 @@
+package models
+
+import (
+	"encoding/json"
+	"github.com/aiscrm/redisgo"
+	"strconv"
+)
+
+type ConditionResp struct {
+	Code int `json:"code"`
+	Data struct {
+		Condition struct {
+			Condition string `json:"condition"` // 实时天气
+			Humidity  string `json:"humidity"`  // 湿度
+			Temp      string `json:"temp"`      // 温度
+			Tips      string `json:"tips"`      // 提示
+			WindDir   string `json:"windDir"`   // 风向
+			WindLevel string `json:"windLevel"` // 风级
+			WindSpeed string `json:"windSpeed"` // 风速
+		} `json:"condition"`
+	} `json:"data"`
+}
+
+type AqiResp struct {
+	Code int `json:"code"`
+	Data struct {
+		Aqi struct {
+			Value string `json:"value"` // 空气质量指数值
+			PM25  string `json:"pm25"`  // PM2.5指数
+			Rank  string `json:"rank"`  // 全国排名
+		} `json:"aqi"`
+	} `json:"data"`
+}
+
+type Weather struct {
+	CityId    int64  `json:"cityId"    gorm:"column:cityId"`   // 城市ID
+	Temp      string `json:"temp"      gorm:"column:temp"`     // 温度
+	Humidity  string `json:"humidity"  gorm:"column:humidity"` // 湿度
+	Condition string `json:"condition" gorm:"-"`               // 天气
+	Aqi       string `json:"aqi"       gorm:"-"`               // 空气指数
+	AqiLevel  string `json:"aqiLevel"  gorm:"-"`               // 空气指数等级
+	PM25      string `json:"pm25"      gorm:"column:pm25"`     // PM2.5
+	Province  string `json:"province"  gorm:"-"`               // 省
+	City      string `json:"city"      gorm:"-"`               // 市
+	Area      string `json:"area"      gorm:"-"`               // 区
+	Icon      string `json:"icon"      gorm:"-"`               // 天气图标
+	Created   string `json:"created"   gorm:"-"`               // 创建日期
+}
+
+func (w *Weather) SaveToRedis(rds *redisgo.Cacher) error {
+	jsonBytes, err := json.Marshal(w)
+	if err != nil {
+		return err
+	}
+	key := "CITY:" + strconv.FormatInt(w.CityId, 10)
+	err = rds.Set(key, string(jsonBytes), 900)
+	return err
+}

+ 101 - 0
pkg/logger/logger.go

@@ -0,0 +1,101 @@
+package logger
+
+import (
+	"github.com/natefinch/lumberjack"
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+	"os"
+	"time"
+)
+
+// error logger
+var log *zap.SugaredLogger
+
+var levelMap = map[string]zapcore.Level{
+	"debug": zapcore.DebugLevel,
+	"info":  zapcore.InfoLevel,
+	"warn":  zapcore.WarnLevel,
+	"error": zapcore.ErrorLevel,
+	"panic": zapcore.PanicLevel,
+	"fatal": zapcore.FatalLevel,
+}
+
+type Config struct {
+	Compress      bool
+	ConsoleStdout bool
+	FileStdout    bool
+	Level         string
+	LocalTime     bool
+	MaxAge        int
+	MaxBackups    int
+	MaxSize       int
+	Path          string
+}
+
+func Init(conf *Config) {
+	var syncWriters []zapcore.WriteSyncer
+	level := getLoggerLevel(conf.Level)
+	fileConfig := &lumberjack.Logger{
+		Filename:   conf.Path,       // 日志文件名
+		MaxSize:    conf.MaxSize,    // 日志文件大小
+		MaxAge:     conf.MaxAge,     // 最长保存天数
+		MaxBackups: conf.MaxBackups, // 最多备份几个
+		LocalTime:  conf.LocalTime,  // 日志时间戳
+		Compress:   conf.Compress,   // 是否压缩文件,使用gzip
+	}
+	encoder := zap.NewProductionEncoderConfig()
+	encoder.EncodeTime = func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
+		enc.AppendString(t.Format("2006-01-02 15:04:05"))
+	}
+	if conf.ConsoleStdout {
+		syncWriters = append(syncWriters, zapcore.AddSync(os.Stdout))
+	}
+	if conf.FileStdout {
+		syncWriters = append(syncWriters, zapcore.AddSync(fileConfig))
+	}
+	core := zapcore.NewCore(
+		zapcore.NewJSONEncoder(encoder),
+		zapcore.NewMultiWriteSyncer(syncWriters...),
+		zap.NewAtomicLevelAt(level))
+	logger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1))
+	log = logger.Sugar()
+}
+
+func getLoggerLevel(lvl string) zapcore.Level {
+	if level, ok := levelMap[lvl]; ok {
+		return level
+	}
+	return zapcore.InfoLevel
+}
+
+func Debug(args ...interface{}) {
+	log.Debug(args...)
+}
+
+func Info(args ...interface{}) {
+	log.Info(args...)
+}
+
+func Infof(format string, args ...interface{}) {
+	log.Infof(format, args...)
+}
+
+func Warn(args ...interface{}) {
+	log.Warn(args...)
+}
+
+func Error(args ...interface{}) {
+	log.Error(args...)
+}
+
+func DPanic(args ...interface{}) {
+	log.DPanic(args...)
+}
+
+func Panic(args ...interface{}) {
+	log.Panic(args...)
+}
+
+func Fatal(args ...interface{}) {
+	log.Fatal(args...)
+}

+ 28 - 0
router/router.go

@@ -0,0 +1,28 @@
+package router
+
+import (
+	"github.com/gin-gonic/gin"
+	"goiot-cronapi/controllers"
+	"gopkg"
+)
+
+func InitRouter() *gin.Engine {
+
+	// Creates a gin router with default middleware:
+	// logger and recovery (crash-free) middleware
+	router := gin.Default()
+
+	g := router.Group("/api")
+	g.Use(gin.Recovery())
+	g.Use(gopkg.CORSMiddleware())
+
+	// use ginSwagger middleware to
+	//router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
+
+	// Cron
+	//g.POST("/cron/sample", controllers.Sample)
+	//g.POST("/cron/consume", controllers.Consume)
+	g.POST("/cron/weather", controllers.WeatherSample)
+
+	return router
+}

+ 45 - 0
services/box.go

@@ -0,0 +1,45 @@
+package services
+
+import (
+	"github.com/jinzhu/gorm"
+	"goiot-cronapi/models"
+)
+
+// GetBoxIdList 获取所有在线的盒子列表
+func GetBoxIdList(db *gorm.DB) []string {
+	var boxList []models.BoxBase
+	sql := "SELECT box_id FROM btk_box WHERE box_is_delete = 0 AND box_state = 1"
+	db.Raw(sql).Find(&boxList)
+	var boxIds = make([]string, 0)
+	for _, box := range boxList {
+		if len(box.BoxId) > 0 {
+			boxIds = append(boxIds, box.BoxId)
+		}
+	}
+	return boxIds
+}
+
+// GetAreaList 获取所有在线的盒子对应的地区
+func GetAreaList(db *gorm.DB) models.AreaList {
+	var list models.AreaList
+	sql := `SELECT p.province, p.city, p.area, p.district, p.city_id FROM btk_box b
+			JOIN btk_project p ON p.project_auto_id = b.project_auto_id 
+			WHERE b.box_is_delete = 0 AND b.box_state = 1 AND p.city_id > 0
+			GROUP BY p.city_id`
+	db.Raw(sql).Find(&list)
+	return list
+}
+
+// 获取每个盒子对应的运行总时长
+func GetBoxRunTime(db *gorm.DB) map[string]int64 {
+	var (
+		list    []models.BoxTime
+		timeMap = make(map[string]int64)
+	)
+	sql := "SELECT box_id, SUM(count) as time FROM btk_run_time GROUP BY box_id"
+	db.Raw(sql).Find(&list)
+	for _, value := range list {
+		timeMap[value.BoxId] = value.Time
+	}
+	return timeMap
+}

+ 67 - 0
services/consume.go

@@ -0,0 +1,67 @@
+package services
+
+import (
+	"goiot-cronapi/models"
+
+	"github.com/jinzhu/gorm"
+)
+
+func GetConsumeDatas(db *gorm.DB, count, pageSize int) []models.ConsumeData {
+	var consumeList []models.ConsumeData
+	sql := `SELECT m.id, m.name, m.box_id, m.utilization_rate, m.consumption_cycle, m.ascription, m.reset_runtime, p.project_name, p.user_id 
+			FROM btk_consumables_management m 
+			LEFT JOIN btk_project p 
+			ON p.project_auto_id = m.project_auto_id 
+			WHERE LENGTH(m.box_id) > 0 AND m.utilization_rate > 0 AND m.consumption_cycle > 0 AND m.is_delete = 0 
+			ORDER BY m.id DESC`
+	db.Raw(sql).Limit(pageSize).Offset(count * pageSize).Find(&consumeList)
+	for i, data := range consumeList {
+		data.Ratio = 100 / float64(data.Cycle)
+		consumeList[i] = data
+	}
+	return consumeList
+}
+
+func UpdateConsumeRate(db *gorm.DB, datas []models.ConsumeData, m map[string]int64) error {
+	var (
+		err error
+		sql string
+	)
+	tx := db.Begin()
+	for _, data := range datas {
+		if data.Cycle <= 0 {
+			continue
+		}
+		if data.Ascription == "盒子" {
+			// 如果是盒子, 计算损耗率
+			runtime := float64(m[data.BoxId])
+			cycle := float64(data.Cycle * 24 * 60)
+			if data.ResetRuntime > runtime {
+				data.ResetRuntime = runtime
+			}
+			data.Rate = (cycle - (runtime - data.ResetRuntime)) / cycle * 100
+			if data.Rate < 0 {
+				data.Rate = 0
+			}
+			sql = "UPDATE btk_consumables_management SET utilization_rate = ? WHERE id = ?"
+			err = tx.Exec(sql, data.Rate, data.ID).Error
+		} else {
+			// 如果是项目, 按照自然日每天扣减损耗率
+			if data.Rate >= data.Ratio {
+				sql = "UPDATE btk_consumables_management SET utilization_rate = utilization_rate - ? WHERE id = ?"
+				err = tx.Exec(sql, data.Ratio, data.ID).Error
+			} else {
+				sql = "UPDATE btk_consumables_management SET utilization_rate = 0 WHERE id = ?"
+				err = tx.Exec(sql, data.ID).Error
+			}
+		}
+		if err != nil {
+			tx.Rollback()
+			break
+		}
+	}
+	if err == nil {
+		tx.Commit()
+	}
+	return err
+}

+ 105 - 0
services/interface.go

@@ -0,0 +1,105 @@
+package services
+
+import (
+	pkg "goiot-pkg/models"
+	"reflect"
+)
+
+// 把结构体转换为Int
+func InterfaceToInt(data interface{}) int {
+	if p, ok := data.(float64); ok {
+		return int(p)
+	}
+	return 0
+}
+
+// 把结构体转换为Bool
+func InterfaceToBool(data interface{}) bool {
+	if p, ok := data.(bool); ok {
+		return p
+	}
+	return false
+}
+
+// 把结构体转换为Float32
+func InterfaceToFloat32(data interface{}) float32 {
+	if p, ok := data.(float64); ok {
+		return float32(p)
+	}
+	return 0.
+}
+
+// 把结构体转换为IntDic
+func InterfaceToIntDic(data interface{}) pkg.IntDic {
+	value := reflect.ValueOf(data)
+	if value.Kind() != reflect.Slice {
+		return nil
+	}
+	var intDic = make(pkg.IntDic, 0)
+	for i := 0; i < value.Len(); i++ {
+		em := reflect.ValueOf(data).Index(i).Elem()
+		if em.Kind() != reflect.Map {
+			continue
+		}
+		intP := pkg.IntP{}
+		for _, key := range em.MapKeys() {
+			if p, ok := em.MapIndex(key).Interface().(string); ok {
+				intP.Key = p
+			} else if p, ok := em.MapIndex(key).Interface().(float64); ok {
+				intP.Value = int(p)
+			}
+		}
+		intDic = append(intDic, intP)
+	}
+	return intDic
+}
+
+// 把结构体转换为BoolDic
+func InterfaceToBoolDic(data interface{}) pkg.BoolDic {
+	value := reflect.ValueOf(data)
+	if value.Kind() != reflect.Slice {
+		return nil
+	}
+	var boolDic = make(pkg.BoolDic, 0)
+	for i := 0; i < value.Len(); i++ {
+		em := reflect.ValueOf(data).Index(i).Elem()
+		if em.Kind() != reflect.Map {
+			continue
+		}
+		boolP := pkg.BoolP{}
+		for _, key := range em.MapKeys() {
+			if p, ok := em.MapIndex(key).Interface().(string); ok {
+				boolP.Key = p
+			} else if p, ok := em.MapIndex(key).Interface().(bool); ok {
+				boolP.Value = p
+			}
+		}
+		boolDic = append(boolDic, boolP)
+	}
+	return boolDic
+}
+
+// 把结构体转换为StringDic
+func InterfaceToStringDic(data interface{}) pkg.StringDic {
+	value := reflect.ValueOf(data)
+	if value.Kind() != reflect.Slice {
+		return nil
+	}
+	var stringDic = make(pkg.StringDic, 0)
+	for i := 0; i < value.Len(); i++ {
+		em := reflect.ValueOf(data).Index(i).Elem()
+		if em.Kind() != reflect.Map {
+			continue
+		}
+		stringP := pkg.StringP{}
+		for _, key := range em.MapKeys() {
+			if p, ok := em.MapIndex(key).Interface().(string); ok {
+				stringP.Key = p
+			} else if p, ok := em.MapIndex(key).Interface().(string); ok {
+				stringP.Value = p
+			}
+		}
+		stringDic = append(stringDic, stringP)
+	}
+	return stringDic
+}

+ 31 - 0
services/message.go

@@ -0,0 +1,31 @@
+package services
+
+import (
+	"github.com/jinzhu/gorm"
+	"goiot-cronapi/models"
+)
+
+// 保存消息
+func SaveMessage(db *gorm.DB, datas []models.ConsumeData) error {
+	var err error
+	tx := db.Begin()
+	for _, data := range datas {
+		rate := data.Rate - data.Ratio
+		if rate <= 20 {
+			m := models.Message{
+				ProjectName: data.ProjectName,
+				ConsumeName: data.Name,
+				SourceId:    data.ID,
+				UserId:      data.UserId,
+			}
+			if err = m.Save(tx); err != nil {
+				tx.Rollback()
+				break
+			}
+		}
+	}
+	if nil == err {
+		tx.Commit()
+	}
+	return nil
+}

+ 109 - 0
services/weather.go

@@ -0,0 +1,109 @@
+package services
+
+import (
+	"encoding/json"
+	"github.com/aiscrm/redisgo"
+	"goiot-cronapi/common"
+	"goiot-cronapi/models"
+	"io/ioutil"
+	"net/http"
+	"strconv"
+	"time"
+)
+
+// GetWeatherFromRedis 从Redis中获取天气信息
+func GetWeatherFromRedis(rds *redisgo.Cacher, cityId int64) models.Weather {
+	var weather models.Weather
+	key := "CITY:" + strconv.FormatInt(cityId, 10)
+	exists, _ := rds.Exists(key)
+	if !exists {
+		return weather
+	}
+	data, _ := rds.GetString(key)
+	if err := json.Unmarshal([]byte(data), &weather); err == nil {
+		return weather
+	}
+	return weather
+}
+
+// RequestCondition 从墨迹天气接口获取天气信息
+func RequestCondition(cityId int64) (*models.ConditionResp, error) {
+	url := "http://aliv13.data.moji.com/whapi/json/alicityweather/condition?cityId=" + strconv.FormatInt(cityId, 10)
+	req, err := http.NewRequest("POST", url, nil)
+	if err != nil {
+		return nil, err
+	}
+	req.Header.Set("Authorization", common.MJAppCode)
+	client := &http.Client{Timeout: time.Second * 10}
+	// Send request
+	resp, err := client.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer func() {
+		_ = resp.Body.Close()
+	}()
+	body, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+	var cr models.ConditionResp
+	if err := json.Unmarshal(body, &cr); err != nil {
+		return nil, err
+	}
+	return &cr, nil
+}
+
+// RequestAqi 从墨迹天气接口获取空气质量
+func RequestAqi(cityId int64) (*models.AqiResp, error) {
+	url := "http://aliv13.data.moji.com/whapi/json/alicityweather/aqi?cityId=" + strconv.FormatInt(cityId, 10)
+	req, err := http.NewRequest("POST", url, nil)
+	if err != nil {
+		return nil, err
+	}
+	req.Header.Set("Authorization", common.MJAppCode)
+	client := &http.Client{Timeout: time.Second * 10}
+	// Send request
+	resp, err := client.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer func() {
+		_ = resp.Body.Close()
+	}()
+	body, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+	var ar models.AqiResp
+	if err := json.Unmarshal(body, &ar); err != nil {
+		return nil, err
+	}
+	return &ar, nil
+}
+
+func GetAirLevel(aqistr string) string {
+	aqi, _ := strconv.Atoi(aqistr)
+	level := "其他"
+	if aqi > 0 && aqi <= 50 {
+		level = "优"
+	} else if aqi > 50 && aqi <= 100 {
+		level = "良"
+	} else if aqi > 100 && aqi <= 150 {
+		level = "轻度污染"
+	} else if aqi > 150 && aqi <= 200 {
+		level = "中度污染"
+	} else if aqi > 200 && aqi <= 300 {
+		level = "重度污染"
+	} else if aqi > 300 && aqi <= 500 {
+		level = "严重污染"
+	} else if aqi > 500 {
+		level = "爆表"
+	}
+	return level
+}
+
+func GetWeatherIcon(condition string) string {
+	iconName := common.WeatherIconMap[condition]
+	return common.ConfigInfo.FileServUrl + "/moji/" + iconName + ".png"
+}

+ 75 - 0
utils/crypto.go

@@ -0,0 +1,75 @@
+package utils
+
+import (
+	"bytes"
+	"crypto/cipher"
+	"encoding/base64"
+	"encoding/hex"
+	"strings"
+)
+
+const (
+	KeyLength = 8
+)
+
+func encrypt(block cipher.Block, src, key, iv []byte) []byte {
+	blockSize := block.BlockSize()
+	src = pkcs5Padding(src, blockSize)
+	mode := cipher.NewCBCEncrypter(block, genBytes(iv, blockSize))
+	encrypted := make([]byte, len(src))
+	mode.CryptBlocks(encrypted, src)
+	return encrypted
+}
+
+func decrypt(block cipher.Block, encrypted, key, iv []byte) []byte {
+	mode := cipher.NewCBCDecrypter(block, genBytes(iv, block.BlockSize()))
+	src := make([]byte, len(encrypted))
+	mode.CryptBlocks(src, encrypted)
+	return pkcs5UnPadding(src)
+}
+
+func pkcs5Padding(data []byte, blockSize int) []byte {
+	padding := blockSize - len(data)%blockSize
+	padtext := bytes.Repeat([]byte{byte(padding)}, padding)
+	return append(data, padtext...)
+}
+
+func pkcs5UnPadding(data []byte) []byte {
+	length := len(data)
+	// 去掉最后一个字节 unpadding 次
+	unpadding := int(data[length-1])
+	return data[:(length - unpadding)]
+}
+
+func genBytes(originalBytes []byte, length int) []byte {
+	tmp := make([]byte, length)
+	if len(originalBytes) < length {
+		for i := 0; i < length; i++ {
+			tmp[i] = originalBytes[i%len(originalBytes)]
+		}
+	} else {
+		for i := 0; i < length; i++ {
+			tmp[i] = originalBytes[i]
+		}
+	}
+	return tmp
+}
+
+func bytes2String(data []byte, base64Encoding bool) string {
+	if base64Encoding {
+		return base64.StdEncoding.EncodeToString(data)
+	} else {
+		return strings.ToUpper(hex.EncodeToString(data))
+	}
+}
+
+func string2Bytes(data string, base64Encoding bool) ([]byte, error) {
+	var tmp []byte
+	var err error
+	if base64Encoding {
+		tmp, err = base64.StdEncoding.DecodeString(data)
+	} else {
+		tmp, err = hex.DecodeString(data)
+	}
+	return tmp, err
+}

+ 56 - 0
utils/des.go

@@ -0,0 +1,56 @@
+package utils
+
+import (
+	"crypto/cipher"
+	"crypto/des"
+	"fmt"
+)
+
+type DES struct {
+	block   cipher.Block
+	key, iv []byte
+}
+
+// 定义Key与向量的值
+var DesKey = []byte{102, 16, 93, 156, 78, 4, 218, 32}
+var DesIv = []byte{55, 103, 246, 79, 36, 99, 167, 3}
+
+func DESEncrypt(src []byte) []byte {
+	des, err := newDESInstance(DesKey, DesIv)
+	if err != nil {
+		fmt.Println("DES Encrypt Error: ", err)
+		return nil
+	}
+	return encrypt(des.block, src, des.key, des.iv)
+}
+
+func DESEncryptString(src string, base64Encoding bool) string {
+	tmp := DESEncrypt([]byte(src))
+	return bytes2String(tmp, base64Encoding)
+}
+
+func DESDecrypt(encrypted []byte) []byte {
+	des, err := newDESInstance(DesKey, DesIv)
+	if err != nil {
+		fmt.Println("DES Encrypt Error: ", err)
+		return nil
+	}
+	return decrypt(des.block, encrypted, des.key, des.iv)
+}
+
+func DESDecryptString(encrypted string, base64Encoding bool) (string, error) {
+	tmp, err := string2Bytes(encrypted, base64Encoding)
+	if err != nil {
+		return "", err
+	}
+	return string(DESDecrypt(tmp)), err
+}
+
+func newDESInstance(key, iv []byte) (*DES, error) {
+	key = genBytes(key, KeyLength)
+	block, err := des.NewCipher(key)
+	if err != nil {
+		return nil, err
+	}
+	return &DES{block: block, key: key, iv: iv}, err
+}

+ 17 - 0
utils/error.go

@@ -0,0 +1,17 @@
+package utils
+
+import "strconv"
+
+// EscapeError Escape Error
+type EscapeError string
+
+func (e EscapeError) Error() string {
+	return "invalid URL escape " + strconv.Quote(string(e))
+}
+
+// InvalidHostError Invalid Host Error
+type InvalidHostError string
+
+func (e InvalidHostError) Error() string {
+	return "invalid character " + strconv.Quote(string(e)) + " in host name"
+}

+ 50 - 0
utils/interface.go

@@ -0,0 +1,50 @@
+package utils
+
+import (
+	"reflect"
+	"strconv"
+)
+
+func InterfaceToMap(data interface{}) map[string]interface{} {
+	t := reflect.TypeOf(data)
+	v := reflect.ValueOf(data)
+	if v.Kind() != reflect.Struct {
+		return nil
+	}
+	fieldNum := t.NumField()
+	result := make(map[string]interface{})
+	for i := 0; i < fieldNum; i++ {
+		kind := t.Field(i).Type.Kind()
+		if kind == reflect.Array || kind == reflect.Slice || kind == reflect.Struct {
+			continue
+		}
+		result[t.Field(i).Name] = v.Field(i).Interface()
+	}
+	return result
+}
+
+// InterfaceToInt 把结构体转换为Int
+func InterfaceToInt(data interface{}) int {
+	if p, ok := data.(string); ok {
+		intVal, _ := strconv.Atoi(p)
+		return intVal
+	} else if p, ok := data.(int64); ok {
+		return int(p)
+	} else if p, ok := data.(float64); ok {
+		return int(p)
+	}
+	return 0
+}
+
+// InterfaceToFloat64 把结构体转换为Float
+func InterfaceToFloat64(data interface{}) float64 {
+	if p, ok := data.(string); ok {
+		floatVal, _ := strconv.ParseFloat(p, 10)
+		return floatVal
+	} else if p, ok := data.(int64); ok {
+		return float64(p)
+	} else if p, ok := data.(float64); ok {
+		return p
+	}
+	return 0.0
+}

+ 14 - 0
utils/md5.go

@@ -0,0 +1,14 @@
+package utils
+
+import "crypto/md5"
+
+func MD5EncodeBytes(str string) []byte {
+	md5Ctx := md5.New()
+	md5Ctx.Write([]byte(str))
+	return md5Ctx.Sum(nil)
+}
+
+func MD5EncodeString(str string) string {
+	bytes := MD5EncodeBytes(str)
+	return string(bytes)
+}

+ 37 - 0
utils/util.go

@@ -0,0 +1,37 @@
+package utils
+
+import "reflect"
+
+const DateTimeFormat = "2006-01-02 15:04:05"
+
+func DatasToMap(datas []string) map[string]string {
+	m := make(map[string]string, len(datas)/2)
+	for i, data := range datas {
+		if i%2 == 0 {
+			m[data] = ""
+		} else {
+			key := datas[i-1]
+			m[key] = data
+		}
+	}
+	return m
+}
+
+// 根据参数名获取对应参数的值
+func GetValue(data interface{}, name string) interface{} {
+	value := reflect.ValueOf(data)
+	for i := 0; i < value.NumField(); i++ {
+		em := value.Field(i)
+		if em.Kind() != reflect.Struct {
+			continue
+		}
+		for j := 0; j < em.NumField(); j++ {
+			varName := em.Type().Field(j).Tag.Get("json")
+			if varName != name {
+				continue
+			}
+			return em.Field(j).Interface()
+		}
+	}
+	return nil
+}