This commit is contained in:
2025-07-07 20:11:59 +08:00
parent ab0fdbc447
commit 06e3aa2eb3
2009 changed files with 193082 additions and 0 deletions

View File

@ -0,0 +1,34 @@
package SpartaApi
//type UniversalEntity struct {
// code string `json:"code" dc:"200:业务处理成功40x:访问权限错误50x:系统错误。"`
// msg string `json:"msg" dc:"结果消息提示success"`
// data string `json:"leftTopPoint" dc:"业务执行结果(obj/arr)"`
//}
type TokenEntity struct {
Token string `json:"token" dc:"token"`
ExpiresAt int `json:"expires_at" dc:"过期时间"`
}
type RecognizeReq struct {
CapUrl string `json:"capUrl" v:"required" dc:"v-在线图片地址"`
RecType string `json:"recType" v:"required" dc:"v-识别算法模型"`
Async string `json:"async" dc:"是否异步处理false默认值算法识别完后同步返回接口请求会等待适用于识别时间较长的模型true: 收到请求后,算法服务器会异步识别"`
CallBackUrl string `json:"callBackUrl" dc:"若为异步则需要此参数系统会将结果post到这个接口中"`
AreaHigh string `json:"area_high" dc:"若为异步则需要此参数系统会将结果post到这个接口中"`
}
type RecognizeRes struct {
HasTarget int `json:"hasTarget" dc:"是否监测到目标10否"`
OriginalImgSize []int `json:"originalImgSize" dc:"原始图片尺寸([宽,高]ex[1920,1080]"`
Targets []TargetsEntity `json:"targets" dc:"目标信息"`
}
type TargetsEntity struct {
PhoNumber int `json:"pho_number" dc:"在光伏板检测的时候会多输出一个参数代表光伏板的数量"`
Type string `json:"type" dc:"目标信息"`
Size []float64 `json:"size" dc:"目标外接矩形像素"`
LeftTopPoint []float64 `json:"leftTopPoint" dc:"目标在画面中左上角位置信息"`
Score float64 `json:"score" dc:"置信度得分0~1"`
}

View File

@ -0,0 +1,635 @@
package SpartaApi
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/tiger1103/gfast/v3/library/liberr"
"io"
"log"
"net/url"
"os"
"strings"
"time"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
"github.com/tiger1103/gfast-cache/cache"
"github.com/tiger1103/gfast/v3/api/v1/common/coryCommon"
commonService "github.com/tiger1103/gfast/v3/internal/app/common/service"
)
const (
Sensitivity = 0.6 // 识别灵敏度 //AI工单摄像头
CAMERA = "head smoke" // AI工单摄像头
UAV_MATRIX = "pho hole shelves pile" // 方阵内容
UAV_MACHINE = "excavator Roller Truck_crane Loader Submersible_drilling_rig Sprinkler Truck_mounted_crane Truck" // 大型机械
//远界
YJCAMERA = "fire,hardhat,smoking"
)
var Url = g.Cfg().MustGet(gctx.New(), "spartaApi.Url").String()
var yjUrl = g.Cfg().MustGet(gctx.New(), "yjSpartaApi.Url").String()
// 定义一个映射来存储不同的类型及其对应的描述
var typeDescriptions = map[string]string{
"head": "未带安全帽",
"smoke": "吸烟",
"belt": "未系安全带",
"excavator": "挖掘机",
"Roller": "压路机",
"Truck_crane": "汽车吊",
"Loader": "装载机",
"Submersible_drilling_rig": "潜挖钻机",
"Sprinkler": "沙水车",
"Truck_mounted_crane": "随车吊",
"Truck": "货车",
//远界
"nohelmet": "未戴安全帽",
"cigarette": "吸烟",
"fire": "火灾",
"smoggy": "烟雾",
}
// @Title GetDescription 2024/8/7 17:34:00
// @Description 可复用的函数,根据输入的类型返回对应的描述
// @Auth Cory
func GetDescription(tp string) string {
if description, exists := typeDescriptions[tp]; exists {
return description
}
return "未知类型"
}
//func init() {
// fmt.Println("斯巴达----------------")
// token, _ := spartaApiTokenFunc()
// fmt.Println("token----------------", token)
//}
// @Title spartaApiTokenFunc 2024/8/7 17:35:00
// @Description 登录获取身份凭证
// @Auth Cory
func spartaApiTokenFunc() (token string, err error) {
key := "arithmetic:token"
ctx := gctx.New()
prefix := g.Cfg().MustGet(ctx, "system.cache.prefix").String()
gfCache := cache.New(prefix)
get, err := g.Redis().Get(ctx, gfCache.CachePrefix+key)
if err != nil && get.String() != "" {
token = get.String()
return token, err
} else {
clientId := g.Cfg().MustGet(gctx.New(), "spartaApi.key")
clientSecret := g.Cfg().MustGet(gctx.New(), "spartaApi.secret")
uri := Url + "/token?clientId=" + clientId.String() + "&clientSecret=" + clientSecret.String()
response, err := g.Client().Get(gctx.New(), uri)
if err != nil {
return "", err
}
var tk *TokenEntity
err = json.Unmarshal([]byte(response.ReadAllString()), &tk)
if err != nil {
return "", err
} else {
tk.Token = "Basic " + tk.Token
}
// 将token存储到redis中tiken默认时间为秒(这里少5000秒,防止token过期还存在redis中)
commonService.Cache().Set(ctx, key, tk.Token, time.Duration(tk.ExpiresAt-5000)*time.Second)
token = tk.Token
return token, err
}
}
//// @Title CommonAlgorithmTwoFunc 2024/8/7 17:06:00
//// @Description 提供算法得到的目标信息
//// @Auth Cory
//func CommonAlgorithmTwoFunc(ctx context.Context, recognize *RecognizeReq) (result *RecognizeRes, flag bool, err error) {
// result = new(RecognizeRes)
// flag = false
// // 1、调用算法
// body, err := ObjectIdentificationFunc(ctx, recognize)
// if err != nil {
// return
// }
// // 2、组装数据
// err = json.Unmarshal(body, &result)
// if err != nil {
// return
// }
// // 3、布尔值
// if len(result.Targets) == 0 { // 表示无违规操作
// return
// } else { // 表述有违规操作
// flag = true
// return
// }
//}
// @Title CommonAlgorithmTwoUav 2024/8/7 17:06:00
// @Description 提供算法得到的目标信息【飞机专用】
// @Auth Cory
func CommonAlgorithmTwoUav(ctx context.Context, recognize *RecognizeReq) (result *RecognizeRes, flag bool, err error) {
result = new(RecognizeRes)
flag = false
//// 1、调用算法
//body, err := ObjectIdentificationFunc(ctx, recognize)
//if err != nil {
// return
//}
recognizeResult := `{
"hasTarget": 1,
"originalImgSize": [1920, 1080],
"targets": [
{
"type": "pho",
"size": [282, 274],
"leftTopPoint": [3192, 2758],
"score": 0.6154276132583618
},
{
"type": "pho",
"size": [343, 139],
"leftTopPoint": [4577, 2836],
"score": 0.722245454788208
},
{
"type": "pho",
"size": [372, 158],
"leftTopPoint": [4554, 3761],
"score": 0.7273581027984619
},
{
"type": "pho",
"size": [351, 129],
"leftTopPoint": [4124, 2251],
"score": 0.749356746673584
},
{
"type": "pho",
"size": [336, 152],
"leftTopPoint": [4211, 2379],
"score": 0.7633119225502014
},
{
"type": "pho",
"size": [343, 147],
"leftTopPoint": [4321, 2515],
"score": 0.7663174867630005
},
{
"type": "pho",
"size": [333, 152],
"leftTopPoint": [4933, 2771],
"score": 0.7939081192016602
},
{
"type": "pho",
"size": [334, 143],
"leftTopPoint": [4574, 2966],
"score": 0.8044442534446716
},
{
"type": "pho",
"size": [332, 138],
"leftTopPoint": [4243, 2921],
"score": 0.8075516819953918
},
{
"type": "pho",
"size": [315, 156],
"leftTopPoint": [3628, 2535],
"score": 0.8091028332710266
},
{
"type": "pho",
"size": [336, 129],
"leftTopPoint": [3913, 3655],
"score": 0.8116179704666138
},
{
"type": "pho",
"size": [342, 139],
"leftTopPoint": [4471, 2290],
"score": 0.814013659954071
},
{
"type": "pho",
"size": [350, 143],
"leftTopPoint": [4528, 2432],
"score": 0.8172851800918579
},
{
"type": "pho",
"size": [342, 171],
"leftTopPoint": [3992, 2469],
"score": 0.8185974359512329
},
{
"type": "pho",
"size": [338, 149],
"leftTopPoint": [4433, 3345],
"score": 0.8222267031669617
},
{
"type": "pho",
"size": [332, 134],
"leftTopPoint": [4284, 2660],
"score": 0.8247078657150269
},
{
"type": "pho",
"size": [328, 134],
"leftTopPoint": [4616, 2713],
"score": 0.8274019956588745
},
{
"type": "pho",
"size": [350, 139],
"leftTopPoint": [4928, 3685],
"score": 0.8295787572860718
},
{
"type": "pho",
"size": [339, 136],
"leftTopPoint": [4908, 2905],
"score": 0.8317150473594666
},
{
"type": "pho",
"size": [339, 135],
"leftTopPoint": [4232, 3708],
"score": 0.8319769501686096
},
{
"type": "pho",
"size": [323, 141],
"leftTopPoint": [3461, 3167],
"score": 0.8382804989814758
},
{
"type": "pho",
"size": [368, 132],
"leftTopPoint": [3761, 3219],
"score": 0.8471034169197083
},
{
"type": "pho",
"size": [338, 140],
"leftTopPoint": [3894, 2986],
"score": 0.8511925339698792
},
{
"type": "pho",
"size": [337, 144],
"leftTopPoint": [3580, 2942],
"score": 0.8524681329727173
},
{
"type": "pho",
"size": [348, 142],
"leftTopPoint": [3620, 3467],
"score": 0.8603440523147583
},
{
"type": "pho",
"size": [352, 145],
"leftTopPoint": [4632, 2584],
"score": 0.8645903468132019
},
{
"type": "pho",
"size": [365, 139],
"leftTopPoint": [4585, 3633],
"score": 0.8663488626480103
},
{
"type": "pho",
"size": [323, 138],
"leftTopPoint": [3919, 2847],
"score": 0.8686794638633728
},
{
"type": "pho",
"size": [394, 155],
"leftTopPoint": [3644, 3335],
"score": 0.8729261755943298
},
{
"type": "pho",
"size": [328, 143],
"leftTopPoint": [4274, 3576],
"score": 0.8783137798309326
},
{
"type": "pho",
"size": [300, 314],
"leftTopPoint": [3247, 2571],
"score": 0.879583477973938
},
{
"type": "pho",
"size": [660, 217],
"leftTopPoint": [4903, 3817],
"score": 0.8797935843467712
},
{
"type": "pho",
"size": [331, 170],
"leftTopPoint": [3923, 2713],
"score": 0.8806164264678955
},
{
"type": "pho",
"size": [332, 140],
"leftTopPoint": [4240, 2782],
"score": 0.8832747936248779
},
{
"type": "pho",
"size": [318, 143],
"leftTopPoint": [4129, 3295],
"score": 0.8922125101089478
},
{
"type": "pho",
"size": [349, 143],
"leftTopPoint": [3867, 3116],
"score": 0.8925455808639526
},
{
"type": "pho",
"size": [333, 138],
"leftTopPoint": [4016, 3404],
"score": 0.8939041495323181
},
{
"type": "pho",
"size": [353, 158],
"leftTopPoint": [3942, 2604],
"score": 0.8950177431106567
},
{
"type": "pho",
"size": [350, 148],
"leftTopPoint": [4655, 3507],
"score": 0.9090152382850647
},
{
"type": "pho",
"size": [322, 153],
"leftTopPoint": [3587, 2792],
"score": 0.9102197289466858
},
{
"type": "pho",
"size": [348, 159],
"leftTopPoint": [4777, 3397],
"score": 0.9125684499740601
}
],
"pho_number": 41
}`
body := []byte(recognizeResult)
// 2、组装数据
err = json.Unmarshal(body, &result)
if err != nil {
fmt.Println("Error:", err)
return
}
// 3、布尔值
if len(result.Targets) == 0 { // 表示无违规操作
flag = false
} else { // 表述有违规操作
flag = true
}
//绘图
replace := strings.Replace(recognize.CapUrl, coryCommon.GlobalPath, "", 1)
imgPath := coryCommon.FileToFunc(replace, 2)
num := 0
mp := make(map[string]string)
entity := coryCommon.TestDrawRectTextEntity{
ImPath: imgPath,
}
var zuobiao []*coryCommon.CoordinatesListEntity
for _, data := range result.Targets {
if data.Score >= Sensitivity {
tp := data.Type
mp[tp] = GetDescription(tp)
flag = true
num = num + 1
zuobiao = append(zuobiao, &coryCommon.CoordinatesListEntity{
X: data.LeftTopPoint[0],
Y: data.LeftTopPoint[1],
W: data.Size[0],
H: data.Size[1],
})
}
}
entity.Coordinates = zuobiao
coryCommon.TestDrawRectTextFunc(&entity)
return
}
/*
hat安全帽识别
head不戴安全帽识别
smoke吸烟识别
belt : 安全带识别;
waste 工程垃圾识别暂无
excavator挖掘机
Roller压路机
Truck_crane汽车吊
Loader装载机
Submersible_drilling_rig潜挖钻机
Sprinkler洒水车
Truck_mounted_crane随车吊
Truck货车
pho光伏板
hole:洞
shelves架子
*/
func ObjectIdentificationFunc(ctx context.Context, recognize *RecognizeReq) (body []byte, err error) {
// 1、获取token
token, err := spartaApiTokenFunc()
if err != nil {
err = errors.New("获取斯巴达token错误" + err.Error())
return
}
// 2、设置请求数据
uri := Url + "/api/recognize?capUrl=" + recognize.CapUrl +
"&recType=" + url.QueryEscape(recognize.RecType) +
"&async=" + recognize.Async +
"&callBackUrl=" + recognize.CallBackUrl +
"&area_high=" + recognize.AreaHigh
response, err := g.Client().ContentJson().SetHeaderMap(map[string]string{
"Authorization": token,
}).Get(gctx.New(), uri)
defer response.Body.Close()
// 3、返回数据
body, err = io.ReadAll(response.Body)
fmt.Println("请求的URL ", uri)
return
}
// @Title CommonAlgorithmFunc 2024/8/7 17:07:00
// @Description 调用算法,得到目标信息,然后圈出来
// @Auth Cory
func CommonAlgorithmFunc(ctx context.Context, recognize *RecognizeReq) (mp map[string]string, flag bool, num int, err error) {
flag = false
num = 0
// 0、记录绝对路径以便后续操作 //absolutePath := "C:\\Users\\MSI\\Pictures\\222.png"
absolutePath := recognize.CapUrl
prefix := g.Cfg().MustGet(gctx.New(), "spartaApi.prefix")
recognize.CapUrl = replacePrefix(recognize.CapUrl, "/resource/public/", prefix.String()+"/file/")
// 1、调用算法
body, err := ObjectIdentificationFunc(ctx, recognize)
if err != nil {
return
}
// 2、组装数据
var data []json.RawMessage
err = json.Unmarshal(body, &data)
if err != nil {
fmt.Println("Error:", err)
return
}
var result RecognizeRes
for i, rawMsg := range data {
switch i {
case 0:
err = json.Unmarshal(rawMsg, &result.HasTarget)
case 1:
err = json.Unmarshal(rawMsg, &result.OriginalImgSize)
default:
var target TargetsEntity
err = json.Unmarshal(rawMsg, &target)
if err == nil {
result.Targets = append(result.Targets, target)
}
}
if err != nil {
fmt.Println("Error:", err)
return
}
}
// 3、遍历数据然后在数据中绘画矩形框
if result.HasTarget == 0 {
return
}
mp = make(map[string]string)
for _, data := range result.Targets {
if data.Score >= Sensitivity {
tp := data.Type
mp[tp] = GetDescription(tp)
flag = true
num = num + 1
coryCommon.Test_draw_rect_text(absolutePath, data.LeftTopPoint[0], data.LeftTopPoint[1], data.Size[0], data.Size[1])
}
}
return
}
// replacePrefix 将路径中指定前缀及其之前的部分替换成新的前缀
func replacePrefix(originalPath, prefixToReplace, newPrefix string) string {
// 查找要替换的前缀的位置
index := strings.Index(originalPath, prefixToReplace)
// 如果找到前缀
if index != -1 {
// 保留前缀之后的部分
remainingPath := originalPath[index+len(prefixToReplace):]
// 拼接新的路径
newPath := newPrefix + remainingPath
return newPath
}
// 如果未找到前缀,返回原始路径
return originalPath
}
/*
=============================================================================
远界算法
=============================================================================
*/
func CORYAInit(ctx context.Context) {
req := RecognizeReq{
CapUrl: "http://xny.yj-3d.com:7363/file/temporary/2025-06-22/sxt_1750583729497_972.jpg",
RecType: YJCAMERA,
}
now := time.Now()
marshal, _ := json.Marshal(req)
fmt.Println("------------", string(marshal))
fmt.Println("------------", now)
result, flag, err := CommonAlgorithmTwoFunc(gctx.New(), &req)
now2 := time.Now()
fmt.Println("result", result, flag, err)
fmt.Println("===========用时", now2.Sub(now))
fmt.Println("===========")
}
// @Title CommonAlgorithmTwoFunc 2024/8/7 17:06:00
// @Description 提供算法得到的目标信息(远界版本)
// @Auth Cory
func CommonAlgorithmTwoFunc(ctx context.Context, recognize *RecognizeReq) (result *RecognizeRes, flag bool, err error) {
err = g.Try(ctx, func(ctx context.Context) {
result = new(RecognizeRes)
flag = false
// 1、调用算法
postData := map[string]interface{}{
"type": recognize.RecType,
"url": recognize.CapUrl,
"extract": false,
}
response, err := g.Client().ContentJson().Post(ctx, yjUrl, postData)
fmt.Println("1请求的URL ", err)
liberr.ErrIsNil(ctx, err, "远界算法请求失败")
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
fmt.Println("2请求的URL ", err)
liberr.ErrIsNil(ctx, err, "远界算法数据获取失败")
fmt.Println("返回的数据为 ", string(body))
// 2、组装数据
err = json.Unmarshal(body, &result)
if err != nil {
return
}
//=======================
//=======================
//1、本地创建一个text来记录日志
// 定义文件路径
filePath := coryCommon.GetCWD() + "/coryLogs/logs/ai_log.txt"
// 打开文件,如果文件不存在则创建
file, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Fatal(err)
}
defer file.Close()
// 写入日志信息
marshal, _ := json.Marshal(result)
marshal2, _ := json.Marshal(postData)
_, err = file.WriteString(fmt.Sprintf("参数:%s,==================结果:%s\n", string(marshal), string(marshal2)))
if err != nil {
log.Fatal(err)
}
// 3、布尔值
if len(result.Targets) == 0 { // 表示无违规操作
return
} else { // 表述有违规操作
flag = true
return
}
})
return
}

668
third/create/a.go Normal file
View File

@ -0,0 +1,668 @@
package create
import (
"context"
"encoding/json"
"fmt"
"math/rand"
"time"
"github.com/gogf/gf/v2/util/gconv"
"github.com/sony/sonyflake"
"github.com/tiger1103/gfast/v3/api/v1/system"
"github.com/tiger1103/gfast/v3/internal/app/system/dao"
"github.com/tiger1103/gfast/v3/internal/app/system/model"
"github.com/tiger1103/gfast/v3/internal/app/system/model/do"
"github.com/tiger1103/gfast/v3/internal/app/system/model/entity"
"github.com/tiger1103/gfast/v3/internal/consts"
)
// 创建 gantt json 数据
func CreateGanttJson(ctx context.Context, fangzhenName string, parentName string, data []entity.ProjectSchedule) (string, error) {
// 层级: 方阵名 --> 施工项目名 --> 子施工项目名
ganttData := make([]system.Gantt, 0, len(data))
// 添加项目名
ganttData = append(ganttData, system.Gantt{Id: 1, StartDate: "2024-01-01 00:00:00", Duration: 15, Text: "方阵1", Parent: 0, Progress: 0, Open: true})
ganttData = append(ganttData, system.Gantt{Id: 2, StartDate: "2024-01-01 00:00:00", EndDate: "2024-01-15 00:00:00", Duration: 15, Text: "防雷接地网", Parent: 1, Progress: 0, Open: true})
i := 3
for _, project := range data {
ganttData = append(ganttData, system.Gantt{
Id: int64(i),
StartDate: project.StartDate + " 00:00:00",
EndDate: project.EndDate + " 00:00:00",
Duration: rand.Intn(10) + 1,
Text: project.Name,
// Progress: float64(project.CurrentProgress),
Parent: 2,
Open: true,
})
aa := entity.ProjectSchedule{}
if err := dao.ProjectSchedule.Ctx(ctx).Where(dao.ProjectSchedule.Columns().ParentId, project.ParentId).
WhereNotNull(dao.ProjectSchedule.Columns().Types).Scan(&aa); err != nil {
return "", err
}
ganttData = append(ganttData, system.Gantt{
Id: int64(i + 1),
StartDate: aa.StartDate + " 00:00:00",
EndDate: aa.EndDate + " 00:00:00",
Duration: 1,
Text: "计划中",
Parent: int64(i),
})
i += 2
}
// 打印 gantt json 数据
dd, _ := json.Marshal(ganttData)
println(string(dd))
return string(dd), nil
}
func CreateGanttJson2(ctx context.Context, fangzhenName string, parentName string, data []entity.ProjectSchedule) ([]system.Gantt, error) {
// 层级: 方阵名 --> 施工项目名 --> 子施工项目名
ganttData := make([]system.Gantt, 0, len(data))
// 添加项目名
ganttData = append(ganttData, system.Gantt{Id: 1, StartDate: "2024-01-01 00:00:00", Duration: 15, Text: fangzhenName, Parent: 0, Progress: 0, Open: true})
ganttData = append(ganttData, system.Gantt{Id: 2, StartDate: "2024-01-01 00:00:00", EndDate: "2024-01-15 00:00:00", Duration: 15, Text: parentName, Parent: 1, Progress: 0, Open: true})
i := 3
index := 0
for i < len(data) {
ganttData = append(ganttData, system.Gantt{
Id: int64(i),
StartDate: data[index].StartDate + " 00:00:00",
EndDate: data[index].EndDate + " 00:00:00",
Duration: rand.Intn(10) + 1,
Text: data[index].Name,
Parent: 2,
Open: true,
})
ganttData = append(ganttData, system.Gantt{
Id: int64(i + 1),
StartDate: data[index+1].StartDate + " 00:00:00",
EndDate: data[index+1].EndDate + " 00:00:00",
Duration: 1,
Text: "计划中",
Parent: int64(i),
})
index += 2
i += 2
if index >= len(data) {
break
}
}
return ganttData, nil
}
type projects struct {
Name string
StartTime string
EndTime string
Sub []subJ
}
type subJ struct {
Name string
ID int
ParentID int
StartTime string
EndTime string
}
func FetchGanttData(ctx context.Context, matrixID int) ([]system.Gantt, error) {
projectData := []entity.ConstructionProject{}
// 获取指定方阵下的所有父项目
if err := dao.ConstructionProject.Ctx(ctx).
Where(dao.ConstructionProject.Columns().FangzhenId, matrixID).Scan(&projectData); err != nil {
return nil, err
}
// 父子项目列表
projectList := []projects{}
// 构造所有的父项目及子项目
for _, project := range projectData {
detailData := []entity.ConstructionDetails{}
if err := dao.ConstructionDetails.Ctx(ctx).
Where(dao.ConstructionDetails.Columns().ConstructionId, project.ConstructionId).Scan(&detailData); err != nil {
return nil, err
}
// 如果 start_time 和 end_time 为空则使用当前时间
var startTime string
var endTime string
if project.StartTime == nil {
startTime = time.Now().Format("2006-01-02 15:04:05")
} else {
startTime = project.StartTime.Format("2006-01-02 15:04:05")
}
if project.EndTime == nil {
endTime = time.Now().Format("2006-01-02 15:04:05")
} else {
endTime = project.EndTime.Format("2006-01-02 15:04:05")
}
// 父
projectItem := projects{
Name: project.ConstructionName,
StartTime: startTime,
EndTime: endTime,
Sub: make([]subJ, 0, len(detailData)),
}
// 子
for _, detail := range detailData {
// 如果 start_time 和 end_time 为空则使用当前时间
var startTime string
var endTime string
if detail.StartTime == nil {
startTime = time.Now().Format("2006-01-02 15:04:05")
} else {
startTime = detail.StartTime.Format("2006-01-02 15:04:05")
}
if detail.EndTime == nil {
endTime = time.Now().Format("2006-01-02 15:04:05")
} else {
endTime = detail.EndTime.Format("2006-01-02 15:04:05")
}
projectItem.Sub = append(projectItem.Sub, subJ{
Name: detail.Name,
ID: int(detail.Id),
ParentID: int(detail.Id),
StartTime: startTime,
EndTime: endTime,
})
}
projectList = append(projectList, projectItem)
}
// 1 获取所有的排期数据
schedulerList := []entity.ProjectSchedule{}
if err := dao.ProjectSchedule.Ctx(ctx).WhereNot(dao.ProjectSchedule.Columns().Types, 0).
Scan(&schedulerList); err != nil {
}
// 子项目存在多个排期的情况下,需要将排期数据进行分组
scheduleMap := make(map[int][]entity.ProjectSchedule, 0)
for _, schedule := range schedulerList {
if _, ok := scheduleMap[schedule.ParentId]; !ok {
scheduleMap[schedule.ParentId] = make([]entity.ProjectSchedule, 0, 1)
}
scheduleMap[schedule.ParentId] = append(scheduleMap[schedule.ParentId], schedule)
}
// 1.1 传入 parentid 返回一个子项目的排期数据
findSub := func(parentID int) ([]entity.ProjectSchedule, bool) {
if schedule, ok := scheduleMap[parentID]; ok {
return schedule, true
}
return nil, false
}
// 主任务
ganttList := make([]system.Gantt, 0, len(projectList))
taskIndex := 1
// 查询任务是否存在排期如果存在则不添加
for _, project := range projectList {
// 父级数据
ganttList = append(ganttList, system.Gantt{
Id: int64(taskIndex),
StartDate: project.StartTime,
Duration: 15,
Text: project.Name,
Parent: 0,
Open: true,
})
taskIndex++
// 记录当前主项目的下标
parentIndex := taskIndex
for _, sub := range project.Sub {
scheduleData, ok := findSub(sub.ParentID)
// 如果有排期的情况下,填充真实数据
if ok {
// 添加子任务
ganttList = append(ganttList, system.Gantt{
Id: int64(taskIndex),
StartDate: sub.StartTime,
EndDate: sub.EndTime,
Duration: rand.Intn(10) + 1,
Text: sub.Name,
Parent: int64(parentIndex) - 1,
ConstructionId: sub.ID,
})
// 记录当前子项目的下标
subIndex := taskIndex
taskIndex++
for _, schedule := range scheduleData {
// 添加子任务排期计划
ganttList = append(ganttList, system.Gantt{
Id: int64(taskIndex),
StartDate: schedule.StartDate + " 00:00:00",
EndDate: schedule.EndDate + " 00:00:00",
Duration: gconv.Int(schedule.PlaneNum),
Text: "计划中",
Parent: int64(subIndex),
ConstructionId: sub.ID,
Open: true,
})
taskIndex++
}
continue
}
// 添加子任务,如果没有排期的情况下,填充假数据,维护队列
ganttList = append(ganttList, system.Gantt{
Id: int64(taskIndex),
StartDate: "2029-01-01 00:00:00",
Duration: 15,
Text: sub.Name,
Parent: int64(parentIndex) - 1,
ConstructionId: sub.ID,
Open: true,
})
taskIndex++
}
}
return ganttList, nil
}
func FetchElementData(ctx context.Context, fangzhenId int) ([]*model.WorkStatusProgressRes, error) {
// 获取所有的父项目
projectData := []entity.WorkStatus{}
if err := dao.WorkStatus.Ctx(ctx).Where(dao.WorkStatus.Columns().FangzhenId, fangzhenId).WhereNull(dao.WorkStatus.Columns().Parent).
Scan(&projectData); err != nil {
return nil, err
}
// 获取所有的子项目
detailData := []entity.WorkStatus{}
if err := dao.WorkStatus.Ctx(ctx).Where(dao.WorkStatus.Columns().FangzhenId, fangzhenId).WhereNotNull(dao.WorkStatus.Columns().Parent).Scan(&detailData); err != nil {
return nil, err
}
// 根据关联关系进行数据拼接
index := 1
projectList := []*model.WorkStatusProgressRes{}
for _, project := range projectData {
// 父级数据
projectItem := model.WorkStatusProgressRes{
ID: index,
Startat: project.StartAt.String(),
Endat: project.EndAt.String(),
Name: project.WorkName,
Total: project.Total,
Finished: project.Finished,
Children: make([]model.WorkStatusProgressRes, 0, len(detailData)),
}
index++
// 子
for _, detail := range detailData {
if detail.Parent == int(project.Id) {
projectItem.Children = append(projectItem.Children, model.WorkStatusProgressRes{
ID: index,
Startat: detail.StartAt.String(),
Endat: detail.EndAt.String(),
Name: detail.WorkName,
Total: detail.Total,
Finished: detail.Finished,
})
index++
}
}
projectList = append(projectList, &projectItem)
}
return projectList, nil
}
func randomDate(dateStr string) string {
t, _ := time.Parse("2006-01-02", dateStr)
randDays := time.Duration(rand.Intn(2)+1) * 24 * time.Hour
newDate := t.Add(-randDays)
return newDate.Format("2006-01-02")
}
// 获取雪花算法生成的 ID
func GetSonyFlakeID() (uint64, error) {
// 初始化 sonyFlake 配置
st := sonyflake.Settings{}
sonyFlake := sonyflake.NewSonyflake(st)
if sonyFlake == nil {
return 0, fmt.Errorf("需要先初始化以后再执行 GetID 函数")
}
// 获取全局 ID
id, err := sonyFlake.NextID()
if err != nil {
return 0, err
}
return id, nil
}
func GetSonyFlakeIDs(mID uint16, count int) (ids []uint64, err error) {
// 初始化 sonyFlake 配置
st := sonyflake.Settings{}
sonyFlake := sonyflake.NewSonyflake(st)
if sonyFlake == nil {
err = fmt.Errorf("需要先初始化以后再执行 GetID 函数 err: %#v \n", err)
return
}
// 获取全局 ID
ids = make([]uint64, count)
for i := 0; i < count; i++ {
id, err := sonyFlake.NextID()
if err != nil {
return nil, err
}
ids[i] = id
}
return ids, nil
}
// FetchTemplateData 为方阵绑定父子项目
func FetchTemplateData(ctx context.Context, protectionNetID int64) ([]do.WorkStatus, error) {
facilityGroups := createFacilities()
// 雪花算法生成 ID
ids, err := GetSonyFlakeIDs(1, 38)
if err != nil {
return nil, err
}
details := make([]do.WorkStatus, 0, len(facilityGroups))
index := 0
for _, group := range facilityGroups {
projectID, err := dao.WorkStatus.Ctx(ctx).Data(do.WorkStatus{
FangzhenId: protectionNetID,
WorkId: ids[index],
WorkName: group.Name,
Total: group.FacilityCount,
Type: group.TypeNumber,
IsPercent: group.IsPercentage,
Status: 0,
}).InsertAndGetId()
if err != nil {
return nil, err
}
index++
for _, component := range group.Components {
details = append(details, do.WorkStatus{
FangzhenId: protectionNetID,
Parent: projectID,
WorkId: ids[index],
WorkName: component.Name,
Total: component.Quantity,
IsPercent: component.IsPercentage,
Type: component.TypeNumber,
Status: 0,
})
index++
}
}
return details, nil
}
// 为方阵绑定施工项目
func CreateConstructionProject(ctx context.Context, protectionNetID int64) error {
facilityGroups := createFacilities()
details := make([]do.ConstructionDetails, len(facilityGroups))
for _, group := range facilityGroups {
projectID, err := dao.ConstructionProject.Ctx(ctx).Data(do.ConstructionProject{
FangzhenId: protectionNetID,
ConstructionName: group.Name,
Total: group.FacilityCount,
IsPercentage: group.IsPercentage,
}).InsertAndGetId()
if err != nil {
return err
}
for _, component := range group.Components {
details = append(details, do.ConstructionDetails{
Name: component.Name,
ConstructionId: projectID,
Total: component.Quantity,
IsPercentage: component.IsPercentage,
})
}
}
_, err := dao.ConstructionDetails.Ctx(ctx).Batch(50).Insert(details)
if err != nil {
return err
}
return nil
}
func createFacilities() []system.FacilityGroup {
return []system.FacilityGroup{{
Name: "防雷接地网",
FacilityCount: nil,
IsPercentage: &[]int{0}[0],
TypeNumber: consts.LightningProtectionNet,
Components: []system.Component{
{
Name: "接地沟",
IsPercentage: &[]int{1}[0],
Quantity: 0,
TypeNumber: consts.GroundingDitch,
},
{
Name: "接地敷设",
IsPercentage: &[]int{1}[0],
Quantity: 0,
TypeNumber: consts.GroundingLaying,
},
{
Name: "检测",
IsPercentage: &[]int{1}[0],
Quantity: 0,
TypeNumber: consts.GroundingTesting,
},
},
}, {
Name: "围栏",
FacilityCount: nil,
IsPercentage: &[]int{0}[0],
TypeNumber: consts.Fence,
Components: []system.Component{
{
Name: "基础",
IsPercentage: &[]int{0}[0],
Quantity: 100,
TypeNumber: consts.FenceFoundation,
},
{
Name: "安装",
IsPercentage: &[]int{0}[0],
Quantity: 100,
TypeNumber: consts.FenceInstallation,
},
},
}, {
Name: "道路",
IsPercentage: &[]int{0}[0],
TypeNumber: consts.Road,
Components: []system.Component{
{
Name: "路基",
IsPercentage: &[]int{0}[0],
Quantity: 100,
TypeNumber: consts.RoadBase,
},
{
Name: "排水沟",
IsPercentage: &[]int{0}[0],
Quantity: 100,
TypeNumber: consts.RoadDrainageDitch,
},
},
}, {
Name: "低压部分",
IsPercentage: &[]int{0}[0],
FacilityCount: &[]int{0}[0],
TypeNumber: consts.LowVoltage,
Components: []system.Component{
{
Name: "钻孔",
IsPercentage: &[]int{1}[0],
Quantity: 0,
TypeNumber: consts.LowVoltageDrilling,
},
{
Name: "桩基",
IsPercentage: &[]int{1}[0],
Quantity: 0,
TypeNumber: consts.LowVoltagePileFoundation,
},
{
Name: "支架",
IsPercentage: &[]int{1}[0],
Quantity: 0,
TypeNumber: consts.LowVoltageBracket,
},
{
Name: "光伏板",
IsPercentage: &[]int{1}[0],
Quantity: 0,
TypeNumber: consts.LowVoltagePhotovoltaicPanel,
},
{
Name: "直流电缆",
IsPercentage: &[]int{1}[0],
Quantity: 0,
TypeNumber: consts.LowVoltageDC,
},
{
Name: "接地线",
IsPercentage: &[]int{1}[0],
Quantity: 0,
TypeNumber: consts.LowVoltageGroundWire,
},
{
Name: "逆变器安装",
IsPercentage: &[]int{1}[0],
TypeNumber: consts.LowVoltageInverterInstallation,
},
{
Name: "电缆沟开挖",
IsPercentage: &[]int{1}[0],
Quantity: 0,
TypeNumber: consts.LowVoltageCableTrenchExcavation,
},
{
Name: "低压电缆敷设",
IsPercentage: &[]int{1}[0],
Quantity: 0,
TypeNumber: consts.LowVoltageLaying,
},
{
Name: "调试",
IsPercentage: &[]int{1}[0],
Quantity: 0,
TypeNumber: consts.LowVoltageDebugging,
},
},
}, {
Name: "高压部分",
IsPercentage: &[]int{1}[0],
TypeNumber: consts.HighVoltage,
Components: []system.Component{
{
Name: "箱变基础",
IsPercentage: &[]int{1}[0],
TypeNumber: consts.HighVoltageBoxChangeFoundation,
},
{
Name: "箱变安装",
IsPercentage: &[]int{1}[0],
TypeNumber: consts.HighVoltageBoxChangeInstallation,
},
{
Name: "电缆沟开挖",
IsPercentage: &[]int{1}[0],
TypeNumber: consts.HighVoltageCableLineExcavation,
},
{
Name: "高压电缆敷设",
IsPercentage: &[]int{1}[0],
TypeNumber: consts.HighVoltageCableLaying,
},
{
Name: "高压电缆试验",
IsPercentage: &[]int{1}[0],
TypeNumber: consts.HighVoltageCableTest,
},
{
Name: "高压电缆调试试验",
IsPercentage: &[]int{1}[0],
TypeNumber: consts.HighVoltageCableDebuggingTest,
},
},
}, {
Name: "环网柜",
IsPercentage: &[]int{1}[0],
TypeNumber: consts.RingMainCabinet,
Components: []system.Component{
{
Name: "基础",
IsPercentage: &[]int{1}[0],
TypeNumber: consts.RingMainCabinetFoundation,
},
{
Name: "安装",
IsPercentage: &[]int{1}[0],
TypeNumber: consts.RingMainCabinetInstallation,
},
{
Name: "敷设",
IsPercentage: &[]int{1}[0],
TypeNumber: consts.RingMainCabinetLaying,
},
{
Name: "试验",
IsPercentage: &[]int{1}[0],
TypeNumber: consts.RingMainCabinetTest,
},
{
Name: "调试试验",
IsPercentage: &[]int{1}[0],
TypeNumber: consts.RingMainCabinetDebuggingTest,
},
},
}}
}

View File

@ -0,0 +1,34 @@
package excel
import (
"context"
"github.com/tiger1103/gfast/v3/internal/app/system/dao"
)
// EquipmentMaterials 设备和材料
type EquipmentMaterials struct {
EquipmentMaterialsName string `json:"equipment_materials_name"`
TotalNumber int `json:"total_number"`
TotalQuantityCount int `json:"total_quantity_count"`
CumulativeArrivalQuantity int `json:"cumulative_arrival_quantity"`
}
// 传入项目ID,开始时间,结束时间返回 EquipmentMaterials 列表
func GetEquipmentMaterials(projectID string, startAt, endAt string) ([]EquipmentMaterials, error) {
var list []EquipmentMaterials
if err := dao.BusEquipmentMaterialsInventory.Ctx(context.Background()).As("bmi").
Fields("bem.equipment_materials_name, SUM(bmi.number) AS total_number, SUM(bem.quantity_count) AS total_quantity_count, (SELECT SUM(bi.number) FROM `bus_equipment_materials_inventory` bi WHERE bi.equipment_materials_id = bmi.equipment_materials_id AND bi.deleted_at IS NULL AND bi.out_put = 2) AS cumulative_arrival_quantity").
LeftJoin("bus_equipment_materials bem", "bmi.equipment_materials_id = bem.equipment_materials_id").
Where("bem.project_id = ?", projectID).
Where("bmi.out_put = ?", 2).
Where("bmi.created_at >= ?", startAt).
Where("bmi.created_at <= ?", endAt).
Group("bem.equipment_materials_name").
Scan(&list); err != nil {
return nil, err
}
return list, nil
}

513
third/excel/export.go Normal file
View File

@ -0,0 +1,513 @@
package excel
import (
"context"
"encoding/json"
"fmt"
"strconv"
"time"
"github.com/samber/lo"
"github.com/tiger1103/gfast/v3/internal/app/system/dao"
"github.com/tiger1103/gfast/v3/internal/app/system/model"
"github.com/tiger1103/gfast/v3/internal/app/system/model/entity"
"github.com/xuri/excelize/v2"
)
// 记录累计完成量和下一周计划量
type ExcelInfo struct {
// 累计完成量
Accumulated int
// 下一周计划量
NextWeek int
}
// 导出单个方阵
func ExportExcel(fangZhenID string, startAt, endAt string) ([]*model.WorkStatusProgressRes, map[string]ExcelInfo, error) {
ctx := context.Background()
type scheduler struct {
// 实体
WorkStatus *entity.WorkSchedule
// 总进度
FinishedPtr int
// 总量
Total int
}
type Schdule map[string]scheduler
Get := func(scheduleMap Schdule, workID string) (int, int) {
if scheduler, ok := scheduleMap[workID]; ok {
return scheduler.FinishedPtr, scheduler.Total
}
return 0, 0
}
// 获取指定方阵ID的所有计划
ScheduleData := []entity.WorkSchedule{}
if err := dao.WorkSchedule.Ctx(ctx).Where(dao.WorkSchedule.Columns().FangzhenId, fangZhenID).
Scan(&ScheduleData); err != nil {
return nil, nil, err
}
// 存储指定 workID 的累计完成量
workMap := make(map[string]ExcelInfo)
// 计算下一周的日期
nextWeekStart := lo.Must(time.Parse("2006-01-02", startAt)).AddDate(0, 0, 7)
nextWeekEnd := lo.Must(time.Parse("2006-01-02", endAt)).AddDate(0, 0, 7)
scheduleMap := make(Schdule)
// 遍历 ScheduleData 将数据按照 WorkID 分组
lo.ForEach(ScheduleData, func(item entity.WorkSchedule, _ int) {
if work, ok := workMap[item.WorkId]; ok {
// 累加完成量
work.Accumulated += item.FinishedNum
workMap[item.WorkId] = work
} else {
workMap[item.WorkId] = ExcelInfo{
Accumulated: item.FinishedNum,
}
}
// 如果计划处于下一周,则添加到下一周计划量中
stime := lo.Must(time.Parse("2006-01-02", item.StartAt.Format("2006-01-02")))
if stime.After(nextWeekStart) && stime.Before(nextWeekEnd) {
var scheduleDetails []model.WorkScheduleDetail
if err := json.Unmarshal([]byte(item.Detail), &scheduleDetails); err != nil {
return
}
nextweekPlanNum := 0
for _, detail := range scheduleDetails {
// 如果 Date 大于开始时间并小于结束时间
if detail.Date <= endAt {
nextweekPlanNum += detail.PlanNum
}
}
work := workMap[item.WorkId]
work.NextWeek += nextweekPlanNum
workMap[item.WorkId] = work
}
// 如果开始时间大于当前时间,则跳过
startTime := item.StartAt.Format("Y-m-d")
if startTime > startAt {
return
}
// 如果不存在则直接添加
_, ok := scheduleMap[item.WorkId]
if !ok {
scheduleMap[item.WorkId] = scheduler{
WorkStatus: &item,
}
}
// 反序列化 item.Detail 字段
var scheduleDetails []model.WorkScheduleDetail
if err := json.Unmarshal([]byte(item.Detail), &scheduleDetails); err != nil {
return
}
lo.ForEach(scheduleDetails, func(detail model.WorkScheduleDetail, _ int) {
// 如果 Date 大于开始时间并小于结束时间
if detail.Date <= endAt {
scheduler := scheduleMap[item.WorkId]
scheduler.Total += detail.PlanNum
scheduler.FinishedPtr += detail.FinishedNum
scheduleMap[item.WorkId] = scheduler
}
})
})
// 获取指定方阵ID的所有项目
workStatusList := []entity.WorkStatus{}
if err := dao.WorkStatus.Ctx(ctx).Where(dao.WorkStatus.Columns().FangzhenId, fangZhenID).Scan(&workStatusList); err != nil {
return nil, nil, err
}
// 父列表
parentList := []entity.WorkStatus{}
// 子列表,使用 map 存储,键为父元素的 ID
childrenMap := make(map[int][]entity.WorkStatus)
// 遍历所有数据,将父级和子集分开
for i := 0; i < len(workStatusList); i++ {
item := workStatusList[i]
if item.Parent == 0 {
parentList = append(parentList, item)
} else {
childrenMap[item.Parent] = append(childrenMap[item.Parent], item)
}
}
index := 1
projectList := make([]*model.WorkStatusProgressRes, 0, len(parentList))
// 遍历父级,将子集添加到父级的字段中
for _, parent := range parentList {
projectItem := model.WorkStatusProgressRes{
ID: index,
WorkID: parent.WorkId,
Startat: parent.StartAt.String(),
Endat: parent.EndAt.String(),
Name: parent.WorkName,
Total: parent.Total,
Finished: parent.Finished,
Status: parent.Status,
WorkType: parent.Type,
Children: make([]model.WorkStatusProgressRes, 0, len(workStatusList)),
}
index++
// 子类的总量和完成量
var subTotal, subDone int
// 从 map 中获取子元素,将子元素添加到父元素的字段中
for _, child := range childrenMap[int(parent.Id)] {
// 如果计划数量等于实际数量且原状态非3则更新状态为2
if child.Total == child.Finished && child.Status != 3 {
// 并且 total,finished 不为 0
if child.Total != 0 && child.Finished != 0 {
child.Status = 2
}
}
// 根据 WorkID 获取计划总量和实际进度
finishedPtr, total := Get(scheduleMap, child.WorkId)
subTotal += child.Total
subDone += child.Finished
projectItem.Children = append(projectItem.Children, model.WorkStatusProgressRes{
ID: index,
WorkID: child.WorkId,
Startat: child.StartAt.String(),
Endat: child.EndAt.String(),
Name: child.WorkName,
Total: child.Total,
Finished: child.Finished,
IsPercent: child.IsPercent,
Status: child.Status,
WorkType: child.Type,
PlanProgress: &model.PlanProgress{
Finished: &finishedPtr,
Total: &total,
},
})
index++
}
// 修改父项目的总量和进度
projectItem.Total = subTotal
projectItem.Finished = subDone
projectList = append(projectList, &projectItem)
}
return projectList, workMap, nil
}
// 导出一个子项目
func AccumulateProject(subProjectID string, startAt, endAt string) ([]*model.WorkStatusProgressRes, map[string]ExcelInfo, error) {
fangzhenList := []entity.QianqiFangzhen{}
if err := dao.QianqiFangzhen.Ctx(context.Background()).
Where(dao.QianqiFangzhen.Columns().ProjectId, subProjectID).Scan(&fangzhenList); err != nil {
return nil, nil, err
}
cumulativeProjects := []*model.WorkStatusProgressRes{}
// 用于判断是否为第一次迭代
firstTime := true
excelMap := make(map[string]ExcelInfo)
// 遍历项目列表
for index := 0; index < len(fangzhenList); index++ {
// 获取当前项目
project := fangzhenList[index]
// 获取当前项目的子项目
childProjects, emap, err := ExportExcel(strconv.Itoa(project.Id), startAt, endAt)
if err != nil {
return nil, nil, err
}
excelMap = lo.Assign(emap, excelMap)
// 如果是第一次迭代,将子项目列表赋值给累积项目列表
if firstTime {
cumulativeProjects = childProjects
// 更新标志变量,表示已经不是第一次迭代
firstTime = false
continue
}
// 遍历子项目列表
for childIndex := 0; childIndex < len(childProjects); childIndex++ {
// 获取当前子项目和对应的累积项目
singleChild := childProjects[childIndex]
cumulativeChild := cumulativeProjects[childIndex]
// 更新累积项目的总数和完成数
cumulativeChild.Total += singleChild.Total
cumulativeChild.Finished += singleChild.Finished
// 如果当前子项目还有子项目,也进行同样的操作
if len(singleChild.Children) > 0 {
for subChildIndex := 0; subChildIndex < len(singleChild.Children); subChildIndex++ {
// 获取当前子项目的子项目和对应的累积项目
singleSubChild := singleChild.Children[subChildIndex]
cumulativeSubChild := cumulativeChild.Children[subChildIndex]
// 更新累积项目的总数和完成数
cumulativeSubChild.Total += singleSubChild.Total
cumulativeSubChild.Finished += singleSubChild.Finished
}
}
}
}
return cumulativeProjects, excelMap, nil
}
// 传递一个 []*model.WorkStatusProgressRes将数据写入到 excel 中
func WriteExcel(tableName string, list []*model.WorkStatusProgressRes, aiResult map[string]int) (*excelize.File, error) {
f, err := excelize.OpenFile("resource/template.xlsx")
if err != nil {
return nil, err
}
// 创建一个工作表
sheetName := "Sheet1"
headerPrefixes := []string{"一、", "二、", "三、", "四、", "五、", "六、", "七、", "八、", "九、", "十、", "十一、", "十二、"}
childPrefixes := []string{"1、", "2、", "3、", "4、", "5、", "6、", "7、", "8、", "9、", "10、", "11、", "12、"}
// 修改表头
f.SetCellValue(sheetName, "A1", tableName)
// 遍历 list
rowIndex := 3
lo.ForEach(list, func(item *model.WorkStatusProgressRes, index int) {
setExcelValues(f, sheetName, rowIndex, item, headerPrefixes[index], aiResult)
rowIndex++
// 遍历子集
lo.ForEach(item.Children, func(child model.WorkStatusProgressRes, childIndex int) {
setExcelValues(f, sheetName, rowIndex, &child, childPrefixes[childIndex], aiResult)
rowIndex++
})
})
return f, nil
}
func setExcelValues(f *excelize.File, sheetName string, i int, item *model.WorkStatusProgressRes, prefix string, aiResult map[string]int) {
// 单元格 A
f.SetCellValue(sheetName, fmt.Sprintf("A%d", i), prefix+item.Name) // 父名
f.SetCellValue(sheetName, fmt.Sprintf("B%d", i), item.Total) // 总量
f.SetCellValue(sheetName, fmt.Sprintf("C%d", i), item.Finished) // 完成
var percentComplete float64
if item.Total != 0 {
percentComplete = float64(item.Finished) / float64(item.Total) * 100
} else {
percentComplete = 0
}
f.SetCellValue(sheetName, fmt.Sprintf("D%d", i), fmt.Sprintf("%.f%%", percentComplete)) // 总量完成百分比
if item.PlanProgress != nil {
total := 0
if item.PlanProgress.Total != nil {
total = *item.PlanProgress.Total
}
f.SetCellValue(sheetName, fmt.Sprintf("E%d", i), total) // 计划量
// AI 填报数量 F
// 用 workID 作为 key,获取 aiResult 中的值,如果不存在则为 0
aiResultValue := aiResult[item.WorkID]
f.SetCellValue(sheetName, fmt.Sprintf("F%d", i), aiResultValue)
finished := 0
if item.PlanProgress.Finished != nil {
finished = *item.PlanProgress.Finished
}
f.SetCellValue(sheetName, fmt.Sprintf("G%d", i), finished) // 计划完成量
var plannedPercentComplete float64
if item.PlanProgress != nil && item.PlanProgress.Finished != nil && item.PlanProgress.Total != nil && *item.PlanProgress.Total != 0 {
plannedPercentComplete = float64(*item.PlanProgress.Finished) / float64(*item.PlanProgress.Total) * 100
} else {
plannedPercentComplete = 0
}
f.SetCellValue(sheetName, fmt.Sprintf("H%d", i), fmt.Sprintf("%.f%%", plannedPercentComplete)) // 计划完成百分比
}
}
// 根据方阵ID获取子项目名
func GetSubProjectName(fangZhenID string) (string, string) {
// select sb.project_name
// from sub_project as sb
// left join qianqi_fangzhen as qf on qf.project_id = sb.id
// where qf.id = 1959;
result, err := dao.SubProject.Ctx(context.Background()).As("sb").
Where("qf.id", fangZhenID).
InnerJoin("qianqi_fangzhen as qf", "qf.project_id = sb.id").
Fields("sb.project_name,qf.name").All()
if err != nil {
return "", ""
}
// 获取子项目名
subProjectName := result[0].GMap().Get("project_name").(string)
// 方阵名
fangZhenName := result[0].GMap().Get("name").(string)
return subProjectName, fangZhenName
}
// 根据子项目ID获取其主项目名和子项目名
func GetNameById(subProjectID string) (string, string) {
// select sp.project_name as projectName,sb.project_name as subProjectName
// from sys_project as sp
// inner join sub_project as sb on sb.project_id = sp.id
// where sb.id = 23;
result, err := dao.SubProject.Ctx(context.Background()).As("sb").
Where("sb.id", subProjectID).
InnerJoin("sys_project as sp", "sb.project_id = sp.id").
Fields("sp.project_name as projectName,sb.project_name as subProjectName").All()
if err != nil {
return "", ""
}
// 获取主项目名
projectName := result[0].GMap().Get("projectName").(string)
// 获取子项目名
subProjectName := result[0].GMap().Get("subProjectName").(string)
return projectName, subProjectName
}
func SetExcelValue(f *excelize.File, sheetName string, index int, item model.WorkStatusProgressRes, excelmap map[string]ExcelInfo, postionName string) {
// 设计数量 F10
total := item.Total
// 本周完成量 G10
finished := item.Finished
// 累计完成量和下周计划量
accumulated, nextWeek := 0, 0
if data, exists := excelmap[item.WorkID]; exists {
accumulated = data.Accumulated
nextWeek = data.NextWeek
}
// 累计完成百分比 单元格 I10
percentComplete := 0.0
if total != 0 {
percentComplete = float64(accumulated) / float64(total) * 100
}
// 名称
f.SetCellValue(sheetName, fmt.Sprintf("%s%d", "C", index), item.Name)
f.SetCellStyle(sheetName, fmt.Sprintf("%s%d", "C", index), fmt.Sprintf("%s%d", "C", index), lo.Must(f.NewStyle(Style)))
// 位置
f.SetCellValue(sheetName, fmt.Sprintf("%s%d", "D", index), postionName)
f.SetCellStyle(sheetName, fmt.Sprintf("%s%d", "D", index), fmt.Sprintf("%s%d", "D", index), lo.Must(f.NewStyle(Style)))
// 单位 样式
f.SetCellStyle(sheetName, fmt.Sprintf("%s%d", "E", index), fmt.Sprintf("%s%d", "E", index), lo.Must(f.NewStyle(Style)))
// 设计数量
f.SetCellValue(sheetName, fmt.Sprintf("%s%d", "F", index), total)
f.SetCellStyle(sheetName, fmt.Sprintf("%s%d", "F", index), fmt.Sprintf("%s%d", "F", index), lo.Must(f.NewStyle(Style)))
// 本周
f.SetCellValue(sheetName, fmt.Sprintf("%s%d", "G", index), finished)
f.SetCellStyle(sheetName, fmt.Sprintf("%s%d", "G", index), fmt.Sprintf("%s%d", "G", index), lo.Must(f.NewStyle(Style)))
// 累计完成量
f.SetCellValue(sheetName, fmt.Sprintf("%s%d", "H", index), accumulated)
f.SetCellStyle(sheetName, fmt.Sprintf("%s%d", "H", index), fmt.Sprintf("%s%d", "H", index), lo.Must(f.NewStyle(Style)))
// 累计完成百分比
f.SetCellValue(sheetName, fmt.Sprintf("%s%d", "I", index), fmt.Sprintf("%.f%%", percentComplete))
f.SetCellStyle(sheetName, fmt.Sprintf("%s%d", "I", index), fmt.Sprintf("%s%d", "I", index), lo.Must(f.NewStyle(Style)))
// 合并 jk 单元格
_ = f.MergeCell(sheetName, fmt.Sprintf("J%d", index), fmt.Sprintf("K%d", index))
f.SetCellStyle(sheetName, fmt.Sprintf("J%d", index), fmt.Sprintf("K%d", index), lo.Must(f.NewStyle(Style)))
// 水平居中 jk 单元格
f.SetCellStyle(sheetName, fmt.Sprintf("J%d", index), fmt.Sprintf("K%d", index), lo.Must(f.NewStyle(&excelize.Style{
Border: []excelize.Border{
{Type: "left", Color: "000000", Style: 1},
{Type: "top", Color: "000000", Style: 1},
{Type: "bottom", Color: "000000", Style: 1},
{Type: "right", Color: "000000", Style: 1},
},
Fill: excelize.Fill{},
Font: nil,
Alignment: &excelize.Alignment{
Horizontal: "center", // 水平居中
Indent: 0,
JustifyLastLine: false,
ReadingOrder: 0,
RelativeIndent: 0,
ShrinkToFit: false,
TextRotation: 0,
Vertical: "", // 垂直居中
WrapText: false,
},
})))
// 下周计划量
f.SetCellValue(sheetName, fmt.Sprintf("%s%d", "J", index), nextWeek)
// L
f.SetCellStyle(sheetName, fmt.Sprintf("%s%d", "L", index), fmt.Sprintf("%s%d", "L", index), lo.Must(f.NewStyle(Style)))
}
func GetWeekNumbers(endTime string) (int, error) {
end, err := time.Parse(time.DateOnly, endTime)
if err != nil {
return 0, fmt.Errorf("解析结束时间错误: %w", err)
}
_, endWeek := end.ISOWeek()
return endWeek, nil
}
func CalculateTotalProgress(list []*model.WorkStatusProgressRes) float64 {
var totalFinished, total int
var calculate func(w model.WorkStatusProgressRes)
calculate = func(w model.WorkStatusProgressRes) {
totalFinished += w.Finished
total += w.Total
for _, child := range w.Children {
calculate(child)
}
}
for _, item := range list {
calculate(*item)
}
if total == 0 {
return 0
}
return (float64(totalFinished) / float64(total)) * 100
}

302
third/excel/template.go Normal file
View File

@ -0,0 +1,302 @@
package excel
import (
"regexp"
"strconv"
"strings"
"github.com/xuri/excelize/v2"
)
var (
GlobalStyle int // 全局样式
GlobalStyleBold int // 基于GlobalStyle的 加粗
GlobalStyleBoldSize int // 基于GlobalStyle的 加粗 加字体大小
)
var Style = &excelize.Style{
// 边框
Border: []excelize.Border{
{Type: "left", Color: "000000", Style: 1},
{Type: "top", Color: "000000", Style: 1},
{Type: "bottom", Color: "000000", Style: 1},
{Type: "right", Color: "000000", Style: 1},
},
// 居中
Alignment: &excelize.Alignment{
Vertical: "center", // 上下居中
Horizontal: "center", // 左右居中
WrapText: true, // 自动换行
},
}
const (
excelName = "Book1.xlsx"
sheetName = "Sheet1"
)
func styleFunc(f *excelize.File) {
baseStyle := excelize.Style{
Border: []excelize.Border{
{Type: "left", Color: "000000", Style: 1},
{Type: "top", Color: "000000", Style: 1},
{Type: "bottom", Color: "000000", Style: 1},
{Type: "right", Color: "000000", Style: 1},
},
Alignment: &excelize.Alignment{
Vertical: "center",
Horizontal: "center",
WrapText: true,
},
}
font := excelize.Font{
Color: "#262626",
Bold: true,
}
// 创建全局样式
GlobalStyle, _ = f.NewStyle(&baseStyle)
// 创建加粗样式
baseStyle.Font = &font
GlobalStyleBold, _ = f.NewStyle(&baseStyle)
// 创建加粗加字体大小样式
font.Size = 12
baseStyle.Font = &font
GlobalStyleBoldSize, _ = f.NewStyle(&baseStyle)
}
// @Title cellMerge
// @Description 合并单元格
// @Author 铁憨憨[cory] 2024-10-18 14:20:40
// @Param f
// @Param qie 单元格切片
// @Param qie 影响行height高度相关
// @Param height 单元格高度(0隐藏当前行 -1取消自定义高度 其他单元格高度)
// @Param style 样式
// @Param str 单元格值
func cellMerge(f *excelize.File, qie []string, height float64, style int, str string) {
if len(qie) > 1 {
f.MergeCell(sheetName, qie[0], qie[1]) // 合并单元格
}
f.SetCellStyle(sheetName, qie[0], qie[len(qie)-1], style) // 设置样式
if height != -1 {
re := regexp.MustCompile("[0-9]+")
rowStr := strings.Join(re.FindAllString(qie[0], -1), "")
if rowNum, err := strconv.Atoi(rowStr); err == nil {
f.SetRowHeight(sheetName, rowNum, height) // 设置行高
}
}
f.SetCellValue(sheetName, qie[0], str) // 设置单元格的值
}
// 创建一个基础的 Excel 模板返回 *excelize.File
func CreateExcelTemplate() (*excelize.File, error) {
f := excelize.NewFile()
defer f.Close()
// 初始化样式
styleFunc(f)
// 初始化单元格布局
businessFunc(f)
index, err := f.NewSheet(sheetName)
if err != nil {
return nil, err
}
f.SetActiveSheet(index)
return f, nil
}
func businessFunc(f *excelize.File) {
// 一、
one := []string{"A1", "L1"}
cellMerge(f, one, 64, GlobalStyleBoldSize, "兴义市捧乍猪场坪农业光伏电站项目EPC总承包项目周报")
// 二、
two := []string{"A2", "B2"}
cellMerge(f, two, 30, GlobalStyle, "项目名称:")
two2 := []string{"C2", "G2"}
cellMerge(f, two2, -1, GlobalStyle, "兴义市捧乍猪场坪农业光伏电站项目EPC总承包项目")
two3 := []string{"H2"}
cellMerge(f, two3, -1, GlobalStyleBold, "周报期数")
two4 := []string{"I2"}
cellMerge(f, two4, -1, GlobalStyle, "53")
two5 := []string{"J2", "J3"}
cellMerge(f, two5, -1, GlobalStyle, "日期:")
two6 := []string{"K2", "L3"}
cellMerge(f, two6, -1, GlobalStyle, "2024.10.05-2024.10.11")
// 三、
three := []string{"A3"}
cellMerge(f, three, -1, GlobalStyle, "序号")
three2 := []string{"B3", "I3"}
cellMerge(f, three2, -1, GlobalStyle, "工程实施主要情况")
// 四、
four := []string{"A4"}
cellMerge(f, four, -1, GlobalStyle, "1")
four2 := []string{"B4"}
cellMerge(f, four2, -1, GlobalStyle, "项目基本信息")
four3 := []string{"C4"}
cellMerge(f, four3, -1, GlobalStyle, "装机容量:")
four4 := []string{"D4", "E4"}
cellMerge(f, four4, -1, GlobalStyle, "开工日期2022-08-15")
four5 := []string{"F4", "H4"}
cellMerge(f, four5, -1, GlobalStyle, "计划并网日期:")
four6 := []string{"I4"}
cellMerge(f, four6, -1, GlobalStyle, "总进度完成:")
four7 := []string{"J4", "L4"}
cellMerge(f, four7, -1, GlobalStyle, "96.5%")
// 五、
five := []string{"A5"}
cellMerge(f, five, -1, GlobalStyle, "2")
five2 := []string{"B5"}
cellMerge(f, five2, -1, GlobalStyle, "本周完成主要形象进度")
five3 := []string{"C5", "L5"}
cellMerge(f, five3, -1, GlobalStyle, "")
// 六、
six := []string{"A6"}
cellMerge(f, six, -1, GlobalStyle, "3")
six2 := []string{"B6"}
cellMerge(f, six2, -1, GlobalStyle, "下周计划主要形象进度")
six3 := []string{"C6", "L6"}
cellMerge(f, six3, -1, GlobalStyle, "")
// 七、
seven := []string{"A7"}
cellMerge(f, seven, -1, GlobalStyle, "4")
seven2 := []string{"B7"}
cellMerge(f, seven2, -1, GlobalStyle, "质量(施工质量、设备材料到货验收)情况")
seven3 := []string{"C7", "L7"}
cellMerge(f, seven3, -1, GlobalStyle, "")
// 八、
eight := []string{"A8"}
cellMerge(f, eight, -1, GlobalStyle, "5")
eight2 := []string{"B8"}
cellMerge(f, eight2, -1, GlobalStyle, "人员到位情况及施工器具")
eight3 := []string{"C8", "L8"}
cellMerge(f, eight3, -1, GlobalStyle, "")
// 九、
nine := []string{"A9", "A23"}
cellMerge(f, nine, -1, GlobalStyle, "6")
nine2 := []string{"B9", "B23"}
cellMerge(f, nine2, -1, GlobalStyle, "")
nine3 := []string{"C9"}
cellMerge(f, nine3, -1, GlobalStyle, "名称")
nine4 := []string{"D9"}
cellMerge(f, nine4, -1, GlobalStyle, "位置")
nine5 := []string{"E9"}
cellMerge(f, nine5, -1, GlobalStyle, "单位")
nine6 := []string{"F9"}
cellMerge(f, nine6, -1, GlobalStyle, "设计数量")
nine7 := []string{"G9"}
cellMerge(f, nine7, -1, GlobalStyle, "本周完成情况")
nine8 := []string{"H9"}
cellMerge(f, nine8, -1, GlobalStyle, "累计完成量")
nine9 := []string{"I9"}
cellMerge(f, nine9, -1, GlobalStyle, "累计完成率")
nine10 := []string{"J9", "K9"}
cellMerge(f, nine10, -1, GlobalStyle, "下周计划完成")
nine11 := []string{"L9"}
cellMerge(f, nine11, -1, GlobalStyle, "备注")
// 十
ten := []string{"B10", "B23"}
cellMerge(f, ten, -1, GlobalStyle, "施工进度(根据项目类型及进度情况自行分项,不用太细,能体现整体进度即可)")
ten2 := []string{"C10"}
cellMerge(f, ten2, -1, GlobalStyle, "基础")
ten3 := []string{"D10"}
cellMerge(f, ten3, -1, GlobalStyle, "光伏区")
ten4 := []string{"E10"}
cellMerge(f, ten4, -1, GlobalStyle, "个")
ten5 := []string{"F10"}
cellMerge(f, ten5, -1, GlobalStyle, "45450")
ten6 := []string{"G10"}
cellMerge(f, ten6, -1, GlobalStyle, "1415")
ten7 := []string{"H10"}
cellMerge(f, ten7, -1, GlobalStyle, "15445")
ten8 := []string{"I10"}
cellMerge(f, ten8, -1, GlobalStyle, "90")
ten9 := []string{"J10", "K10"}
cellMerge(f, ten9, -1, GlobalStyle, "545")
ten10 := []string{"L10"}
cellMerge(f, ten10, -1, GlobalStyle, "备注")
// 二十四、
twentyFour := []string{"A24", "A41"}
cellMerge(f, twentyFour, -1, GlobalStyle, "7")
twentyFour2 := []string{"B24", "B41"}
cellMerge(f, twentyFour2, -1, GlobalStyle, "主要设备材料到货情况(列出主要材料即可)")
twentyFour3 := []string{"C24"}
cellMerge(f, twentyFour3, -1, GlobalStyle, "设备材料名称")
twentyFour4 := []string{"D24"}
cellMerge(f, twentyFour4, -1, GlobalStyle, "")
twentyFour5 := []string{"E24"}
cellMerge(f, twentyFour5, -1, GlobalStyle, "单位")
twentyFour6 := []string{"F24"}
cellMerge(f, twentyFour6, -1, GlobalStyle, "设计数量")
twentyFour7 := []string{"G24"}
cellMerge(f, twentyFour7, -1, GlobalStyle, "本周到货量")
twentyFour8 := []string{"H24"}
cellMerge(f, twentyFour8, -1, GlobalStyle, "累计到货量")
twentyFour9 := []string{"I24"}
cellMerge(f, twentyFour9, -1, GlobalStyle, "累计到货率")
twentyFour10 := []string{"J24", "K24"}
cellMerge(f, twentyFour10, -1, GlobalStyle, "计划到货日期")
twentyFour11 := []string{"L24"}
cellMerge(f, twentyFour11, -1, GlobalStyle, "备注")
// 二十五
twentyFive2 := []string{"C25"}
cellMerge(f, twentyFive2, -1, GlobalStyle, "角钢")
twentyFive3 := []string{"D25"}
cellMerge(f, twentyFive3, -1, GlobalStyle, "")
twentyFive4 := []string{"E25"}
cellMerge(f, twentyFive4, -1, GlobalStyle, "")
twentyFive5 := []string{"F25"}
cellMerge(f, twentyFive5, -1, GlobalStyle, "")
twentyFive6 := []string{"G25"}
cellMerge(f, twentyFive6, -1, GlobalStyle, "")
twentyFive7 := []string{"H25"}
cellMerge(f, twentyFive7, -1, GlobalStyle, "")
twentyFive8 := []string{"I25"}
cellMerge(f, twentyFive8, -1, GlobalStyle, "90")
twentyFive9 := []string{"J25", "K25"}
cellMerge(f, twentyFive9, -1, GlobalStyle, "")
twentyFive10 := []string{"L25"}
cellMerge(f, twentyFive10, -1, GlobalStyle, "")
// 四十二
fortyTwo := []string{"A42"}
cellMerge(f, fortyTwo, -1, GlobalStyle, "8")
fortyTwo2 := []string{"B42"}
cellMerge(f, fortyTwo2, -1, GlobalStyle, "存在问题及需要协调的事项")
fortyTwo3 := []string{"C42", "L42"}
cellMerge(f, fortyTwo3, -1, GlobalStyle, "无")
// 四十三
fortyThree := []string{"A43"}
cellMerge(f, fortyThree, 90, GlobalStyle, "9")
fortyThree2 := []string{"B43"}
cellMerge(f, fortyThree2, -1, GlobalStyle, "照片")
fortyThree3 := []string{"C43", "E43"}
cellMerge(f, fortyThree3, -1, GlobalStyle, "")
fortyThree4 := []string{"F43", "H43"}
cellMerge(f, fortyThree4, -1, GlobalStyle, "")
fortyThree5 := []string{"I43", "L43"}
cellMerge(f, fortyThree5, -1, GlobalStyle, "")
// 四十四
fortyFour := []string{"A44"}
cellMerge(f, fortyFour, -1, GlobalStyle, "10")
fortyFour2 := []string{"B44"}
cellMerge(f, fortyFour2, -1, GlobalStyle, "填报:")
fortyFour3 := []string{"C44", "L44"}
cellMerge(f, fortyFour3, -1, GlobalStyleBold, "审核:")
// 整体样式,不需要动
f.SetColWidth(sheetName, "A", "A", 8)
f.SetColWidth(sheetName, "B", "B", 11)
f.SetColWidth(sheetName, "C", "C", 26)
f.SetColWidth(sheetName, "D", "D", 26)
f.SetColWidth(sheetName, "H", "H", 13)
f.SetCellStyle(sheetName, "A1", "B44", GlobalStyleBold) // 注意B44到时候需要取excel最大行数
}

32
third/isc/api.go Normal file
View File

@ -0,0 +1,32 @@
package isc
type camera struct {
CameraIndexCode string `json:"cameraIndexCode"`
CameraName string `json:"cameraName"`
Marker_id string `json:"marker_id"`
Status int `json:"status"`
}
var ca Camera
var cameras []Camera
var DbCameraMap = make(map[string]camera)
func InitIscApi() {
//getAllCameraFromDb()
//getCameraFromIsc()
//GetStatusList()
//createTimer()
//GetCameraList(1, 20)
}
type Ret struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
}
func getCameraFromIsc() {
GetCameraList(1, 1000)
}

263
third/isc/isc_auth.go Normal file
View File

@ -0,0 +1,263 @@
package isc
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"crypto/tls"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"math/rand"
"net/http"
"strconv"
"strings"
"time"
)
var AppKey = "22422818" //海康平台的key
var AppSecret = "smyUkLf2Rd9VOkLV9Ndv"
//var AppKey = "22422818" //海康平台的key
//var AppSecret = "smyUkLf2Rd9VOkLV9Ndv"
var Protocol = "https://"
var HOST = "192.168.1.153"
var PORT = "2443"
var reqError = "2003"
var reqErrorMsg = "请求失败"
var Ok = "0"
var Method = "POST"
func GetStatusList() {
config := map[string]interface{}{}
config["includeSubNode"] = 1
config["pageNo"] = 1
config["pageSize"] = 1000
config["regionId"] = "root000000"
//config["indexCodes"] = []string{"7cbd5a4c2dab46579b09c297cf649721", "7ec4f9689dc1466d813c801a5a0686b2"}
t := CameraStatus{Code: reqError, Msg: reqErrorMsg}
//content, _ := request("/api/nms/v1/online/camera/get", config)
content, _ := request("/api/nms/v1/online/camera/get", config)
fmt.Println("获取设备状态")
_ = json.Unmarshal([]byte(content), &t)
for _, v := range t.Data.List {
if ca, ok := DbCameraMap[v.IndexCode]; ok {
ca.Status = v.Online
DbCameraMap[v.IndexCode] = ca
}
//fmt.Println("v.Cn", v.Cn)
//fmt.Println("v.Online", v.Online)
//fmt.Println("v.DeviceIndexCode", v.DeviceIndexCode)
//fmt.Println("v.IndexCode", v.IndexCode)
//fmt.Println("v.CollectTime", v.CollectTime)
//fmt.Println("************************")
}
}
/*抓图接口*/
func ManualCapture(cameraIndexCode string) CameraCap {
config := map[string]interface{}{}
config["cameraIndexCode"] = cameraIndexCode
t := CameraCap{Code: reqError, Msg: reqErrorMsg}
content, _ := request("/api/video/v1/manualCapture", config)
_ = json.Unmarshal([]byte(content), &t)
return t
}
/*
云台控制
action number 0-开始 1-停止
GOTO_PRESET命令下填任意值均可转到预置点,建议填0即可
command string 不区分大小写
说明:
LEFT 左转
RIGHT右转
UP 上转
DOWN 下转
ZOOM_IN 焦距变大
ZOOM_OUT 焦距变小
LEFT_UP 左上
LEFT_DOWN 左下
RIGHT_UP 右上
RIGHT_DOWN 右下
FOCUS_NEAR 焦点前移
FOCUS_FAR 焦点后移
IRIS_ENLARGE 光圈扩大
IRIS_REDUCE 光圈缩小
WIPER_SWITCH 接通雨刷开关
START_RECORD_TRACK 开始记录轨迹
STOP_RECORD_TRACK 停止记录轨迹
START_TRACK 开始轨迹
STOP_TRACK 停止轨迹;
以下命令presetIndex不可为空
GOTO_PRESET到预置点
*/
func CtrlPtz(cameraIndexCode string, action int, command string) Usual {
config := map[string]interface{}{}
config["cameraIndexCode"] = cameraIndexCode
config["action"] = action
config["command"] = command
config["speed"] = 50 //默认速度为50
t := Usual{Code: reqError, Msg: reqErrorMsg}
content, _ := request("/api/video/v1/ptzs/controlling", config)
_ = json.Unmarshal([]byte(content), &t)
return t
}
/*获取对讲地址*/
func GettalkURLs(cameraIndexCode string) CameraTalk {
config := map[string]interface{}{}
config["cameraIndexCode"] = cameraIndexCode
config["protocol"] = "ws"
t := CameraTalk{Code: reqError, Msg: reqErrorMsg}
content, _ := request("/api/video/v1/cameras/talkURLs", config)
_ = json.Unmarshal([]byte(content), &t)
return t
}
/*获取预览地址 ws协议*/
func GetpreviewURLs(cameraIndexCode string) CameraPreview {
config := map[string]interface{}{}
config["cameraIndexCode"] = cameraIndexCode
config["protocol"] = "ws"
t := CameraPreview{Code: reqError, Msg: reqErrorMsg}
content, _ := request("/api/video/v1/cameras/previewURLs", config)
//fmt.Println(api)
//content, _ := request(api, config)
fmt.Println(content)
_ = json.Unmarshal([]byte(content), &t)
return t
}
/*获取监控点列表*/
func GetCameraList(pageNo, pageSize int) {
fmt.Println("开始获取第一页数据")
config := map[string]interface{}{}
config["pageNo"] = pageNo
config["pageSize"] = pageSize
t := CameraData{Code: reqError, Msg: reqErrorMsg}
content, err := request("/api/resource/v1/cameras", config)
if err != nil {
return
}
fmt.Println("数据完成")
fmt.Println(content)
err = json.Unmarshal([]byte(content), &t)
if err != nil {
fmt.Println("数据反序列化失败")
fmt.Println(err)
return
}
if t.Code == Ok {
fmt.Println("监控点总数", t.Data.Total)
fmt.Println("页码", t.Data.PageNo)
fmt.Println("当前页数量", t.Data.PageSize)
//fmt.Println(t.Data.List)
for _, v := range t.Data.List {
fmt.Println(v.CameraIndexCode, v.Name, v.Status)
//if v.Status == 1 {
// ca = v
//}
}
} else {
fmt.Println("数据获取失败")
fmt.Println(t)
}
}
// 请求
func request(url string, body map[string]interface{}) (string, error) {
configData, _ := json.Marshal(body)
param := bytes.NewBuffer(configData)
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{Transport: tr}
url = "/artemis" + url
headers, _ := getSignature(url, Method)
url = Protocol + HOST + ":" + PORT + url
fmt.Println(url)
// 2. 创建请求实例
req, err := http.NewRequest(Method, url, param)
if err != nil {
panic(err)
}
//header
for i := 0; i < len(headers); i++ {
//fmt.Println(headers[i].Key, headers[i].Val)
req.Header.Add(headers[i].Key, headers[i].Val)
}
//发送请求
res, err := client.Do(req)
if err != nil {
return "", err
}
defer res.Body.Close()
content, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", err
}
return string(content), nil
}
func getSignature(url, method string) ([]Header, string) {
var now = time.Now()
var headers = []Header{
{Key: "Accept", Val: "*/*"},
{Key: "Content-Type", Val: "application/json"},
{Key: "Date", Val: now.Format("2006-01-02 15:04:05")},
{Key: "x-ca-key", Val: AppKey, Use: true},
{Key: "x-ca-nonce", Val: RandAllString(32), Use: true},
{Key: "x-ca-timestamp", Val: strconv.Itoa(int(now.UnixNano() / 1e6)), Use: true},
}
var arr = append(headers, Header{Key: "url", Val: url})
var arr2 = append([]Header{{Val: method, Key: "method"}}, arr...)
var str []string
for i := 0; i < len(arr2); i++ {
if arr2[i].Use {
str = append(str, arr2[i].Key+":"+arr2[i].Val)
} else {
str = append(str, arr2[i].Val)
}
}
sign := ComputeHmac256(strings.Join(str, "\n"), AppSecret)
headers = append(headers, Header{Key: "x-ca-signature-headers", Val: "x-ca-key,x-ca-nonce,x-ca-timestamp"}, Header{Key: "x-ca-signature", Val: sign})
return headers, sign
}
func ComputeHmac256(message string, secret string) string {
key := []byte(secret)
h := hmac.New(sha256.New, key)
h.Write([]byte(message))
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
type Header struct {
Key string `json:"key"`
Val string `json:"val"`
Use bool `json:"use"`
}
/*
RandAllString 生成随机字符串([a~zA~Z0~9])
lenNum 长度
*/
func RandAllString(lenNum int) string {
var CHARS = []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0"}
str := strings.Builder{}
length := len(CHARS)
for i := 0; i < lenNum; i++ {
l := CHARS[rand.Intn(length)]
str.WriteString(l)
}
return str.String()
}

92
third/isc/struct.go Normal file
View File

@ -0,0 +1,92 @@
package isc
// 通用结构体
type Usual struct {
Code string `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
}
type Camera struct {
CameraIndexCode string `json:"cameraIndexCode"` //监控点编号
Name string `json:"name"` //监控点名称
CameraType int `json:"cameraType"` //监控点类型
CameraTypeName string `json:"cameraTypeName"` //监控点类型名称
Longitude string `json:"longitude"` //监控点类型名称
Latitude string `json:"latitude"` //监控点类型名称
Altitude string `json:"altitude"` //监控点类型名称
ChannelNo string `json:"channelNo"` //监控点类型名称
Status int `json:"status"` //监控点在线状态 在线状态0-未知1-在线2-离线),扩展字段,暂不使用
}
// 监控点列表信息结构
type CameraData struct {
Code string `json:"code"`
Msg string `json:"msg"`
Data struct {
Total int `json:"total"`
PageNo int `json:"pageNo"`
PageSize int `json:"pageSize"`
List []Camera `json:"list"`
} `json:"data"`
}
// 监控点的预览接口返回值结构
type CameraPreview struct {
Code string `json:"code"`
Msg string `json:"msg"`
Data struct {
Url string `json:"url"`
} `json:"data"`
}
//监控点的对讲接口返回值结构
type CameraTalk struct {
Code string `json:"code"`
Msg string `json:"msg"`
Data struct {
Url string `json:"url"`
} `json:"data"`
}
//监控点的抓图接口返回值结构
type CameraCap struct {
Code string `json:"code"`
Msg string `json:"msg"`
Data struct {
PicUrl string `json:"picUrl"`
} `json:"data"`
}
// 监控点的在线状态接口返回值结构
type statusList struct {
Online int `json:"online"`
IndexCode string `json:"indexCode"`
DeviceIndexCode string `json:"deviceIndexCode"`
Cn string `json:"cn"`
CollectTime string `json:"collectTime"`
//"deviceType": "HIK%2FDS-9116HW-ST%2F-AF-DVR",
//"deviceIndexCode": null,
//"regionIndexCode": "ce91c758-5af4-4539-845a",
//"collectTime": "2018-12-28T10:21:40.000+08:00",
//"regionName": "NMS自动化",
//"indexCode": "82896441ced946d5a51c6d6ca8e65851",
//"cn": "Onvif-IPC(10.67.172.13 )",
//"treatyType": "1",
//"manufacturer": "hikvision",
//"ip": null,
//"port": null,
//"online": 1
}
type CameraStatus struct {
Code string `json:"code"`
Msg string `json:"msg"`
Data struct {
PageNo int `json:"pageNo"`
PageSize int `json:"pageSize"`
TotalPage int `json:"totalPage"`
Total int `json:"total"`
List []statusList `json:"list"`
} `json:"data"`
}

103
third/jiguang/config.go Normal file
View File

@ -0,0 +1,103 @@
package jiguang
import (
"encoding/base64"
"fmt"
"github.com/go-resty/resty/v2"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
"golang.org/x/net/context"
)
var appKey = "9bbb84675085d124417ceb27"
var masterSecret = "1a7dff8f3ce58df231481fce"
// @Title Notification 2024/7/25 15:45:00
// @Description 通知
// @Auth Cory
type Notification struct {
Platform interface{} `json:"platform" dc:"推送平台设置"`
Audience interface{} `json:"audience" dc:"推送设备指定"`
Notification struct {
Android struct {
Title string `json:"title" dc:"通知标题"`
Alert string `json:"alert" dc:"通知内容"`
} `json:"android" dc:"Android 平台上的通知"`
} `json:"notification" dc:"通知内容体,是被推送到客户端的内容。"`
}
// @Title SendAppClockingNotice 2024/7/25 15:35:00
// @Description 发送APP的打卡通知信息 需注意registration_id一次最多只能发送1000
// @Auth Cory
// @param 输入参数名 ---> "参数解释"
// @Return error ---> "错误信息"
func SendAppClockingNotice(ctx context.Context, deviceIds []string, dataStr string, flagStr string) {
//1、发送位置及authorization
url := "https://api.jpush.cn/v3/push"
authorization := base64.StdEncoding.EncodeToString([]byte(appKey + ":" + masterSecret))
//2、创建 Resty 客户端对象
client := resty.New()
//3、POST 请求示例
splitIds := splitDeviceIds(deviceIds)
for _, ids := range splitIds {
//reqBody := map[string]interface{}{
// "platform": []string{"android"},
// "audience": map[string]interface{}{"registration_id": []string{"", ""}},
// "notification": Notification{Android: AndroidEntity(struct {
// Title string
// Alert string
// }{Title: "上下班打卡提醒", Alert: "即将进入打卡时间,请立即前往【定位考勤】页面进行打卡(如若已打卡或休息请忽略)"})},
//}
notification := Notification{
Platform: []string{"android"},
Audience: map[string]interface{}{"registration_id": ids},
Notification: struct {
Android struct {
Title string `json:"title" dc:"通知标题"`
Alert string `json:"alert" dc:"通知内容"`
} `json:"android" dc:"Android 平台上的通知"`
}(struct {
Android struct {
Title string
Alert string
}
}{
Android: struct {
Title string
Alert string
}{
Title: "中煤" + dataStr + flagStr + "打卡提醒",
Alert: "即将进入打卡时间,请立即前往【定位考勤】页面进行打卡(如若已打卡或休息请忽略)",
},
}),
}
//4、发送post请求
_, err := client.R().
SetHeader("Content-Type", "application/json").
SetHeader("Authorization", "Basic "+authorization).
SetBody(notification).
Post(url)
if err != nil {
fmt.Println("发送 POST 请求失败:", err)
g.Log().Error(gctx.New(), err)
return
}
}
}
// @Title splitDeviceIds 2024/7/26 15:56:00
// @Description 数据切分900一条
// @Auth Cory
func splitDeviceIds(deviceIds []string) [][]string {
var result [][]string
for i := 0; i < len(deviceIds); i += 900 {
end := i + 900
if end > len(deviceIds) {
end = len(deviceIds)
}
result = append(result, deviceIds[i:end])
}
return result
}

3
third/livegbs/livegbs.go Normal file
View File

@ -0,0 +1,3 @@
package livegbs
var Token = ""

View File

@ -0,0 +1,112 @@
// @Author cory 2025/4/24 15:52:00
package officeWeb365
import (
"bytes"
"encoding/json"
"errors"
"github.com/tiger1103/gfast/v3/api/v1/common/coryCommon"
"golang.org/x/net/context"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"strings"
)
func SendFile199(ctx context.Context, relativePath string, typeStr string) (err error, urlPath string) {
//1、根据相对路径获取到文件信息
filePath := coryCommon.FileToFunc(relativePath, 2)
fieldName := "file" // 表单字段名
// 获取文件名
fileName := filepath.Base(filePath) // 自动获取 "222.xlsx"
// 打开文件
file, err := os.Open(filePath)
if err != nil {
panic(err)
}
defer file.Close()
// 创建 multipart body
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
// 创建文件字段(自动带文件名)
part, err := writer.CreateFormFile(fieldName, fileName)
if err != nil {
panic(err)
}
// 将文件内容复制进去
_, err = io.Copy(part, file)
if err != nil {
panic(err)
}
// 关闭 multipart writer
err = writer.Close()
if err != nil {
panic(err)
}
url := ""
sip := "http://192.168.1.177:8188"
switch typeStr {
case "1":
url = sip + "/api/doc/view/uploadfile"
case "2":
url = sip + "/OnlineEditing/GetPreviewUrl"
case "3":
url = sip + "/api/ppt/view/uploadfile"
}
// 创建请求
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
if err != nil {
panic(err)
}
// 设置 multipart Content-Type
req.Header.Set("Content-Type", writer.FormDataContentType())
// 发出请求
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err, ""
}
defer resp.Body.Close()
// 读取响应
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return errors.New("请求错误!"), ""
}
// 解析响应 JSON
var responseData ResponseData
err = json.Unmarshal(respBody, &responseData)
if err != nil {
return errors.New(string(respBody)), ""
}
// 输出结果
if responseData.Code == 200 {
return nil, strings.Replace(responseData.Data.ViewURL, sip, "", -1)
} else {
return errors.New("请求错误!"), ""
}
}
// ResponseData 定义响应数据的结构体
type ResponseData struct {
Code int `json:"code"`
Data struct {
RequestID string `json:"request_id"`
ViewURL string `json:"view_url"`
} `json:"data"`
Msg string `json:"msg"`
}

View File

@ -0,0 +1 @@
package dj

1217
third/plane/dj/dj.go Normal file

File diff suppressed because it is too large Load Diff

203
third/plane/dj/errorcode.go Normal file
View File

@ -0,0 +1,203 @@
package dj
var errorMap map[int]Error
type Error struct {
Code int `json:"code"`
Msg string `json:"msg"`
}
func InitError() {
errorMap = make(map[int]Error)
errors := []Error{
{0, "成功"},
{314000, "设备当前无法支持该操作,建议检查设备当前工作状态"},
{314001, "飞行任务下发失败,请稍后重试"},
{314002, "飞行任务下发失败,请稍后重试"},
{314003, "航线文件格式不兼容,请检查航线文件是否正确"},
{314004, "飞行任务下发失败,请稍后重试或重启机场后重试"},
{314005, "飞行任务下发失败,请稍后重试或重启机场后重试"},
{314006, "飞行器初始化失败,请重启机场后重试"},
{314007, "机场传输航线至飞行器失败,请重启机场后重试"},
{314008, "飞行器起飞前准备超时,请重启机场后重试"},
{314009, "飞行器初始化失败,请重启机场后重试"},
{314010, "航线执行失败,请重启机场后重试"},
{314011, "机场系统异常,无法获取飞行任务执行结果"},
{314012, "飞行器起飞前准备失败,无法执行飞行任务,请重启机场后重试"},
{314013, "飞行任务下发失败,机场无法获取到本次飞行任务的航线,无法执行飞行任务,请稍后重试"},
{314014, "机场系统异常,飞行任务执行失败,请稍后重试"},
{314015, "机场传输精准复拍航线至飞行器失败,无法执行飞行任务,请稍后重试或重启机场后重试"},
{314016, "航线文件解析失败,无法执行飞行任务,请检查航线文件"},
{314017, "机场系统异常,飞行任务执行失败,请稍后重试"},
{314018, "飞行器 RTK 定位异常,无法执行飞行任务,请稍后重试或重启机场后重试"},
{314019, "飞行器 RTK 收敛失败,无法执行飞行任务,请稍后重试或重启机场后重试"},
{314020, "飞行器不在停机坪正中间或飞行器朝向不正确,无法执行飞行任务,请检查飞行器位置和朝向"},
{314021, "飞行器 RTK 定位异常,无法执行飞行任务,请稍后重试或重启机场后重试"},
{316001, "飞行器参数配置失败,请重启机场后重试"},
{316002, "飞行器参数配置失败,请重启机场后重试"},
{316003, "飞行器参数配置失败,请重启机场后重试"},
{316004, "飞行器参数配置失败,请重启机场后重试"},
{316005, "飞行器 RTK 收敛失败,无法执行飞行任务,请重启机场后重试"},
{316006, "飞行器降落前机场未开启舱盖或展开推杆,飞行器无法降落至机场,请尽快至机场部署现场检查飞行器状况"},
{316007, "飞行器初始化失败,请重启机场后重试"},
{316008, "机场获取飞行器控制权失败,无法执行飞行任务,请确认遥控器未锁定控制权"},
{316009, "飞行器电量低于30%,无法执行飞行任务,请充电后重试(建议电量≥50%)"},
{316010, "机场未检测到飞行器,无法执行飞行任务,请检查舱内是否有飞行器,机场与飞行器是否已对频,或重启机场后重试"},
{316011, "飞行器降落位置偏移过大,请检查飞行器是否需要现场摆正"},
{316012, "飞行器起飞前准备失败,无法执行飞行任务,请重启机场后重试"},
{316013, "飞行器起飞前准备失败,无法执行飞行任务,请重启机场后重试"},
{316014, "飞行器起飞前准备失败,无法执行飞行任务,请重启机场后重试"},
{316015, "飞行器 RTK 收敛位置距离机场过远,无法执行飞行任务,请重启机场后重试"},
{316016, "飞行器降落至机场超时,可能是机场与飞行器断连导致,请通过直播查看飞行器是否降落至舱内"},
{316017, "获取飞行器媒体数量超时,可能是机场与飞行器断连导致,请通过直播查看飞行器是否降落至舱内"},
{316018, "飞行任务执行超时,可能是机场与飞行器断连导致,请通过直播查看飞行器是否降落至舱内"},
{316019, "服务器内部错误,无法执行飞行任务,请稍后重试"},
{316020, "飞行器使用的 RTK 信号源错误,请稍后重试"},
{316021, "飞行器 RTK 信号源检查超时,请稍后重试"},
{316022, "飞行器无法执行返航指令,请检查飞行器是否已开机,机场与飞行器是否已断连,请确认无以上问题后重试"},
{316023, "飞行器无法执行返航指令,飞行器已被 B 控接管,请在 B 控操控飞行器,或关闭 B 控后重试"},
{316024, "飞行器执行返航指令失败,请检查飞行器是否已起飞,确认飞行器已起飞后请重试"},
{316025, "飞行器参数配置失败,请稍后重试或重启机场后重试"},
{316026, "机场急停按钮被按下,无法执行飞行任务,请释放急停按钮后重试"},
{316027, "飞行器参数配置超时,请稍后重试或重启机场后重试"},
{316029, "机场急停按钮被按下,飞行器将飞往备降点降落,请立即检查飞行器是否已安全降落并将飞行器放回至机场"},
{317001, "获取飞行器媒体文件数量失败,请重启机场后重试"},
{317002, "飞行器存储格式化失败,飞行器未开机、未连接或未检测到相机,请确认无以上问题后重试,或重启飞行器后重试"},
{317003, "飞行器存储格式化失败,请重启飞行器后重试"},
{317004, "机场媒体文件格式化失败,请稍后重试或重启机场后重试"},
{317005, "飞行器结束录像失败,本次飞行任务的媒体文件可能无法上传"},
{319001, "机场作业中或(设备异常反馈)上传日志中,无法执行飞行任务,请等待当前飞行任务或操作执行完成后重试"},
{319002, "机场系统运行异常,请重启机场后重试"},
{319003, "机场系统运行异常,请重新下发任务"},
{319004, "飞行任务执行超时,已自动终止本次飞行任务"},
{319005, "云端与机场通信异常,无法执行飞行任务"},
{319006, "取消飞行任务失败,飞行任务已经在执行中"},
{319007, "修改飞行任务失败,飞行任务已经在执行中"},
{319008, "机场时间与云端时间不同步,机场无法执行飞行任务"},
{319009, "飞行任务下发失败,请稍后重试或重启机场后重试"},
{319010, "机场固件版本过低,无法执行飞行任务,请升级机场固件为最新版本后重试"},
{319015, "机场正在初始化中,无法执行飞行任务,请等待机场初始化完成后重试"},
{319016, "机场正在执行其他飞行任务,无法执行本次飞行任务"},
{319017, "机场正在处理上次飞行任务媒体文件,无法执行本次飞行任务,请稍后重试"},
{319018, "机场正在自动导出日志中(设备异常反馈),无法执行飞行任务,请稍后重试"},
{319019, "机场正在拉取日志中(设备异常反馈),无法执行飞行任务,请稍后重试"},
{319025, "机场未准备完成,无法执行云端下发的飞行任务,请稍后重试"},
{319026, "飞行器电池电量低于用户设置的任务开始执行的电量,请等待充电完成后再执行飞行任务"},
{319027, "机场或飞行器剩余存储容量过低,无法执行飞行任务,请等待媒体文件上传,机场和飞行器存储容量释放后再执行飞行任务"},
{319999, "机场系统运行异常,请重启机场后重试"},
{321000, "航线执行异常,请稍后重试或重启机场后重试"},
{321004, "航线文件解析失败,无法执行飞行任务,请检查航线文件"},
{321005, "航线缺少断点信息,机场无法执行飞行任务"},
{321257, "飞行任务已在执行中,请勿重复执行"},
{321258, "飞行任务无法终止,请检查飞行器状态"},
{321259, "飞行任务未开始执行,无法终止飞行任务"},
{321260, "飞行任务未开始执行,无法中断飞行任务"},
{321513, "航线规划高度已超过飞行器限高,机场无法执行飞行任务"},
{321514, "任务失败,起点或终点位于限远区域的缓冲区内或超过了限远距离"},
{321515, "航线穿过限飞区,机场无法执行飞行任务"},
{321517, "飞行器触发避障,飞行任务执行被终止"},
{321519, "飞行器接近限飞区或限远距离自动返航,无法完成航线飞行"},
{321523, "飞行器起桨失败,请稍后重试,如果仍报错请联系大疆售后。"},
{321524, "飞行器起飞前准备失败,可能是飞行器无发定位或档位错误导致,请检查飞行器状态"},
{321769, "飞行器卫星定位信号差,无法执行飞行任务,请重启机场后重试"},
{321770, "飞行器挡位错误,无法执行飞行任务,请重启机场后重试"},
{321771, "飞行器返航点未设置,无法执行飞行任务,请重启机场后重试"},
{321772, "飞行器电量低于30%,无法执行飞行任务,请充电后重试(建议电量≥50%)"},
{321773, "飞行器执行飞行任务过程中低电量返航,无法完成航线飞行"},
{321775, "飞行器航线飞行过程中失联,无法完成航线飞行"},
{321776, "飞行器 RTK 收敛失败,无法执行飞行任务,请重启机场后重试"},
{321777, "飞行器未悬停,无法开始执行飞行任务"},
{321778, "用户使用 B 控操控飞行器起桨,机场无法执行飞行任务"},
{322282, "机场执行飞行任务过程中被中断飞行器被云端用户或B控接管"},
{322283, "机场执行飞行任务过程中被用户触发返航,无法完成航线飞行"},
{322539, "航线的断点信息错误,机场无法执行飞行任务"},
{324012, "日志压缩过程超时,所选日志过多,请减少选择的日志后重试"},
{324013, "设备日志列表获取失败,请稍后重试"},
{324014, "设备日志列表为空,请刷新页面或重启机场后重试"},
{324015, "飞行器已关机或未连接,无法获取日志列表,请确认飞行器在舱内,通过远程调试将飞行器开机后重试"},
{324016, "机场存储空间不足,日志压缩失败,请清理机场存储空间或稍后重试"},
{324017, "日志压缩失败,无法获取所选飞行器日志,请刷新页面或重启机场后重试"},
{324018, "日志文件拉取失败,导致本次设备异常反馈上传失败,请稍后重试或重启机场后重试"},
{324019, "因机场网络异常,日志上传失败,请稍后重试。如果连续多次出现该问题,请联系代理商或大疆售后进行网络排障"},
{325001, "云端下发给机场的命令不符合格式要求,机场无法执行"},
{327500, "飞行器镜头除雾失败,请稍后重试"},
{386535, "航线执行异常,请稍后重试或重启机场后重试"},
{513001, "直播失败,飞行器不存在或飞行器类型错误"},
{513002, "直播失败,相机不存在或相机类型错误"},
{513003, "相机已经在直播中,请勿重复开启直播"},
{513005, "直播失败,直播参数清晰度设置错误"},
{513005, "直播失败,直播参数清晰度设置错误"},
{513006, "操作失败,相机未开启直播"},
{513008, "直播失败,设备端图传数据异常"},
{513010, "直播失败,设备无法联网"},
{513011, "操作失败,设备未开启直播"},
{513012, "操作失败,设备已在直播中,不支持切换镜头"},
{513013, "直播失败,直播使用的视频传输协议不支持"},
{513014, "直播失败,直播参数错误或者不完整"},
{513015, "直播异常,网络卡顿"},
{513016, "直播异常,视频解码失败"},
{513099, "直播失败,请稍后重试"},
{514100, "机场运行异常,请重启机场后重试"},
{514101, "推杆闭合失败,请检查停机坪上是否存在异物,飞行器方向是否放反,或重启机场后重试"},
{514102, "推杆展开失败,请检查停机坪上是否存在异物,或重启机场后重试"},
{514103, "飞行器电量低于30%,无法执行飞行任务,请充电后重试(建议电量≥50%)"},
{514104, "飞行器电池开始充电失败,请重启机场后重试"},
{514105, "飞行器电池停止充电失败,请重启机场后重试"},
{514106, "飞行器电源控制异常,请重启机场后重试"},
{514107, "舱盖开启失败,请检查舱盖周围是否存在异物,或重启机场后重试"},
{514108, "舱盖关闭失败,请检查舱盖周围是否存在异物,或重启机场后重试"},
{514109, "飞行器开机失败,请重启机场后重试"},
{514110, "飞行器关机失败,请重启机场后重试"},
{514111, "飞行器慢转收桨控制异常,请重启机场后重试"},
{514112, "飞行器慢转收桨控制异常,请重启机场后重试"},
{514113, "机场推杆与飞行器无法连接,请检查飞行器是否在舱内,推杆闭合时是否被卡住,充电连接器是否脏污或损坏"},
{514114, "获取飞行器电源状态失败,请重启机场后重试"},
{514116, "无法执行当前操作,机场正在执行其他控制指令,请稍后重试"},
{514117, "舱盖开启或关闭未到位,请重启机场后重试"},
{514118, "推杆展开或闭合未到位,请重启机场后重试"},
{514120, "机场与飞行器断连,请重启机场后重试或重新对频"},
{514121, "机场急停按钮被按下,请释放急停按钮"},
{514122, "获取飞行器充电状态失败,请重启机场后重试"},
{514123, "飞行器电池电量过低无法开机"},
{514124, "获取飞行器电池信息失败,无法执行飞行任务,请重启机场后重试"},
{514125, "飞行器电池电量已接近满电状态无法开始充电请使用至95%以下再进行充电"},
{514134, "雨量过大,机场无法执行飞行任务,请稍后重试"},
{514135, "风速过大≥12m/s机场无法执行飞行任务请稍后重试"},
{514136, "机场供电断开,机场无法执行飞行任务,请恢复机场供电后重试"},
{514137, "环境温度过低<-20℃机场无法执行飞行任务请稍后重试"},
{514138, "飞行器电池正在保养中,机场无法执行飞行任务,请等待保养结束后重试"},
{514139, "飞行器电池无法执行保养指令,飞行器电池无需保养"},
{514140, "飞行器电池无法执行保养指令,飞行器电池无需保养"},
{514141, "机场系统运行异常,请重启机场后重试"},
{514142, "飞行器起飞前,机场推杆与飞行器无法连接,请检查飞行器是否在舱内,推杆闭合时是否被卡住,充电连接器是否脏污或损坏"},
{514143, "推杆未闭合或闭合不到位,请稍后重试或重启机场后重试"},
{514144, "舱盖未关闭或关闭不到位,请稍后重试或重启机场后重试"},
{514145, "机场处于现场调试中,无法执行当前操作或执行飞行任务,请断开遥控器和机场的数据线连接后重试"},
{514146, "机场处于远程调试中,无法执行飞行任务,请退出远程调试后重试"},
{514147, "设备升级中,无法执行飞行任务,请等待升级完成后重试"},
{514148, "机场已经在作业中,无法进行远程调试或再次执行飞行任务,请等待当前任务执行完成后重试"},
{514149, "机场系统运行异常,无法执行飞行任务,请重启机场后重试"},
{514150, "设备重启中,无法执行飞行任务,请等待重启完成后重试"},
{514151, "设备升级中,无法执行设备重启指令,请等待升级完成后重试"},
{514153, "机场已退出远程调试模式,无法执行当前操作"},
{514170, "机场系统初始化中,无法执行当前操作或指令,请等待机场系统初始化完成后重试"},
{514171, "云端下发给机场的命令不符合格式要求,机场无法执行"},
{514180, "停止空调制冷或停止空调制热失败,请稍后重试"},
{514181, "开启空调制冷失败,请稍后重试"},
{514182, "开启空调制热失败,请稍后重试"},
{514183, "开启空调除湿失败,请稍后重试"},
{514184, "当前温度低于 0 ℃,无法开启空调制冷"},
{514185, "当前温度高于 45 ℃,无法开启空调制热"},
{514300, "网关异常"},
{514301, "请求超时,连接断开"},
{514302, "网络证书异常,连接失败"},
{514303, "网络异常,连接断开"},
{514304, "机场请求被拒,连接失败"},
}
for _, e := range errors {
errorMap[e.Code] = e
}
}
func GetErrorMap() map[int]Error {
return errorMap
}

259
third/plane/dj/minio.go Normal file
View File

@ -0,0 +1,259 @@
package dj
import (
"context"
"fmt"
"github.com/aws/aws-sdk-go/aws"
awscs "github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/sts"
"github.com/gogf/gf/v2/frame/g"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"log"
"os"
"path/filepath"
"time"
)
//// minio的配置信息
//const (
// Endpoint string = coryCommon.Global + ":9999"
// AccessKeyID string = "admin"
// SecretAccessKey string = "12345678"
// BucketName string = "cory-create"
// RoleARN string = "arn:aws:s3:::cory-create/*"
// RoleSessionName string = "anysession"
// Region string = "cn-chengdu"
// UseSSL bool = true
// DurationSeconds int64 = 3600
//
// // Endpoint string = "jl.yj-3d.com:8154"
// // AccessKeyID string = "minioadmin"
// // SecretAccessKey string = "minioadmin"
// // BucketName string = "cory-create"
// // RoleARN string = "arn:aws:s3:::cory-create/*"
// // RoleSessionName string = "anysession"
// // Region string = "cn-chengdu"
// // UseSSL bool = true
// // DurationSeconds int64 = 3600
//)
var (
Endpoint string
AccessKeyID string
SecretAccessKey string
BucketName string
RoleARN string
RoleSessionName string
Region string
UseSSL bool
DurationSeconds int64
)
func init() {
Endpoint = g.Cfg().MustGet(context.Background(), "minio.endpoint").String()
AccessKeyID = g.Cfg().MustGet(context.Background(), "minio.accessKeyID").String()
SecretAccessKey = g.Cfg().MustGet(context.Background(), "minio.secretAccessKey").String()
BucketName = g.Cfg().MustGet(context.Background(), "minio.bucketName").String()
RoleSessionName = g.Cfg().MustGet(context.Background(), "minio.roleSessionName").String()
Region = g.Cfg().MustGet(context.Background(), "minio.region").String()
UseSSL = g.Cfg().MustGet(context.Background(), "minio.useSSL").Bool()
DurationSeconds = g.Cfg().MustGet(context.Background(), "minio.durationSeconds").Int64()
RoleARN = g.Cfg().MustGet(context.Background(), "minio.roleARN").String()
}
var (
IsEnablingSSL = false //是否开启SSL安全 (只针对初始化客户端)
)
// PublicMinioClient 初始化MinIO客户端
func PublicMinioClient() (minioClient *minio.Client, err error) {
// 获取临时凭证
token, accessKey, secretAccessKey := MinioVoucher()
// 初始化MinIO客户端
minioClient, err = minio.New(Endpoint, &minio.Options{
Creds: credentials.NewStaticV4(accessKey, secretAccessKey, token),
Secure: IsEnablingSSL,
})
return
}
// MinioVoucher 获取minio的临时凭证
func MinioVoucher() (sessionToken, accessKey, secretKey string) {
// 创建AWS会话
sess, err := session.NewSession(&aws.Config{
Endpoint: aws.String(Endpoint),
DisableSSL: aws.Bool(UseSSL),
Region: aws.String(Region),
Credentials: awscs.NewStaticCredentials(AccessKeyID, SecretAccessKey, ""),
})
if err != nil {
log.Printf("创建AWS会话错误%v", err)
return
}
// 创建STS服务客户端
stsSvc := sts.New(sess)
// 获取STS凭证
input := &sts.AssumeRoleInput{
RoleArn: aws.String(RoleARN),
RoleSessionName: aws.String(RoleSessionName),
DurationSeconds: aws.Int64(DurationSeconds),
}
result, err := stsSvc.AssumeRole(input)
if err != nil {
fmt.Println("获取STS凭证错误:", err)
return
}
// 获取STS凭证的Token、AccessKey和SecretKey
sessionToken = *result.Credentials.SessionToken
accessKey = *result.Credentials.AccessKeyId
secretKey = *result.Credentials.SecretAccessKey
return
}
/*
TheTemporaryAccessUrlIsObtainedBasedOnThePrefixOfTheSpecifiedBucket 根据指定桶的前缀获取到临时访问URL
bucketName桶名
prefix前缀
expiryURL能够访问的时间最长7天或至少1秒 expiry := 3 * 24 * time.Hour 3天时间访问Url
isRecursion是否递归
*/
func TheTemporaryAccessUrlIsObtainedBasedOnThePrefixOfTheSpecifiedBucket(ctx context.Context, minioClient *minio.Client, bucketName string, prefix string, expiry time.Duration, isRecursion bool) (url []string) {
//1、获取递归集
objectSet := ListAndGenerateURLs(ctx, minioClient, bucketName, prefix, isRecursion)
//2、根据集获取到临时可访问的URL
for _, object := range objectSet {
preSignature := GenerateAnOnlineScopedUrlBasedOnTheObject(ctx, minioClient, bucketName, object.Key, expiry)
url = append(url, preSignature)
}
return
}
/*
DownloadTheFileWithTheSpecifiedPrefixBasedOnTheObjectSet 根据对象集下载指定前缀的文件
bucketName桶名
prefix前缀
filePath下载后保存路径
isRecursion是否递归获取对象集
*/
func DownloadTheFileWithTheSpecifiedPrefixBasedOnTheObjectSet(ctx context.Context, minioClient *minio.Client, bucketName string, prefix string, filePath string, isRecursion bool) (err error) {
objectList := ListAndGenerateURLs(ctx, minioClient, bucketName, prefix, isRecursion)
for _, object := range objectList {
//split := strings.Split(object.Key, "/")
//wz := split[len(split)-1]
//fileName := strings.Split(wz, ".")[0]
//if strings.Contains(fileName, "W") {
// fmt.Println(wz)
//}
err = minioClient.FGetObject(ctx, bucketName, object.Key, filepath.ToSlash(filePath+"/"+object.Key), minio.GetObjectOptions{})
if err != nil {
fmt.Println("下载minio资源失败", err)
return err
}
}
return
}
/*
DeletesTheFileWithTheSpecifiedPrefixBasedOnTheObjectSet 根据对象集删除指定前缀的文件
bucketName桶名
prefix前缀
isRecursion是否递归获取对象集
*/
func DeletesTheFileWithTheSpecifiedPrefixBasedOnTheObjectSet(ctx context.Context, minioClient *minio.Client, bucketName string, prefix string, isRecursion bool) (err error) {
objectList := ListAndGenerateURLs(ctx, minioClient, bucketName, prefix, isRecursion)
for _, object := range objectList {
err = minioClient.RemoveObject(ctx, bucketName, object.Key, minio.RemoveObjectOptions{})
if err != nil {
fmt.Println("删除minio资源失败", err)
return err
}
}
return
}
/*
UploadDataAccordingToTemporaryCredentials 根据临时凭证上传数据
filePath本地文件路径
objectName存储到minio的地址桶+前缀) 示例cory-create/uav/key.txt
*/
func UploadDataAccordingToTemporaryCredentials(filePath string, objectName string) (err error) {
minioClient, err := PublicMinioClient()
if err != nil {
return
}
// 打开文件
file, err := os.Open(filePath)
if err != nil {
fmt.Println("Error opening file:", err)
return
}
defer file.Close()
// 使用minio-go的PutObject方法上传文件
n, err := minioClient.PutObject(context.Background(), BucketName, objectName, file, -1, minio.PutObjectOptions{})
if err != nil {
fmt.Println("Error uploading file:", err)
return
}
fmt.Printf("Successfully uploaded %s of size %d\n", objectName, n)
return err
}
//=========================================================================================================================
//=========================================================================================================================
//=========================================================================================================================
//=========================================================================================================================
//=========================================================================================================================
//=========================================================================================================================
//=========================================================================================================================
//=========================================================================================================================
/*
ListAndGenerateURLs 获取指定前缀下的对象集
bucketName桶名
prefix前缀获取哪个下面的对象集 示例:"uav/9b0af64e1c274105b157a9781961ed99/DJI_202405170903_001_9b0af64e1c274105b157a9781961ed99"
isRecursion是否递归
*/
func ListAndGenerateURLs(ctx context.Context, minioClient *minio.Client, bucketName string, prefix string, isRecursion bool) (objectInfo []minio.ObjectInfo) {
// 设置递归为 true以获取所有对象包括子目录中的对象
objectCh := minioClient.ListObjects(ctx, bucketName, minio.ListObjectsOptions{
Prefix: prefix,
Recursive: isRecursion,
})
// 等待通道中的对象被填充
for object := range objectCh {
if object.Err != nil {
fmt.Println("获取对象列表失败:", object.Err)
continue
}
objectInfo = append(objectInfo, object)
//fmt.Println("对象名称:", object.Key)
}
return
}
// GenerateAnOnlineScopedUrlBasedOnTheObject 根据对象集生成可在线访问的URL
func GenerateAnOnlineScopedUrlBasedOnTheObject(ctx context.Context, minioClient *minio.Client, bucketName string, objectName string, expiry time.Duration) string {
// 生成预签名的 GET URL
presignedURL, err := minioClient.PresignedGetObject(ctx, bucketName, objectName, expiry, nil)
if err != nil {
log.Fatalf("生成预签名 URL 失败:%v", err)
}
//// 使用预签名 URL 访问对象
//resp, err := minioClie.
//nt.GetObject(ctx, bucketName, objectName, minio.GetObjectOptions{})
//if err != nil {
// log.Fatalf("获取对象失败:%v", err)
//}
//defer resp.Close()
return presignedURL.String()
}
// StatObjectFunc 获取对象的元数据
func StatObjectFunc(ctx context.Context, minioClient *minio.Client, bucketName string, objectName string) (objInfo minio.ObjectInfo, err error) {
objInfo, err = minioClient.StatObject(ctx, bucketName, objectName, minio.GetObjectOptions{})
return
}

View File

@ -0,0 +1,88 @@
package event
// eventHandlerInfo定义了事件处理器的信息
type eventHandlerInfo struct {
handler EventHandler // 事件处理函数
once bool // 是否只执行一次
}
// EventHandler定义了事件处理函数的类型
type EventHandler func(params ...any)
// Event表示一个事件对象
type Event struct {
handlers []eventHandlerInfo // 事件处理器列表
}
// Attach将事件处理器附加到事件对象中并返回处理器在列表中的索引
func (e *Event) Attach(handler EventHandler) int {
handlerInfo := eventHandlerInfo{handler, true} //默认只执行一次
for i, h := range e.handlers {
if h.handler == nil {
e.handlers[i] = handlerInfo
return i
}
}
e.handlers = append(e.handlers, handlerInfo)
return len(e.handlers) - 1
}
// Detach从事件对象中移除指定索引处的处理器
func (e *Event) Detach(handle int) {
e.handlers[handle].handler = nil
}
// Once将事件处理器附加到事件对象中并将处理器标记为只执行一次
func (e *Event) Once(handler EventHandler) {
i := e.Attach(handler)
e.handlers[i].once = true
}
// EventPublisher表示事件发布者
type EventPublisher struct {
event Event // 事件对象
}
// Event返回事件对象的指针
func (p *EventPublisher) Event() *Event {
return &p.event
}
// Publish触发事件依次执行事件对象中的处理器并在处理器标记为只执行一次时将其从事件对象中移除
func (p *EventPublisher) Publish(params ...any) {
for i, h := range p.event.handlers {
if h.handler != nil {
h.handler(params...)
if h.once {
p.event.Detach(i)
}
}
}
}
func GetEvent(fn func(params ...any)) *EventPublisher {
publisher := &EventPublisher{}
// 定义一个事件处理器函数
handler := fn
// 将事件处理器附加到事件列表中
publisher.Event().Attach(handler)
return publisher
}
/*func main() {
// 在这里编写您的代码逻辑
// 创建一个事件发布者
publisher := &EventPublisher{}
// 定义一个事件处理器函数
handler := func() {
fmt.Println("Event handled!")
}
// 将事件处理器附加到事件列表中
publisher.Event().Attach(handler)
// 发布事件
publisher.Publish()
}*/

125
third/plane/globe/globe.go Normal file
View File

@ -0,0 +1,125 @@
package globe
import (
"errors"
"github.com/gogf/gf/v2/net/ghttp"
"gorm.io/gorm"
"net/http"
"strconv"
"sync"
)
const (
ALL = -1 //所有
ENABLE = 1
DISABLE = 0
DESC = "desc"
ASC = "asc"
PAGE = 1
PAGESIZE = 10
ONLINE = 1
OFFLINE = 0
DEFAULTPWD = "123456"
DEFAULTUSR = "admin"
)
var DepartName = "一级部门" //默认的一级部门名称
var IS_OFFLINE_VERSION = true //是否为单机版本
const SOURCE = "static/source/"
type WS_MAP map[string]*ghttp.WebSocket
var WS_Status_map WS_MAP //状态数据ws
var WS_Progress_drone_open_map WS_MAP //飞行器开机进度ws
var WS_Progress_drone_close_map WS_MAP //飞行器关机进度ws
var WS_Progress_cover_open_map WS_MAP //机库打开舱盖进度ws
var WS_Progress_cover_close_map WS_MAP //机库关闭舱盖进度ws
var WS_Progress_device_reboot_map WS_MAP //机库重启进度ws
var WS_Progress_putter_open_map WS_MAP //推杆展开进度ws
var WS_Progress_putter_close_map WS_MAP //推杆闭合进度ws
var WS_Reply WS_MAP //状态恢复
var MutexRw sync.RWMutex //map的读写锁
const (
TILESET = "tileset"
BIM = "bim"
LAYER = "layer"
TERRAIN = "terrain"
POINT = "point"
LINE = "line"
AREA = "area"
MODEL = "model"
KML = "kml"
GEOJSON = "geojson"
)
const (
PAK = ".pak"
MBTILES = ".mbtiles"
CLT = ".clt"
JCT = ".jct"
)
var (
PORT = "80"
HOST = ""
PROTOCOL = ""
KEY = ""
CRT = ""
)
const (
HTTP = "http"
HTTPS = "https"
)
func GetErrors(msg string) error {
return errors.New(msg)
}
func GetAddr() string {
//单机版本时 无代理,需要补全地址
if IS_OFFLINE_VERSION {
return PROTOCOL + "://" + HOST + ":" + PORT + "/"
}
//网络版时 有代理 不需要补全地址
return ""
}
/*clt数据包*/
type Tile struct {
MD5 string `json:"md5"`
PATH string `json:"path"`
Tile []byte `json:"tile"`
Type string `json:"type"`
}
func RenderData(request *ghttp.Request, data []byte) {
request.Response.Header().Set("Cache-Control", "private,max-age="+strconv.Itoa(60*60))
request.Response.WriteHeader(http.StatusOK)
request.Response.Writer.Write(data)
}
func CloseDB(db *gorm.DB) {
s, err := db.DB()
if err != nil {
return
}
s.Close()
}
var IS_AUTH_SUCCESS = false //是否授权通过
func CheckAuth() bool {
return IS_AUTH_SUCCESS
}
func SetAuth(auth bool) {
IS_AUTH_SUCCESS = auth
}
/*坐标点*/
type Point struct {
Lng float64 `json:"lng" v:"required"`
Lat float64 `json:"lat" v:"required"`
Alt float64 `json:"alt" v:"required"`
}

197
third/plane/mqtt/emqx.go Normal file
View File

@ -0,0 +1,197 @@
package mqtt
import (
"context"
"fmt"
mqtt "github.com/eclipse/paho.mqtt.golang"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/os/gtimer"
"github.com/tiger1103/gfast/v3/third/plane/dj"
"github.com/tiger1103/gfast/v3/third/plane/event"
"strings"
"time"
)
var timer *gtimer.Timer
var client mqtt.Client
var eventMap map[string]*event.EventPublisher
const (
osd = "osd" //设备端定频向云平台推送的设备属性properties 具体内容范围参见物模型内容
state = "state" //设备端按需上报向云平台推送的设备属性properties 具体内容范围参见物模型内容
services_reply = "services_reply" //设备对 service 的回复、处理结果
events = "events" //设备端向云平台发送的,需要关注和处理的事件。 比如SD满了飞机解禁禁飞区等信息事件范围参见物模型内容
status = "status" //设备上下线、更新拓扑
set_reply = "set_reply" //设备属性设置的响应
requests = "requests" //设备端向云平台发送请求,为了获取一些信息,比如上传的临时凭证
services = "services" //云平台向设备发送的服务具体service identifier 见物模型内容)。
)
var MsgCallBackMap map[string]CB
func InitMQTT() {
eventMap = make(map[string]*event.EventPublisher)
timer = gtimer.New()
initMsgs()
dj.InitError()
connect()
}
func initMsgs() {
MsgCallBackMap = make(map[string]CB)
msgs := []CB{
{osd, dealOsd, nil},
{state, deal_state, nil},
{requests, deal_requests, nil},
{services_reply, deal_services_reply, nil},
{status, deal_status, nil},
{events, deal_events, nil},
{services, deal_services, nil},
}
for _, msg := range msgs {
MsgCallBackMap[msg.Flag] = msg
}
}
type CB struct {
Flag string
Recv func(mqtt.Client, mqtt.Message) //接收消息
Reply func(mqtt.Client, mqtt.Message) //回复消息
}
func connect() {
host := g.Cfg().MustGet(gctx.New(), "mqtt.host").String()
port := g.Cfg().MustGet(gctx.New(), "mqtt.port").String()
username := g.Cfg().MustGet(gctx.New(), "mqtt.username").String()
password := g.Cfg().MustGet(gctx.New(), "mqtt.password").String()
clientid := g.Cfg().MustGet(gctx.New(), "mqtt.clientid").String()
topics := g.Cfg().MustGet(gctx.New(), "mqtt.topics").String()
fmt.Println(host, port, username, password, clientid, topics, "tcp://"+host+":"+port)
opts := mqtt.NewClientOptions()
opts.AddBroker("tcp://" + host + ":" + port) // 设置MQTT代理服务器地址
opts.SetClientID(clientid) // 设置客户端ID
opts.SetPassword(password) // 设置客户端ID
opts.SetUsername(username) // 设置客户端ID
// 处理消息接收
opts.SetDefaultPublishHandler(receiveHandler)
opts.SetConnectionLostHandler(disconnected)
opts.SetOnConnectHandler(connected)
opts.SetKeepAlive(1000 * time.Second) //超时等待,防止客户端掉线
opts.SetAutoReconnect(true)
client = mqtt.NewClient(opts)
// 连接MQTT代理服务器
if token := client.Connect(); token.Wait() && token.Error() != nil {
panic(token.Error())
}
arr := strings.Split(topics, ",")
for _, topic := range arr {
// 订阅主题
fmt.Println("订阅主题", topic)
if token := client.Subscribe(topic, 0, nil); token.Wait() && token.Error() != nil {
fmt.Println(token.Error())
}
//fmt.Println(topic)
}
//ctx := gctx.New()
////格式化
//DeviceFormat(ctx, "4SEDL9C001X8GE")
//DroneFormat(ctx, "4SEDL9C001X8GE")
//DeviceFormat(ctx, "7CTDM4100BG7HV")
//DroneFormat(ctx, "7CTDM4100BG7HV")
//飞机升级
//OtaCreateFunc(ctx, "8UUXN4P00A06NK")
//gtimer.SetTimeout(gctx.New(), 2*time.Second, func(ctx context.Context) {
//sn := "4SEDL9C001X8GE"
//err, d := speaker_audio_play_start(sn)
//if err != nil {
// fmt.Println(err)
// return
//}
//整一个pcm的音频文件
// point := dj.Point{}
// point.Longitude = 106.541923087
// point.Latitude = 23.458531397
// point.Height = 823.8873291015625 + 10
// //err, d := Flight_authority_grab(sn)
// //if err != nil {
// // return
// //}
// err, d := Fly_to_point(sn, point)
//fmt.Println(d)
// fmt.Println(err)
//
// sn = "4TADL210010014" //中煤广西机库的sn
// //// //sn = "4TADL2E001002D" //机库的sn
// //// //sn = "1581F5BMD2323001S928" //飞机的sn
// ////dev_sn := "1581F5BMD23280014131" //吉林飞机的sn
// //dev_sn := "1581F5BMD238V00172JR" //吉林飞机的sn
// //camera_index := "165-0-7" //机库的摄像头id
// ////camera_index = "53-0-0" //飞机摄像头1的id
// //camera_index = "39-0-7" //飞机摄像头2的id
// //Debug_mode_open(sn) //调试模式,先调试模式才能飞行器开机,推流不需要調試
// //Debug_mode_close(sn)
// //err, d := Cover_open(sn) //打开舱盖
// //err, d := Cover_close(sn)
// //err, d := Drone_open(sn) //飞行器开机
// //err, d := Drone_close(sn) //飞行器关机
// //err, d := Device_reboot(sn)
// //
// //fmt.Println(d)
// //if err != nil {
// // fmt.Println(err)
// //} //打开舱盖
// ////fmt.Println("打开舱盖")
// //err, d := Live_start_push(sn, sn, camera_index)
// //err, d := Live_stop_push(sn, sn, camera_index)
//
// //fmt.Println(d)
// //if err != nil {
// // fmt.Println(err)
// // return
// //} //关闭舱盖
// ////fmt.Println(d.Data.Result)
// //// //device_reboot()
// //// //if err != nil {
// //// // return
// //// //}
// //// //fmt.Println(d)
// //// //live_stop_push(sn, sn, camera_index)
// ////
//})
}
func receiveHandler(client mqtt.Client, msg mqtt.Message) {
topic := msg.Topic()
arrs := strings.Split(topic, "/")
tp := arrs[len(arrs)-1]
if v, ok := MsgCallBackMap[tp]; ok {
v.Recv(client, msg)
if v.Reply != nil {
v.Reply(client, msg)
}
} else {
fmt.Println("非法topic", topic)
}
}
func disconnected(client mqtt.Client, err error) {
fmt.Println("断开了,准备重连")
if err != nil {
fmt.Println(err)
g.Log("uav").Error(gctx.New(), err)
}
timer.Start()
timer.Add(gctx.New(), time.Second*5, func(ctx context.Context) {
connect()
})
}
func connected(client mqtt.Client) {
fmt.Println("链接成功")
timer.Stop()
}

View File

@ -0,0 +1,31 @@
package mqtt
// LiveStreamingCapabilityUpdateEntity 直播能力更新 UP
type LiveStreamingCapabilityUpdateEntity struct {
Tid string `json:"tid"`
Bid string `json:"bid"`
Timestamp int64 `json:"timestamp"`
Gateway string `json:"gateway"`
Method string `json:"method"`
Data struct {
LiveCapacity struct {
AvailableVideoNumber int `json:"available_video_number"`
CoexistVideoNumberMax int `json:"coexist_video_number_max"`
DeviceList []struct {
Sn string `json:"sn"`
AvailableVideoNumber int `json:"available_video_number"`
CoexistVideoNumberMax int `json:"coexist_video_number_max"`
CameraList []struct {
CameraIndex string `json:"camera_index"`
AvailableVideoNumber int `json:"available_video_number"`
CoexistVideoNumberMax int `json:"coexist_video_number_max"`
VideoList []struct {
VideoIndex string `json:"video_index"`
VideoType string `json:"video_type"`
SwitchableVideoTypes []string `json:"switchable_video_types"`
} `json:"video_list"`
} `json:"camera_list"`
} `json:"device_list"`
} `json:"live_capacity"`
} `json:"data"`
}

1282
third/plane/mqtt/msg_deal.go Normal file

File diff suppressed because it is too large Load Diff

334
third/plane/plane.go Normal file
View File

@ -0,0 +1,334 @@
package plane
import (
"context"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/tiger1103/gfast/v3/internal/app/system/service"
"github.com/tiger1103/gfast/v3/third/plane/dj"
"github.com/tiger1103/gfast/v3/third/plane/globe"
"github.com/tiger1103/gfast/v3/third/plane/mqtt"
"strconv"
)
type PLANE struct {
}
func InitPlaneApi(group *ghttp.RouterGroup) {
init_map()
group.Group("/plane", func(group *ghttp.RouterGroup) {
group.Bind(new(PLANE))
group.Middleware(service.Middleware().Ctx, service.Middleware().Auth)
})
}
func init_map() {
globe.WS_Status_map = make(globe.WS_MAP)
globe.WS_Progress_drone_open_map = make(globe.WS_MAP)
globe.WS_Progress_drone_close_map = make(globe.WS_MAP)
globe.WS_Progress_cover_open_map = make(globe.WS_MAP)
globe.WS_Progress_cover_close_map = make(globe.WS_MAP)
globe.WS_Progress_device_reboot_map = make(globe.WS_MAP)
globe.WS_Progress_putter_close_map = make(globe.WS_MAP)
globe.WS_Progress_putter_open_map = make(globe.WS_MAP)
globe.WS_Reply = make(globe.WS_MAP)
}
type UsualRes struct {
}
type DeviceGoHomeReq struct {
g.Meta `path:"gohome" summary:"一键返航" method:"post" tags:"无人机相关"`
GatewaySn string `json:"gateway_sn" v:"required" dc:"机库sn"`
}
func (receiver PLANE) DeviceGoHome(ctx context.Context, req *DeviceGoHomeReq) (res *UsualRes, err error) {
err, d := mqtt.Return_home(ctx, req.GatewaySn)
if err != nil {
return nil, err
}
if d.Data.Result > 0 {
err = geterror(d.Data.Result)
return
}
return
}
type DeviceListReq struct {
g.Meta `path:"list" summary:"获取飞机列表" method:"get" tags:"无人机相关"`
}
type DeviceListRes struct {
}
func (receiver PLANE) DeviceList(ctx context.Context, req *DeviceListReq) (res *DeviceListRes, err error) {
return nil, err
}
type DeviceRebootReq struct {
g.Meta `path:"device_reboot" summary:"机库重启" method:"post" tags:"无人机相关"`
GatewaySn string `json:"gateway_sn" v:"required" dc:"机库sn"`
}
func (receiver PLANE) DeviceReboot(ctx context.Context, req *DeviceRebootReq) (res *UsualRes, err error) {
err, d := mqtt.Device_reboot(req.GatewaySn)
if err != nil {
return nil, err
}
if d.Data.Result > 0 {
err = geterror(d.Data.Result)
return
}
return
}
type ClosePutterReq struct {
g.Meta `path:"close_putter" summary:"关闭推杆" method:"post" tags:"无人机相关"`
GatewaySn string `json:"gateway_sn" v:"required" dc:"机库sn"`
}
func (receiver PLANE) ClosePutterReq(ctx context.Context, req *ClosePutterReq) (res *UsualRes, err error) {
err, d := mqtt.Putter_close(ctx, req.GatewaySn)
if err != nil {
return nil, err
}
if d.Data.Result > 0 {
err = geterror(d.Data.Result)
return
}
g.DB().Model("manage_button_state").
Where("mq_client_id", req.GatewaySn).
Update(g.Map{"push_rod": 2})
return
}
type OpenPutterReq struct {
g.Meta `path:"open_putter" summary:"打开推杆" method:"post" tags:"无人机相关"`
GatewaySn string `json:"gateway_sn" v:"required" dc:"机库sn"`
}
func (receiver PLANE) OpenPutterReq(ctx context.Context, req *OpenPutterReq) (res *UsualRes, err error) {
err, d := mqtt.Putter_open(ctx, req.GatewaySn)
if err != nil {
return nil, err
}
if d.Data.Result > 0 {
err = geterror(d.Data.Result)
return
}
g.DB().Model("manage_button_state").
Where("mq_client_id", req.GatewaySn).
Update(g.Map{"push_rod": 1})
return
}
type OpenChargeReq struct {
g.Meta `path:"open_charge" summary:"打开充电" method:"post" tags:"无人机相关"`
GatewaySn string `json:"gateway_sn" v:"required" dc:"机库sn"`
}
func (receiver PLANE) OpenChargeReq(ctx context.Context, req *OpenChargeReq) (res *UsualRes, err error) {
err, d := mqtt.Charge_open(ctx, req.GatewaySn)
if err != nil {
return nil, err
}
if d.Data.Result > 0 {
err = geterror(d.Data.Result)
return
}
g.DB().Model("manage_button_state").
Where("mq_client_id", req.GatewaySn).
Update(g.Map{"recharge": 1})
return
}
type CloseChargeReq struct {
g.Meta `path:"close_charge" summary:"关闭充电" method:"post" tags:"无人机相关"`
GatewaySn string `json:"gateway_sn" v:"required" dc:"机库sn"`
}
func (receiver PLANE) CloseCharge(ctx context.Context, req *CloseChargeReq) (res *UsualRes, err error) {
err, d := mqtt.Charge_close(ctx, req.GatewaySn)
if err != nil {
return nil, err
}
if d.Data.Result > 0 {
err = geterror(d.Data.Result)
return
}
g.DB().Model("manage_button_state").
Where("mq_client_id", req.GatewaySn).
Update(g.Map{"recharge": 2})
return
}
type CloseDroneReq struct {
g.Meta `path:"close_drone" summary:"飞行器关机(调试模式下)" method:"post" tags:"无人机相关"`
GatewaySn string `json:"gateway_sn" v:"required" dc:"机库sn"`
}
func (receiver PLANE) CloseDrone(ctx context.Context, req *CloseDroneReq) (res *UsualRes, err error) {
err, d := mqtt.Drone_close(ctx, req.GatewaySn)
if err != nil {
return nil, err
}
if d.Data.Result > 0 {
err = geterror(d.Data.Result)
return
}
g.DB().Model("manage_button_state").
Where("mq_client_id", req.GatewaySn).
Update(g.Map{"power": 2})
return
}
type OpenDroneReq struct {
g.Meta `path:"open_drone" summary:"飞行器开机(调试模式下)" method:"post" tags:"无人机相关"`
GatewaySn string `json:"gateway_sn" v:"required" dc:"机库sn"`
}
func (receiver PLANE) OpenDrone(ctx context.Context, req *OpenDroneReq) (res *UsualRes, err error) {
err, d := mqtt.Drone_open(ctx, req.GatewaySn)
if err != nil {
return nil, err
}
if d.Data.Result > 0 {
err = geterror(d.Data.Result)
return
}
g.DB().Model("manage_button_state").
Where("mq_client_id", req.GatewaySn).
Update(g.Map{"power": 1})
return
}
type CloseCoverReq struct {
g.Meta `path:"close_cover" summary:"关闭舱盖(调试模式下)" method:"post" tags:"无人机相关"`
GatewaySn string `json:"gateway_sn" v:"required" dc:"机库sn"`
}
func (receiver PLANE) CloseCover(ctx context.Context, req *CloseCoverReq) (res *UsualRes, err error) {
err, d := mqtt.Cover_close(ctx, req.GatewaySn)
if err != nil {
return nil, err
}
if d.Data.Result > 0 {
err = geterror(d.Data.Result)
return
}
g.DB().Model("manage_button_state").
Where("mq_client_id", req.GatewaySn).
Update(g.Map{"hatch_cover": 2})
return
}
type OpenCoverReq struct {
g.Meta `path:"open_cover" summary:"打开舱盖(调试模式下)" method:"post" tags:"无人机相关"`
GatewaySn string `json:"gateway_sn" v:"required" dc:"机库sn"`
}
func (receiver PLANE) OpenCover(ctx context.Context, req *OpenCoverReq) (res *UsualRes, err error) {
err, d := mqtt.Cover_open(ctx, req.GatewaySn)
if err != nil {
return nil, err
}
if d.Data.Result > 0 {
err = geterror(d.Data.Result)
return
}
g.DB().Model("manage_button_state").
Where("mq_client_id", req.GatewaySn).
Update(g.Map{"hatch_cover": 1})
return
}
type CloseDebugReq struct {
g.Meta `path:"close_debug" summary:"关闭调试模式" method:"post" tags:"无人机相关"`
GatewaySn string `json:"gateway_sn" v:"required" dc:"机库sn"`
}
func (receiver PLANE) CloseDebug(ctx context.Context, req *CloseDebugReq) (res *UsualRes, err error) {
err, d := mqtt.Debug_mode_close(ctx, req.GatewaySn)
if err != nil {
return nil, err
}
if d.Data.Result > 0 {
err = geterror(d.Data.Result)
return
}
g.DB().Model("manage_button_state").
Where("mq_client_id", req.GatewaySn).
Update(g.Map{"debug": 2})
return
}
type OpenDebugReq struct {
g.Meta `path:"open_debug" summary:"打开调试模式" method:"post" tags:"无人机相关"`
GatewaySn string `json:"gateway_sn" v:"required" dc:"机库sn"`
}
func (receiver PLANE) OpenDebug(ctx context.Context, req *OpenDebugReq) (res *UsualRes, err error) {
err, d := mqtt.Debug_mode_open(ctx, req.GatewaySn)
if err != nil {
return nil, err
}
if d.Data.Result > 0 {
err = geterror(d.Data.Result)
return
}
g.DB().Model("manage_button_state").
Where("mq_client_id", req.GatewaySn).
Update(g.Map{"debug": 1})
return
}
type StopLiveReq struct {
g.Meta `path:"stop_live" summary:"停止直播" method:"post" tags:"无人机相关"`
GatewaySn string `json:"gateway_sn" v:"required" dc:"机库sn"`
Devsn string `json:"devsn" v:"required" dc:"设备sn"`
Camera_index string `json:"camera_index" v:"required" dc:"摄像头"`
}
func (receiver PLANE) StopLive(ctx context.Context, req *StopLiveReq) (res *UsualRes, err error) {
err, d := mqtt.Live_stop_push(req.GatewaySn, req.Devsn, req.Camera_index)
if err != nil {
return nil, err
}
if d.Data.Result > 0 {
err = geterror(d.Data.Result)
return
}
return
}
type StartLiveReq struct {
g.Meta `path:"start_live" summary:"开始直播" method:"post" tags:"无人机相关"`
GatewaySn string `json:"gateway_sn" v:"required" dc:"机库sn"`
Devsn string `json:"devsn" v:"required" dc:"设备sn"`
Camera_index string `json:"camera_index" v:"required" dc:"摄像头"`
}
type StartLiveRes struct {
Url string `json:"url"`
}
func geterror(code int) error {
if v, ok := dj.GetErrorMap()[code]; ok {
return globe.GetErrors(v.Msg)
} else {
return globe.GetErrors("未知错误:" + strconv.Itoa(code))
}
}
func (receiver PLANE) StartLive(ctx context.Context, req *StartLiveReq) (res *StartLiveRes, err error) {
err, d := mqtt.Live_start_push(req.GatewaySn, req.Devsn, req.Camera_index)
if err != nil {
return nil, err
}
if d.Data.Result > 0 && d.Data.Result != 513003 /*正在直播中 ,可直接返回地址*/ {
err = geterror(d.Data.Result)
return
}
res = &StartLiveRes{}
res.Url = d.Data.Url
return
}

107
third/plane/progress.go Normal file
View File

@ -0,0 +1,107 @@
package plane
import (
"context"
"fmt"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/tiger1103/gfast/v3/third/plane/globe"
)
type Progerss_putter_close_Req struct {
g.Meta `path:"progress/putter_close" summary:"推杆闭合进度" method:"get" tags:"无人机进度数据相关ws"`
}
func (receiver PLANE) Progerss_putter_close(ctx context.Context, req *Progerss_putter_close_Req) (res *UsualRes, err error) {
return save_ws(ctx, &globe.WS_Progress_putter_close_map)
}
type Progerss_putter_open_Req struct {
g.Meta `path:"progress/putter_open" summary:"推杆展开进度" method:"get" tags:"无人机进度数据相关ws"`
}
func (receiver PLANE) Progerss_putter_open(ctx context.Context, req *Progerss_putter_open_Req) (res *UsualRes, err error) {
return save_ws(ctx, &globe.WS_Progress_putter_open_map)
}
type Progerss_Reply_Req struct {
g.Meta `path:"progress/err_reply" summary:"up回复错误状态" method:"get" tags:"无人机进度数据相关ws"`
}
func (receiver PLANE) Progerss_Reply(ctx context.Context, req *Progerss_Reply_Req) (res *UsualRes, err error) {
return save_ws(ctx, &globe.WS_Reply)
}
type Progerss_device_reboot_Req struct {
g.Meta `path:"progress/device_reboot" summary:"机库重启进度推送" method:"get" tags:"无人机进度数据相关ws"`
}
func (receiver PLANE) Progerss_device_reboot(ctx context.Context, req *Progerss_device_reboot_Req) (res *UsualRes, err error) {
return save_ws(ctx, &globe.WS_Progress_device_reboot_map)
}
type Progerss_cover_close_Req struct {
g.Meta `path:"progress/cover_close" summary:"机库关闭舱盖进度推送" method:"get" tags:"无人机进度数据相关ws"`
}
func (receiver PLANE) Progerss_cover_close(ctx context.Context, req *Progerss_cover_close_Req) (res *UsualRes, err error) {
return save_ws(ctx, &globe.WS_Progress_cover_close_map)
}
type Progerss_cover_open_Req struct {
g.Meta `path:"progress/cover_open" summary:"机库打开舱盖进度推送" method:"get" tags:"无人机进度数据相关ws"`
}
func (receiver PLANE) Progerss_cover_open(ctx context.Context, req *Progerss_cover_open_Req) (res *UsualRes, err error) {
return save_ws(ctx, &globe.WS_Progress_cover_open_map)
}
type Progerss_drone_close_Req struct {
g.Meta `path:"progress/drone_close" summary:"飞行器关机进度推送" method:"get" tags:"无人机进度数据相关ws"`
}
func (receiver PLANE) Progerss_drone_close(ctx context.Context, req *Progerss_drone_close_Req) (res *UsualRes, err error) {
return save_ws(ctx, &globe.WS_Progress_drone_close_map)
}
type Progerss_drone_open_Req struct {
g.Meta `path:"progress/drone_open" summary:"飞行器开机进度推送" method:"get" tags:"无人机进度数据相关ws"`
}
func (receiver PLANE) Progerss_drone_open(ctx context.Context, req *Progerss_drone_open_Req) (res *UsualRes, err error) {
return save_ws(ctx, &globe.WS_Progress_drone_open_map)
}
type DeviceStatusReq struct {
g.Meta `path:"device_status" summary:"ws推送飞机状态" method:"get" tags:"无人机进度数据相关ws"`
}
type DeviceStatusRes struct {
Code int `json:"code"`
Data interface{} `json:"data"`
}
func (receiver PLANE) DeviceStatus(ctx context.Context, req *DeviceStatusReq) (res *UsualRes, err error) {
return save_ws(ctx, &globe.WS_Status_map)
}
func save_ws(ctx context.Context, _map *globe.WS_MAP) (res *UsualRes, err error) {
r := ghttp.RequestFromCtx(ctx)
ws, err := r.WebSocket()
if err != nil {
return nil, err
}
ip := r.Get("token").String()
globe.MutexRw.Lock()
(*_map)[ip] = ws
globe.MutexRw.Unlock()
fmt.Println("ws连接了", ip)
for {
_, _, err = ws.ReadMessage()
if err != nil {
fmt.Println("ws断开了", ip)
delete(*_map, ip)
return nil, err
}
}
}

View File

@ -0,0 +1,157 @@
package _struct
import "encoding/xml"
// 飞向首航点模式
const (
Safely = "safely"
PointToPoint = "pointToPoint"
)
// 航线结束动作
const (
GoHome = "goHome"
AutoLand = "autoLand"
GotoFirstWaypoint = "gotoFirstWaypoint"
)
//失控是否继续执行航线
const (
GoContinue = "goContinue" //继续执行航线
ExecuteLostAction = "executeLostAction" //退出航线,执行失控动作
)
// 失控动作类型
const (
GoBack = "goBack" //返航。飞行器从失控位置飞向起飞点
Landing = "landing" //降落。飞行器从失控位置原地降落
Hover = "hover" //悬停。飞行器从失控位置悬停
)
// 预定义模板类型
const (
Waypoint = "waypoint" //航点飞行
Mapping2d = "mapping2d" //建图航拍
Mapping3d = "mapping3d" //倾斜摄影
MappingStrip = "mappingStrip" //航带飞行
)
// Placemark 结构表示 KML 的地点标记
type Placemark struct {
Point Point `xml:"Point"`
Index int `xml:"wpml:index"`
EllipsoidHeight float64 `xml:"wpml:ellipsoidHeight"`
Height float64 `xml:"wpml:height"`
UseGlobalSpeed int `xml:"wpml:useGlobalSpeed"`
UseGlobalHeadingParam int `xml:"wpml:useGlobalHeadingParam"`
UseGlobalTurnParam int `xml:"wpml:useGlobalTurnParam"`
UseStraightLine int `xml:"wpml:useStraightLine"`
//ActionGroup ActionGroup `xml:"wpml:actionGroup"`
}
// Point 结构表示地点坐标
type Point struct {
Coordinates string `xml:"coordinates"`
}
type PayloadParam struct {
PayloadPositionIndex int `xml:"wpml:payloadPositionIndex"`
ImageFormat string `xml:"wpml:imageFormat"`
}
type Doc struct {
Author string `xml:"wpml:author"`
UpdateTime int64 `xml:"wpml:updateTime"`
CreateTime int64 `xml:"wpml:createTime"`
MissionConfig MissionConfig `xml:"wpml:missionConfig"`
Folder interface{} `xml:"Folder"`
}
type PublicTemplate struct {
TemplateType string `xml:"wpml:templateType"`
TemplateId int `xml:"wpml:templateId"`
AutoFlightSpeed float64 `xml:"wpml:autoFlightSpeed"`
WaylineCoordinateSysParam struct {
CoordinateMode string `xml:"wpml:coordinateMode"`
HeightMode string `xml:"wpml:heightMode"`
GlobalShootHeight float64 `xml:"wpml:globalShootHeight"`
PositioningType string `xml:"wpml:positioningType"`
SurfaceFollowModeEnable int `xml:"wpml:surfaceFollowModeEnable"`
SurfaceRelativeHeight string `xml:"wpml:surfaceRelativeHeight"`
} `xml:"wpml:waylineCoordinateSysParam"`
}
type ActionGroup struct {
ActionGroupId int `xml:"wpml:actionGroupId"`
ActionGroupStartIndex int `xml:"wpml:actionGroupStartIndex"`
ActionGroupEndIndex int `xml:"wpml:actionGroupEndIndex"`
ActionGroupMode string `xml:"wpml:actionGroupMode"`
ActionTrigger struct {
ActionTriggerType string `xml:"actionTriggerType"`
} `xml:"actionTrigger"`
Action Action `xml:"action"`
}
type Action struct {
ActionId int `xml:"actionId"`
ActionActuatorFunc string `xml:"actionActuatorFunc"`
ActionActuatorFuncParam ActionActuatorFuncParam `xml:"actionActuatorFuncParam"`
}
type ActionActuatorFuncParam struct {
FileSuffix string `xml:"fileSuffix"`
PayloadPositionIndex string `xml:"payloadPositionIndex"`
}
type MissionConfig struct {
FlyToWaylineMode string `xml:"wpml:flyToWaylineMode"`
FinishAction string `xml:"wpml:finishAction"`
ExitOnRCLost string `xml:"wpml:exitOnRCLost"`
ExecuteRCLostAction string `xml:"wpml:executeRCLostAction"`
TakeOffSecurityHeight float64 `xml:"wpml:takeOffSecurityHeight"`
GlobalTransitionalSpeed float64 `xml:"wpml:globalTransitionalSpeed"`
GlobalRTHHeight float64 `xml:"wpml:globalRTHHeight"`
//TakeOffRefPoint xml.Name `xml:"wpml:takeOffRefPoint"`
//
//TakeOffRefPointAGLHeight xml.Name `xml:"wpml:takeOffRefPointAGLHeight"`
//DroneInfo struct {
// DroneEnumValue xml.Name `xml:"wpml:droneEnumValue"`
// DroneSubEnumValue xml.Name `xml:"wpml:droneSubEnumValue"`
//} `xml:"wpml:droneInfo"`
//PayloadInfo struct {
// PayloadEnumValue xml.Name `xml:"wpml:payloadEnumValue"`
// PayloadSubEnumValue xml.Name `xml:"wpml:payloadSubEnumValue"`
// PayloadPositionIndex xml.Name `xml:"wpml:payloadPositionIndex"`
//} `xml:"wpml:payloadInfo"`
}
type WayPointTemplate struct {
PublicTemplate
GlobalWaypointTurnMode string `xml:"wpml:globalWaypointTurnMode"` //全局航点类型全局航点转弯模式coordinateTurn协调转弯不过点提前转弯 toPointAndStopWithDiscontinuityCurvature直线飞行飞行器到点停 toPointAndStopWithContinuityCurvature曲线飞行飞行器到点停 toPointAndPassWithContinuityCurvature曲线飞行飞行器过点不停
GlobalUseStraightLine int `xml:"wpml:globalUseStraightLine"` //全局航段轨迹是否尽量贴合直线
GimbalPitchMode string `xml:"wpml:gimbalPitchMode"` //云台俯仰角控制模式 manual手动控制。飞行器从一个航点飞向下一个航点的过程中
// 支持用户手动控制云台的俯仰角度。若无用户控制,则保持飞离航点时的云台俯仰角度。
//usePointSetting依照每个航点设置。飞行器从一个航点飞向下一个航点的过程中云台俯仰角均匀过渡至下一个航点的俯仰角。
GlobalHeight float64 `xml:"wpml:globalHeight"` //全局航线高度(相对起飞点高度
Placemark []Placemark `xml:"Placemark"`
//UseGlobalTransitionalSpeed string `xml:"wpml:useGlobalTransitionalSpeed"`
//TransitionalSpeed string `xml:"wpml:transitionalSpeed"`
//GlobalWaypointHeadingParam struct {
// WaypointHeadingMode string `xml:"wpml:waypointHeadingMode"`
// WaypointHeadingAngle string `xml:"wpml:waypointHeadingAngle"`
// WaypointPoiPoint string `xml:"wpml:waypointPoiPoint"`
// WaypointHeadingPathMode string `xml:"wpml:waypointHeadingPathMode"`
//} `xml:"wpml:globalWaypointHeadingParam"` //全局偏航角模式参数
}
// KML 结构表示完整的 KML 文件
type KML struct {
//XMLName xml.Attr `xml:"kml"`
XMLName xml.Name `xml:"http://www.opengis.net/kml/2.2 kml"`
XmlnsStream string `xml:"xmlns:wpml,attr"`
//Test []Test `xml:"test"`
Document Doc `xml:"Document"`
}

View File

@ -0,0 +1,78 @@
package template
import (
"encoding/xml"
"fmt"
"github.com/gogf/gf/v2/os/gtime"
"github.com/tiger1103/gfast/v3/third/plane/wayline/struct"
"os"
)
func CreateWayPointTempalteWpml() {
}
func CreateWayPointTempalteKml() {
kml := _struct.KML{
//XmlnsStream: "http://www.dji.com/wpmz/1.0.3",
Document: _struct.Doc{
Author: "重庆远界大数据研究院有限公司",
CreateTime: gtime.Now().TimestampMilli(),
UpdateTime: gtime.Now().TimestampMilli(),
},
}
msconfig := _struct.MissionConfig{
FlyToWaylineMode: _struct.Safely,
FinishAction: _struct.GoHome,
ExitOnRCLost: _struct.ExecuteLostAction,
ExecuteRCLostAction: _struct.Hover,
TakeOffSecurityHeight: 5,
GlobalTransitionalSpeed: 5,
GlobalRTHHeight: 5,
}
//设置公共配置
kml.Document.MissionConfig = msconfig
tp := _struct.WayPointTemplate{}
tp.TemplateType = _struct.Waypoint //航点飞行
tp.TemplateId = 0 //模板ID * 注在一个kmz文件内该ID唯一。建议从0开始单调连续递增。在template.kml和waylines.wpml文件中将使用该id将模板与所生成的可执行航线进行关联。
tp.AutoFlightSpeed = 5 //全局航线飞行速度
tp.WaylineCoordinateSysParam.CoordinateMode = "WGS84"
tp.WaylineCoordinateSysParam.HeightMode = "EGM96"
tp.WaylineCoordinateSysParam.PositioningType = "GPS"
tp.WaylineCoordinateSysParam.SurfaceFollowModeEnable = 0
tp.GlobalHeight = 100
kml.Document.Folder = tp
createXML(kml, "template.xml")
}
func createXML(v interface{}, file string) {
// 生成 XML
xmlBytes, err := xml.MarshalIndent(v, "", " ")
if err != nil {
fmt.Println("生成XML失败:", err)
return
}
// 将 XML 写入文件
xmlFile, err := os.Create(file)
if err != nil {
fmt.Println("创建文件失败:", err)
return
}
defer xmlFile.Close()
xmlWithVersion := []byte(xml.Header + string(xmlBytes))
_, err = xmlFile.Write(xmlWithVersion)
if err != nil {
fmt.Println("写入文件失败:", err)
return
}
fmt.Println("生成的KML文件output.kml")
}
//func CreateWayPointTempalte(points []globe.Point) {
//
//}

View File

@ -0,0 +1 @@
package wayline

149
third/progress/parent.go Normal file
View File

@ -0,0 +1,149 @@
package progress
import (
"context"
"fmt"
"strconv"
"github.com/tiger1103/gfast/v3/internal/app/system/dao"
"github.com/tiger1103/gfast/v3/internal/app/system/model"
"github.com/tiger1103/gfast/v3/internal/app/system/model/entity"
)
type Scheduler struct {
// 总进度
FinishedPtr int
// 总量
Total int
// 实体
WorkStatus *entity.WorkSchedule
}
type Schdule map[string]Scheduler
// 获取指定 WorkID 的计划和总量
func (s Schdule) Get(workID string) (int, int) {
if scheduler, ok := s[workID]; ok {
return scheduler.FinishedPtr, scheduler.Total
}
return 0, 0
}
// WorkStatusGetProgress 传入一个方阵ID 获取其所有父级的进度
func WorkStatusGetProgress(fangzhenID string) ([]*model.WorkStatusProgressRes, error) {
var parentList []*model.WorkStatusProgressRes
err := dao.WorkStatus.Ctx(context.Background()).As("parent").
Fields("parent.id, parent.work_name as name ,parent.work_id,IFNULL(SUM(child.total), 0) AS total, IFNULL(SUM(child.finished), 0) AS finished").
LeftJoin("work_status AS child", "parent.id = child.parent").
Where("parent.parent IS NULL").
Where("parent.fangzhen_id", fangzhenID).
Group("parent.id, parent.work_name, parent.total").
Scan(&parentList)
if err != nil {
return nil, err
}
if len(parentList) == 0 {
return parentList, fmt.Errorf("未找到父级计划")
}
return parentList, err
}
// GetProgressBySubID 获取一共子项目下所有的父级进度
func GetProgressBySubID(subProjectID string) ([]*model.WorkStatusProgressRes, error) {
var fangzhenList []entity.QianqiFangzhen
if err := dao.QianqiFangzhen.Ctx(context.Background()).
Where(dao.QianqiFangzhen.Columns().ProjectId, subProjectID).Scan(&fangzhenList); err != nil {
return nil, err
}
if len(fangzhenList) == 0 {
return nil, fmt.Errorf("未找到方阵")
}
cumulativeProjects := []*model.WorkStatusProgressRes{}
firstTime := true
// 遍历项目列表
for index := 0; index < len(fangzhenList); index++ {
// 获取当前项目
project := fangzhenList[index]
// 获取当前项目的子项目
childProjects, err := WorkStatusGetProgress(strconv.Itoa(project.Id))
if err != nil {
return nil, err
}
// 如果是第一次迭代,将子项目列表赋值给累积项目列表
if firstTime {
cumulativeProjects = childProjects
// 更新标志变量,表示已经不是第一次迭代
firstTime = false
continue
}
// 遍历子项目列表后叠加到累积项目列表
for childIndex := 0; childIndex < len(childProjects); childIndex++ {
// 获取根据索引获取当前的 cumulativeProjects
singleChild := childProjects[childIndex]
comulativeChild := cumulativeProjects[childIndex]
// 将 singleChild 的 Total 和 Finished 叠加到 comulativeChild
comulativeChild.Total += singleChild.Total
comulativeChild.Finished += singleChild.Finished
}
}
return cumulativeProjects, nil
}
// 传入主项目ID
func GetProgressByProjectID(projectID string) ([]*model.WorkStatusProgressRes, error) {
subProjects := []entity.SubProject{}
if err := dao.SubProject.Ctx(context.Background()).
Where(dao.SubProject.Columns().ProjectId, projectID).Scan(&subProjects); err != nil {
return nil, err
}
cumulativeProjects := []*model.WorkStatusProgressRes{}
firstTime := true
// 遍历项目列表
for index := 0; index < len(subProjects); index++ {
// 获取当前项目
project := subProjects[index]
// 获取当前项目的子项目
childProjects, err := GetProgressBySubID(strconv.Itoa(int(project.Id)))
if err != nil {
continue
}
// 如果是第一次迭代,将子项目列表赋值给累积项目列表
if firstTime {
cumulativeProjects = childProjects
// 更新标志变量,表示已经不是第一次迭代
firstTime = false
continue
}
// 遍历子项目列表后叠加到累积项目列表
for childIndex := 0; childIndex < len(childProjects); childIndex++ {
// 获取根据索引获取当前的 cumulativeProjects
singleChild := childProjects[childIndex]
if childIndex < 0 || childIndex >= len(cumulativeProjects) {
continue
}
comulativeChild := cumulativeProjects[childIndex]
// 将 singleChild 的 Total 和 Finished 叠加到 comulativeChild
comulativeChild.Total += singleChild.Total
comulativeChild.Finished += singleChild.Finished
}
}
return cumulativeProjects, nil
}

110
third/reminders/reminder.go Normal file
View File

@ -0,0 +1,110 @@
package reminders
import (
"context"
"fmt"
"github.com/jinzhu/copier"
"github.com/samber/lo"
"github.com/tiger1103/gfast/v3/internal/app/system/dao"
"github.com/tiger1103/gfast/v3/internal/app/system/model/do"
)
type Reminder struct {
Content string `copier:"ViolationType"` // 内容
Title string // 标题
ReceiverID string `copier:"UserId"` // 消息接收者的 User_id
Type int `copier:"ReminderType"` // 类型
Status int // 状态
ProjectID int `copier:"ProjectId"` // 项目 ID
TargetID int `copier:"OrderId"` // 对应跳转的主键ID
//TargetID int `copier:"OrderId"` // 是否需要转换
}
// UserIDOrderIDDel 根据userID与orderID 删除提醒
func UserIDOrderIDDel(userId string, orderID int64) error {
columns := dao.Reminders.Columns()
_, err := dao.Reminders.Ctx(context.Background()).Where(columns.UserId, userId).Where(columns.OrderId, orderID).Delete()
return err
}
// PublishReminder 发布提醒
func PublishReminder(message Reminder, convertOpenID bool) error {
if convertOpenID && HasLetter(message.ReceiverID) {
message.ReceiverID = GetUserIDByOpenID(message.ReceiverID)
if message.ReceiverID == "" {
return fmt.Errorf("未找到对应的用户")
}
}
var reminder do.Reminders
copier.Copy(&reminder, &message)
// 保存到数据库
_, err := dao.Reminders.Ctx(context.Background()).Insert(&reminder)
return err
}
// CheckInReminder 打卡提醒
// Reminders.CheckIn 表示上班打卡
// Reminders.CheckOut 表示下班打卡
func CheckInReminder(checkType ReminderType, projectID int, userID, time string) error {
// 1. 构建消息模板
message := Reminder{
Type: checkType, // 打卡类型
ProjectID: projectID, // 项目 ID
ReceiverID: userID, // 消息接收者的 sys_user.Id
Title: time, // 时间,只需要多少分钟,如 "10""20"
}
return PublishReminder(message, false)
}
// hasLetter 判断字符串中是否包含字母
func HasLetter(s string) bool {
for _, c := range s {
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') {
return true
}
}
return false
}
// GetUserIDByOpenID 通过 OpenID 获取用户对应的 sys_user.Id
func GetUserIDByOpenID(openID string) string {
openid, err := dao.BusConstructionUser.Ctx(context.Background()).As("bu").
Fields("su.id").
InnerJoin("sys_user AS su", "bu.phone = su.mobile").
Where("bu.openid", openID).Value()
if err != nil {
return ""
}
return openid.String()
}
type userOpenID struct {
OpenID string
ID int
}
// GetUserIDByOpenIDs 传入多个 OpenID返回对应的 User.ID
func GetUserIDByOpenIDs(openIDs []string) map[string]string {
// SELECT su.id,bu.openid
// FROM bus_construction_user AS bu
// INNER JOIN sys_user AS su ON bu.phone = su.mobile
// WHERE bu.openid in ("oLYsI4x-L6ZVKCinQ4Rw1D9XtFZ0","oLYsI49aMJHTYorhmvVuRrasPTbU")
var userOpenIDList []userOpenID
err := dao.BusConstructionUser.Ctx(context.Background()).As("bu").
Fields("su.id", "bu.openid").
InnerJoin("sys_user AS su", "bu.phone = su.mobile").
WhereIn("bu.openid", openIDs).Scan(&userOpenIDList)
if err != nil {
return nil
}
return lo.SliceToMap(userOpenIDList, func(item userOpenID) (string, string) {
return item.OpenID, fmt.Sprintf("%d", item.ID)
})
}

View File

@ -0,0 +1,20 @@
package reminders
type ReminderType = int
// 提醒类型
const (
Security ReminderType = iota // 安全
Quality // 质量
AI // AI
CheckIn // 上班打卡
CheckOut // 下班打卡
)
// 提醒状态
const (
Remind = iota // 提醒
Reform // 整改
Recheck // 复检
)

77
third/richText/strip.go Normal file
View File

@ -0,0 +1,77 @@
package richtext
import (
"fmt"
"regexp"
"strings"
)
func RemoveRichText(text string) string {
// Regular expression to replace <br/> and <p> tags with newline
reBr := regexp.MustCompile(`<(br|p)(\s[^>]*)?>`)
text = reBr.ReplaceAllString(text, "\n")
// Add four full-width spaces after each newline
reNewline := regexp.MustCompile(`\n`)
text = reNewline.ReplaceAllString(text, "\n  ") // 两个全角空格等于四个半角空格,即四个汉字的距离
// Regular expression to replace &nbsp; with empty string
reNbsp := regexp.MustCompile(`&nbsp;`)
text = reNbsp.ReplaceAllString(text, "")
// Regular expression to match other HTML tags
re := regexp.MustCompile("<[^>]*>")
plainText := re.ReplaceAllString(text, "")
plainText = strings.TrimSpace(plainText)
// plainText = "  " + plainText
return plainText
}
func ExtractImageURLs(text string) []string {
re := regexp.MustCompile(`<img[^>]+\bsrc=["']([^"']+)["']`)
matches := re.FindAllStringSubmatch(text, -1)
var urls []string
for _, match := range matches {
urls = append(urls, match[1])
}
return urls
}
func ConvertToRichText(plainText string, imageURLs []string) string {
// 将纯文本分割成段落
paragraphs := strings.Split(plainText, "\n")
// 创建一个StringBuilder来构建富文本
var richText strings.Builder
// 将每个段落包装在<p>标签中
for _, paragraph := range paragraphs {
richText.WriteString("<p>")
richText.WriteString(paragraph)
richText.WriteString("</p>")
}
// 将每个图片URL包装在<img>标签中,并附加到富文本中
for _, imageURL := range imageURLs {
richText.WriteString("<img src=\"")
richText.WriteString(imageURL)
richText.WriteString("\" />")
}
// 返回富文本字符串
return richText.String()
}
// 传入一个字符串数组,将其包装为一个富文本图片标签
func ConvertImageURLsToRichText(imageURLs []string) string {
var richText strings.Builder
for _, imageURL := range imageURLs {
richText.WriteString(fmt.Sprintf(`<p><img src="%s" /></p>`, imageURL))
}
return richText.String()
}

112
third/schduler/schduler.go Normal file
View File

@ -0,0 +1,112 @@
package schduler
import (
"context"
"time"
"github.com/gogf/gf/v2/frame/g"
"github.com/tiger1103/gfast/v3/internal/app/system/dao"
"github.com/tiger1103/gfast/v3/internal/app/system/model/entity"
)
type scheduler struct {
// 是否延期
IsDelay int
// 总进度
FinishedPtr int
// 总量
Total int
// 实体
WorkStatus *entity.WorkSchedule
}
type Schdule map[string]scheduler
// New 初始化计划
func New(ScheduleData []entity.WorkSchedule) (Schdule, error) {
ctx := context.Background()
scheduleMap := make(Schdule)
for _, task := range ScheduleData {
// 计划进度
expectedProgress := task.PlanNum
// 实际完成度
actualProgress := task.FinishedNum
// 判断是否延期
isDelayed := isDelay(task.EndAt.Format("Y-m-d"), expectedProgress, actualProgress)
// 如果 WorkID 不存在
if _, exists := scheduleMap[task.WorkId]; !exists {
newScheduler := scheduler{
IsDelay: isDelayed,
WorkStatus: &task,
Total: expectedProgress,
FinishedPtr: actualProgress,
}
scheduleMap[task.WorkId] = newScheduler // 添加到 map 中
continue
}
existingScheduler := scheduleMap[task.WorkId] // 存在则取出 Value
// 判断延期
if existingScheduler.IsDelay != 1 { // 为了防止在存在多个计划的情况下,未延期的计划覆盖了已经延期的状态。
// 修改 Work_Status 中 is_delay 修改为延期状态 1
_, err := dao.WorkStatus.Ctx(ctx).Where(dao.WorkStatus.Columns().WorkId, task.WorkId).
Data(g.Map{dao.WorkStatus.Columns().IsDelay: 1}).Update()
if err != nil {
return nil, err
}
existingScheduler.IsDelay = isDelayed // 修改 map 中的状态
}
// 多个计划的情况下叠加数据
existingScheduler.Total += expectedProgress // 计划总量叠加
existingScheduler.FinishedPtr += actualProgress // 实际进度叠加
scheduleMap[task.WorkId] = existingScheduler
}
return scheduleMap, nil
}
// 获取指定 WorkID 的计划和总量
func (s Schdule) Get(workID string) (finishedPtr, total int) {
if sch, ok := s[workID]; ok {
return sch.FinishedPtr, sch.Total
}
return 0, 0
}
// GetStatus 根据 WorkID 获取工作的状态,判断是否延期
// s[workID] 中记录了该 WorkID 的计划总量和实际进度
// total 是计划的总量
// 如果实际进度FinishedPtr小于计划总量则判断工作为延期返回1
// 如果实际进度达到或超过计划总量,或者 WorkID 不存在于计划中判断工作未延期返回0
func (s Schdule) GetStatus(workID string, total int) int {
if sch, ok := s[workID]; ok {
if sch.FinishedPtr < total {
return sch.IsDelay
}
}
return 0
}
// isDelay 如果实际完成数量小于计划数量,且当前时间大于结束时间,则判断为延期
// endAt 结束时间
// planNum 计划数量
// finishedNum 实际数量
func isDelay(endAt string, planNum, finishedNum int) int {
// 当天时间
nowTime := time.Now().AddDate(0, 0, -1).Format("2006-01-02")
// 如果实际完成数量小于计划数量,且当前时间大于结束时间,则判断为延期
if finishedNum < planNum && nowTime > endAt {
return 1
}
return 0
}

114
third/securityRequest.go Normal file
View File

@ -0,0 +1,114 @@
// Package utility 安全请求思路来源https://blog.csdn.net/David_jiahuan/article/details/106391956
// @Author 铁憨憨[cory] 2024/9/11 9:38:00
package utility
import (
"crypto/md5"
"crypto/sha256"
"encoding/hex"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/util/gconv"
"math/big"
"strconv"
"time"
)
// 签名验证中间件
func SignatureMiddleware(r *ghttp.Request) {
if r.URL.Path == "/zm/api/wxApplet/wxApplet/appPort/releaseAppVersion" {
r.Middleware.Next()
return
}
// 获取请求中的参数
signature := gconv.String(r.Header.Get("sign"))
timestamp := gconv.Int64(r.Header.Get("timestamp"))
nonce := gconv.String(r.Header.Get("nonce"))
if signature == "" || timestamp == 0 || nonce == "" {
r.Response.WriteJson(g.Map{"error": "无效验证"})
r.ExitAll()
return
}
//验证时间戳
if !validateTimestamp(timestamp) {
r.Response.WriteJson(g.Map{"error": "请求过期"})
r.ExitAll()
return
}
// 验证 nonce 防止重放攻击
if !validateNonce(nonce) {
r.Response.WriteJson(g.Map{"error": "无效请求:检测到重放"})
r.ExitAll()
return
}
serverSignature := generateServerSignature(strconv.FormatInt(timestamp, 10), nonce)
// 验证签名
if signature != serverSignature {
r.Response.WriteJson(g.Map{"error": "签名错误"})
r.ExitAll()
return
}
//签名验证通过
r.Middleware.Next()
}
func validateTimestamp(unixTimestamp int64) bool {
// 获取当前时间
now := time.Now().UnixMilli()
result := subtractBigFromInt64(now, unixTimestamp)
// 将整数 1500 转换为 *big.Int 类型
big1500 := big.NewInt(30000)
// 验证时间差是否在 1500 毫秒内
if result.Cmp(big1500) == -1 && result.Cmp(big.NewInt(0)) >= 0 {
return true
}
return false
}
func subtractBigFromInt64(currentMillis int64, unixTimestamp int64) *big.Int {
// 将两个 int64 类型的值转换为 *big.Int 类型
bigCurrent := big.NewInt(currentMillis)
bigUnixTimestamp := big.NewInt(unixTimestamp)
// 计算差值
result := big.NewInt(0).Sub(bigCurrent, bigUnixTimestamp)
// 求差值的绝对值
return result.Abs(result)
}
// 验证 nonce
func validateNonce(nonce string) bool {
nonceKey := "reset" + nonce
// 使用 Redis 来存储 nonce防止重放
redisClient := g.Redis()
ctx := gctx.New()
exists, err := redisClient.Exists(ctx, nonceKey)
if err != nil {
return false
}
if exists > 0 {
return false // nonce 已经存在,说明是重放请求
}
// nonce 不存在,存储到 Redis 并设置60秒过期时间
err = redisClient.SetEX(ctx, nonceKey, nonce, 60)
if err != nil {
return false
} else {
return true
}
}
// 生成服务端签名
func generateServerSignature(timestamp, nonce string) string {
sum := md5.Sum([]byte("coyrOrtiehanhan1223202409111457"))
md5Sum := hex.EncodeToString(sum[:])
// 拼接时间戳、nonce 和 secretKey
signStr := md5Sum + timestamp + nonce
// 对拼接后的字符串进行 MD5 加密
md5Hash := md5.Sum([]byte(signStr))
md5Hex := hex.EncodeToString(md5Hash[:])
// 对 MD5 结果进行 SHA-256 加密
sha256Hash := sha256.Sum256([]byte(md5Hex))
sha256Hex := hex.EncodeToString(sha256Hash[:])
return sha256Hex
}

View File

@ -0,0 +1,88 @@
package solaranalyzer
import (
"encoding/json"
"os/exec"
"path/filepath"
"strings"
"github.com/tidwall/gjson"
)
const (
drillingHole = "hole" // 钻孔
pillar = "pile" // 桩基
bracket = "shelves" // 支架
solarPanel = "pho" // 光伏板
)
type Point struct {
Points []XYPosition // 光伏板的坐标点
ID int // pv_module 表中的自增ID
}
type XYPosition struct {
X float64 `json:"x"`
Y float64 `json:"y"`
}
// getSolarPanelRanges 解析光伏板 JSON
func getSolarPanelRanges(file string) []Point {
var points []Point
gjson.Parse(file).ForEach(func(_, record gjson.Result) bool {
record.Get("detail").ForEach(func(_, detail gjson.Result) bool {
id := record.Get("id").Int()
var longitudeAndLatitude []XYPosition
gjson.Get(detail.String(), "positions").ForEach(func(_, position gjson.Result) bool {
longitude := position.Get("lng").Float()
latitude := position.Get("lat").Float()
longitudeAndLatitude = append(longitudeAndLatitude, XYPosition{X: longitude, Y: latitude})
return true
})
points = append(points, Point{ID: int(id), Points: longitudeAndLatitude})
return true
})
return true
})
return points
}
// convertToLongitudeAndLatitude 根据 tif 文件将坐标转换为经纬度
func convertToLongitudeAndLatitude(tifFilePath string, coordinatePoints []XYPosition) []XYPosition {
positionsJSON, err := json.Marshal(coordinatePoints)
if err != nil {
panic(err)
}
convertPath, err := filepath.Abs("./convert")
if err != nil {
panic(err)
}
output, err := exec.Command(convertPath, filepath.Clean(tifFilePath), string(positionsJSON)).CombinedOutput()
if err != nil {
panic(err)
}
outputLines := strings.Split(string(output), "\n")
lastLine := outputLines[len(outputLines)-2] // 获取最后一行
// 解析二进制返回的 JSON, 并将其转换为 XYPosition 结构
var longitudeAndLatitude []XYPosition
gjson.Get(lastLine, "data").ForEach(func(_, position gjson.Result) bool {
longitudeAndLatitude = append(longitudeAndLatitude, XYPosition{
X: position.Get("y").Float(),
Y: position.Get("x").Float(),
})
return true
})
return longitudeAndLatitude
}

View File

@ -0,0 +1,57 @@
package solaranalyzer
import (
"math"
"github.com/tomchavakis/geojson/geometry"
)
const (
earthRadius = 6371000 // 地球半径(米)
radiusMeters = 0.07 // 半径7厘米
numPoints = 8 // 点的数量
)
type GeoPoint struct {
ID int
Coordinates Coordinates
}
type Coordinates struct {
Lng float64
Lat float64
Alt float64
}
// ExpandToCircle 用于桩点和支架
// 以一个点为中心,扩展出一个圆形
func (p Coordinates) ExpandToCircle() geometry.Polygon {
circle := make([]Coordinates, numPoints)
for i := 0; i < numPoints; i++ {
angle := 2 * math.Pi * float64(i) / float64(numPoints)
// 计算偏移量(米)
dx := radiusMeters * math.Cos(angle)
dy := radiusMeters * math.Sin(angle)
// 将米转换为经纬度
dLng := dx / (earthRadius * math.Cos(p.Lat*math.Pi/180)) * 180 / math.Pi
dLat := dy / earthRadius * 180 / math.Pi
circle[i] = Coordinates{
Lng: p.Lng + dLng,
Lat: p.Lat + dLat,
Alt: p.Alt,
}
}
var coords []geometry.Point
for _, point := range circle {
coords = append(coords, geometry.Point{Lat: point.Lat, Lng: point.Lng})
}
return geometry.Polygon{Coordinates: []geometry.LineString{{Coordinates: coords}}}
}

View File

@ -0,0 +1,695 @@
package solaranalyzer
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/samber/lo"
"github.com/tidwall/gjson"
"github.com/tiger1103/gfast/v3/internal/app/system/dao"
"github.com/tiger1103/gfast/v3/internal/app/system/model/do"
"github.com/tiger1103/gfast/v3/internal/app/system/model/entity"
"github.com/tiger1103/gfast/v3/library/liberr"
"github.com/tomchavakis/geojson/geometry"
"github.com/tomchavakis/turf-go"
)
type AIResult struct {
SolarPanels []XYPosition // 光伏板的坐标点
Brackets []XYPosition // 支架的坐标点
Pillars []XYPosition // 立柱的坐标点
Holes []XYPosition // 钻孔的坐标点
}
// ToGeoPoints 将坐标转换为经纬度坐标
func (r *AIResult) ToGeoPoints(tifPath string) *AIResult {
r.SolarPanels = convertToLongitudeAndLatitude(tifPath, r.SolarPanels) // 光伏板
r.Brackets = convertToLongitudeAndLatitude(tifPath, r.Brackets) // 支架
r.Pillars = convertToLongitudeAndLatitude(tifPath, r.Pillars) // 立柱
r.Holes = convertToLongitudeAndLatitude(tifPath, r.Holes) // 钻孔
return r
}
// IsCircleContainsPoint 判断圆形是否包含点返回桩点的主键ID
func (r *AIResult) IsCircleContainsPoint(m map[string]map[string]GeoPoint, usePillars string) []int {
var ids []int
var elements []XYPosition
switch usePillars {
case drillingHole:
elements = r.Holes
case pillar:
elements = r.Pillars
case bracket:
elements = r.Brackets
}
for _, aiPillar := range elements {
// AI 识别点
aiPoint := geometry.Point{Lng: aiPillar.X, Lat: aiPillar.Y}
// 方阵层
for _, points := range m {
// 立柱层
for _, dbPillar := range points {
circle := dbPillar.Coordinates.ExpandToCircle()
if ok, err := turf.PointInPolygon(aiPoint, circle); ok && err == nil {
ids = append(ids, dbPillar.ID)
}
}
}
}
return ids
}
// GetFromDB 从数据库中获取光伏板、支架、桩点的数据
func (r *AIResult) GetFromDB(projectID string) ([]Point, map[string]map[string]GeoPoint) {
// 光伏板、支架
var solarPanels []Point
var pillars map[string]map[string]GeoPoint
if len(r.SolarPanels) != 0 {
if points, err := getSolarPanelCenters(projectID); err == nil {
solarPanels = points
}
}
if len(r.Pillars) != 0 {
pillars = getPillars(projectID)
}
return solarPanels, pillars
}
// 传入 AI 结果
// 1. 将其周围的组串,全部标记为已完成
// 2. 根据其 【方阵ID】【Type = 13】更新其在 work_status 中的总数 ok
// 3. 根据当前的 Name Etc: G1.123.1.1 将 pv_module 中的 G1.123.1 状态修改为已完成
// 4. 记录该数据由 AI 识别。
func (r *AIResult) Run(projectID string, id int) {
// 获取光伏板和支架,桩点的数据
panels, brackets := r.GetFromDB(projectID)
fmt.Println("(内容)光伏板和支架: ", panels)
fmt.Println("(内容)桩点: ", brackets)
// 预处理光伏板为多边形
polygons := make([]geometry.Polygon, len(panels))
for i, rect := range panels {
coords := make([]geometry.Point, len(rect.Points))
for j, p := range rect.Points {
coords[j] = geometry.Point{Lng: p.X, Lat: p.Y}
}
polygons[i] = geometry.Polygon{Coordinates: []geometry.LineString{{Coordinates: coords}}}
}
fmt.Println("(内容)预处理光伏板为多边形: ", polygons)
fmt.Println("(内容)光伏板坐标后点: ", r.SolarPanels)
// 光伏板的主键ID
var matchedRectangles []int
// 匹配AI识别的光伏板中心点
for _, point := range r.SolarPanels {
for i, polygon := range polygons {
if ok, err := turf.PointInPolygon(geometry.Point{Lng: point.X, Lat: point.Y}, polygon); err == nil && ok {
matchedRectangles = append(matchedRectangles, panels[i].ID)
break
}
}
}
_, err := dao.ManageTaskResult.Ctx(context.Background()).Insert(
lo.Map(matchedRectangles, func(v int, _ int) do.ManageTaskResult {
return do.ManageTaskResult{
TaskId: id,
PvId: v,
}
}))
liberr.ErrIsNil(context.Background(), err)
if len(matchedRectangles) > 0 { // TODO: 去除写入数据库的逻辑
// if err := ProcessMatchedRectangles(matchedRectangles); err != nil {
// g.Log("uav").Error(context.Background(), "更新匹配到的光伏板失败: ", err)
// }
}
// 更新钻孔
if len(r.Holes) > 0 {
ids := r.IsCircleContainsPoint(brackets, drillingHole)
if len(ids) > 0 {
if err := processPillarIDs(ids, 12, id); err != nil {
g.Log("uav").Error(context.Background(), "更新匹配到的支架失败: ", err)
}
}
}
// 更新支架和桩点
if len(r.Pillars) > 0 {
ids := r.IsCircleContainsPoint(brackets, pillar)
if len(ids) > 0 {
if err := processPillarIDs(ids, 13, id); err != nil {
g.Log("uav").Error(context.Background(), "更新匹配到的支架失败: ", err)
}
}
}
if len(r.Brackets) > 0 {
ids := r.IsCircleContainsPoint(brackets, bracket)
if len(ids) > 0 {
if err := processPillarIDs(ids, 14, id); err != nil {
g.Log("uav").Error(context.Background(), "更新匹配到的支架失败: ", err)
}
}
}
}
// 通过主键ID可以从 qianqi_guangfuban_ids_zhijia 表中查询到对应的 “方阵ID” 和 "子项目ID"
// 将得到的结果分组,第一层为 方阵ID第二层 G01.12.18,第三层数组,[ G01.12.18.1,G01.12.18.2,G01.12.18.3 ]
// 如果第三层中有数据则从数据库中向前后查询相邻组串并将其组串修改为已完成同时取出第二层和方阵ID用于更新 pv_module 和 work_status 表中的数据。
func processPillarIDs(ids []int, t, id int) error {
return nil
// 查询 qianqi_guangfuban_ids_zhijia 表
var zhijias []entity.QianqiGuangfubanIdsZhijia
if err := dao.QianqiGuangfubanIdsZhijia.Ctx(context.Background()).
Fields("id,fangzhen_id,sub_projectid,name").
WhereIn(dao.QianqiGuangfubanIdsZhijia.Columns().Id, ids).
Scan(&zhijias); err != nil {
return err
}
// 第一层方阵ID, 第二层支架名称, 第三层支架信息
realGeoPoint := make(map[string]map[string][]entity.QianqiGuangfubanIdsZhijia)
for _, zhijia := range zhijias {
fangzhenID := zhijia.FangzhenId // 方阵ID
zhijiaName := zhijia.Name[:strings.LastIndex(zhijia.Name, ".")] // 支架名称
if _, ok := realGeoPoint[fangzhenID]; !ok {
realGeoPoint[fangzhenID] = make(map[string][]entity.QianqiGuangfubanIdsZhijia)
}
realGeoPoint[fangzhenID][zhijiaName] = append(realGeoPoint[fangzhenID][zhijiaName], zhijia)
}
// 查询支架及其相邻的组串
for _, geoPoints := range realGeoPoint {
for pointKey, pointList := range geoPoints {
if len(pointList) > 1 {
firstPoint := pointList[0]
simulationID := firstPoint.FangzhenId
namePattern := firstPoint.Name[:strings.LastIndex(firstPoint.Name, ".")] + ".%"
var updatedPoints []entity.QianqiGuangfubanIdsZhijia
if err := dao.QianqiGuangfubanIdsZhijia.Ctx(context.Background()).Where(dao.QianqiGuangfubanIdsZhijia.Columns().FangzhenId, simulationID).
Where(dao.QianqiGuangfubanIdsZhijia.Columns().Name+" LIKE ?", namePattern).
Scan(&updatedPoints); err == nil && len(updatedPoints) > 0 {
realGeoPoint[simulationID][pointKey] = updatedPoints
}
}
}
}
// 更新数据库状态
for _, geoPoints := range realGeoPoint {
for key, pointList := range geoPoints {
// 取出所有的ID
primaryIDs := lo.Map(pointList, func(v entity.QianqiGuangfubanIdsZhijia, _ int) int {
return v.Id
})
g.Try(context.Background(), func(ctx context.Context) {
// 根据主键修改为已完成
// 修改组串的状态
// G01.12.18.1 -- G01.12.18.2 -- G01.12.18.3
_, err := dao.QianqiGuangfubanIdsZhijia.Ctx(ctx).WhereIn(dao.QianqiGuangfubanIdsZhijia.Columns().Id, primaryIDs).
Data(g.Map{
dao.QianqiGuangfubanIdsZhijia.Columns().Status: 2,
}).Update()
liberr.ErrIsNil(ctx, err)
fangzhenID := pointList[0].FangzhenId
// 根据名字从 pv_module 中获取主键id
result, err := dao.PvModule.Ctx(ctx).Fields("id").Where(dao.PvModule.Columns().Name, key).
Where(dao.PvModule.Columns().FangzhenId, fangzhenID).
Where(dao.PvModule.Columns().Type, t).One()
liberr.ErrIsNil(ctx, err)
// 获取主键ID
keyID := result.Map()["id"].(int)
dao.ManageTaskResult.Ctx(ctx).Data(do.ManageTaskResult{
TaskId: id,
PvId: keyID,
}).Insert() // 记录
// 更新 PVModule 表中的状态
// G01.12.18
lastID, err := dao.PvModule.Ctx(ctx).WhereIn(dao.PvModule.Columns().Id, keyID).
Where(dao.PvModule.Columns().FangzhenId, fangzhenID).
Where(dao.PvModule.Columns().Type, t).
Data(g.Map{
dao.PvModule.Columns().Status: 2,
}).Update()
// 获取影响行数并更新 work_status 表
rows, err := lastID.RowsAffected()
liberr.ErrIsNil(ctx, err)
if rows > 0 {
_, err := dao.WorkStatus.Ctx(ctx).Where(dao.WorkStatus.Columns().FangzhenId, fangzhenID).
Where(dao.WorkStatus.Columns().Type, t).
Data(g.Map{
dao.WorkStatus.Columns().Finished: gdb.Raw(
"finished + 1",
),
}).Update()
liberr.ErrIsNil(ctx, err)
}
// 根据 类型和方阵ID 获取对应的 work_id
dd := entity.WorkStatus{}
err = dao.WorkStatus.Ctx(ctx).Fields("work_id").Where(dao.WorkStatus.Columns().FangzhenId, fangzhenID).
Where(dao.WorkStatus.Columns().Type, t).Scan(&dd)
liberr.ErrIsNil(ctx, err)
work_id := dd.WorkId
err = UpdateWorkSchedule(work_id)
liberr.ErrIsNil(ctx, err)
})
}
}
return nil
}
// 处理匹配到的光伏板
func ProcessMatchedRectangles(matchedRectangles []int) error {
if len(matchedRectangles) == 0 {
return nil
}
return g.Try(context.Background(), func(ctx context.Context) {
timeNow := time.Now().Format("2006-01-02")
// 更新光伏板状态
_, err := dao.PvModule.Ctx(ctx).
WhereIn(dao.PvModule.Columns().Id, matchedRectangles).
Data(g.Map{
dao.PvModule.Columns().Status: 3,
dao.PvModule.Columns().DoneTime: timeNow,
}).Update()
liberr.ErrIsNil(ctx, err, "更新光伏板状态失败")
// 获取所有的 work_id
pvModules := []entity.PvModule{}
err = dao.PvModule.Ctx(ctx).Fields("work_id").WhereIn("id", matchedRectangles).Scan(&pvModules)
liberr.ErrIsNil(ctx, err, "查询 pv_module 表失败")
// 提取所有的 work_id
workIds := lo.Map(pvModules, func(module entity.PvModule, _ int) string {
return module.WorkId
})
// 查询 work_status 表
var workStatus []entity.WorkStatus
err = dao.WorkStatus.Ctx(ctx).WhereIn(dao.WorkStatus.Columns().WorkId, workIds).Scan(&workStatus)
liberr.ErrIsNil(ctx, err, "查询 work_status 表失败")
statusMap := make(map[string]entity.WorkStatus)
for _, status := range workStatus {
statusMap[status.WorkId] = status
}
// 遍历 pv_module 表,根据 work_id 更新 work_status 表
for _, pvModule := range pvModules {
if _, ok := statusMap[pvModule.WorkId]; !ok {
continue
}
// 2. 更新计划表 work_schdule 表中计划
// timeNow := time.Now().Format("2006-01-02")
// 根据 work_id 查询所有的计划
var workSchedules []entity.WorkSchedule
err = dao.WorkSchedule.Ctx(ctx).Where(dao.WorkSchedule.Columns().WorkId, pvModule.WorkId).Scan(&workSchedules)
if len(workSchedules) == 0 {
liberr.ErrIsNil(ctx, errors.New("需要前往进度计划对应的方阵中,设置计划日期,随后再来实现进度复查功能!"))
return
}
liberr.ErrIsNil(ctx, err, "查询 work_schedule 表失败")
// 根据计划起始和结束时间,当天位于哪个计划中
for _, schedule := range workSchedules {
startAt := schedule.StartAt.Format("Y-m-d")
endAt := schedule.EndAt.Format("Y-m-d")
if timeNow >= startAt && timeNow <= endAt {
// 反序列化 Detail
var details []struct {
Date string `json:"date"`
PlanNum int `json:"planNum"`
FinishedNum int `json:"finishedNum"` // 手动填充的完成数量
AutoFill int `json:"autoFill"` // 是否自动填充
}
if err := json.Unmarshal([]byte(schedule.Detail), &details); err != nil {
continue
}
// 遍历 details找到当天的计划
for i := range details {
if details[i].Date == timeNow {
details[i].AutoFill++ // 标记为 AI 识别
}
}
// 序列化 details
detailBytes, err := json.Marshal(details)
if err != nil {
continue
}
// 更新 work_schedule 表
_, err = dao.WorkSchedule.Ctx(ctx).Where(dao.WorkSchedule.Columns().WorkId, schedule.WorkId).
Data(g.Map{
dao.WorkSchedule.Columns().Detail: string(detailBytes),
dao.WorkSchedule.Columns().FinishedNum: schedule.FinishedNum + 1,
}).Update()
liberr.ErrIsNil(ctx, err, "更新 work_schedule 表失败")
var currentStatus entity.WorkStatus
err = dao.WorkStatus.Ctx(ctx).Where(dao.WorkStatus.Columns().WorkId, schedule.WorkId).Scan(&currentStatus)
liberr.ErrIsNil(ctx, err, "查询 work_status 表失败")
if currentStatus.Finished+1 <= currentStatus.Total {
finished := currentStatus.Finished + 1
_, err = dao.WorkStatus.Ctx(ctx).Where(dao.WorkStatus.Columns().WorkId, currentStatus.WorkId).
Where(dao.WorkStatus.Columns().Type, currentStatus.Type).
Data(g.Map{
dao.WorkStatus.Columns().Finished: finished,
}).Update()
liberr.ErrIsNil(ctx, err, "更新 work_status 表失败")
}
}
}
}
})
}
// 传入一个 workID 修改其对应的计划数据
func UpdateWorkSchedule(workID string) error {
return g.Try(context.Background(), func(ctx context.Context) {
// 查询 work_status 表
var workStatus []entity.WorkStatus
err := dao.WorkStatus.Ctx(ctx).Where(dao.WorkStatus.Columns().WorkId, workID).Scan(&workStatus)
liberr.ErrIsNil(ctx, err, "查询 work_status 表失败")
// 遍历 work_status 表,如果自增后的完成量小于等于总数,则将完成量加一
for _, status := range workStatus {
if status.Finished+1 <= status.Total {
finished := status.Finished + 1
_, err = dao.WorkStatus.Ctx(ctx).Where(dao.WorkStatus.Columns().WorkId, status.WorkId).
Data(g.Map{
dao.WorkStatus.Columns().Finished: finished,
}).Update()
liberr.ErrIsNil(ctx, err, "更新 work_status 表失败")
}
// 2. 更新计划表 work_schdule 表中计划
timeNow := time.Now().Format("2006-01-02")
// 根据 work_id 查询所有的计划
var workSchedules []entity.WorkSchedule
err = dao.WorkSchedule.Ctx(ctx).Where(dao.WorkSchedule.Columns().WorkId, status.WorkId).Scan(&workSchedules)
liberr.ErrIsNil(ctx, err, "查询 work_schedule 表失败")
// 会出现多个计划的情况下,需要根据计划的起始和结束时间来确定具体的计划
for _, schedule := range workSchedules {
startAt := schedule.StartAt.Format("Y-m-d")
endAt := schedule.EndAt.Format("Y-m-d")
if timeNow >= startAt && timeNow <= endAt {
// 反序列化 Detail
var details []struct {
Date string `json:"date"`
PlanNum int `json:"planNum"`
FinishedNum int `json:"finishedNum"` // 手动填充的完成数量
AutoFill int `json:"autoFill"` // 是否自动填充
}
if err := json.Unmarshal([]byte(schedule.Detail), &details); err != nil {
continue
}
// 遍历 details找到当天的计划
for i := range details {
if details[i].Date == timeNow {
// 为自动填充的数量加一
details[i].AutoFill++
}
}
// 序列化 details
detailBytes, err := json.Marshal(details)
if err != nil {
continue
}
// 更新 work_schedule 表
_, err = dao.WorkSchedule.Ctx(ctx).Where(dao.WorkSchedule.Columns().WorkId, status.WorkId).
Where(dao.WorkSchedule.Columns().Id, schedule.Id).
Data(g.Map{
dao.WorkSchedule.Columns().Detail: string(detailBytes),
}).Update()
liberr.ErrIsNil(ctx, err, "更新 work_schedule 表失败")
}
}
}
})
}
// parseAIResult 解析识别的结果,提取出光伏板、支架和立柱的中心点
// 该函数解析的 JSON 格式如下:
//
// {
// "targets": [
// {
// "type": "pho", // 光伏板
// "size": [100, 100],
// "leftTopPoint": [10, 10]
// }
// ]
// }
func ParseAIResul2(fileContent string) *AIResult {
// 钻孔、桩基、支架、光伏板
var solarPanels, brackets, pillars, holes []XYPosition
gjson.Get(fileContent, "targets").ForEach(func(_, target gjson.Result) bool {
targetType := target.Get("type").String()
if targetType != solarPanel && targetType != bracket && targetType != pillar && targetType != drillingHole {
return true
}
size := lo.FilterMap(target.Get("size").Array(), func(size gjson.Result, _ int) (int, bool) {
return int(size.Int()), true
})
leftTopPoint := lo.FilterMap(target.Get("leftTopPoint").Array(), func(point gjson.Result, _ int) (int, bool) {
return int(point.Int()), true
})
// 获取中心点
centroidX := float64(leftTopPoint[0]) + float64(size[0])/2
centroidY := float64(leftTopPoint[1]) + float64(size[1])/2
position := XYPosition{X: centroidX, Y: centroidY}
switch targetType {
case solarPanel:
solarPanels = append(solarPanels, position)
case bracket:
brackets = append(brackets, position)
case pillar:
pillars = append(pillars, position)
case drillingHole:
holes = append(holes, position)
}
return true
})
marshal1, _ := json.Marshal(solarPanels)
marshal2, _ := json.Marshal(brackets)
marshal3, _ := json.Marshal(pillars)
marshal4, _ := json.Marshal(holes)
fmt.Println("光伏板的坐标点", string(marshal1))
fmt.Println("支架的坐标点", string(marshal2))
fmt.Println("立柱的坐标点", string(marshal3))
fmt.Println("钻孔的坐标点", string(marshal4))
return &AIResult{
SolarPanels: solarPanels,
Brackets: brackets,
Pillars: pillars,
Holes: holes,
}
}
// getSolarPanelCenters 获取数据库中光伏板的中心点
func getSolarPanelCenters(projectID string) ([]Point, error) {
// 获取所有的子项目
subProjectQuery := g.Model("sub_project").Fields("id").Where("project_id", projectID)
var pvModules []entity.PvModule
if err := dao.PvModule.Ctx(context.Background()).
WhereIn(dao.PvModule.Columns().SubProjectid, subProjectQuery).
Where(dao.PvModule.Columns().Type, 15).
WhereNot(dao.PvModule.Columns().Status, 2).
WhereNot(dao.PvModule.Columns().Status, 3).
Scan(&pvModules); err != nil {
return nil, err
}
var points []Point
// tojson
dd, _ := json.Marshal(pvModules)
gjson.Parse(string(dd)).ForEach(func(_, record gjson.Result) bool {
record.Get("detail").ForEach(func(_, detail gjson.Result) bool {
id := record.Get("id").Int()
var longitudeAndLatitude []XYPosition
gjson.Get(detail.String(), "positions").ForEach(func(_, position gjson.Result) bool {
longitude := position.Get("lng").Float()
latitude := position.Get("lat").Float()
longitudeAndLatitude = append(longitudeAndLatitude, XYPosition{X: longitude, Y: latitude})
return true
})
points = append(points, Point{ID: int(id), Points: longitudeAndLatitude})
return true
})
return true
})
return points, nil
}
func getPillars(projectID string) map[string]map[string]GeoPoint {
ctx := context.Background()
// 获取所有的子项目
subProjectQuery := g.Model("sub_project").Fields("id").Where("project_id", projectID)
// 查询 qianqi_guangfuban_ids_zhuangdian 表
var zhuangdians []entity.QianqiGuangfubanIdsZhuangdian
if err := dao.QianqiGuangfubanIdsZhuangdian.Ctx(ctx).
Fields("id,fangzhen_id,detail,name").
WhereIn(dao.QianqiGuangfubanIdsZhuangdian.Columns().SubProjectid, subProjectQuery).
Scan(&zhuangdians); err != nil {
panic(err)
}
// 第一层方阵ID, 第二层立柱名称, 第三层立柱信息
realGeoPoint := make(map[string]map[string]GeoPoint)
for _, zhuangdian := range zhuangdians {
fangzhenID := zhuangdian.FangzhenId
pillarName := zhuangdian.Name
var coordinates struct {
Position struct {
Lng float64 `json:"lng"`
Lat float64 `json:"lat"`
Alt float64 `json:"alt"`
}
}
if err := json.Unmarshal([]byte(zhuangdian.Detail), &coordinates); err != nil {
continue
}
if _, ok := realGeoPoint[fangzhenID]; !ok {
realGeoPoint[fangzhenID] = make(map[string]GeoPoint)
}
realGeoPoint[fangzhenID][pillarName] = GeoPoint{
ID: zhuangdian.Id,
Coordinates: Coordinates{
Lng: coordinates.Position.Lng,
Lat: coordinates.Position.Lat,
Alt: coordinates.Position.Alt,
},
}
}
return realGeoPoint
}
// UpdateTableData 更新表中的数据
func UpdateTableData(task_id int) error {
ctx := context.Background()
// 根据任务ID查询出所有的 PV Module ID
var pvlist []struct {
PvID int `orm:"pv_id"`
FangZhenID int `orm:"fangzhen_id"`
WorkID string `orm:"work_id"`
TypeID int `orm:"type"`
}
// 查询出基本信息
err := dao.PvModule.Ctx(ctx).As("pv").Fields("pv.id as pv_id", "pv.fangzhen_id", "pv.work_id", "pv.type").
LeftJoin("manage_task_result as mts", "pv.id = mts.pv_id").
Where("mts.task_id = ?", task_id).Scan(&pvlist)
if err != nil {
return fmt.Errorf("查询 PV Module 表失败: %w", err)
}
if len(pvlist) == 0 {
return nil
}
return g.Try(ctx, func(ctx context.Context) {
for _, v := range pvlist {
if v.TypeID == 15 { // 光伏板
if err := ProcessMatchedRectangles([]int{v.PvID}); err != nil {
return
}
continue
}
// 更新 PV Module 表
lastID, err := dao.PvModule.Ctx(ctx).Where(dao.PvModule.Columns().Id, v.PvID).
Where(dao.PvModule.Columns().FangzhenId, v.FangZhenID).
Where(dao.PvModule.Columns().Type, v.TypeID).
Data(g.Map{
dao.PvModule.Columns().Status: 2,
}).Update()
liberr.ErrIsNil(ctx, err)
rows, err := lastID.RowsAffected()
liberr.ErrIsNil(ctx, err)
if rows > 0 {
// 更新 work_status 表
_, err := dao.WorkStatus.Ctx(ctx).Where(dao.WorkStatus.Columns().FangzhenId, v.FangZhenID).
Where(dao.WorkStatus.Columns().Type, v.TypeID).
Data(g.Map{
dao.WorkStatus.Columns().Finished: gdb.Raw(
"finished + 1",
),
}).Update()
liberr.ErrIsNil(ctx, err)
UpdateWorkSchedule(v.WorkID)
}
}
})
}

176
third/thirdParty/tiandon.go vendored Normal file
View File

@ -0,0 +1,176 @@
// Package thirdParty
// @Author 铁憨憨[cory] 2025/7/4 14:06:00
package thirdParty
import (
"bytes"
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/gogf/gf/v2/frame/g"
"github.com/tiger1103/gfast-cache/cache"
commonService "github.com/tiger1103/gfast/v3/internal/app/common/service"
"github.com/tiger1103/gfast/v3/library/liberr"
"net/http"
"time"
)
/*
第三方接口:
1、获取token
2、推送第三方数据
3、获取第三方数据
*/
const publicKeyBase64 = `MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCoaejbjbttHyHuEzHL8lIX5GZZ6zIYrqJpEDlPM4V5LHn19rSAYp2FyAr8y5Ctny9uUdaYbkoFiVQgxWrAYo4X/3O0OFDsowE25FMOLQY0Mn5B6CvVR7Sdt3DqzIzM1tUnJCIbVGNfDMgxLrLwFN8RvOW8MPlB6LgOvlGMDbj+OQIDAQAB`
type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
type LoginResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Token string `json:"token"`
}
// CheckIfThereAreTokens redis中获取token没有就获取
func CheckIfThereAreTokens(ctx context.Context) (token string, err error) {
key := "thirdParty:tiandon:token"
prefix := g.Cfg().MustGet(ctx, "system.cache.prefix").String()
gfCache := cache.New(prefix)
get, err := g.Redis().Get(ctx, gfCache.CachePrefix+key)
if err != nil && get.String() != "" {
token = get.String()
return token, err
} else {
token, err = getTheToken(ctx)
//过期时间为30分钟1800秒我这里少300秒防止过期
commonService.Cache().Set(ctx, key, token, time.Duration(1500)*time.Second)
return token, err
}
}
// getTheToken token获取
func getTheToken(ctx context.Context) (token string, err error) {
err = g.Try(ctx, func(ctx g.Ctx) {
// 1、构造加密密码
encryptedPassword, err := rsaEncryptWithBase64PublicKey([]byte("Hkrsoft@#2023"), publicKeyBase64)
if err != nil {
liberr.ErrIsNil(ctx, err, "密码加密失败")
}
// 2、构造 JSON 请求体
data, _ := json.Marshal(LoginRequest{
Username: "zhangweiwei",
Password: encryptedPassword,
})
// 3、构造 HTTP 请求
req, _ := http.NewRequest("POST", "https://claritypm.powerchina.cn/neSmartsite-api/loginCli", bytes.NewReader(data))
req.Header.Set("Content-Type", "application/json; charset=UTF-8")
req.Header.Set("User-Agent", "Mozilla/5.0")
req.Header.Set("Accept", "application/json")
req.Header.Set("Origin", "https://claritypm.powerchina.cn")
req.Header.Set("Referer", "https://claritypm.powerchina.cn/")
// 4、发送请求
resp, err := http.DefaultClient.Do(req)
if err != nil {
liberr.ErrIsNil(ctx, err, "推送第三方数据失败")
}
defer resp.Body.Close()
// 5、解析响应体
var result LoginResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
liberr.ErrIsNil(ctx, err, "解析第三方数据失败")
}
token = result.Token
})
return
}
// rsaEncryptWithBase64PublicKey RSA加密函数
func rsaEncryptWithBase64PublicKey(data []byte, base64PubKey string) (string, error) {
decoded, err := base64.StdEncoding.DecodeString(base64PubKey)
if err != nil {
return "", err
}
pubInterface, err := x509.ParsePKIXPublicKey(decoded)
if err != nil {
return "", err
}
pub := pubInterface.(*rsa.PublicKey)
encrypted, err := rsa.EncryptPKCS1v15(rand.Reader, pub, data)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(encrypted), nil
}
type RealUser struct {
UserName string `json:"userName"`
ClassManagerFlag string `json:"classManagerFlag,omitempty"`
Phone string `json:"phone"`
Sex string `json:"sex"`
CardType string `json:"cardType"`
CardNumber string `json:"cardNumber"`
UserType string `json:"userType"`
CardDept string `json:"cardDept,omitempty"`
Nation string `json:"nation,omitempty"`
Birthday string `json:"birthday,omitempty"` // 格式2025-01-01 00:00:00
Address string `json:"address,omitempty"`
Avatar string `json:"avatar,omitempty"` // 限一张 http 地址
Pic string `json:"pic,omitempty"` // 限一张 http 地址
SafetyEdu string `json:"safetyEdu,omitempty"` // 限一张 http 地址
Technology string `json:"technology,omitempty"` // 限一张 http 地址
CardStartTime string `json:"cardStartTime,omitempty"` // 格式2025-01-01 00:00:00
CardEndTime string `json:"cardEndTime,omitempty"`
StudyLevel string `json:"studyLevel,omitempty"`
PoliticsType string `json:"politicsType,omitempty"`
InsuranceFlag string `json:"insuranceFlag,omitempty"`
DiseaseFlag string `json:"diseaseFlag,omitempty"`
LaborContractFlag string `json:"laborContractFlag,omitempty"`
Contacts string `json:"contacts,omitempty"`
ContactsPhone string `json:"contactsPhone,omitempty"`
MarryRemark string `json:"marryRemark,omitempty"`
}
// SendRy 发送工人信息
func SendRy(ctx context.Context, user []*RealUser) (err error) {
err = g.Try(ctx, func(ctx context.Context) {
//1、获取token
token, err := CheckIfThereAreTokens(ctx)
liberr.ErrIsNil(ctx, err, "获取token失败")
// 2、创建请求
jsonData, _ := json.Marshal(user)
fmt.Println(":::::::::::::::::::::: " + string(jsonData))
req, err := http.NewRequest("POST",
"https://claritypm.powerchina.cn/neSmartsite-api/realUser/tiandong/insertRealUser",
bytes.NewReader(jsonData),
)
liberr.ErrIsNil(ctx, err, "创建客户端失败")
// 3、设置请求头
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("User-Agent", "Mozilla/5.0")
req.Header.Set("Accept", "application/json")
// 4、发送请求
resp, err := http.DefaultClient.Do(req)
if err != nil {
liberr.ErrIsNil(ctx, err, "发送数据到第三方失败")
return
}
defer resp.Body.Close()
var result LoginResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
liberr.ErrIsNil(ctx, err, "解析第三方数据失败")
}
fmt.Println("l111响应状态码:", result.Code)
fmt.Println("c222响应内容:", result.Msg)
})
return err
}

246
third/todo/todo.go Normal file
View File

@ -0,0 +1,246 @@
package todo
import (
"context"
"fmt"
"strings"
"github.com/google/uuid"
"github.com/samber/lo"
"github.com/tiger1103/gfast/v3/internal/app/system/model/do"
"github.com/tiger1103/gfast/v3/third/reminders"
"github.com/gogf/gf/v2/frame/g"
"github.com/tiger1103/gfast/v3/internal/app/system/dao"
"github.com/tiger1103/gfast/v3/library/liberr"
)
type TaskGroup struct {
Role int // 0 管理员 1 施工人员
TsakType int // 补卡提醒
ProjectID int // 项目 ID
UserID string // sys.user.id 或 wx.openid
MissingCardTime string // 缺卡时间
}
// 考勤审批 struct
type ReissueReminder struct {
UserID string // sys.user.id 或 wx.openid
Role int // 0 管理员 1 施工人员
ProjectID int // 项目 ID
CreatorID string // 创建人 ID
TargetID int // 对应跳转的主键ID
}
func CreateCardReminderTasks(todoList []TaskGroup) error {
// 获取 OpenID 对应的 sys_user.id
openIDToUserIDMap := reminders.GetUserIDByOpenIDs( // 2. 获取所有的 openid 对应的 sys_user.id
lo.FilterMap(todoList, func(item TaskGroup, _ int) (string, bool) { // 1. 筛选出所有的 openid
if reminders.HasLetter(item.UserID) {
return item.UserID, true
}
return item.UserID, false
}),
)
// 替换 todoList 中的 userID
for i := range todoList {
if newUserID, ok := openIDToUserIDMap[todoList[i].UserID]; ok {
todoList[i].UserID = newUserID
}
}
// 同一组的消息拥有相同的 UUID
groupUUID := uuid.New().String()
todoTasks := lo.Map(todoList, func(item TaskGroup, _ int) do.TodoTasks {
return do.TodoTasks{
TaskType: item.TsakType, // 补卡提醒
ProjectId: item.ProjectID, // 项目 ID
UserId: item.UserID, // 消息接收者的 sys_User.Id
Role: item.Role,
MissingCardTime: item.MissingCardTime, // 缺卡时间 示例:`03月14日 08:30上班卡18:00下班卡`
Uuid: groupUUID,
}
})
_, err := dao.TodoTasks.Ctx(context.Background()).Insert(todoTasks)
return err
}
// CreateMissingCardReminder 创建并发布一个补卡提醒
// userID: 消息接收者的 sys_user.id
// role: 0:普通员工 1:管理员
// projectID: 项目 ID
// missingCardTime: 缺卡时间见设计图
func CreateMissingCardReminder(userID string, role int, projectID int, missingCardTime string, orderId int64) error {
if reminders.HasLetter(userID) {
userID = reminders.GetUserIDByOpenID(userID)
if userID == "" {
return fmt.Errorf("未找到对应的用户")
}
}
if strings.Contains(userID, "_") {
return fmt.Errorf("未找到对应的用户")
}
// 构建消息模板
task := do.TodoTasks{
TaskType: Reissue, // 补卡提醒
ProjectId: projectID, // 项目 ID
UserId: userID, // 消息接收者的 sys_User.Id
Role: role,
OrderId: orderId,
MissingCardTime: missingCardTime, // 缺卡时间 示例:`03月14日 08:30上班卡18:00下班卡`
}
// 发布提醒
_, err := dao.TodoTasks.Ctx(context.Background()).Insert(&task)
return err
}
// CreateAttendanceApprovalReminder 发布一条考勤审批提醒
// projectID: 项目 ID
// userID: 消息接收者的 sys_user.id
// creatorID创建人 ID
// targetID: 对应跳转的主键ID
func CreateAttendanceApprovalReminder(userID string, projectID int, creatorID string, targetID int) error {
if reminders.HasLetter(creatorID) || reminders.HasLetter(userID) {
if creatorID == userID {
creatorID = reminders.GetUserIDByOpenID(creatorID)
userID = creatorID
} else {
if reminders.HasLetter(creatorID) {
creatorID = reminders.GetUserIDByOpenID(creatorID)
}
if reminders.HasLetter(userID) {
userID = reminders.GetUserIDByOpenID(userID)
}
}
if creatorID == "" || userID == "" {
return fmt.Errorf("未找到对应的用户")
}
}
// 构建消息模板
task := do.TodoTasks{
TaskType: AttendanceApproval, // 考勤审批提醒
ProjectId: projectID, // 项目 ID
UserId: userID, // 消息接收者的 sys_User.Id
Applicant: creatorID, // 创建人ID
OrderId: targetID, // 对应跳转的主键ID
Status: 0,
}
// 发布提醒
_, err := dao.TodoTasks.Ctx(context.Background()).Insert(&task)
return err
}
// CreateAskForLeaveReminder 发布一条请假审批提醒
// userID: 消息接收者的 sys_user.id
// projectID: 项目 ID
// creatorID创建人 ID
// targetID: 对应跳转的主键ID
func CreateAskForLeaveReminder(userID string, projectID int, creatorID string, targetID int) error {
if reminders.HasLetter(creatorID) || reminders.HasLetter(userID) {
if creatorID == userID {
creatorID = reminders.GetUserIDByOpenID(creatorID)
userID = creatorID
} else {
if reminders.HasLetter(creatorID) {
creatorID = reminders.GetUserIDByOpenID(creatorID)
}
if reminders.HasLetter(userID) {
userID = reminders.GetUserIDByOpenID(userID)
}
}
if creatorID == "" || userID == "" {
return fmt.Errorf("未找到对应的用户")
}
}
// 构建消息模板
task := do.TodoTasks{
TaskType: ApprovalReminder, // 请假审批提醒
UserId: userID, // 消息接收者的 sys_User.Id
ProjectId: projectID, // 项目 ID
Applicant: creatorID, // 创建人ID
OrderId: targetID, // 对应跳转的主键ID
Status: 0, // 未处理
Role: "1", // 发送给施工人员
}
// 发布提醒
_, err := dao.TodoTasks.Ctx(context.Background()).Insert(&task)
return err
}
func CreateAttendanceApprovalGroup(list []ReissueReminder, num int) error {
UserIDs := lo.FilterMap(list, func(item ReissueReminder, _ int) (string, bool) {
if reminders.HasLetter(item.UserID) {
return item.UserID, true
}
return item.UserID, false
})
CreatorIDs := lo.FilterMap(list, func(item ReissueReminder, _ int) (string, bool) {
if reminders.HasLetter(item.CreatorID) {
return item.CreatorID, true
}
return item.CreatorID, false
})
// 获取 OpenID 对应的 sys_user.id
openIDToUserIDMap := reminders.GetUserIDByOpenIDs(append(UserIDs, CreatorIDs...))
// 替换 todoList 中的 userID
for i := range list {
if newUserID, ok := openIDToUserIDMap[list[i].UserID]; ok {
list[i].UserID = newUserID
}
if newUserID, ok := openIDToUserIDMap[list[i].CreatorID]; ok {
list[i].CreatorID = newUserID
}
}
// 同一组的消息拥有相同的 UUID
groupUUID := uuid.New().String()
todoTasks := lo.Map(list, func(item ReissueReminder, _ int) do.TodoTasks {
return do.TodoTasks{
TaskType: num, // 考勤审批提醒
ProjectId: item.ProjectID, // 项目 ID
UserId: item.UserID, // 消息接收者的 sys_User.Id
Applicant: item.CreatorID, // 创建人ID
OrderId: item.TargetID, // 对应跳转的主键ID
Role: item.Role,
Uuid: groupUUID,
}
})
_, err := dao.TodoTasks.Ctx(context.Background()).Insert(todoTasks)
return err
}
// MarkTaskAsProcessed 标记特定的待办任务为已处理
// 每个待办任务都与一个 OrderID 关联,可以通过 OrderID 查找任务详情
// 用户完成补卡或审批操作后,应将相应的待办任务标记为已处理
func MarkTaskAsProcessed(taskID int) error {
return g.Try(context.Background(), func(ctx context.Context) {
_, err := dao.TodoTasks.Ctx(ctx).Where("order_id", taskID).Update(g.Map{"status": 1})
liberr.ErrIsNil(ctx, err, "标记代办任务为已处理失败")
})
}
// 根据 uuid 将一组待办任务标记为已处理
func MarkTasksAsProcessedByUUID(id int64) error {
return g.Try(context.Background(), func(ctx context.Context) {
value, err2 := dao.TodoTasks.Ctx(ctx).Where("order_id", id).Where("status", 0).Limit(1).Fields("uuid").Value()
liberr.ErrIsNil(ctx, err2)
_, err := dao.TodoTasks.Ctx(ctx).Where("uuid", value.String()).Update(g.Map{"status": 1})
liberr.ErrIsNil(ctx, err, "标记代办任务为已处理失败")
})
}

9
third/todo/todo_var.go Normal file
View File

@ -0,0 +1,9 @@
package todo
type TodoType = int
const (
Reissue TodoType = iota // 补卡
AttendanceApproval // 考勤审批
ApprovalReminder // 请假审批
)

94
third/ws/business.go Normal file
View File

@ -0,0 +1,94 @@
// Package ws
// @Author 铁憨憨[cory] 2025/2/12 15:45:00
package ws
/*
TheSenderInformationOfTheAssemblyPersonnel 组装下发人员信息
*/
func TheSenderInformationOfTheAssemblyPersonnel(sn string, userId string, name string, face string) (err error) {
sUuid := GenerateUUIDWithSixRandomDigits()
people := PeopleInformation{
Cmd: "to_device",
From: sUuid,
To: sn,
Data: PeopleInData{
Cmd: "addUser",
UserID: userId,
Name: name,
FaceTemplate: face,
IDValid: "",
},
}
_, err = SendRequestAndWaitResponse(sn, sUuid, people)
return
}
/*
SelectUserAll 获取打卡设备所有人员
*/
func SelectUserAll(sn string) (CommonResponse, error) {
sUuid := GenerateUUIDWithSixRandomDigits()
people := PersonnelInformationAcquisition{
Cmd: "to_device",
From: sUuid,
To: sn,
Data: PersonnelInformationAcquisitionTwo{
Cmd: "getUserInfo",
Value: 1,
},
}
return SendRequestAndWaitResponse(sn, sUuid, people)
}
/*
DelByUserId 删除指定人员
*/
func DelByUserId(sn string, userId string) (CommonResponse, error) {
sUuid := GenerateUUIDWithSixRandomDigits()
people := DeletionOfPersonnel{
Cmd: "to_device",
From: sUuid,
To: sn,
Data: DeletionOfPersonnelData{
Cmd: "delUser",
UserID: userId,
UserType: 0,
},
}
return SendRequestAndWaitResponse(sn, sUuid, people)
}
/*
BatchDelete 批量删除指定人员
*/
func BatchDelete(sn string, userIds []string) (CommonResponse, error) {
sUuid := GenerateUUIDWithSixRandomDigits()
people := BatchDeletion{
Cmd: "to_device",
From: sUuid,
To: sn,
Data: BatchDeletionData{
Cmd: "delMultiUser",
UserIds: userIds,
UserType: 0,
},
}
return SendRequestAndWaitResponse(sn, sUuid, people)
}
/*
DelAll 删除指定考勤机全部人员
*/
func DelAll(sn string) (CommonResponse, error) {
sUuid := GenerateUUIDWithSixRandomDigits()
people := DeletionALlOfPersonnel{
Cmd: "to_device",
From: sUuid,
To: sn,
Data: DeletionALlOfPersonnelData{
Cmd: "delAllUser",
UserType: 0,
},
}
return SendRequestAndWaitResponse(sn, sUuid, people)
}

167
third/ws/construction.go Normal file
View File

@ -0,0 +1,167 @@
// Package ws
// @Author 铁憨憨[cory] 2025/2/12 15:20:00
package ws
import "github.com/gorilla/websocket"
/*
============================================常量============================================
============================================常量============================================
============================================常量============================================
*/
const (
DECLARE = "declare" //设备初上线
PING = "ping" //心跳
ToClient = "to_client" // 服务器消息下发到客户端的响应
)
/*
============================================公共============================================
============================================公共============================================
============================================公共============================================
*/
// DeviceInfo 存储连接设备信息
type DeviceInfo struct {
IP string `json:"ip"`
Port string `json:"port"`
Conn *websocket.Conn `json:"conn"`
}
// GenericMessage 通用消息结构,用于解析初始的 cmd 字段
type GenericMessage struct {
CMD string `json:"cmd"`
}
// CommonResponse 公共响应结构
type CommonResponse struct {
Cmd string `json:"cmd"`
From string `json:"from"`
To string `json:"to"`
//Extra string `json:"extra"`
Data CommonResponseData `json:"data"`
}
type CommonResponseData struct {
Cmd string `json:"cmd"`
UserIds []string `json:"userIds" dc:"用户IDS"`
UserID string `json:"user_id"`
Code int `json:"code"`
Msg string `json:"msg"`
DelFailed []DelMultiUserData `json:"delFailed"`
}
type DelMultiUserData struct {
UserID string `json:"user_id"`
Code int `json:"code"`
Msg string `json:"msg"`
}
/*
============================================基础============================================
============================================基础============================================
============================================基础============================================
*/
// DeclareMessage 设备上线消息
type DeclareMessage struct {
Cmd string `json:"cmd"`
Type string `json:"type"`
SN string `json:"sn"`
VersionCode string `json:"version_code"`
VersionName string `json:"version_name"`
Token string `json:"token"`
Ip string `json:"ip"`
Timestamp float64 `json:"timestamp"`
}
type PongMessage struct {
Cmd string `json:"cmd"`
From string `json:"from"`
To string `json:"to"`
Data PongMessageData `json:"data"`
}
// PongMessageData 心跳回复消息结构
type PongMessageData struct {
Cmd string `json:"cmd"`
}
/*
============================================ws请求============================================
============================================ws请求============================================
============================================ws请求============================================
*/
// PeopleInformation 人员信息下发
type PeopleInformation struct {
Cmd string `json:"cmd"` //该接口固定为to_device
From string `json:"from"` //可不填写,填写uuid来做为发送请求或响应的标识
To string `json:"to"` //设备号(请查看公共设置中的设备号)
//Extra string `json:"extra"` //给服务端预留的补充字段,设备端不处理这个字段内容。设备在响应这条指令时原样返回
Data PeopleInData `json:"data"`
}
type PeopleInData struct {
Cmd string `json:"cmd"`
UserID string `json:"user_id"`
Name string `json:"name"`
FaceTemplate string `json:"face_template"` // http 链接图
IDValid string `json:"id_valid"` // 人员有效期人员在这个时间点后无法通行格式yyyy-MM-dd 或者 yyyy-MM-dd HH:mm为 “” 则为永久
}
// DeletionOfPersonnel 删除人员
type DeletionOfPersonnel struct {
Cmd string `json:"cmd"`
From string `json:"from"`
To string `json:"to"`
Data DeletionOfPersonnelData `json:"data"`
}
type DeletionOfPersonnelData struct {
Cmd string `json:"cmd"`
UserID string `json:"user_id"`
UserType int `json:"user_type"` // 删除的用户类型0-人脸接口下发的数据 1-人证比对接口下发的数据
}
// BatchDeletion 批量删除
type BatchDeletion struct {
Cmd string `json:"cmd"`
From string `json:"from"`
To string `json:"to"`
Data BatchDeletionData `json:"data"`
}
type BatchDeletionData struct {
Cmd string `json:"cmd"`
UserIds []string `json:"user_ids"`
UserType int `json:"user_type"` // 删除的用户类型0-人脸接口下发的数据 1-人证比对接口下发的数据
}
// DeletionALlOfPersonnel 删除全部人员
type DeletionALlOfPersonnel struct {
Cmd string `json:"cmd"`
From string `json:"from"`
To string `json:"to"`
Data DeletionALlOfPersonnelData `json:"data"`
}
type DeletionALlOfPersonnelData struct {
Cmd string `json:"cmd"`
UserType int `json:"user_type"` // 删除的用户类型0-人脸接口下发的数据 1-人证比对接口下发的数据
}
// PersonnelInformationAcquisition 人员信息获取
type PersonnelInformationAcquisition struct {
Cmd string `json:"cmd"` //该接口固定为to_device
From string `json:"from"` //可不填写,填写uuid来做为发送请求或响应的标识
To string `json:"to"` //设备号(请查看公共设置中的设备号)
Data PersonnelInformationAcquisitionTwo `json:"data"`
}
type PersonnelInformationAcquisitionTwo struct {
Cmd string `json:"cmd"`
Value int `json:"value"`
}
/*
============================================http请求============================================
============================================http请求============================================
============================================http请求============================================
*/
//loggerMiddlewareloggerMiddlewareloggerMiddleware

265
third/ws/ws.go Normal file
View File

@ -0,0 +1,265 @@
// Package ws
// @Author 铁憨憨[cory] 2025/2/12 15:18:00
package ws
import (
"encoding/json"
"fmt"
"github.com/gogf/gf/v2/os/gctx"
"github.com/google/uuid"
"github.com/gorilla/websocket"
"github.com/tiger1103/gfast/v3/api/v1/system"
"github.com/tiger1103/gfast/v3/internal/app/system/service"
"golang.org/x/net/context"
"log"
"math/rand"
"net/http"
"strconv"
"sync"
"time"
)
// 存储所有连接的设备信息
var connectedDevices = make(map[string]*DeviceInfo)
// 存储每个 uuid 对应的通道
var responseChannels = make(map[string]chan CommonResponse)
var responseChannelsMutex sync.Mutex
// HandleWebSocket 处理WebSocket连接的函数
func HandleWebSocket(w http.ResponseWriter, r *http.Request) {
ctx := gctx.New()
// 将HTTP连接升级为WebSocket连接
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("WebSocket升级失败:", err)
return
}
defer conn.Close()
// 读取设备发送的第一条消息,从中提取 SN然后添加到设备列表中
declare, err := addDevice(ctx, conn, r)
if err != nil {
log.Println("添加设备信息时出错:", err)
return
}
// 持续读取从客户端(其他服务器)发送过来的数据
for {
// 读取消息类型和消息内容
messageType, message, err := conn.ReadMessage()
if err != nil {
log.Println("读取消息时出错:", err)
// 移除设备信息
delete(connectedDevices, declare.SN)
log.Printf("设备断开连接,设备信息: %+v", declare)
err = service.BusAttendanceMachine().Change(ctx, &system.BusAttendanceMachineChangeReq{
Sn: declare.SN,
Status: "0",
})
log.Println("修改考勤设备状态时出错:", err)
break
}
// 先解析出 cmd 字段
var genericMsg GenericMessage
err = json.Unmarshal(message, &genericMsg)
if err != nil {
log.Println("解析 cmd 字段时出错:", err)
continue
}
// 根据 cmd 字段进行不同处理
switch genericMsg.CMD {
case DECLARE:
log.Println("设备在线:", declare)
case PING:
declareMessage := DeclareMessage{}
if json.Unmarshal(message, &declareMessage) != nil {
return
}
if err = handlePing(ctx, conn, r, declareMessage.SN); err != nil {
log.Println("处理心跳回复:", err)
}
case ToClient:
if err := requestResponse(message); err != nil {
log.Println("处理响应:", err)
}
default:
log.Printf("收到未知消息---> 类型: %d, 消息内容: %s", messageType, string(message))
}
}
// 清理通道资源
responseChannelsMutex.Lock()
for key, ch := range responseChannels {
if connectedDevices[declare.SN] != nil {
delete(responseChannels, key)
close(ch)
}
}
responseChannelsMutex.Unlock()
}
// addDevice 将设备信息添加到设备列表中
func addDevice(ctx context.Context, conn *websocket.Conn, r *http.Request) (declareMessage DeclareMessage, err error) {
// 读取设备发送的第一条消息,从中提取 SN
_, message, err := conn.ReadMessage()
if err != nil {
return
}
declareMessage = DeclareMessage{}
if json.Unmarshal(message, &declareMessage) != nil {
return
}
// 假设设备信息可以从请求中获取,这里简单从 RemoteAddr 解析
ip, port := parseRemoteAddr(r.RemoteAddr)
deviceInfo := &DeviceInfo{
IP: ip,
Port: port,
Conn: conn,
}
if declareMessage.SN != "" {
// 存储设备信息
connectedDevices[declareMessage.SN] = deviceInfo
log.Printf("新设备连接,设备信息: %+v", declareMessage)
// 变更状态
err = service.BusAttendanceMachine().Register(ctx, &system.BusAttendanceMachineRegisterReq{
Sn: declareMessage.SN,
})
if err != nil {
return
}
}
return declareMessage, nil
}
// handlePing 处理 ping 消息
func handlePing(ctx context.Context, conn *websocket.Conn, r *http.Request, sn string) (err error) {
pongMsg := PongMessageData{
Cmd: "pong",
}
pongJSON, _ := json.Marshal(pongMsg)
err = conn.WriteMessage(websocket.TextMessage, pongJSON)
if err != nil {
log.Println("发送 Pong 消息时出错:", err)
return err
}
// 存储设备信息、变更状态
connectedDevices[sn] = &DeviceInfo{
Conn: conn,
}
err = service.BusAttendanceMachine().Register(ctx, &system.BusAttendanceMachineRegisterReq{
Sn: sn,
})
return nil
}
// requestResponse 处理请求得到的响应
func requestResponse(message []byte) (err error) {
log.Println("请求响应:", string(message))
var common CommonResponse
err = json.Unmarshal(message, &common)
if err != nil {
log.Println("解析 cmd 字段时出错:", err)
return err
}
// 根据UUID查找对应的通道
responseChannelsMutex.Lock()
if ch, ok := responseChannels[common.To]; ok {
ch <- common
delete(responseChannels, common.To)
}
responseChannelsMutex.Unlock()
return nil
}
// 根据 SN 发送消息给对应的设备
func sendMessageToDevice(sn string, uuid string, message interface{}) error {
conn, exists := connectedDevices[sn]
if !exists {
return nil
}
msgJSON, err := json.Marshal(message)
if err != nil {
return err
}
err = conn.Conn.WriteMessage(websocket.TextMessage, msgJSON)
if err != nil {
responseChannelsMutex.Lock()
delete(responseChannels, uuid)
responseChannelsMutex.Unlock()
return err
}
return nil
}
func SendRequestAndWaitResponse(sn string, sUuid string, payload interface{}) (CommonResponse, error) {
responseChan := make(chan CommonResponse, 1)
// 存入全局映射
responseChannelsMutex.Lock()
responseChannels[sUuid] = responseChan
responseChannelsMutex.Unlock()
// 发送请求
err := sendMessageToDevice(sn, sUuid, payload)
if err != nil {
return CommonResponse{}, err
}
// 等待响应
select {
case resp := <-responseChan:
fmt.Printf("收到响应: %+v\n", resp)
return resp, nil
case <-time.After(10 * time.Second):
responseChannelsMutex.Lock()
delete(responseChannels, sUuid)
responseChannelsMutex.Unlock()
return CommonResponse{}, fmt.Errorf("等待响应超时")
}
}
/*
=========================================================业务无关=========================================================
=========================================================业务无关=========================================================
=========================================================业务无关=========================================================
*/
// 定义一个升级器用于将HTTP连接升级为WebSocket连接
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
// 允许跨域访问,这里设置为允许所有来源
CheckOrigin: func(r *http.Request) bool {
return true
},
}
// 解析远程地址,获取 IP 和端口
func parseRemoteAddr(addr string) (string, string) {
// 简单处理,假设地址格式为 IP:Port
for i := len(addr) - 1; i >= 0; i-- {
if addr[i] == ':' {
return addr[:i], addr[i+1:]
}
}
return "", ""
}
// GenerateUUIDWithSixRandomDigits 生成一个 UUID 并拼接 6 位随机数
func GenerateUUIDWithSixRandomDigits() string {
// 生成 UUID
uuidStr := uuid.New().String()
// 初始化随机数种子
rand.Seed(time.Now().UnixNano())
// 生成 6 位随机数
randomNum := rand.Intn(900000) + 100000
// 将随机数转换为字符串
randomNumStr := strconv.Itoa(randomNum)
// 拼接 UUID 和 6 位随机数
result := uuidStr + "-" + randomNumStr
return result
}

51
third/ys7/struct.go Normal file
View File

@ -0,0 +1,51 @@
package ys7
const OK = "200"
//type Public struct {
// Code string `json:"code"`
// Msg string `json:"msg"`
//}
type TP struct {
Public
Data struct {
AccessToken string `json:"accessToken"`
ExpireTime int64 `json:"expireTime"`
} `json:"data"`
}
type HelMet struct {
Public
RequestId string `json:"requestId"`
Data []struct {
Id string `json:"id"`
Height string `json:"height"`
TargetList []struct {
BodyRect struct {
VmodelHF string `json:"vmodel_h_f"`
VmodelWF string `json:"vmodel_w_f"`
VmodelXF string `json:"vmodel_x_f"`
VmodelYF string `json:"vmodel_y_f"`
} `json:"body_rect"`
AlarmFlg int `json:"alarm_flg"`
UniformType int `json:"uniform_type"`
ID int `json:"ID"`
HelmetType int `json:"helmet_type"`
HeadRect struct {
VmodelHF string `json:"vmodel_h_f"`
VmodelWF string `json:"vmodel_w_f"`
VmodelXF string `json:"vmodel_x_f"`
VmodelYF string `json:"vmodel_y_f"`
} `json:"head_rect"`
ColorType int `json:"color_type"`
} `json:"target_list"`
Width string `json:"width"`
RuleInfo []interface{} `json:"rule_info"`
RuleList [][]struct {
Y string `json:"y"`
X string `json:"x"`
} `json:"rule_list"`
ErrorCode int `json:"errorCode"`
} `json:"data"`
}

558
third/ys7/ys7.go Normal file
View File

@ -0,0 +1,558 @@
package ys7
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
"github.com/tiger1103/gfast/v3/internal/app/system/dao"
)
var accessToken = "at.ddhhz6ea49jorccubhg3elqs19nvmxta-1754nu5ce8-017egq8-fmymwvvhv"
const (
Ok = "200"
Online = 1
)
// 改变 accessToken
func SetAccessToken(token string) {
accessToken = token
}
// 传入 serial 返回 Devicemap 中的设备信息
func GetDevice(serial string) (Ys7Device, error) {
device, ok := deviceMap[serial]
if ok {
return device, nil
}
return Ys7Device{}, fmt.Errorf("设备不存在")
}
var deviceMap = make(map[string]Ys7Device)
/*当前包的入口方法*/
func InitYs7() {
/*获取一次token*/
getAccessToken()
/*获取一次设备列表*/
getDeviceList(0, 50) // 可以提前获取数据库中的记录进行缓存,比对当前设备是否在数据库中已经存在,不存在则自动入库
}
func GetAccessToken() string {
return accessToken
}
/*获取设备表*/
func GetDeviceMap() map[string]Ys7Device {
return deviceMap
}
type TokenRes struct {
Public
Data struct {
AccessToken string `json:"accessToken"`
ExpireTime int64 `json:"expireTime"`
} `json:"data"`
}
func getAccessToken() {
// ctx := gctx.New()
// appKey := g.Cfg().MustGet(ctx, "ys7.appKey").String()
// appSecret := g.Cfg().MustGet(ctx, "ys7.appSecret").String()
appKey := "3acf9f1a43dc4209841e0893003db0a2"
appSecret := "4bbf3e9394f55d3af6e3af27b2d3db36"
url := "https://open.ys7.com/api/lapp/token/get"
dt := struct {
AppKey string `json:"appKey"`
AppSecret string `json:"appSecret"`
}{AppKey: appKey, AppSecret: appSecret}
post, err := Post(url, dt)
if err != nil {
} else {
res := TokenRes{}
err = json.Unmarshal(post, &res)
if err != nil {
} else {
accessToken = res.Data.AccessToken
}
}
}
type DeviceListRes struct {
Public
Page struct {
Total int `json:"total"`
Page int `json:"page"`
Size int `json:"size"`
} `json:"page"`
Data []Ys7Device `json:"data"`
}
func delayRequest(page, pageSize int) {
time.Sleep(time.Second * 5)
getDeviceList(page, pageSize)
}
func getDeviceList(page, pageSize int) {
url := "https://open.ys7.com/api/lapp/device/list"
dt := struct {
AccessToken string `json:"accessToken"`
PageStart int `json:"pageStart"`
PageSize int `json:"pageSize"`
}{AccessToken: accessToken, PageSize: pageSize, PageStart: page}
post, err := Post(url, dt)
if err != nil {
delayRequest(page, pageSize)
} else {
res := DeviceListRes{}
err = json.Unmarshal(post, &res)
if err != nil {
delayRequest(page, pageSize)
} else {
if res.Code == Ok {
for _, datum := range res.Data {
deviceMap[datum.DeviceSerial] = datum
//marshal, err := json.Marshal(datum)
//if err != nil {
// return
//}
//fmt.Println(string(marshal))
//if datum.Online() {
// capRes, err := datum.Capture()
// if err != nil {
// fmt.Println("抓图识别")
// } else {
// SavePicture(capRes.Data.PicUrl, "static/"+gmd5.MustEncryptString(capRes.Data.PicUrl)+".jpg")
// }
//}
}
if (page+1)*pageSize < res.Page.Total {
getDeviceList(page+1, pageSize)
}
} else {
delayRequest(page, pageSize)
}
}
}
}
type Ys7Device struct {
DeviceSerial string `json:"deviceSerial"`
DeviceName string `json:"deviceName"`
DeviceType string `json:"deviceType"`
Status int `json:"status"`
Defence int `json:"defence"`
DeviceVersion string `json:"deviceVersion"`
}
type Public struct {
Code string `json:"code"`
Msg string `json:"msg"`
}
func (receiver *Ys7Device) Online() bool {
if receiver.Status == Online {
return true
} else {
return false
}
}
func Post(url string, data interface{}) (res []byte, err error) {
response, err := g.Client().Header(map[string]string{"Content-Type": "application/x-www-form-urlencoded"}).Post(gctx.New(), url, data)
if err != nil {
return nil, err
}
return response.ReadAll(), nil
}
/*开始云台控制*/
func (receiver *Ys7Device) PTZStart(direction int) (res *Public, err error) {
url := "https://open.ys7.com/api/lapp/device/ptz/start"
dt := struct {
ChannelNo int `json:"channelNo"`
Direction int `json:"direction"`
Speed int `json:"speed"`
DeviceSerial string `json:"deviceSerial"`
AccessToken string `json:"accessToken"`
}{
AccessToken: accessToken, DeviceSerial: receiver.DeviceSerial, ChannelNo: 1, Speed: 1, Direction: direction,
}
post, err := Post(url, dt)
if err != nil {
return nil, err
} else {
res = &Public{}
err = json.Unmarshal(post, &res)
if err != nil {
return nil, err
}
return
}
}
/*停止云台控制*/
func (receiver *Ys7Device) PTZEnd(direction int) (res *Public, err error) {
url := "https://open.ys7.com/api/lapp/device/ptz/stop"
dt := struct {
ChannelNo int `json:"channelNo"`
Direction int `json:"direction"`
DeviceSerial string `json:"deviceSerial"`
AccessToken string `json:"accessToken"`
}{
AccessToken: accessToken, DeviceSerial: receiver.DeviceSerial, ChannelNo: 1, Direction: direction,
}
post, err := Post(url, dt)
if err != nil {
return nil, err
} else {
res = &Public{}
err = json.Unmarshal(post, &res)
if err != nil {
return nil, err
}
return
}
}
type PresetAddRes struct {
Public
Data struct {
Index int `json:"index"` // 预置点序号C6设备是1-12该参数需要开发者自行保存
} `json:"data"`
}
/*添加预制点*/
func (receiver *Ys7Device) AddPreset() (res *PresetAddRes, err error) {
url := "https://open.ys7.com/api/lapp/device/preset/add"
dt := struct {
ChannelNo int `json:"channelNo"`
DeviceSerial string `json:"deviceSerial"`
AccessToken string `json:"accessToken"`
}{
AccessToken: accessToken, DeviceSerial: receiver.DeviceSerial, ChannelNo: 1,
}
post, err := Post(url, dt)
if err != nil {
return nil, err
} else {
res = &PresetAddRes{}
err = json.Unmarshal(post, &res)
if err != nil {
return nil, err
}
return
}
}
/*调用预制点*/
func (receiver *Ys7Device) MovePreset(index int) (res *Public, err error) {
url := "https://open.ys7.com/api/lapp/device/preset/move"
dt := struct {
ChannelNo int `json:"channelNo"`
DeviceSerial string `json:"deviceSerial"`
AccessToken string `json:"accessToken"`
Index int `json:"index"`
}{ChannelNo: 1, DeviceSerial: receiver.DeviceSerial, AccessToken: accessToken, Index: index}
post, err := Post(url, dt)
if err != nil {
return nil, err
} else {
res = &Public{}
err = json.Unmarshal(post, &res)
if err != nil {
return nil, err
}
return
}
}
/*清除预制点*/
func (receiver *Ys7Device) ClearPreset(index int) (res *Public, err error) {
url := "https://open.ys7.com/api/lapp/device/preset/clear"
dt := struct {
ChannelNo int `json:"channelNo"`
DeviceSerial string `json:"deviceSerial"`
AccessToken string `json:"accessToken"`
Index int `json:"index"`
}{ChannelNo: 1, DeviceSerial: receiver.DeviceSerial, AccessToken: accessToken, Index: index}
post, err := Post(url, dt)
if err != nil {
return nil, err
} else {
res = &Public{}
err = json.Unmarshal(post, &res)
if err != nil {
return nil, err
}
return
}
}
// 开启设备视频加密
func (r *Ys7Device) OpenEncrypt() error {
url := "https://open.ys7.com/api/lapp/device/encrypt/on"
dt := struct {
AccessToken string `json:"accessToken"`
DeviceSerial string `json:"deviceSerial"`
}{
AccessToken: accessToken,
DeviceSerial: r.DeviceSerial,
}
post, err := Post(url, dt)
if err != nil {
return err
}
if gjson.New(string(post)).Get("code").Int() != 200 {
return fmt.Errorf("%s", gjson.New(string(post)).Get("msg").String())
}
// 更新数据库
_, err = dao.Ys7Devices.Ctx(context.Background()).
Where(dao.Ys7Devices.Columns().DeviceSerial, r.DeviceSerial).
Update(g.Map{
"VideoEncrypted": 1,
})
if err != nil {
return err
}
return nil
}
// 关闭设备视频加密
func (r *Ys7Device) CloseEncrypt() error {
url := "https://open.ys7.com/api/lapp/device/encrypt/off"
dt := struct {
AccessToken string `json:"accessToken"`
DeviceSerial string `json:"deviceSerial"`
}{
AccessToken: accessToken,
DeviceSerial: r.DeviceSerial,
}
post, err := Post(url, dt)
if err != nil {
return err
}
if gjson.New(string(post)).Get("code").Int() != 200 {
return fmt.Errorf("%s", gjson.New(string(post)).Get("msg").String())
}
_, err = dao.Ys7Devices.Ctx(context.Background()).
Where(dao.Ys7Devices.Columns().DeviceSerial, r.DeviceSerial).
Update(g.Map{
"VideoEncrypted": 0,
})
if err != nil {
return err
}
return nil
}
// GetLiveAddress 获取播放地址
func (r *Ys7Device) GetLiveAddress() (string, error) {
url := "https://open.ys7.com/api/lapp/v2/live/address/get"
dt := struct {
AccessToken string `json:"accessToken"`
DeviceSerial string `json:"deviceSerial"`
ChannelNo int `json:"channelNo"`
}{
AccessToken: accessToken,
DeviceSerial: r.DeviceSerial,
ChannelNo: 1,
}
post, err := Post(url, dt)
if err != nil {
return "", err
}
Body := string(post)
println(Body)
if gjson.New(Body).Get("code").Int() != 200 {
return "", fmt.Errorf("获取播放地址失败: %s", gjson.New(Body).Get("msg").String())
}
return gjson.New(Body).Get("data.url").String(), nil
}
/*抓图结果返回*/
type CapRes struct {
Public
Data struct {
PicUrl string `json:"picUrl"`
} `json:"data"`
}
/*抓拍图片*/
func (receiver *Ys7Device) Capture() (cap *CapRes, err error) {
url := "https://open.ys7.com/api/lapp/device/capture"
dt := struct {
DeviceSerial string `json:"deviceSerial"`
ChannelNo int `json:"channelNo"`
AccessToken string `json:"accessToken"`
}{DeviceSerial: receiver.DeviceSerial, ChannelNo: 1, AccessToken: accessToken}
res, err := Post(url, dt)
if err != nil {
return nil, err
}
cap = &CapRes{}
err = json.Unmarshal(res, &cap)
if err != nil {
return nil, err
}
// cap.Data.PicUrl = string(TransHtmlJson([]byte(cap.Data.PicUrl)))
return cap, nil
}
// download file会将url下载到本地文件它会在下载时写入而不是将整个文件加载到内存中。
func SavePicture(url, dst string) (err error) {
// Get the data
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
// Create the file
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
// Write the body to file
_, err = io.Copy(out, resp.Body)
return err
}
//var appKey = ""
//var secret = ""
//var token = ""
//
//// InitYs7 安全帽 absolutePath绝对路径
//func InitYs7(absolutePath string) (flag bool, num int, err error) {
// getConfig()
// GetAccesstoken()
// imageData, err := os.ReadFile(absolutePath)
// if err != nil {
// err = errors.New("读取图片文件失败")
// return
// }
//
// // 将图片数据转换为Base64字符串
// base64String := base64.StdEncoding.EncodeToString(imageData)
// flag, num, err = HelmetCheck(base64String, absolutePath)
// return
//}
//
//func getConfig() {
// appKey = g.Cfg().MustGet(gctx.New(), "ys7.key").String()
// secret = g.Cfg().MustGet(gctx.New(), "ys7.secret").String()
//}
//
//func GetAccesstoken() {
// key := "ys7"
// //从缓存捞取key
// ctx := gctx.New()
// get := commonService.Cache().Get(ctx, key)
// if get != nil && get.String() != "" {
// token = get.String()
// } else {
// getConfig()
// url := "https://open.ys7.com/api/lapp/token/get"
// dt := struct {
// AppKey string `json:"appKey"`
// AppSecret string `json:"appSecret"`
// }{appKey, secret}
// err, s := post(url, dt)
// if err != nil {
// return
// }
// tp := TP{}
// err = json.Unmarshal(s, &tp)
// if err != nil {
// return
// }
// if tp.Code == OK {
// token = tp.Data.AccessToken
// //将token存储到redis中tiken默认时间为秒实际计算为7天,(这里少100秒,防止token过期还存在redis中)
// commonService.Cache().Set(ctx, key, tp.Data.AccessToken, time.Duration(tp.Data.ExpireTime-100)*time.Second)
// token = tp.Data.AccessToken
// }
// }
//
//}
//
//func post(uri string, data interface{}) (error, []byte) {
// response, err := g.Client().ContentType("application/x-www-form-urlencoded").Post(gctx.New(), uri, data)
// if err != nil {
// return err, nil
// }
// return nil, response.ReadAll()
//}
//
//// HelmetCheck 图片中标记安全帽
//func HelmetCheck(image string, absolutePath string) (flag bool, num int, err error) {
// flag = false
// num = 0
// url := "https://open.ys7.com/api/lapp/intelligence/target/analysis"
// dt := struct {
// AccessToken string `json:"accessToken"`
// DataType int `json:"dataType"`
// Image string `json:"image"`
// ServiceType string `json:"serviceType"`
// }{token, 1, image, "helmet"}
// err, s := post(url, dt)
// if err != nil {
// return
// }
// tp := HelMet{}
// err = json.Unmarshal(s, &tp)
// if err != nil {
// return
// }
// if tp.Code == OK && len(tp.Data) > 0 {
// for _, data := range tp.Data {
// height, _ := strconv.ParseFloat(data.Height, 64)
// width, _ := strconv.ParseFloat(data.Width, 64)
// if len(tp.Data) <= 0 {
// continue
// }
// for _, dataTwo := range data.TargetList {
// HeadRect := dataTwo.HeadRect
// helmetType := dataTwo.HelmetType
// if helmetType == 2 || helmetType == 0 {
// flag = true
// num = num + 1
// vmodel_h_f, _ := strconv.ParseFloat(HeadRect.VmodelHF, 64)
// vmodel_w_f, _ := strconv.ParseFloat(HeadRect.VmodelWF, 64)
// vmodel_x_f, _ := strconv.ParseFloat(HeadRect.VmodelXF, 64)
// vmodel_y_f, _ := strconv.ParseFloat(HeadRect.VmodelYF, 64)
//
// vmodel_h_f = vmodel_h_f * height
// vmodel_w_f = vmodel_w_f * width
// vmodel_x_f = vmodel_x_f * width
// vmodel_y_f = vmodel_y_f * height
// coryCommon.Test_draw_rect_text(absolutePath,
// vmodel_x_f, vmodel_y_f, vmodel_w_f, vmodel_h_f)
// }
// }
// }
// }
// return
//}

338
third/ys7/ys72.go Normal file
View File

@ -0,0 +1,338 @@
package ys7
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/frame/g"
"github.com/tidwall/gjson"
"github.com/tiger1103/gfast/v3/internal/app/system/dao"
"github.com/tiger1103/gfast/v3/internal/app/system/model/do"
"github.com/tiger1103/gfast/v3/internal/app/system/model/entity"
)
type Ys72 struct {
FnLogin func() (string, error) // 登录回调
Devices map[string]Ys7Device // 设备列表
Total int // 总页数 和 当前页数
}
var Ys72Instance = NewYs72()
// NewYs72 初始化
func NewYs72() *Ys72 {
ys7 := &Ys72{
FnLogin: DefaultLogin,
Devices: make(map[string]Ys7Device),
Total: 999, // 第一次获取设备列表时,会刷新其实际值
}
// 刷新 ys7.go 中的 AccessToken
_ = ys7.GetAccessToken()
// 初始化设备列表
//if err := ys7.Init(); err != nil {
// panic(err)
//}
// 定时任务更新设备列表
go func() {
for range time.Tick(3 * time.Minute) {
if err := ys7.Init(); err != nil {
panic(err)
}
if err := ys7.GetAllDeviceList(); err != nil {
log.Println("更新设备列表失败:", err)
}
// 获取设备截图并存入 Redis
ys7.CaptureAllDevice()
}
}()
return ys7
}
// Init 第一次初始化时从数据库获取设备列表存入 Devices
func (ys *Ys72) Init() error {
device := []entity.Ys7Devices{}
// 获取设备列表
err := dao.Ys7Devices.Ctx(context.Background()).Scan(&device)
if err != nil {
return err
}
// 将设备列表存入 Devices
for _, v := range device {
ys.Devices[v.DeviceSerial] = Ys7Device{
DeviceSerial: v.DeviceSerial,
DeviceName: v.DeviceName,
DeviceType: v.DeviceType,
Status: v.Status,
Defence: int(v.Defence),
DeviceVersion: v.DeviceVersion,
}
}
return nil
}
// 遍历 ys.Devices 调用 Capture 获取设备截图并存入Redis
func (ys *Ys72) CaptureAllDevice() {
for _, device := range ys.Devices {
// 获取设备截图
resp, err := device.Capture()
if err != nil {
log.Println("获取设备截图失败:", err)
return
}
// 存入 Redis
ctx := context.Background()
g.Redis().Set(ctx, "ys7:capture:"+device.DeviceSerial, resp.Data.PicUrl)
// 两小时后过期
g.Redis().Expire(ctx, "ys7:capture:"+device.DeviceSerial, 2*60*60)
}
}
// 由 Redis 管理 AccessToken 的生命周期,如果获取失败则重新刷新
func (ys *Ys72) GetAccessToken() string {
ctx := context.Background()
// 从 Redis 获取 AccessToken
accessToken, err := g.Redis().Get(ctx, "ys7:accessToken")
// 兼容原本获取 AccessToken 的逻辑
SetAccessToken(accessToken.String())
if err == nil && accessToken.String() != "" {
return accessToken.String()
}
// 未获取到 AccessToken 则重新登录
if accessToken.String() == "" {
// 如果登录失败则递归重试
if err := ys.login(); err != nil {
return ys.GetAccessToken()
}
accessToken, err = g.Redis().Get(ctx, "ys7:accessToken")
if err != nil {
return accessToken.String()
}
}
return accessToken.String()
}
// 传入 deviceSerial 获取设备信息
func (ys *Ys72) GetDevice(deviceSerial string) (Ys7Device, error) {
if device, ok := ys.Devices[deviceSerial]; ok {
return device, nil
}
return Ys7Device{}, fmt.Errorf("设备不存在: %s", deviceSerial)
}
// 获取单页设备列表并存入 Devices
func (ys *Ys72) getDeviceList(page int, deviceMapping map[string]Ys7Device) error {
accessToken := ys.GetAccessToken()
req := struct {
AccessToken string `json:"accessToken"`
PageStart int `json:"pageStart"`
PageSize int `json:"pageSize"`
}{AccessToken: accessToken, PageSize: 50, PageStart: page}
resp, err := Post("https://open.ys7.com/api/lapp/device/list", req)
if err != nil {
return err
}
body := string(resp)
// 解析返回数据
if gjson.Get(body, "code").Int() != 200 {
return fmt.Errorf("获取设备列表失败: %s", gjson.Get(body, "msg").String())
}
// 解析设备列表
gjson.Get(body, "data").ForEach(func(_, value gjson.Result) bool {
deviceSerial := value.Get("deviceSerial").String() // 设备序列号
deviceName := value.Get("deviceName").String() // 设备名称
deviceType := value.Get("deviceType").String() // 设备类型
deviceStatus := int(value.Get("status").Int()) // 设备状态 1:在线 2:离线
defence := int(value.Get("defence").Int()) // 是否布撤防 0:未布防 1:布防 不用管
deviceVersion := value.Get("deviceVersion").String() // 设备版本
deviceMapping[deviceSerial] = Ys7Device{
DeviceSerial: deviceSerial,
DeviceName: deviceName,
DeviceType: deviceType,
Status: deviceStatus,
Defence: defence,
DeviceVersion: deviceVersion,
}
return true
})
ys.Total = int(gjson.Get(body, "page.total").Int())
return nil
}
// 依次调用 getDeviceList 获取所有设备列表
func (ys *Ys72) GetAllDeviceList() error {
// 创建一个设备map
deviceMapping := make(map[string]Ys7Device)
// 遍历页码获取所有设备列表
for page := 0; page <= ys.Total; page++ {
if err := ys.getDeviceList(page, deviceMapping); err != nil {
return err
}
}
newDevices := []do.Ys7Devices{} // 新设备列表
updateDevices := []do.Ys7Devices{} // 更新设备列表
// 遍历 deviceMapping 对比 ys.Devices 筛选出增量或更新设备列表
for deviceSerial, device := range deviceMapping {
// 如果设备不存在则为新增设备
if _, ok := ys.Devices[deviceSerial]; !ok {
newDevices = append(newDevices, do.Ys7Devices{
DeviceSerial: device.DeviceSerial,
DeviceName: device.DeviceName,
DeviceType: device.DeviceType,
Status: device.Status,
Defence: device.Defence,
DeviceVersion: device.DeviceVersion,
})
} else {
// 如果设备存在则对比设备状态
if ok, newDevice := ys.CompareDevice(ys.Devices[deviceSerial], device); !ok {
// 如果设备状态不同则更新设备
updateDevices = append(updateDevices, do.Ys7Devices{
DeviceSerial: newDevice.DeviceSerial,
DeviceName: newDevice.DeviceName,
DeviceType: newDevice.DeviceType,
Status: newDevice.Status,
Defence: newDevice.Defence,
DeviceVersion: newDevice.DeviceVersion,
})
}
}
}
// 新旧状态替换
ys.Devices = deviceMapping
// 新设备列表入库
if len(newDevices) > 0 {
_, err := dao.Ys7Devices.Ctx(context.Background()).Data(newDevices).Batch(20).Insert()
if err != nil {
return err
}
}
// 更新设备列表
if len(updateDevices) > 0 {
for _, device := range updateDevices {
_, err := dao.Ys7Devices.Ctx(context.Background()).Data(device).
Where(dao.Ys7Devices.Columns().DeviceSerial, device.DeviceSerial).Update()
if err != nil {
return err
}
}
// _, err := dao.Ys7Devices.Ctx(context.Background()).Data(updateDevices).Batch(20).Update()
// if err != nil {
// log.Println("更新错误:", err)
// return err
// }
}
return nil
}
// 登录并将 AccessToken 存入 Redis 由 Redis 管理其有效期
// 当获取失败则重新登录后刷新周期
func (ys *Ys72) login() error {
accessToken, err := ys.FnLogin()
if err != nil {
return err
}
ctx := context.Background()
g.Redis().Set(ctx, "ys7:accessToken", accessToken)
// 设置 key 将在 5 天后过期
g.Redis().Expire(ctx, "ys7:accessToken", 5*24*60*60)
return nil
}
// CompareDevice 对比传递的两个设备结构体是否相同
// 如果不同则返回 false 并构造出新的设备结构体
func (ys *Ys72) CompareDevice(old, new Ys7Device) (bool, Ys7Device) {
// 对比old 和 new 结构体的所有字段,是否相同
if old == new {
return true, Ys7Device{}
}
// 构造出新的ys7Device
return false, Ys7Device{
DeviceSerial: new.DeviceSerial,
DeviceName: new.DeviceName,
DeviceType: new.DeviceType,
Status: new.Status,
Defence: new.Defence,
DeviceVersion: new.DeviceVersion,
}
}
type defalutFn func() (string, error)
var DefaultLogin = defalutFn(func() (string, error) {
appKey := "3acf9f1a43dc4209841e0893003db0a2"
appSecret := "4bbf3e9394f55d3af6e3af27b2d3db36"
req := struct {
AppKey string `json:"appKey"`
AppSecret string `json:"appSecret"`
}{AppKey: appKey, AppSecret: appSecret}
// 登录
resp, err := Post("https://open.ys7.com/api/lapp/token/get", req)
if err != nil {
return "", err
}
body := string(resp)
// 解析返回数据
if gjson.Get(body, "code").Int() != 200 {
return "", fmt.Errorf("登录失败: %s", gjson.Get(body, "msg").String())
}
// 返回 AccessToken
return gjson.Get(body, "data.accessToken").String(), nil
})
func StartAI(device Ys7Device) {
capture, err := device.Capture()
if err != nil {
fmt.Println("定时快照错误!", err)
} else {
get, err := g.Client().Get(gctx.New(), capture.Data.PicUrl)
if err != nil {
} else {
os.WriteFile("", get.ReadAll(), 0o666)
}
}
}