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

19
api/v1/common/captcha.go Normal file
View File

@ -0,0 +1,19 @@
/*
* @desc:验证码参数
* @company:云南奇讯科技有限公司
* @Author: yixiaohu
* @Date: 2022/3/2 17:47
*/
package common
import "github.com/gogf/gf/v2/frame/g"
type CaptchaReq struct {
g.Meta `path:"/get" tags:"验证码" method:"get" summary:"获取验证码"`
}
type CaptchaRes struct {
g.Meta `mime:"application/json"`
Key string `json:"key"`
Img string `json:"img"`
}

View File

@ -0,0 +1,491 @@
package coryCommon
import (
"encoding/json"
"errors"
"fmt"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/gclient"
"github.com/gogf/gf/v2/os/gctx"
"github.com/tiger1103/gfast-cache/cache"
commonService "github.com/tiger1103/gfast/v3/internal/app/common/service"
"github.com/tiger1103/gfast/v3/library/liberr"
tool "github.com/tiger1103/gfast/v3/utility/coryUtils"
"golang.org/x/net/context"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
)
/**
功能:
文字识别百度API识别身份证、银行卡内容、人脸检测、人脸对比
*/
// OcrReq 请求体参数 身份证和银行卡都是此结构体,
type OcrReq struct {
Image string `json:"image"` // 二选一图像数据base64编码后进行urlencode需去掉编码头data:image/jpeg;base64, 要求base64编码和urlencode后大小不超过4M最短边至少15px最长边最大4096px,支持jpg/jpeg/png/bmp格式
Url string `json:"url"` // 二选一图片完整URLURL长度不超过1024字节URL对应的图片base64编码后大小不超过4M最短边至少15px最长边最大4096px,支持jpg/jpeg/png/bmp格式当image字段存在时url字段失效 请注意关闭URL防盗链
IdCardSide string `json:"id_card_side"` // 身份证需要此参数人头面填写front国徽面填写back 银行卡不需要
DetectPhoto bool `json:"detect_photo"` // 是否检测身份证进行裁剪默认不检测。可选值true-检测身份证并返回证照的 base64 编码及位置信息
}
/*
1、clientId 必须参数应用的APIKey
2、clientSecret 必须参数应用的Secret Key
3、存在redis的数据前缀
*/
var clientId = ""
var clientSecret = ""
var cacheRedis = "zmGoBaiDuOcrAccessToken"
func init() {
one, err := g.Cfg().Get(gctx.New(), "baiDuYun.clientId")
if err != nil {
fmt.Println("百度云API未找到")
}
two, err := g.Cfg().Get(gctx.New(), "baiDuYun.clientSecret")
if err != nil {
fmt.Println("百度云Secret未找到")
}
clientId = one.String()
clientSecret = two.String()
}
/*
IDCardInfo 获取身份证相关数据
*/
type IDCardInfo struct {
WordsResult WordsResult `json:"words_result"`
WordsResultNum int `json:"words_result_num"`
IDCardNumberType int `json:"idcard_number_type"`
ImageStatus string `json:"image_status"`
LogID int64 `json:"log_id"`
Photo string `json:"photo"`
}
type WordsResult struct {
Name Field `json:"姓名"`
Nation Field `json:"民族"`
Address Field `json:"住址"`
CitizenIdentification Field `json:"公民身份号码"`
Birth Field `json:"出生"`
Gender Field `json:"性别"`
ExpirationDate Field `json:"失效日期"`
IssuingAuthority Field `json:"签发机关"`
IssueDate Field `json:"签发日期"`
}
type Field struct {
Location Location `json:"location"`
Words string `json:"words"`
}
type Location struct {
Top int `json:"top"`
Left int `json:"left"`
Width int `json:"width"`
Height int `json:"height"`
}
func ImgOCR(vr OcrReq) (m map[string]interface{}) {
//请求路径+token
baseUrl := "https://aip.baidubce.com/rest/2.0/ocr/v1/idcard"
//先从缓存里面捞取token,如果没得就重新获取token
var atStr = redisCacheStr()
// 构造 URL 参数
params := url.Values{}
params.Set("access_token", atStr)
// 构造完整的请求 URL
requestURL := fmt.Sprintf("%s?%s", baseUrl, params.Encode())
response, err := gclient.New().ContentJson().ContentType("application/x-www-form-urlencoded").Post(gctx.New(), requestURL, vr)
if err != nil {
return
}
var dataInfo = strings.ReplaceAll(response.ReadAllString(), " ", "")
//解析数据
bodyData := []byte(dataInfo)
var idCardInfo IDCardInfo
err = json.Unmarshal(bodyData, &idCardInfo)
if err != nil {
fmt.Println("Failed to parse JSON data:", err)
return
}
m = make(map[string]interface{})
//身份证正反面颠倒了,直接返回空
if idCardInfo.ImageStatus == "reversed_side" {
return
}
result := idCardInfo.WordsResult
//if idCardInfo.Photo != "" {
// m["pacePhoto"] = idCardInfo.Photo
//}
if result.Name.Words != "" {
m["userName"] = result.Name.Words
}
if result.Nation.Words != "" {
m["sfzNation"] = result.Nation.Words
}
if result.Address.Words != "" {
m["sfzSite"] = result.Address.Words
}
if result.CitizenIdentification.Words != "" {
m["sfzNumber"] = result.CitizenIdentification.Words
}
if result.Birth.Words != "" {
str, _ := tool.New().TimeCycle(result.Birth.Words)
m["sfzBirth"] = str
}
if result.Gender.Words != "" {
var se = result.Gender.Words
if se == "男" {
se = "1"
} else if se == "女" {
se = "2"
} else {
se = "3"
}
m["sex"] = se
}
if result.ExpirationDate.Words != "" {
str, err := tool.New().TimeCycle(result.ExpirationDate.Words)
if err == nil {
m["sfzEnd"] = str
} else {
str := result.ExpirationDate.Words
m["sfzEnd"] = str
}
}
if result.IssuingAuthority.Words != "" {
m["IssuingAuthority"] = result.IssuingAuthority.Words
}
if result.IssueDate.Words != "" {
str, _ := tool.New().TimeCycle(result.IssueDate.Words)
m["sfzStart"] = str
}
return m
}
/*
BankData 获取银行卡相关数据
针对卡号、有效期、发卡行、卡片类型、持卡人5个关键字段进行结构化识别识别准确率超过99%
*/
type BankData struct {
ValidDate string `json:"valid_date"` //有效期
BankCardNumber string `json:"bank_card_number"` //银行卡卡号
BankName string `json:"bank_name"` //银行名,不能识别时为空
BankCardType int `json:"bank_card_type"` //银行卡类型0不能识别; 1借记卡; 2贷记卡原信用卡大部分为贷记卡; 3准贷记卡; 4预付费卡
HolderName string `json:"holder_name"` //持卡人姓名,不能识别时为空
}
type Result struct {
Res BankData `json:"result"` //具体数据
Direction int `json:"direction"` //图像方向。 - - 1未定义; - 0正向; - 1逆时针90度; - 2逆时针180度; - 3逆时针270度
LogID int64 `json:"log_id"` //请求标识码,随机数,唯一。
}
func ImgYhkOCR(vr OcrReq) (m map[string]interface{}) {
m = make(map[string]interface{})
//请求路径+token
baseUrl := "https://aip.baidubce.com/rest/2.0/ocr/v1/bankcard"
//先从缓存里面捞取token
var atStr = redisCacheStr()
// 构造 URL 参数
params := url.Values{}
params.Set("access_token", atStr)
// 构造完整的请求 URL
requestURL := fmt.Sprintf("%s?%s", baseUrl, params.Encode())
response, err := gclient.New().ContentJson().ContentType("application/x-www-form-urlencoded").Post(gctx.New(), requestURL, vr)
if err != nil {
return
}
//解析数据
allString := response.ReadAllString()
bodyData := []byte(allString)
var result Result
err = json.Unmarshal(bodyData, &result)
if err != nil {
fmt.Println("Failed to parse JSON data:", err)
return
}
if result.Res.ValidDate != "" {
m["ValidDate"] = result.Res.ValidDate
}
if result.Res.BankCardNumber != "" {
m["yhkNumber"] = strings.ReplaceAll(result.Res.BankCardNumber, " ", "")
}
if result.Res.BankName != "" {
m["yhkOpeningBank"] = result.Res.BankName
}
if result.Res.BankCardType >= 0 {
m["BankCardType"] = result.Res.BankCardType
}
if result.Res.HolderName != "" {
m["yhkCardholder"] = result.Res.HolderName
}
return m
}
/*
HumanFaceReq 请求参数 人脸识别+人脸检测
*/
type HumanFaceReq struct {
Image string `json:"image"` //图片此处填写base64
ImageType string `json:"image_type" gf:"default:BASE64" ` //图片类型-BASE64-URL-BASE64此封装固定用base64
FaceField string `json:"face_field" gf:"default:face_type,quality"` //包括age,expression,face_shape,gender,glasses,landmark,landmark150, quality,eye_status,emotion,face_type,mask,spoofing信息 逗号分隔. 默认只返回face_token、人脸框、概率和旋转角度
}
/*
HumanFaceRep 返回参数 人脸识别
*/
type HumanFaceRep struct {
ErrorCode int `json:"error_code"`
ErrorMsg string `json:"error_msg"`
LogId int `json:"log_id"`
Timestamp int `json:"timestamp"`
Cached int `json:"cached"`
Result struct {
FaceNum int `json:"face_num"`
FaceList []struct {
FaceToken string `json:"face_token"`
Location struct {
Left float64 `json:"left"`
Top float64 `json:"top"`
Width int `json:"width"`
Height int `json:"height"`
Rotation int `json:"rotation"`
} `json:"location"`
FaceProbability float64 `json:"face_probability"`
Angle struct {
Yaw float64 `json:"yaw"`
Pitch float64 `json:"pitch"`
Roll float64 `json:"roll"`
} `json:"angle"`
FaceType struct {
Type string `json:"type"`
Probability float64 `json:"probability"`
} `json:"face_type"`
Quality struct {
Occlusion struct {
LeftEye float64 `json:"left_eye"`
RightEye float64 `json:"right_eye"`
Nose float64 `json:"nose"`
Mouth float64 `json:"mouth"`
LeftCheek float64 `json:"left_cheek"`
RightCheek float64 `json:"right_cheek"`
ChinContour float64 `json:"chin_contour"`
} `json:"occlusion"`
Blur float64 `json:"blur"`
Illumination float64 `json:"illumination"`
Completeness int64 `json:"completeness"`
} `json:"quality"`
} `json:"face_list"`
} `json:"result"`
}
func HumanFace(hf *HumanFaceReq) (err error) {
ctx := gctx.New()
err = g.Try(ctx, func(ctx context.Context) {
err = nil
//1、请求地址+token
url := "https://aip.baidubce.com/rest/2.0/face/v3/detect?access_token=" + redisCacheStr()
marshal, err := json.Marshal(hf)
if err != nil {
liberr.ErrIsNil(ctx, err)
return
}
//3、准备请求
payload := strings.NewReader(string(marshal))
client := &http.Client{}
req, err := http.NewRequest("POST", url, payload)
if err != nil {
liberr.ErrIsNil(ctx, err)
return
}
//4、设置请求头
req.Header.Add("Content-Type", "application/json")
//5、发送请求
res, err := client.Do(req)
if err != nil {
liberr.ErrIsNil(ctx, err)
return
}
defer res.Body.Close()
//6、返回数据
body, err := io.ReadAll(res.Body)
if err != nil {
liberr.ErrIsNil(ctx, err)
return
}
var aaa = body
//解析数据
var result HumanFaceRep
err = json.Unmarshal(aaa, &result)
if err != nil {
liberr.ErrIsNil(ctx, err)
return
}
if result.ErrorCode != 0 {
if result.ErrorMsg == "pic not has face" {
err = errors.New("这张照片没有人脸")
liberr.ErrIsNil(ctx, err)
} else {
err = errors.New(result.ErrorMsg)
liberr.ErrIsNil(ctx, err)
}
return
}
//1、人脸置信度范围【0~1】代表这是一张人脸的概率0最小、1最大。其中返回0或1时数据类型为Integer
dataInfo := result.Result.FaceList[0]
reliabilityOne := dataInfo.FaceProbability
if reliabilityOne != 1.0 {
err = errors.New("识别不清晰!")
}
//2、判断是否真是人脸 human: 真实人脸 cartoon: 卡通人脸
reliabilityTwo := dataInfo.FaceType.Type
if reliabilityTwo == "cartoon" {
err = errors.New("请传入真实人脸!")
} else {
//判断可信度 置信度范围0~1
reliabilityThree := dataInfo.FaceType.Probability
if reliabilityThree < 0.8 {
err = errors.New("请勿化妆太过夸张!")
}
}
//3、人脸模糊程度范围[0~1]0表示清晰1表示模糊
reliabilityFour := dataInfo.Quality.Blur
if reliabilityFour >= 0.1 {
err = errors.New("人脸过于模糊!")
}
//4、光线太暗 0~255 值越大光线越好
reliabilityFive := dataInfo.Quality.Illumination
if reliabilityFive < 80.0 {
err = errors.New("光线太暗!")
}
//5、人脸是否完整 1完整 0不完整
reliabilitySix := dataInfo.Quality.Completeness
if reliabilitySix != 1 {
err = errors.New("请确定人脸在图框内!")
}
return
})
return
}
// ComparisonRep 人脸检测返回数据
type ComparisonRep struct {
ErrorCode int `json:"error_code"`
ErrorMsg string `json:"error_msg"`
LogId int `json:"log_id"`
Timestamp int `json:"timestamp"`
Cached int `json:"cached"`
Result struct {
Score float64 `json:"score"` //人脸相似度得分推荐阈值80分
FaceList []struct {
FaceToken string `json:"face_token"`
} `json:"face_list"`
} `json:"result"`
}
func Comparison(arrObject []*HumanFaceReq) (score float64, err error) {
//1、请求地址+token
url := "https://aip.baidubce.com/rest/2.0/face/v3/match?access_token=" + redisCacheStr()
//2、请求参数转字符串json
marshal, err := json.Marshal(arrObject)
if err != nil {
return
}
payload := strings.NewReader(string(marshal))
//3、准备post请求
client := &http.Client{}
req, err := http.NewRequest("POST", url, payload)
if err != nil {
fmt.Println(err)
return
}
//4、设置请求头
req.Header.Add("Content-Type", "application/json")
//5、发送请求关闭连接
res, err := client.Do(req)
if err != nil {
fmt.Println(err)
return
}
defer res.Body.Close()
//6、数据结果
body, err := ioutil.ReadAll(res.Body)
if err != nil {
fmt.Println(err)
return
}
//7、解析数据
var result ComparisonRep
err = json.Unmarshal(body, &result)
if err != nil {
fmt.Println("Failed to parse JSON data:", err)
return
}
score = result.Result.Score
return score, err
}
/*
AccessTokenResponse 获取Access_token,有效期(秒为单位有效期30天)
*/
type AccessTokenResponse struct {
AccessToken string `json:"access_token"`
}
func AccessTokenFunc() (str string) {
url := "https://aip.baidubce.com/oauth/2.0/token?client_id=" + clientId + "&client_secret=" + clientSecret + "&grant_type=client_credentials"
payload := strings.NewReader(``)
client := &http.Client{}
req, err := http.NewRequest("POST", url, payload)
if err != nil {
fmt.Println(err)
return
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
res, err := client.Do(req)
if err != nil {
fmt.Println(err)
return
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
var tokenResponse AccessTokenResponse
json.Unmarshal(body, &tokenResponse)
if err != nil {
fmt.Println(err)
return
}
return tokenResponse.AccessToken
}
// 缓存捞取token,如果没得就重新获取token
func redisCacheStr() (atStr string) {
atStr = ""
ctx := gctx.New()
prefix := g.Cfg().MustGet(ctx, "system.cache.prefix").String()
gfCache := cache.New(prefix)
at := commonService.Cache().Get(ctx, gfCache.CachePrefix+cacheRedis)
if at == nil || at.String() == "" {
atStr = AccessTokenFunc()
//存储到redis时间为29天
commonService.Cache().Set(ctx, cacheRedis, atStr, time.Hour*24*29)
} else {
atStr = at.String()
}
//atStr = "24.c8814d3fc7961820f0e23ee9d80cf96c.2592000.1696671067.282335-38777216"
return atStr
}

View File

@ -0,0 +1,104 @@
package coryCommon
import (
"encoding/base64"
"errors"
"fmt"
"github.com/gogf/gf/v2/os/gfile"
"io/ioutil"
"math/rand"
"os"
"path/filepath"
"strings"
"time"
)
// Base64ToImgFunc 将base64转成图片保存在本地
func Base64ToImgFunc(base64Str string, numTyoe string, cdPath string) (outputPath string, err error) {
// 获取当前时间+随机数得到文件名
currentTime := time.Now()
timestamp := currentTime.UnixNano() / int64(time.Millisecond)
randomNum := rand.Intn(1000)
uniqueFileName := fmt.Sprintf("%d_%d", timestamp, randomNum)
// 用户指定的本地文件路径
//ynr := Ynr(Portrait + "/")
ynr := Ynr(cdPath + "/")
path := ynr + uniqueFileName + ".png"
path = filepath.ToSlash(path)
// Base64编码的图像字符串
b64 := "data:image/png;"
base64Image := ""
if strings.Contains(base64Str, "base64,") { // 判断是否有【base64,】如果有就替换
base64Image = strings.Replace(base64Str, base64Str[:strings.Index(base64Str, "base64,")], b64, 1)
} else {
base64Image = b64 + "base64," + base64Str
}
// 调用函数将Base64图像保存到指定路径
err = SaveBase64ImageToFile(base64Image, path)
if err != nil {
return
} else {
if numTyoe == "1" {
outputPath = strings.Replace(path, "resource/public", "file", 1)
return
} else if numTyoe == "2" {
outputPath = strings.Replace(path, "resource/public", "wxfile", 1)
return
} else {
err = errors.New("第二参数只能为1 or 2")
return
}
}
}
func Base64ToFileFunc(base64Str string, filePath string, suffix string, numTyoe string) (outputPath string, err error) {
// 获取当前时间+随机数得到文件名
timestamp := time.Now().UnixNano() / int64(time.Millisecond)
uniqueFileName := fmt.Sprintf("%d_%d", timestamp, rand.Intn(1000))
// 用户指定的本地文件路径,filePath路径最后必须是/
path := filepath.ToSlash(filePath + uniqueFileName + suffix)
// 调用函数将Base64图像保存到指定路径
err = SaveBase64ImageToFile(base64Str, path)
if err != nil {
return
} else {
if numTyoe == "1" {
outputPath = strings.Replace(path, "resource/public", "file", 1)
return
} else if numTyoe == "2" {
outputPath = strings.Replace(path, "resource/public", "wxfile", 1)
return
} else {
err = errors.New("第二参数只能为1 or 2")
return
}
}
}
// SaveBase64ImageToFile 将Base64编码的图像保存到指定的本地文件路径
func SaveBase64ImageToFile(base64Image string, outputPath string) error {
if len(outputPath) > 0 && outputPath[0] == '/' {
outputPath = outputPath[1:]
}
getwd, _ := os.Getwd()
outputPath = gfile.Join(getwd, outputPath)
outputPath = strings.ReplaceAll(outputPath, "\\", "/")
// 1. 解码Base64字符串
parts := strings.Split(base64Image, ",")
if len(parts) != 2 {
return errors.New("Base64字符串格式不正确")
}
data, err := base64.StdEncoding.DecodeString(parts[1])
if err != nil {
return errors.New("转码错误!")
}
// 2. 将字节数组保存为图像文件
err = ioutil.WriteFile(outputPath, data, 0644)
if err != nil {
return errors.New("报错图像失败!")
}
return nil
}

View File

@ -0,0 +1,107 @@
package coryCommon
import (
"context"
"errors"
"github.com/tiger1103/gfast/v3/internal/app/system/dao"
"reflect"
"strconv"
)
func New() *coryCom {
return &coryCom{}
}
type coryCom struct{}
// CreateByOrUpdateBy 专门用来反射结构体中的创建人和更新人,然后返回回去,避免重复造轮子去写重复代码
/**
*使用实例代码
* by := coryCommon.New().CreateByOrUpdateBy(ctx, res)
* infoRes := by.(model.BusCompanyInfoRes)
* res = &infoRes
*
*其中 updateByFieldVal.Set(reflect.ValueOf(updateByValue.Interface().(*gvar.Var).String())) 必须类型一致
*/
func (s *coryCom) CreateByOrUpdateBy(ctx context.Context, data interface{}) (dataRes interface{}) {
// 使用反射获取 data 的值和类型信息
val := reflect.ValueOf(data)
typ := reflect.TypeOf(data)
// 判断 data 是否为指针类型,并获取实际值
if val.Kind() == reflect.Ptr {
val = val.Elem()
typ = typ.Elem()
}
flagCreate := true
flagUpdate := true
// 获取 createBy 字段在结构体中的索引
createByField, ok := typ.FieldByName("CreateBy")
if !ok {
flagCreate = false
//return data // 如果结构体中不存在 createBy 字段,则直接返回原始值
createByField2, ok2 := typ.FieldByName("CreatedBy")
if ok2 {
createByField = createByField2
flagCreate = true
}
}
updateByField, ok := typ.FieldByName("UpdateBy")
if !ok {
flagUpdate = false
//return data // 如果结构体中不存在 createBy 字段,则直接返回原始值
updateByField2, ok2 := typ.FieldByName("UpdatedBy")
if ok2 {
updateByField = updateByField2
flagCreate = true
}
}
if flagCreate {
// 判断 createBy 字段的类型是否为 string
if createByField.Type.Kind() != reflect.String {
// 如果 createBy 字段的类型不是 string请根据需要进行相应的处理或返回错误
// 此处假设 createBy 必须为 string 类型,如果不是则返回错误
return errors.New("createBy字段类型不匹配")
}
// 获取原始的 createBy 字段的值并获取创建人
createId := val.FieldByIndex(createByField.Index).String()
ve := SelectByString(ctx, IsNumeric(createId), createId)
// 设置 createBy 字段的值为指定参数
createByFieldVal := val.FieldByIndex(createByField.Index)
createByFieldVal.SetString(ve)
}
if flagUpdate {
// 判断 updateBy 字段的类型是否为 string
if updateByField.Type.Kind() != reflect.String {
// 如果 createBy 字段的类型不是 string请根据需要进行相应的处理或返回错误
// 此处假设 createBy 必须为 string 类型,如果不是则返回错误
return errors.New("updateBy字段类型不匹配")
}
// 获取原始的 createBy 字段的值并获取创建人
updateId := val.FieldByIndex(updateByField.Index).String()
ve := SelectByString(ctx, IsNumeric(updateId), updateId)
// 设置 createBy 字段的值为指定参数
updateByFieldVal := val.FieldByIndex(updateByField.Index)
updateByFieldVal.SetString(ve)
}
// 返回修改后的结构体
return val.Interface()
}
func IsNumeric(str string) bool {
_, err := strconv.ParseFloat(str, 64)
return err == nil
}
// SelectByString 根据字符串查询到具体人对应的名称
func SelectByString(ctx context.Context, flag bool, str string) (ve string) {
if flag {
value, _ := dao.SysUser.Ctx(ctx).Fields("user_nickname").Where("id", str).Value()
ve = value.String()
} else {
value, _ := dao.BusConstructionUser.Ctx(ctx).Fields("user_name").Where("openid", str).Value()
ve = value.String()
}
return
}

View File

@ -0,0 +1,17 @@
package camera
type LoginCamera struct {
URLToken string `json:"URLToken"`
TokenTimeout int64 `json:"TokenTimeout"`
}
type ChannelSnapReq struct {
Serial string `json:"serial" dc:"设备编号"`
Stime int64 `json:"stime" dc:"快照时间, 从录像截取指定时间的历史快照, now 表示取实时快照, 即抓图允许值: now, YYYYMMDDHHmmss"`
Format int64 `json:"format" dc:"stime 快照格式 允许值: jpeg, png"`
}
type ChannelSnapRes struct {
Serial string `json:"serial" dc:"设备编号"`
Stime int64 `json:"stime" dc:"快照时间, 从录像截取指定时间的历史快照, now 表示取实时快照, 即抓图允许值: now, YYYYMMDDHHmmss"`
Format int64 `json:"format" dc:"stime 快照格式 允许值: jpeg, png"`
}

View File

@ -0,0 +1,303 @@
package camera
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/go-redis/redis/v8"
"github.com/gogf/gf/v2/crypto/gmd5"
"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"
"github.com/tiger1103/gfast/v3/api/v1/system"
commonService "github.com/tiger1103/gfast/v3/internal/app/common/service"
"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/service"
"github.com/tiger1103/gfast/v3/third/arithmetic/SpartaApi"
"io"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)
// LoginCameraFunc 登录获取 身份凭证
func LoginCameraFunc(ctx context.Context) (token string) {
api, _ := g.Cfg().Get(ctx, "LiveGBS.safety.api")
acc, _ := g.Cfg().Get(ctx, "LiveGBS.safety.acc")
pas, _ := g.Cfg().Get(ctx, "LiveGBS.safety.pas")
key := "loginCamera"
//从缓存捞取key
prefix := g.Cfg().MustGet(ctx, "system.cache.prefix").String()
gfCache := cache.New(prefix)
get := commonService.Cache().Get(ctx, gfCache.CachePrefix+key)
if get != nil && get.String() != "" {
token = get.String()
return token
} else {
account := acc.String()
password := gmd5.MustEncryptString(pas.String())
uri := api.String() + "api/v1/login?username=" + account + "&password=" + password + "&url_token_only=true"
response, err := g.Client().Get(gctx.New(), uri)
if err != nil {
return
}
var lc *LoginCamera
err = json.Unmarshal([]byte(response.ReadAllString()), &lc)
if err != nil {
return
}
//将token存储到redis中tiken默认时间为秒实际计算为7天,(这里少100秒,防止token过期还存在redis中)
commonService.Cache().Set(ctx, key, lc.URLToken, time.Duration(lc.TokenTimeout-100)*time.Second)
token = lc.URLToken
return token
}
}
var Rdb *redis.Client
// camera.DYFunc()
// 发布小程序需要关闭 camera.DYFunc()
// DYFunc 连接redis
func DYFunc() {
ctx := gctx.New()
err := g.Try(ctx, func(ctx context.Context) {
fmt.Println("redis订阅已开启")
// 创建第一个 Redis 连接
address, _ := g.Cfg().Get(ctx, "LiveGBS.redis.address")
password, _ := g.Cfg().Get(ctx, "LiveGBS.redis.password")
// 创建一个 Redis 客户端连接
options := &redis.Options{
Addr: address.String(), // 替换为你的 Redis 地址和端口
Password: password.String(), // 替换为你的 Redis 密码
DB: 1, // 替换为你的 Redis 数据库索引
}
Rdb = redis.NewClient(options)
// 创建一个订阅频道
channelName := "device" // 替换为你要订阅的频道名
pubsub := Rdb.Subscribe(context.Background(), channelName)
// 处理订阅消息的 Goroutine
go func() {
for {
msg, err := pubsub.ReceiveMessage(context.Background())
if err != nil {
log.Println("Error receiving message:", err)
time.Sleep(time.Second) // 出错时等待一段时间后重试
continue
}
var strData = msg.Payload
log.Println("Received message:", strData)
//1、获取到数据然后查看是否''拼接(先不管拼接),不是''就直接操作数据库
split := strings.Split(strData, ":")
if len(split) < 2 {
onOrOff := strings.Split(split[0], " ")[1]
if strings.EqualFold(onOrOff, "on") {
onOrOff = "1"
} else {
onOrOff = "0"
}
_, err = dao.QianqiCamera.Ctx(ctx).Where("code", split[0]).Update(g.Map{"country_state": onOrOff})
//if err != nil {
// id, _ := result.LastInsertId()
// if id > 0 {
// dao.BusCameraChannel.Ctx(ctx).Where("country_id", id).Update(g.Map{"status": onOrOff})
// }
//}
}
time.Sleep(time.Second * 3)
}
}()
})
fmt.Println("订阅错误问题:", err)
}
// ChannelSnapFunc 快照
func ChannelSnapFunc(ctx context.Context, id int64, serial string, code string, projectId int64) (err error) {
//获取预制位,前提是 PresetEnable == true
pb, err := PeeeresettingBitFunc(gctx.New(), serial, code)
if err != nil {
return err
}
for _, pi := range pb.PresetItemList {
if pi.PresetEnable == true {
//0、预置位
err = PresettingBitFunc(ctx, serial, code, "goto", pi.PresetID, "")
//1、请求接口得到二进制数据
suffix := "png"
api, _ := g.Cfg().Get(ctx, "LiveGBS.safety.api")
tokens := LoginCameraFunc(ctx)
uri := api.String() + "api/v1/device/channelsnap?serial=" + serial + "&code=" + code + "&stime=now&format=" + suffix + "&token=" + tokens
response, err := g.Client().Get(gctx.New(), uri)
if err != nil {
return err
}
//2、生成时间文件夹生成文件名然后同一斜杠得到完成路径
ht := coryCommon.Helmet
str := coryCommon.Ynr(ht)
fn := coryCommon.FileName("helmet")
dir, err := os.Getwd()
str = dir + "/" + str + fn + "." + suffix
str = filepath.ToSlash(str)
//3、创建一个文件来保存图片数据将响应体中的图片数据复制到文件中
file, err := os.Create(str)
if err != nil {
err = errors.New("创建文件出错")
return err
}
// 创建一个缓冲区,用于读取数据(从 body 中读取数据)
var re = response
var by = re.Body
lenStr, err := io.Copy(file, by)
if err != nil {
err = errors.New("复制图片数据到文件出错")
return err
} else {
file.Close()
}
if lenStr < 10240 {
file.Close() // 关闭文件
os.Remove(str)
return err
}
//4、《斯巴达》调用算法接口圈出未带安全帽的人
req := SpartaApi.RecognizeReq{
CapUrl: str,
//RecType: "head smoke belt waste excavator Roller Truck_crane Loader Submersible_drilling_rig Sprinkler Truck_mounted_crane Truck",
RecType: "head smoke",
Async: "False",
CallBackUrl: "",
AreaHigh: "",
}
mp, flag, num, err := SpartaApi.CommonAlgorithmFunc(ctx, &req)
if err != nil {
os.Remove(str)
return err
}
////4、《ys7》调用算法接口圈出未带安全帽的人
//flag, num, err := ys7.InitYs7(str)
//if err != nil {
// os.Remove(str)
// return err
//}
//5、flag为true表示有违规数据
if flag {
// 使用range遍历map
mpkStr := ""
mpvStr := ""
for key, value := range mp {
mpkStr = mpkStr + key + ","
mpvStr = mpvStr + value + "、"
}
mpkStr = strings.TrimRight(mpkStr, ",")
mpvStr = strings.TrimRight(mpvStr, "、")
//5、生成数据存储到数据表中识别记录
path := strings.ReplaceAll(ht, "/resource/public/", coryCommon.GetWd)
currentTime := time.Now()
dateString := currentTime.Format("2006-01-02")
path = path + dateString + "/" + fn + "." + suffix
addReq := system.BusTourAddReq{
ProjectId: projectId,
TourCategory: "2",
TourType: "1",
Picture: path,
Describe: mpvStr,
Num: num,
TableName: dao.QianqiCamera.Table(),
TableId: id,
}
service.BusTour().Add(ctx, &addReq)
//6、生成数据存储到违规记录里面
var bvl model.BusViolationLevelInfoRes
err = dao.BusViolationLevel.Ctx(ctx).Where("tour_type", mpkStr).Fields("id,grade").Scan(&bvl)
recordAddReq := do.BusViolationRecord{
ProjectId: projectId,
LevelId: bvl.Id,
Level: bvl.Grade,
TourType: mpkStr,
DataSource: "camera",
//Picture: path,
//WxOrPc: "1",
}
dao.BusViolationRecord.Ctx(ctx).Insert(recordAddReq)
}
time.Sleep(20 * time.Second)
}
}
return err
}
/*
PresettingBitFunc 设备控制-预置位控制
serial: 国标号
code: 通道号
command: 指令
preset: 预置位编号
name: 预置位名称
*/
func PresettingBitFunc(ctx context.Context, serial string, code string, command string, preset int, name string) (err error) {
if name != "" && (name == "set" || name == "goto" || name == "remove") {
err = errors.New("控制指令允许值: set, goto, remove")
return err
}
if !(preset > 0 && preset <= 255) {
err = errors.New("预置位编号范围为1~255")
return err
}
tokens := LoginCameraFunc(ctx)
api, _ := g.Cfg().Get(ctx, "LiveGBS.safety.api")
uri := api.String() + "api/v1/control/preset?serial=" + serial +
"&code=" + code +
"&command=" + command +
"&preset=" + strconv.Itoa(preset) +
"&token=" + tokens
if name != "" {
uri = uri + "&name=" + name
}
g.Client().Get(gctx.New(), uri)
return err
}
func PeeeresettingBitFunc(ctx context.Context, serial string, code string) (pb *PeeeresettingBitEntity, err error) {
tokens := LoginCameraFunc(ctx)
api, _ := g.Cfg().Get(ctx, "LiveGBS.safety.api")
uri := api.String() + "api/v1/device/fetchpreset?serial=" + serial +
"&code=" + code +
"&token=" + tokens +
"&fill=false"
ft, errft := g.Client().Get(gctx.New(), uri)
var str = ft.ReadAllString()
err = json.Unmarshal([]byte(str), &pb)
if err != nil {
if strings.Contains(str, "offline") {
err = errors.New("当前设备不在线!")
} else if strings.Contains(str, "not found") {
err = errors.New("当前设备沒找到!")
} else {
err = errft
}
return
}
return
}
type PeeeresettingBitEntity struct {
DeviceID string `json:"DeviceID"`
Result string `json:"Result"`
SumNum int `json:"SumNum"`
PresetItemList []*PeeeresettingBitListEntity `json:"PresetItemList"`
}
type PeeeresettingBitListEntity struct {
PresetID int `json:"PresetID"`
PresetName string `json:"PresetName"`
PresetEnable bool `json:"PresetEnable"`
}

View File

@ -0,0 +1,58 @@
package coryCommon
const Global = "xny.yj-3d.com"
var GlobalPath = "http://" + Global + ":7363"
var GlobalFile = "/file"
// ==========================错误类型==========================
// SYSERRHINT 系统错误
var syserrhint = "系统错误,请联系管理员!"
// ImportFile 系统错误
var ImportFile = "导入文件有误!"
// ===========================文件===========================
var GetWd = "/file/" // GetWd 静态文件路径
var LargeFileClt = "/resource/public/clt/" // LargeFileClt 文件上传地址-【倾斜模型/光伏板】
var LargeFileShp = "/resource/public/shp/" // LargeFileShp 文件上传地址-shp 红线
var ProjectSplitTable = "/resource/public/projectSplitTable/" // ProjectSplitTable 项目划分表临时文件
var Temporary = "/resource/public/temporary" // Temporary 临时文件存放地址
var LargeFilePrivacy = "/resource/public/privacy/" // LargeFilePrivacy 身份证银行卡的存储位置
var Portrait = "/resource/public/portrait" // Portrait 人像 - 人脸存储的地方(人脸对比的人脸和打卡的人脸都存放在此处)
var Helmet = "/resource/public/upload_file/" // Helmet 公共位置
var GisModelLib = "/resource/public/mx/" // GisModelLib 模型库
var Uav = "/resource/public/uav/" // uav 无人机资源
var UavMerge = "resource/public/tif" // uav 无人机资源大图合并资源
/*
网盘位置
*/
var Template = "/resource/public/masterMask"
var Template2 = "/resource/public/masterMask/dataFolder" //(工程资料)资料文件保存的位置
var Template3 = "/resource/public/masterMask/sourceData" //(工程资料)源数据文件保存的位置
var Report = "/resource/public/networkDisk/report" //(网盘)科研及专题报告
var ProductionDrawing = "/resource/public/networkDisk/productionDrawing" //(网盘)施工图
var Completion = "/resource/public/networkDisk/completion" //(网盘)竣工图
var QualityMeeting = "/resource/public/networkDisk/qualityMeeting" //(网盘)质量会议
var SafetyMeeting = "/resource/public/networkDisk/safetyMeeting" //(网盘)安全会议
// ==========================文件后缀==========================
// PictureSuffix 常见图片后缀
var PictureSuffix = "JPEG|JPG|HEIF|PNG"
// ==========================项目常量==========================
// gispath gis文件
var gispath = "gisfile/"
// commonpath common文件
var commonpath = "commonfile/"
// addFilePath GIS路径
var addFilePath = "/yjearth4.0/static/source/"
// suffix 文件后缀名称
var suffix = "clt,pdf,zip"

View File

@ -0,0 +1,277 @@
package coryCommon
import (
"math"
"strconv"
)
// WGS84坐标系即地球坐标系国际上通用的坐标系。
// GCJ02坐标系即火星坐标系WGS84坐标系经加密后的坐标系。Google Maps高德在用。
// BD09坐标系即百度坐标系GCJ02坐标系经加密后的坐标系。
const (
X_PI = math.Pi * 3000.0 / 180.0
OFFSET = 0.00669342162296594323
AXIS = 6378245.0
)
// BD09toGCJ02 百度坐标系->火星坐标系
func BD09toGCJ02(lon, lat float64) (float64, float64) {
x := lon - 0.0065
y := lat - 0.006
z := math.Sqrt(x*x+y*y) - 0.00002*math.Sin(y*X_PI)
theta := math.Atan2(y, x) - 0.000003*math.Cos(x*X_PI)
gLon := z * math.Cos(theta)
gLat := z * math.Sin(theta)
return gLon, gLat
}
// GCJ02toBD09 火星坐标系->百度坐标系
func GCJ02toBD09(lon, lat float64) (float64, float64) {
z := math.Sqrt(lon*lon+lat*lat) + 0.00002*math.Sin(lat*X_PI)
theta := math.Atan2(lat, lon) + 0.000003*math.Cos(lon*X_PI)
bdLon := z*math.Cos(theta) + 0.0065
bdLat := z*math.Sin(theta) + 0.006
return bdLon, bdLat
}
// WGS84toGCJ02 WGS84坐标系->火星坐标系
func WGS84toGCJ02(lon, lat float64) (float64, float64) {
if isOutOFChina(lon, lat) {
return lon, lat
}
mgLon, mgLat := delta(lon, lat)
return mgLon, mgLat
}
// GCJ02toWGS84 火星坐标系->WGS84坐标系
func GCJ02toWGS84(lon, lat float64) (float64, float64) {
if isOutOFChina(lon, lat) {
return lon, lat
}
mgLon, mgLat := delta(lon, lat)
return lon*2 - mgLon, lat*2 - mgLat
}
// BD09toWGS84 百度坐标系->WGS84坐标系
func BD09toWGS84(lon, lat float64) (float64, float64) {
lon, lat = BD09toGCJ02(lon, lat)
return GCJ02toWGS84(lon, lat)
}
// WGS84toBD09 WGS84坐标系->百度坐标系
func WGS84toBD09(lon, lat float64) (float64, float64) {
lon, lat = WGS84toGCJ02(lon, lat)
return GCJ02toBD09(lon, lat)
}
func delta(lon, lat float64) (float64, float64) {
dlat := transformlat(lon-105.0, lat-35.0)
dlon := transformlng(lon-105.0, lat-35.0)
radlat := lat / 180.0 * math.Pi
magic := math.Sin(radlat)
magic = 1 - OFFSET*magic*magic
sqrtmagic := math.Sqrt(magic)
dlat = (dlat * 180.0) / ((AXIS * (1 - OFFSET)) / (magic * sqrtmagic) * math.Pi)
dlon = (dlon * 180.0) / (AXIS / sqrtmagic * math.Cos(radlat) * math.Pi)
mgLat := lat + dlat
mgLon := lon + dlon
return mgLon, mgLat
}
func transformlat(lon, lat float64) float64 {
var ret = -100.0 + 2.0*lon + 3.0*lat + 0.2*lat*lat + 0.1*lon*lat + 0.2*math.Sqrt(math.Abs(lon))
ret += (20.0*math.Sin(6.0*lon*math.Pi) + 20.0*math.Sin(2.0*lon*math.Pi)) * 2.0 / 3.0
ret += (20.0*math.Sin(lat*math.Pi) + 40.0*math.Sin(lat/3.0*math.Pi)) * 2.0 / 3.0
ret += (160.0*math.Sin(lat/12.0*math.Pi) + 320*math.Sin(lat*math.Pi/30.0)) * 2.0 / 3.0
return ret
}
func transformlng(lon, lat float64) float64 {
var ret = 300.0 + lon + 2.0*lat + 0.1*lon*lon + 0.1*lon*lat + 0.1*math.Sqrt(math.Abs(lon))
ret += (20.0*math.Sin(6.0*lon*math.Pi) + 20.0*math.Sin(2.0*lon*math.Pi)) * 2.0 / 3.0
ret += (20.0*math.Sin(lon*math.Pi) + 40.0*math.Sin(lon/3.0*math.Pi)) * 2.0 / 3.0
ret += (150.0*math.Sin(lon/12.0*math.Pi) + 300.0*math.Sin(lon/30.0*math.Pi)) * 2.0 / 3.0
return ret
}
func isOutOFChina(lon, lat float64) bool {
return !(lon > 73.66 && lon < 135.05 && lat > 3.86 && lat < 53.55)
}
/*
===========================================================================================
===========================================================================================
===========================================================================================
===========================================================================================
===========================================================================================
===========================================================================================
===========================================================================================
*/
// GPSUtil is a utility class for GPS calculations.
// 小写方法是私有方法,大写方法是公有方法 可根据需要调整
func News() *GPSUtil {
return &GPSUtil{}
}
type GPSUtil struct {
}
const (
pi = 3.1415926535897932384626 // 圆周率
x_pi = 3.14159265358979324 * 3000.0 / 180.0 // 圆周率对应的经纬度偏移
a = 6378245.0 // 长半轴
ee = 0.00669342162296594323 // 扁率
)
func (receiver *GPSUtil) transformLat(x, y float64) float64 {
ret := -100.0 + 2.0*x + 3.0*y + 0.2*y*y + 0.1*x*y + 0.2*math.Sqrt(math.Abs(x))
ret += (20.0*math.Sin(6.0*x*pi) + 20.0*math.Sin(2.0*x*pi)) * 2.0 / 3.0
ret += (20.0*math.Sin(y*pi) + 40.0*math.Sin(y/3.0*pi)) * 2.0 / 3.0
ret += (160.0*math.Sin(y/12.0*pi) + 320*math.Sin(y*pi/30.0)) * 2.0 / 3.0
return ret
}
func (receiver *GPSUtil) transformlng(x, y float64) float64 {
ret := 300.0 + x + 2.0*y + 0.1*x*x + 0.1*x*y + 0.1*math.Sqrt(math.Abs(x))
ret += (20.0*math.Sin(6.0*x*pi) + 20.0*math.Sin(2.0*x*pi)) * 2.0 / 3.0
ret += (20.0*math.Sin(x*pi) + 40.0*math.Sin(x/3.0*pi)) * 2.0 / 3.0
ret += (150.0*math.Sin(x/12.0*pi) + 300.0*math.Sin(x/30.0*pi)) * 2.0 / 3.0
return ret
}
func (receiver *GPSUtil) outOfChina(lat, lng float64) bool {
if lng < 72.004 || lng > 137.8347 {
return true
}
if lat < 0.8293 || lat > 55.8271 {
return true
}
return false
}
func (receiver *GPSUtil) transform(lat, lng float64) []float64 {
if receiver.outOfChina(lat, lng) {
return []float64{lat, lng}
}
dLat := receiver.transformLat(lng-105.0, lat-35.0)
dlng := receiver.transformlng(lng-105.0, lat-35.0)
radLat := lat / 180.0 * pi
magic := math.Sin(radLat)
magic = 1 - ee*magic*magic
SqrtMagic := math.Sqrt(magic)
dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * SqrtMagic) * pi)
dlng = (dlng * 180.0) / (a / SqrtMagic * math.Cos(radLat) * pi)
mgLat := lat + dLat
mglng := lng + dlng
return []float64{mgLat, mglng}
}
// WGS84_To_Gcj02 84 to 火星坐标系 (GCJ-02) World Geodetic System ==> Mars Geodetic System
// @param lat
// @param lng
// @return
func (receiver *GPSUtil) WGS84_To_Gcj02(lat, lng float64) []float64 {
if receiver.outOfChina(lat, lng) {
return []float64{lat, lng}
}
dLat := receiver.transformLat(lng-105.0, lat-35.0)
dlng := receiver.transformlng(lng-105.0, lat-35.0)
radLat := lat / 180.0 * pi
magic := math.Sin(radLat)
magic = 1 - ee*magic*magic
SqrtMagic := math.Sqrt(magic)
dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * SqrtMagic) * pi)
dlng = (dlng * 180.0) / (a / SqrtMagic * math.Cos(radLat) * pi)
mgLat := lat + dLat
mglng := lng + dlng
return []float64{mgLat, mglng}
}
// GCJ02_To_WGS84
// 火星坐标系 (GCJ-02) to WGS84
// @param lng
// @param lat
// @return
func (receiver *GPSUtil) GCJ02_To_WGS84(lng, lat float64) []float64 {
gps := receiver.transform(lat, lng)
lngtitude := lng*2 - gps[1]
latitude := lat*2 - gps[0]
return []float64{lngtitude, latitude}
}
/**
* 火星坐标系 (GCJ-02) 与百度坐标系 (BD-09) 的转换算法 将 GCJ-02 坐标转换成 BD-09 坐标
*
* @param lat
* @param lng
*/
func (receiver *GPSUtil) gcj02_To_Bd09(lat, lng float64) []float64 {
x := lng
y := lat
z := math.Sqrt(x*x+y*y) + 0.00002*math.Sin(y*x_pi)
theta := math.Atan2(y, x) + 0.000003*math.Cos(x*x_pi)
templng := z*math.Cos(theta) + 0.0065
tempLat := z*math.Sin(theta) + 0.006
gps := []float64{tempLat, templng}
return gps
}
/**
* * 火星坐标系 (GCJ-02) 与百度坐标系 (BD-09) 的转换算法 * * 将 BD-09 坐标转换成GCJ-02 坐标 * * @param
* bd_lat * @param bd_lng * @return
*/
func (receiver *GPSUtil) bd09_To_Gcj02(lat, lng float64) []float64 {
x := lng - 0.0065
y := lat - 0.006
z := math.Sqrt(x*x+y*y) - 0.00002*math.Sin(y*x_pi)
theta := math.Atan2(y, x) - 0.000003*math.Cos(x*x_pi)
templng := z * math.Cos(theta)
tempLat := z * math.Sin(theta)
gps := []float64{tempLat, templng}
return gps
}
/**将WGS84转为bd09
* @param lat
* @param lng
* @return
*/
func (receiver *GPSUtil) WGS84_To_bd09(lat, lng float64) []float64 {
gcj02 := receiver.WGS84_To_Gcj02(lat, lng)
bd09 := receiver.gcj02_To_Bd09(gcj02[0], gcj02[1])
return bd09
}
func (receiver *GPSUtil) bd09_To_WGS84(lat, lng float64) []float64 {
gcj02 := receiver.bd09_To_Gcj02(lat, lng)
WGS84 := receiver.GCJ02_To_WGS84(gcj02[0], gcj02[1])
//保留小数点后六位
WGS84[0] = receiver.retain6(WGS84[0])
WGS84[1] = receiver.retain6(WGS84[1])
return WGS84
}
/**保留小数点后六位
* @param num
* @return
*/
func (receiver *GPSUtil) retain6(num float64) float64 {
value, _ := strconv.ParseFloat(strconv.FormatFloat(num, 'f', 6, 64), 64)
return value
}

View File

@ -0,0 +1,329 @@
package coryCommon
import (
"archive/zip"
"fmt"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
"github.com/tiger1103/gfast/v3/internal/app/system/model"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
"time"
)
// CreateZipFile 生成一个压缩文件夹,然后将指定文件夹的数据(文件及文件夹)存放到压缩文件下
func CreateZipFile(sourceDir, zipFile string) error {
zipFileToCreate, err := os.Create(zipFile)
if err != nil {
return err
}
defer zipFileToCreate.Close()
zipWriter := zip.NewWriter(zipFileToCreate)
defer zipWriter.Close()
err = filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
return addFileToZip(zipWriter, path, sourceDir)
})
if err != nil {
return err
}
return nil
}
func addFileToZip(zipWriter *zip.Writer, filePath, baseDir string) error {
fileToZip, err := os.Open(filePath)
if err != nil {
return err
}
defer fileToZip.Close()
info, err := fileToZip.Stat()
if err != nil {
return err
}
// 获取文件相对路径
relPath, err := filepath.Rel(baseDir, filePath)
if err != nil {
return err
}
// 替换路径分隔符确保在压缩文件中使用正斜杠
relPath = strings.ReplaceAll(relPath, `\`, "/")
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
header.Name = relPath
if info.IsDir() {
header.Name += "/"
header.Method = zip.Store // Directory
} else {
header.Method = zip.Deflate // File
}
writer, err := zipWriter.CreateHeader(header)
if err != nil {
return err
}
if !info.IsDir() {
_, err = io.Copy(writer, fileToZip)
if err != nil {
return err
}
}
return nil
}
// MultifileDownload 【功能:多文件下载】===【参数relativelyTemporaryPath相对路径、filesToCopy需要放在压缩包下载的文件】
func MultifileDownload(relativelyTemporaryPath string, filesToCopy []string) (path string, err error) {
//网络资源下载到本地
for i := range filesToCopy {
url := filesToCopy[i]
pathParts := strings.Split(url, "/")
fileName := pathParts[len(pathParts)-1]
filePath := filepath.ToSlash(GetCWD() + Temporary + "/" + fileName)
err = DownloadFile(url, filePath)
if err != nil {
return "", err
}
filesToCopy[i] = filePath
}
// 1、创建临时压缩包
zipFile, zipWriter, err := createTempZip(relativelyTemporaryPath)
if err != nil {
fmt.Println("Error creating temp zip:", err)
return "", err
}
defer func() {
zipWriter.Close()
zipFile.Close()
//暂时不删除、创建了个每月定时清除临时文件的定时器
//for i := range filesToCopy {
// delFile(filesToCopy[i], 0) //删除临时文件
//}
//go delFile(zipFile.Name(), 20) // 删除临时压缩文件20秒后执行防止文件还没下载完成就给删除了
}()
// 2、复制文件夹到压缩包
for _, filePath := range filesToCopy {
err := copyFileToZip(zipWriter, filePath)
if err != nil {
fmt.Printf("Error copying %s to zip: %s\n", filePath, err)
return "", err
}
}
path = strings.ReplaceAll(filepath.ToSlash(zipFile.Name()), filepath.ToSlash(GetCWD())+"/resource/public", "/file") //如果服务器同步需要注意wxfile
return
}
// 创建临时压缩包,并且提供写入数据
func createTempZip(relativelyTemporaryPath string) (*os.File, *zip.Writer, error) {
cwd := GetCWD() + relativelyTemporaryPath
zipFile, err := os.CreateTemp(cwd, "temp_zip_*.zip") //*自动分配
if err != nil {
return nil, nil, err
}
zipWriter := zip.NewWriter(zipFile)
return zipFile, zipWriter, nil
}
// 复制文件到压缩包
func copyFileToZip(zipWriter *zip.Writer, filePath string) error {
file, err := os.Open(filePath)
if err != nil {
return err
}
defer file.Close()
// 获取文件信息
info, err := file.Stat()
if err != nil {
return err
}
// 创建zip文件中的文件头
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
// 指定文件名
header.Name = filepath.Base(filePath)
// 创建zip文件中的文件
writer, err := zipWriter.CreateHeader(header)
if err != nil {
return err
}
// 复制文件内容到zip文件中
_, err = io.Copy(writer, file)
return err
}
// delFile 删除文件
func delFile(file string, second int) {
if second > 0 {
time.Sleep(time.Duration(second) * time.Second)
}
if err := os.Remove(file); err != nil {
fmt.Println("Failed to delete temporary file:", err)
}
}
// DownloadFile URL资源下载到自定位置
func DownloadFile(url, localPath string) error {
// 发起HTTP GET请求
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
// 检查响应状态码
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("HTTP request failed with status code: %d", resp.StatusCode)
}
// 创建本地文件
file, err := os.Create(localPath)
if err != nil {
return err
}
defer file.Close()
// 将HTTP响应体的内容拷贝到本地文件
_, err = io.Copy(file, resp.Body)
if err != nil {
return err
}
return nil
}
// RemoveAllFilesInDirectory 删除指定目录下的所有文件 例子:"D:\\Cory\\go\\中煤\\zmkg-back\\resource\\public\\temporary"
func RemoveAllFilesInDirectory(directoryPath string) error {
// 获取指定目录下的所有文件和子目录
files, err := filepath.Glob(filepath.Join(directoryPath, "*"))
if err != nil {
return err
}
// 遍历所有文件并删除
for _, file := range files {
if err := os.RemoveAll(file); err != nil {
return err
}
fmt.Println("Deleted:", file)
}
return nil
}
// GetFiles 获取目录下所有文件(包括文件夹中的文件)
func GetFiles(folder string) (filesList []string) {
files, _ := ioutil.ReadDir(folder)
for _, file := range files {
if file.IsDir() {
GetFiles(folder + "/" + file.Name())
} else {
filesList = append(filesList, file.Name())
}
}
return
}
// GetAllFile 获取目录下直属所有文件(不包括文件夹及其中的文件)
func GetAllFile(pathname string) (s []string, err error) {
rd, err := ioutil.ReadDir(pathname)
if err != nil {
fmt.Println("read dir fail:", err)
return s, err
}
for _, fi := range rd {
if !fi.IsDir() {
fullName := pathname + "/" + fi.Name()
s = append(s, fullName)
}
}
return s, nil
}
// Xcopy 复制文件
func Xcopy(source, target string) (err error) {
// 打开源文件
sourceFile, err := os.Open(source)
if err != nil {
return err
}
defer sourceFile.Close()
// 创建目标文件
destinationFile, err := os.Create(target)
if err != nil {
return err
}
defer destinationFile.Close()
// 使用 io.Copy() 函数复制文件内容
_, err = io.Copy(destinationFile, sourceFile)
return err
}
// CustomizationMultifileDownload 定制下载(安全考试专用)
func CustomizationMultifileDownload(relativelyTemporaryPath string, mw []*model.ModelWeChatPdfWoRes) (path string, err error) {
//1、创建文件夹
paht := filepath.ToSlash(GetCWD() + "/" + Ynr(Temporary+"/"))
folder := paht + FileName("aqks") //文件夹名
folder = filepath.ToSlash(folder)
folder = folder[0 : len(folder)-1]
err = os.MkdirAll(folder, 0777)
if err != nil {
return
}
//2、网络资源下载到本地
for i := range mw {
url := mw[i].Path
str := folder + "/" + mw[i].UserName + mw[i].Openid
os.MkdirAll(str, 0777) //根据名字创建子目录
//url看看是几个文件
fileNum := strings.Split(url, ",")
for j := range fileNum {
if fileNum[j] != "" {
//因为pdf是另外一个服务器所以需要下载但是有的又是本地服务器所以直接复制
if strings.Contains(fileNum[j], "/wxfile/") {
pathstr := g.Cfg().MustGet(gctx.New(), "cory").String() + fileNum[j]
pathParts := strings.Split(pathstr, "/")
filePath := str + "/" + pathParts[len(pathParts)-1]
err = DownloadFile(pathstr, filePath) //下载网络图片
if err != nil {
return "", err
}
} else {
source := FileToFunc(fileNum[j], 2)
pathParts := strings.Split(fileNum[j], "/")
target := str + "/" + pathParts[len(pathParts)-1]
err := Xcopy(source, target)
if err != nil {
return "", err
}
}
}
}
}
//3、压缩成压缩包zip
path = paht + FileName("aqks") + ".zip"
err = CreateZipFile(folder, path)
return
}

View File

@ -0,0 +1,515 @@
package excelUtil
import (
"fmt"
"github.com/tiger1103/gfast/v3/api/v1/common/coryCommon"
"github.com/tiger1103/gfast/v3/api/wxApplet/wxApplet"
"github.com/xuri/excelize/v2"
"os"
"strconv"
"time"
)
// ExcelOne 每条数据一个工作簿
func ExcelOne(oneDateOneList []*wxApplet.PunchingCardRecordOne) (file string, filePath string, fileName string) {
f := excelize.NewFile()
defer func() {
if err := f.Close(); err != nil {
fmt.Println(err)
}
}()
for x, one := range oneDateOneList {
itoa := strconv.Itoa(x + 1)
// -----创建一个工作表
_, err := f.NewSheet("Sheet" + itoa)
if err != nil {
fmt.Println(err)
return
}
// -----表数据
//设置1、2行高度
err = f.SetRowHeight("Sheet"+itoa, 1, 36)
err = f.SetRowHeight("Sheet"+itoa, 2, 14)
//1、左侧固定 ABC的1~2
f.SetCellValue("Sheet"+itoa, "A1", "序号")
f.MergeCell("Sheet"+itoa, "A1", "A2")
f.SetCellValue("Sheet"+itoa, "B1", "姓名")
f.MergeCell("Sheet"+itoa, "B1", "B2")
f.SetCellValue("Sheet"+itoa, "C1", "")
f.MergeCell("Sheet"+itoa, "C1", "C2")
//2、左侧固定 ABC的3~4
f.SetCellValue("Sheet"+itoa, "A3", "1")
f.MergeCell("Sheet"+itoa, "A3", "A4")
f.SetCellValue("Sheet"+itoa, "B3", one.Name)
f.MergeCell("Sheet"+itoa, "B3", "B4")
f.SetCellValue("Sheet"+itoa, "C3", "打卡记录")
f.SetCellValue("Sheet"+itoa, "C4", "工时")
//3、中间数据---------从4开始创建列
var num = "0" //循环生成列
var numInt = 1 //循环列对应的编号
var numi = 0 //循环过后需要的列
for i := 0; i < len(one.PunchCard); i++ {
num = getColumnName(4 + i)
f.SetCellValue("Sheet"+itoa, num+"2", numInt) //循环列对应的编号
f.SetCellValue("Sheet"+itoa, num+"3", one.PunchCard[i].Clock) //循环生成列 时间
f.SetCellValue("Sheet"+itoa, num+"4", one.PunchCard[i].Hour) //循环生成列 统计
numi = 4 + i
numInt = numInt + 1
//最后一个总计
if i == len(one.PunchCard)-1 {
f.SetCellValue("Sheet"+itoa, num+"5", "总计") //循环生成列
}
}
f.MergeCell("Sheet"+itoa, getColumnName(4)+"1", getColumnName(numi)+"1")
f.SetCellValue("Sheet"+itoa, num+"1", "8月")
numi = numi + 1
num = getColumnName(numi)
//4、右侧不清楚
f.SetCellValue("Sheet"+itoa, num+"1", "合计工时")
f.SetCellValue("Sheet"+itoa, num+"4", one.SumHour)
f.SetCellValue("Sheet"+itoa, num+"5", one.SumHour)
f.MergeCell("Sheet"+itoa, num+"1", num+"2")
numi = numi + 1
num = getColumnName(numi)
f.SetCellValue("Sheet"+itoa, num+"1", "合计工天")
f.SetCellValue("Sheet"+itoa, num+"4", one.SumDay)
f.SetCellValue("Sheet"+itoa, num+"5", one.SumDay)
f.MergeCell("Sheet"+itoa, num+"1", num+"2")
numi = numi + 1
num = getColumnName(numi)
f.SetCellValue("Sheet"+itoa, num+"1", "工人签名")
f.MergeCell("Sheet"+itoa, num+"1", num+"2")
numi = numi + 1
num = getColumnName(numi)
f.SetCellValue("Sheet"+itoa, num+"1", "备注")
f.MergeCell("Sheet"+itoa, num+"1", num+"2")
}
// 设置工作簿的默认工作表
f.SetActiveSheet(1)
// 根据指定路径保存文件
str := FileName()
filePath = coryCommon.Temporary + "/" + str
getwd, err := os.Getwd()
err = f.SaveAs(getwd + filePath)
if err != nil {
fmt.Println(err)
return "", "", ""
}
return getwd + filePath, filePath, str
}
// ExcelTwo 多条数据在一个工作簿
func ExcelTwo(oneDateOneList []*wxApplet.PunchingCardRecordOne) (file string, filePath string, fileName string) {
f := excelize.NewFile()
defer func() {
if err := f.Close(); err != nil {
fmt.Println(err)
}
}()
bottomStyleId, err := f.NewStyle(&excelize.Style{
Border: []excelize.Border{
{Type: "bottom", Color: "000000", Style: 1},
//{Type: "diagonalDown", Color: "000000", Style: 5},
//{Type: "diagonalUp", Color: "A020F0", Style: 6},
},
//背景
Fill: excelize.Fill{
Type: "pattern",
Color: []string{"#e6ecf0"},
Pattern: 1,
},
////字体
//Font: &excelize.Font{
// Color: "#ffffff", // 字体颜色,这里使用蓝色 (#0000FF)
//},
})
//全样式
styleId, err := 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},
},
//居中
Alignment: &excelize.Alignment{
Vertical: "center", // 上下居中
Horizontal: "center", // 左右居中
},
//背景
Fill: excelize.Fill{
Type: "pattern",
Color: []string{"#e6ecf0"},
Pattern: 1,
},
////字体
//Font: &excelize.Font{
// Color: "#ffffff", // 字体颜色,这里使用蓝色 (#0000FF)
//},
})
// 每组数据隔N行另起
bookNum := 0
for x, one := range oneDateOneList {
if one.Name == "" {
continue
}
itoa := "1" //第一个工作簿
var num = "0" //循环生成列
var numInt = 1 //循环列对应的编号
var numi = 0 //循环过后需要的列
for i := 0; i < len(one.PunchCard); i++ {
//设置1、2行高度
err := f.SetRowHeight("Sheet"+itoa, bookNum+1, 36)
err = f.SetRowHeight("Sheet"+itoa, bookNum+2, 14)
if err != nil {
fmt.Println(err)
return
}
//1、左侧固定 ABC的1~2
f.SetCellValue("Sheet"+itoa, "A"+strconv.Itoa((bookNum+1)), "序号")
f.MergeCell("Sheet"+itoa, "A"+strconv.Itoa((bookNum+1)), "A"+strconv.Itoa((bookNum+2)))
f.SetCellStyle("Sheet"+itoa, "A"+strconv.Itoa((bookNum+1)), "A"+strconv.Itoa((bookNum+2)), styleId)
f.SetCellValue("Sheet"+itoa, "B"+strconv.Itoa((bookNum+1)), "姓名")
f.MergeCell("Sheet"+itoa, "B"+strconv.Itoa((bookNum+1)), "B"+strconv.Itoa((bookNum+2)))
f.SetCellStyle("Sheet"+itoa, "B"+strconv.Itoa((bookNum+1)), "B"+strconv.Itoa((bookNum+2)), styleId)
f.SetCellValue("Sheet"+itoa, "C"+strconv.Itoa((bookNum+1)), "")
f.MergeCell("Sheet"+itoa, "C"+strconv.Itoa((bookNum+1)), "C"+strconv.Itoa((bookNum+2)))
f.SetCellStyle("Sheet"+itoa, "C"+strconv.Itoa((bookNum+1)), "C"+strconv.Itoa((bookNum+2)), styleId)
//2、左侧固定 ABC的3~4
f.SetCellValue("Sheet"+itoa, "A"+strconv.Itoa((bookNum+3)), x+1)
f.MergeCell("Sheet"+itoa, "A"+strconv.Itoa((bookNum+3)), "A"+strconv.Itoa((bookNum+4)))
f.SetCellStyle("Sheet"+itoa, "A"+strconv.Itoa((bookNum+3)), "A"+strconv.Itoa((bookNum+4)), styleId)
f.SetCellValue("Sheet"+itoa, "B"+strconv.Itoa((bookNum+3)), one.Name)
f.MergeCell("Sheet"+itoa, "B"+strconv.Itoa((bookNum+3)), "B"+strconv.Itoa((bookNum+4)))
f.SetCellStyle("Sheet"+itoa, "B"+strconv.Itoa((bookNum+3)), "B"+strconv.Itoa((bookNum+4)), styleId)
//f.SetCellValue("Sheet"+itoa, "c"+strconv.Itoa((bookNum+3)), "打卡记录")
f.SetCellValue("Sheet"+itoa, "c"+strconv.Itoa((bookNum+3)), "工时")
f.MergeCell("Sheet"+itoa, "c"+strconv.Itoa((bookNum+3)), "c"+strconv.Itoa((bookNum+4)))
f.SetCellStyle("Sheet"+itoa, "c"+strconv.Itoa((bookNum+3)), "c"+strconv.Itoa((bookNum+4)), styleId)
//3、中间数据---------从4开始创建列
num = getColumnName(4 + i)
f.SetCellValue("Sheet"+itoa, num+strconv.Itoa((bookNum+2)), numInt) //循环列对应的编号
f.SetCellStyle("Sheet"+itoa, num+strconv.Itoa((bookNum+2)), num+strconv.Itoa((bookNum+2)), styleId) //循环列对应的编号
f.SetCellValue("Sheet"+itoa, num+strconv.Itoa((bookNum+3)), one.PunchCard[i].Clock) //循环生成列 时间
f.SetCellStyle("Sheet"+itoa, num+strconv.Itoa((bookNum+3)), num+strconv.Itoa((bookNum+3)), styleId) //循环生成列 时间
f.SetCellValue("Sheet"+itoa, num+strconv.Itoa((bookNum+4)), one.PunchCard[i].Hour) //循环生成列 统计
f.SetCellStyle("Sheet"+itoa, num+strconv.Itoa((bookNum+4)), num+strconv.Itoa((bookNum+4)), styleId) //循环生成列 统计
numi = 4 + i
numInt = numInt + 1
//最后一个总计
if i == len(one.PunchCard)-1 {
f.SetCellValue("Sheet"+itoa, num+strconv.Itoa((bookNum+5)), "总计") //循环生成列
f.SetCellStyle("Sheet"+itoa, num+strconv.Itoa((bookNum+5)), "总计", styleId) //循环生成列
}
}
f.SetCellValue("Sheet"+itoa, "D"+strconv.Itoa((bookNum+1)), one.Years)
f.MergeCell("Sheet"+itoa, getColumnName(4)+strconv.Itoa(bookNum+1), getColumnName(numi)+strconv.Itoa(bookNum+1))
f.SetCellStyle("Sheet"+itoa, getColumnName(4)+strconv.Itoa(bookNum+1), getColumnName(numi)+strconv.Itoa(bookNum+1), styleId)
numi = numi + 1
num = getColumnName(numi)
//4、右侧不清楚
f.SetCellValue("Sheet"+itoa, num+strconv.Itoa((bookNum+1)), "合计工时")
f.MergeCell("Sheet"+itoa, num+strconv.Itoa(bookNum+1), getColumnName(numi)+strconv.Itoa(bookNum+3))
f.SetCellStyle("Sheet"+itoa, num+strconv.Itoa(bookNum+1), getColumnName(numi)+strconv.Itoa(bookNum+3), styleId)
f.SetCellValue("Sheet"+itoa, num+strconv.Itoa((bookNum+4)), one.SumHour)
f.MergeCell("Sheet"+itoa, num+strconv.Itoa(bookNum+4), getColumnName(numi)+strconv.Itoa(bookNum+4))
f.SetCellStyle("Sheet"+itoa, num+strconv.Itoa(bookNum+4), getColumnName(numi)+strconv.Itoa(bookNum+4), styleId)
f.SetCellValue("Sheet"+itoa, num+strconv.Itoa((bookNum+5)), one.SumHour)
f.MergeCell("Sheet"+itoa, num+strconv.Itoa(bookNum+5), getColumnName(numi)+strconv.Itoa(bookNum+5))
f.SetCellStyle("Sheet"+itoa, num+strconv.Itoa(bookNum+5), getColumnName(numi)+strconv.Itoa(bookNum+5), styleId)
numi = numi + 1
num = getColumnName(numi)
f.SetCellValue("Sheet"+itoa, num+strconv.Itoa((bookNum+1)), "合计工天")
f.MergeCell("Sheet"+itoa, num+strconv.Itoa(bookNum+1), getColumnName(numi)+strconv.Itoa(bookNum+3))
f.SetCellStyle("Sheet"+itoa, num+strconv.Itoa(bookNum+1), getColumnName(numi)+strconv.Itoa(bookNum+3), styleId)
f.SetCellValue("Sheet"+itoa, num+strconv.Itoa((bookNum+4)), one.SumDay)
f.MergeCell("Sheet"+itoa, num+strconv.Itoa(bookNum+4), getColumnName(numi)+strconv.Itoa(bookNum+4))
f.SetCellStyle("Sheet"+itoa, num+strconv.Itoa(bookNum+4), getColumnName(numi)+strconv.Itoa(bookNum+4), styleId)
f.SetCellValue("Sheet"+itoa, num+strconv.Itoa((bookNum+5)), one.SumDay)
f.MergeCell("Sheet"+itoa, num+strconv.Itoa(bookNum+5), getColumnName(numi)+strconv.Itoa(bookNum+5))
f.SetCellStyle("Sheet"+itoa, num+strconv.Itoa(bookNum+5), getColumnName(numi)+strconv.Itoa(bookNum+5), styleId)
numi = numi + 1
num = getColumnName(numi)
f.SetCellValue("Sheet"+itoa, num+strconv.Itoa((bookNum+1)), "工人签名")
f.MergeCell("Sheet"+itoa, num+strconv.Itoa((bookNum+1)), num+strconv.Itoa((bookNum+3)))
f.SetCellStyle("Sheet"+itoa, num+strconv.Itoa((bookNum+1)), num+strconv.Itoa((bookNum+3)), styleId)
f.SetCellValue("Sheet"+itoa, num+strconv.Itoa((bookNum+4)), "")
f.MergeCell("Sheet"+itoa, num+strconv.Itoa((bookNum+4)), num+strconv.Itoa((bookNum+5)))
f.SetCellStyle("Sheet"+itoa, num+strconv.Itoa((bookNum+4)), num+strconv.Itoa((bookNum+5)), styleId)
numi = numi + 1
num = getColumnName(numi)
f.SetCellValue("Sheet"+itoa, num+strconv.Itoa((bookNum+1)), "备注")
f.MergeCell("Sheet"+itoa, num+strconv.Itoa((bookNum+1)), num+strconv.Itoa((bookNum+3)))
f.SetCellStyle("Sheet"+itoa, num+strconv.Itoa((bookNum+1)), num+strconv.Itoa((bookNum+3)), styleId)
f.SetCellValue("Sheet"+itoa, num+strconv.Itoa((bookNum+4)), "")
f.MergeCell("Sheet"+itoa, num+strconv.Itoa((bookNum+4)), num+strconv.Itoa((bookNum+5)))
f.SetCellStyle("Sheet"+itoa, num+strconv.Itoa((bookNum+4)), num+strconv.Itoa((bookNum+5)), styleId)
f.SetCellStyle("Sheet"+itoa, "A"+strconv.Itoa((bookNum+5)), getColumnName(numi-4)+strconv.Itoa((bookNum+5)), bottomStyleId)
bookNum = bookNum + 10
}
// 设置工作簿的默认工作表
f.SetActiveSheet(1)
// 根据指定路径保存文件
str := FileName()
filePath = coryCommon.Temporary + "/" + str
getwd, err := os.Getwd()
err = f.SaveAs(getwd + filePath)
if err != nil {
fmt.Println(err)
return "", "", ""
}
return getwd + filePath, filePath, str
}
// ExcelThree 多个工作簿,每个工作簿有多条数据
func ExcelThree(gey []*wxApplet.GroupEntity) (file string, filePath string, fileName string) {
f := excelize.NewFile()
defer func() {
if err := f.Close(); err != nil {
fmt.Println(err)
}
}()
bottomStyleId, err := f.NewStyle(&excelize.Style{
Border: []excelize.Border{
{Type: "bottom", Color: "000000", Style: 1},
//{Type: "diagonalDown", Color: "000000", Style: 5},
//{Type: "diagonalUp", Color: "A020F0", Style: 6},
},
//背景
Fill: excelize.Fill{
Type: "pattern",
Color: []string{"#e6ecf0"},
Pattern: 1,
},
////字体
//Font: &excelize.Font{
// Color: "#ffffff", // 字体颜色,这里使用蓝色 (#0000FF)
//},
})
//全样式
styleId, err := 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},
},
//居中
Alignment: &excelize.Alignment{
Vertical: "center", // 上下居中
Horizontal: "center", // 左右居中
},
//背景
Fill: excelize.Fill{
Type: "pattern",
Color: []string{"#e6ecf0"},
Pattern: 1,
},
////字体
//Font: &excelize.Font{
// Color: "#ffffff", // 字体颜色,这里使用蓝色 (#0000FF)
//},
})
//工作簿
for _, data := range gey {
//itoa := strconv.Itoa(y + 1)
itoa := data.GroupName
_, err = f.NewSheet(itoa)
oneDateOneList := data.PunchingCardRecordOne
//工作簿里面的数据
bookNum := 0 // 每组数据隔N行另起
for x, one := range oneDateOneList {
//itoa := "1" //第一个工作簿
var num = "0" //循环生成列
var numInt = 1 //循环列对应的编号
var numi = 0 //循环过后需要的列
for i := 0; i < len(one.PunchCard); i++ {
//设置1、2行高度
err := f.SetRowHeight(itoa, bookNum+1, 36)
err = f.SetRowHeight(itoa, bookNum+2, 14)
if err != nil {
fmt.Println(err)
return
}
//1、左侧固定 ABC的1~2
f.SetCellValue(itoa, "A"+strconv.Itoa((bookNum+1)), "序号")
f.MergeCell(itoa, "A"+strconv.Itoa((bookNum+1)), "A"+strconv.Itoa((bookNum+2)))
f.SetCellStyle(itoa, "A"+strconv.Itoa((bookNum+1)), "A"+strconv.Itoa((bookNum+2)), styleId)
f.SetCellValue(itoa, "B"+strconv.Itoa((bookNum+1)), "姓名")
f.MergeCell(itoa, "B"+strconv.Itoa((bookNum+1)), "B"+strconv.Itoa((bookNum+2)))
f.SetCellStyle(itoa, "B"+strconv.Itoa((bookNum+1)), "B"+strconv.Itoa((bookNum+2)), styleId)
f.SetCellValue(itoa, "C"+strconv.Itoa((bookNum+1)), "")
f.MergeCell(itoa, "C"+strconv.Itoa((bookNum+1)), "C"+strconv.Itoa((bookNum+2)))
f.SetCellStyle(itoa, "C"+strconv.Itoa((bookNum+1)), "C"+strconv.Itoa((bookNum+2)), styleId)
//2、左侧固定 ABC的3~4
f.SetCellValue(itoa, "A"+strconv.Itoa((bookNum+3)), x+1)
f.MergeCell(itoa, "A"+strconv.Itoa((bookNum+3)), "A"+strconv.Itoa((bookNum+4)))
f.SetCellStyle(itoa, "A"+strconv.Itoa((bookNum+3)), "A"+strconv.Itoa((bookNum+4)), styleId)
f.SetCellValue(itoa, "B"+strconv.Itoa((bookNum+3)), one.Name)
f.MergeCell(itoa, "B"+strconv.Itoa((bookNum+3)), "B"+strconv.Itoa((bookNum+4)))
f.SetCellStyle(itoa, "B"+strconv.Itoa((bookNum+3)), "B"+strconv.Itoa((bookNum+4)), styleId)
//f.SetCellValue(itoa, "c"+strconv.Itoa((bookNum+3)), "打卡记录")
f.SetCellValue(itoa, "c"+strconv.Itoa((bookNum+3)), "工时")
f.MergeCell(itoa, "c"+strconv.Itoa((bookNum+3)), "c"+strconv.Itoa((bookNum+4)))
f.SetCellStyle(itoa, "c"+strconv.Itoa((bookNum+3)), "c"+strconv.Itoa((bookNum+4)), styleId)
//3、中间数据---------从4开始创建列
num = getColumnName(4 + i)
f.SetCellValue(itoa, num+strconv.Itoa((bookNum+2)), numInt) //循环列对应的编号
f.SetCellStyle(itoa, num+strconv.Itoa((bookNum+2)), num+strconv.Itoa((bookNum+2)), styleId) //循环列对应的编号
f.SetCellValue(itoa, num+strconv.Itoa((bookNum+3)), one.PunchCard[i].Clock) //循环生成列 时间
f.SetCellStyle(itoa, num+strconv.Itoa((bookNum+3)), num+strconv.Itoa((bookNum+3)), styleId) //循环生成列 时间
f.SetCellValue(itoa, num+strconv.Itoa((bookNum+4)), one.PunchCard[i].Hour) //循环生成列 统计
f.SetCellStyle(itoa, num+strconv.Itoa((bookNum+4)), num+strconv.Itoa((bookNum+4)), styleId) //循环生成列 统计
numi = 4 + i
numInt = numInt + 1
//最后一个总计
if i == len(one.PunchCard)-1 {
f.SetCellValue(itoa, num+strconv.Itoa((bookNum+5)), "总计") //循环生成列
f.SetCellStyle(itoa, num+strconv.Itoa((bookNum+5)), "总计", styleId) //循环生成列
}
}
f.SetCellValue(itoa, "D"+strconv.Itoa((bookNum+1)), one.Years)
f.MergeCell(itoa, getColumnName(4)+strconv.Itoa(bookNum+1), getColumnName(numi)+strconv.Itoa(bookNum+1))
f.SetCellStyle(itoa, getColumnName(4)+strconv.Itoa(bookNum+1), getColumnName(numi)+strconv.Itoa(bookNum+1), styleId)
numi = numi + 1
num = getColumnName(numi)
//4、右侧不清楚
f.SetCellValue(itoa, num+strconv.Itoa((bookNum+1)), "合计工时")
f.MergeCell(itoa, num+strconv.Itoa(bookNum+1), getColumnName(numi)+strconv.Itoa(bookNum+3))
f.SetCellStyle(itoa, num+strconv.Itoa(bookNum+1), getColumnName(numi)+strconv.Itoa(bookNum+3), styleId)
f.SetCellValue(itoa, num+strconv.Itoa((bookNum+4)), one.SumHour)
f.MergeCell(itoa, num+strconv.Itoa(bookNum+4), getColumnName(numi)+strconv.Itoa(bookNum+4))
f.SetCellStyle(itoa, num+strconv.Itoa(bookNum+4), getColumnName(numi)+strconv.Itoa(bookNum+4), styleId)
f.SetCellValue(itoa, num+strconv.Itoa((bookNum+5)), one.SumHour)
f.MergeCell(itoa, num+strconv.Itoa(bookNum+5), getColumnName(numi)+strconv.Itoa(bookNum+5))
f.SetCellStyle(itoa, num+strconv.Itoa(bookNum+5), getColumnName(numi)+strconv.Itoa(bookNum+5), styleId)
numi = numi + 1
num = getColumnName(numi)
f.SetCellValue(itoa, num+strconv.Itoa((bookNum+1)), "合计工天")
f.MergeCell(itoa, num+strconv.Itoa(bookNum+1), getColumnName(numi)+strconv.Itoa(bookNum+3))
f.SetCellStyle(itoa, num+strconv.Itoa(bookNum+1), getColumnName(numi)+strconv.Itoa(bookNum+3), styleId)
f.SetCellValue(itoa, num+strconv.Itoa((bookNum+4)), one.SumDay)
f.MergeCell(itoa, num+strconv.Itoa(bookNum+4), getColumnName(numi)+strconv.Itoa(bookNum+4))
f.SetCellStyle(itoa, num+strconv.Itoa(bookNum+4), getColumnName(numi)+strconv.Itoa(bookNum+4), styleId)
f.SetCellValue(itoa, num+strconv.Itoa((bookNum+5)), one.SumDay)
f.MergeCell(itoa, num+strconv.Itoa(bookNum+5), getColumnName(numi)+strconv.Itoa(bookNum+5))
f.SetCellStyle(itoa, num+strconv.Itoa(bookNum+5), getColumnName(numi)+strconv.Itoa(bookNum+5), styleId)
numi = numi + 1
num = getColumnName(numi)
f.SetCellValue(itoa, num+strconv.Itoa((bookNum+1)), "工人签名")
f.MergeCell(itoa, num+strconv.Itoa((bookNum+1)), num+strconv.Itoa((bookNum+3)))
f.SetCellStyle(itoa, num+strconv.Itoa((bookNum+1)), num+strconv.Itoa((bookNum+3)), styleId)
f.SetCellValue(itoa, num+strconv.Itoa((bookNum+4)), "")
f.MergeCell(itoa, num+strconv.Itoa((bookNum+4)), num+strconv.Itoa((bookNum+5)))
f.SetCellStyle(itoa, num+strconv.Itoa((bookNum+4)), num+strconv.Itoa((bookNum+5)), styleId)
numi = numi + 1
num = getColumnName(numi)
f.SetCellValue(itoa, num+strconv.Itoa((bookNum+1)), "备注")
f.MergeCell(itoa, num+strconv.Itoa((bookNum+1)), num+strconv.Itoa((bookNum+3)))
f.SetCellStyle(itoa, num+strconv.Itoa((bookNum+1)), num+strconv.Itoa((bookNum+3)), styleId)
f.SetCellValue(itoa, num+strconv.Itoa((bookNum+4)), "")
f.MergeCell(itoa, num+strconv.Itoa((bookNum+4)), num+strconv.Itoa((bookNum+5)))
f.SetCellStyle(itoa, num+strconv.Itoa((bookNum+4)), num+strconv.Itoa((bookNum+5)), styleId)
f.SetCellStyle(itoa, "A"+strconv.Itoa((bookNum+5)), getColumnName(numi-4)+strconv.Itoa((bookNum+5)), bottomStyleId)
//err = f.SetCellStyle("Sheet1", "A"+strconv.Itoa((bookNum+1)), "A"+strconv.Itoa((bookNum+5)), leftStyleId)
//err = f.SetCellStyle("Sheet1", "I"+strconv.Itoa((bookNum+1)), "I"+strconv.Itoa((bookNum+5)), rightStyleId)
//err = f.SetCellStyle("Sheet1", "A"+strconv.Itoa((bookNum+1)), "I"+strconv.Itoa((bookNum+1)), topStyleId)
//err = f.SetCellStyle("Sheet1", "A"+strconv.Itoa((bookNum+5)), "I"+strconv.Itoa((bookNum+5)), bottomStyleId)
bookNum = bookNum + 10
}
}
// 设置工作簿的默认工作表
f.SetActiveSheet(1)
// 根据指定路径保存文件
str := FileName()
filePath = coryCommon.Temporary + "/" + str
getwd, err := os.Getwd()
err = f.SaveAs(getwd + filePath)
if err != nil {
fmt.Println(err)
return "", "", ""
}
return getwd + filePath, filePath, str
}
// 根据列索引获取列的字母标识 比如1就是A 30就是AD
func getColumnName(index int) string {
var columnName string
for index > 0 {
mod := (index - 1) % 26
columnName = string('A'+mod) + columnName
index = (index - 1) / 26
}
return columnName
}
// FileName 生成时间戳文件名
func FileName() (str string) {
// 获取当前时间
currentTime := time.Now()
// 格式化时间戳为字符串
timestamp := currentTime.Format("20060102150405")
// 生成文件名
fileName := fmt.Sprintf("zm_%s.xlsx", timestamp)
return fileName
}

View File

@ -0,0 +1,55 @@
package excelUtil
type Style struct {
Border []Border
Fill Fill
Font *Font
Alignment *Alignment
Protection *Protection
NumFmt int
DecimalPlaces int
CustomNumFmt *string
NegRed bool
}
type Fill struct {
Type string
Pattern int
Color []string
Shading int
}
type Protection struct {
Hidden bool
Locked bool
}
type Font struct {
Bold bool
Italic bool
Underline string
Family string
Size float64
Strike bool
Color string
ColorIndexed int
ColorTheme *int
ColorTint float64
VertAlign string
}
type Border struct {
Type string
Color string
Style int
}
type Alignment struct {
Horizontal string
Indent int
JustifyLastLine bool
ReadingOrder uint64
RelativeIndent int
ShrinkToFit bool
TextRotation int
Vertical string
WrapText bool
}

View File

@ -0,0 +1,209 @@
package coryCommon
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
)
const (
MinShortEdge = 15
MaxLongEdge = 4096
MaxSize = 1024 * 1024 // 4MB
SupportedExts = ".jpg .jpeg .png .bmp"
)
func ImgDataBase64(filePath string) (err error, encodedURL string) {
file, err := os.Open(filePath)
if err != nil {
fmt.Println("打开图像文件失败:", err)
err = errors.New("打开图像文件失败")
return
}
defer file.Close()
fileInfo, err := file.Stat()
if err != nil {
fmt.Println("读取图像文件信息失败:", err)
err = errors.New("读取图像文件信息失败")
return
}
fileSize := fileInfo.Size()
if fileSize > MaxSize {
fmt.Println("图像文件大小超过限制:", fileSize)
err = errors.New("图像文件大小超过限制")
return
}
imgData, err := ioutil.ReadAll(file)
if err != nil {
fmt.Println("读取图像文件内容失败:", err)
err = errors.New("读取图像文件内容失败")
return
}
bounds, format, err := image.DecodeConfig(bytes.NewReader(imgData))
if err != nil {
fmt.Println("解码图像配置失败:", err)
err = errors.New("解码图像配置失败")
return
}
if format != "jpeg" && format != "jpg" && format != "png" && format != "bmp" {
fmt.Println("不支持的图像格式:", format)
//err = errors.New("不支持的图像格式")
err = errors.New("支持的图像格式为jpg、png、gif、bmp")
return
}
width := bounds.Width
height := bounds.Height
shortEdge := width
if height < width {
shortEdge = height
}
if shortEdge < MinShortEdge {
fmt.Println("图像尺寸的最短边小于要求:", shortEdge)
str := "图像尺寸的最短边小于要求:" + strconv.Itoa(shortEdge)
err = errors.New(str)
return
}
if width > MaxLongEdge || height > MaxLongEdge {
fmt.Println("图像尺寸的最长边超过限制:", width, height)
str := "图像尺寸的最长边超过限制:" + strconv.Itoa(width) + strconv.Itoa(height)
err = errors.New(str)
return
}
// Base64编码图像数据
encodedStr := base64.StdEncoding.EncodeToString(imgData)
// URL编码
//urlEncodedStr := url.QueryEscape(encodedStr)
//fmt.Println("Base64编码并URL编码后的图像数据:", urlEncodedStr)
return err, encodedStr
}
/*
EncodeAndUrlEncodeImage 图片文件的绝对路径
功能:
图像数据base64编码后进行urlencode需去掉编码头data:image/jpeg;base64, 要求base64编码和urlencode后大小不超过N兆最短边至少15px最长边最大4096px,支持jpg/jpeg/png/bmp格式
*/
func EncodeAndUrlEncodeImage(filePath string, size int64) (string, error) {
// 检查文件是否存在
_, err := os.Stat(filePath)
if os.IsNotExist(err) {
return "", fmt.Errorf("文件 %s 不存在", filePath)
}
// 检查文件大小
fileSize := getFileSize(filePath)
if fileSize > (size * MaxSize) {
return "", fmt.Errorf("文件大小超过限制")
}
// 检查图像尺寸
img, err := loadImage(filePath)
if err != nil {
return "", fmt.Errorf("无法加载图像: %s", err)
}
bounds := img.Bounds()
if !isValidSize(bounds) {
return "", fmt.Errorf("图像尺寸不符合要求")
}
// 读取文件
fileData, err := ioutil.ReadFile(filePath)
if err != nil {
return "", fmt.Errorf("无法读取文件: %s", err)
}
// 获取文件后缀名
ext := filepath.Ext(filePath)
// 检查是否支持的文件格式
if !isSupportedFormat(ext) {
return "", fmt.Errorf("不支持的文件格式")
}
// 将图像数据进行 base64 编码
encodedData := base64.StdEncoding.EncodeToString(fileData)
// 去掉编码头(如:"data:image/jpeg;base64,"
encodedData = removeEncodingHeader(encodedData, ext)
//// 对 base64 编码后的数据进行 URL 编码
//encodedData = urlEncode(encodedData)
return encodedData, nil
}
func getFileSize(filePath string) int64 {
fileInfo, err := os.Stat(filePath)
if err != nil {
return 0
}
return fileInfo.Size()
}
func loadImage(filePath string) (image.Image, error) {
file, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer file.Close()
img, _, err := image.Decode(file)
if err != nil {
return nil, err
}
return img, nil
}
func isValidSize(bounds image.Rectangle) bool {
width := bounds.Dx()
height := bounds.Dy()
if width < MinShortEdge || height < MinShortEdge {
return false
}
if width > MaxLongEdge || height > MaxLongEdge {
return false
}
return true
}
func isSupportedFormat(ext string) bool {
ext = strings.ToLower(ext)
return strings.Contains(SupportedExts, ext)
}
func removeEncodingHeader(encodedData string, ext string) string {
header := fmt.Sprintf("data:image/%s;base64,", ext[1:])
if strings.HasPrefix(encodedData, header) {
encodedData = strings.TrimPrefix(encodedData, header)
}
return encodedData
}
func urlEncode(data string) string {
return url.PathEscape(data)
}

View File

@ -0,0 +1,38 @@
package coryCommon
import (
"errors"
"math/big"
)
// PercentageFunc 百分比计算精度很高比如说总数100完成100最终得到结果为99.99999999%那么会直接操作成100
// precision设置精度
// total总数据量
// finish完成度
func PercentageFunc(precision uint, total, finish float64) (consequence float64, err error) {
if total == 0 {
err = errors.New("总数据不能为初始值")
return 0, err
}
if precision == 0 {
err = errors.New("精度不能为初始值")
return 0, err
}
consequence = 0
// 定义大浮点数
numerator := big.NewFloat(100)
denominator := big.NewFloat(total)
result := big.NewFloat(finish)
// 设置精度
//var precision uint = 100 // 设置为你需要的精度
numerator.SetPrec(precision)
denominator.SetPrec(precision)
result.SetPrec(precision)
// 计算结果
result.Quo(result, denominator)
result.Mul(result, numerator)
// 截取到两位小数
resultRounded, _ := result.Float64()
consequence = float64(int(resultRounded*100)) / 100 // 保留两位小数
return
}

View File

@ -0,0 +1,111 @@
package coryCommon
import (
"fmt"
"github.com/golang/geo/s2"
toolTurf "github.com/tiger1103/gfast/v3/api/v1/common/tool/turf"
"github.com/tomchavakis/geojson/geometry"
"github.com/tomchavakis/turf-go"
)
// DetailedMap shp文件数据
type DetailedMap struct {
Positions []struct {
Lng float64 `json:"lng"`
Lat float64 `json:"lat"`
Alt float64 `json:"alt"`
} `json:"positions"`
Width string `json:"width"`
Color string `json:"color"`
Alpha string `json:"alpha"`
Name string `json:"name"`
Property string `json:"property"`
TxtMemo string `json:"TxtMemo"`
ShapeLeng string `json:"Shape_Leng"`
ShapeArea string `json:"Shape_Area"`
Range struct {
MinX float64 `json:"min_x"`
MinY float64 `json:"min_y"`
MaxX float64 `json:"max_x"`
MaxY float64 `json:"max_y"`
} `json:"range"`
}
// RectangularFrameRange 是否在矩形框范围内,是否在打卡范围内
func RectangularFrameRange(dataInfo DetailedMap, locationLng float64, locationLat float64) (flag bool) {
//1、组装数据 84坐标
polygon := [][]float64{}
for _, data := range dataInfo.Positions {
polygon = append(polygon, []float64{data.Lng, data.Lat})
}
//3、判断位置
distance := toolTurf.BooleanPointInPolygon([]float64{locationLng, locationLat}, polygon)
if distance {
fmt.Println("点在矩形框内")
return true
} else {
fmt.Println("点在矩形框外")
return false
}
}
// FEIQI_RectangularFrameRange 是否在矩形框范围内,是否在打卡范围内 !!!!!!!!!!!!!!!!!有问题
func FEIQI_RectangularFrameRange(dataInfo DetailedMap, locationLng float64, locationLat float64) (flag bool) {
//1、组装数据
var pl geometry.Polygon
var ls []geometry.LineString
var pt []geometry.Point
for _, data := range dataInfo.Positions {
wgs84 := LatLng{Latitude: data.Lat, Longitude: data.Lng}
t1 := WGS84ToEPSG900913(wgs84)
var p geometry.Point
p.Lng = t1.Latitude
p.Lat = t1.Longitude
pt = append(pt, p)
}
var lsTwo geometry.LineString
lsTwo.Coordinates = pt
ls = append(ls, lsTwo)
pl.Coordinates = append(pl.Coordinates, lsTwo)
//2、当前人所在位置
locationLng, locationLat = GCJ02toWGS84(locationLng, locationLat)
wgs84 := LatLng{Latitude: locationLng, Longitude: locationLat}
t1 := WGS84ToEPSG900913(wgs84)
myPoint := geometry.Point{
Lng: t1.Longitude,
Lat: t1.Latitude,
}
//3、判断myPoint是否在pl框内
distance, _ := turf.PointInPolygon(myPoint, pl)
if distance {
fmt.Println("点在矩形框内")
return true
} else {
fmt.Println("点在矩形框外")
return false
}
}
type LatLng struct {
Latitude float64
Longitude float64
}
// WGS84ToEPSG900913 将WGS84坐标转换为EPSG-900913坐标
func WGS84ToEPSG900913(wgs84 LatLng) LatLng {
// 将WGS84坐标转换为s2.LatLng
ll := s2.LatLngFromDegrees(wgs84.Latitude, wgs84.Longitude)
// 创建s2.Point
p := s2.PointFromLatLng(ll)
// 计算EPSG-900913坐标
//epsgX, epsgY := p.Normalize()
normalize := p.Normalize()
// 将坐标范围映射到EPSG-900913的范围
epsgX := normalize.X * 20037508.34 / 180.0
epsgY := normalize.Y * 20037508.34 / 180.0
return LatLng{Latitude: epsgY, Longitude: epsgX}
}

View File

@ -0,0 +1,23 @@
package coryCommon
import (
"crypto/md5"
"encoding/hex"
"strings"
)
// MD5Hash 将字符串进行 MD5 加密,并返回 32 位长度的 MD5 散列值
func MD5Hash(input string) string {
// 将输入字符串转换为小写,以确保不区分大小写
input = strings.ToLower(input)
// 创建 MD5 散列对象
md5Hash := md5.New()
// 将输入字符串转换为字节数组,并传递给 MD5 散列对象
_, _ = md5Hash.Write([]byte(input))
// 计算 MD5 散列值
hashBytes := md5Hash.Sum(nil)
// 将散列值转换为 32 位的十六进制字符串
md5HashString := hex.EncodeToString(hashBytes)
return md5HashString
}

View File

@ -0,0 +1,69 @@
package coryCommon
import (
"github.com/gogf/gf/v2/net/gclient"
"github.com/gogf/gf/v2/os/gctx"
)
// 高德地图API https://lbs.amap.com/api/webservice/guide/api/georegeo
// InverseGeocoding 逆地理编码
func InverseGeocoding(location string) (we string) {
//请求路径
key := "3bbede95174c607a1ed4c479d3f637cc"
requestURL := "https://restapi.amap.com/v3/geocode/regeo?location=" + location + "&key=" + key
response, err := gclient.New().ContentJson().ContentType("application/x-www-form-urlencoded").Get(gctx.New(), requestURL)
if err != nil {
return
}
var dataInfo = ""
dataInfo = response.ReadAllString()
return dataInfo
}
type InverseGeocodingRep struct {
Status string `json:"status"`
Regeocode Regeocode `json:"regeocode"`
Info string `json:"info"`
Infocode string `json:"infocode"`
}
type Regeocode struct {
FormattedAddress string `json:"formatted_address"`
AddressComponent AddressComponent `json:"addressComponent"`
}
type AddressComponent struct {
//City string `json:"city"`
Province string `json:"province"`
Adcode string `json:"adcode"`
District string `json:"district"`
Towncode string `json:"towncode"`
//StreetNumber StreetNumber `json:"streetNumber"`
Country string `json:"country"`
Township string `json:"township"`
//BusinessAreas []One `json:"businessAreas"`
//Building Two `json:"building"`
//Neighborhood Two `json:"neighborhood"`
Citycode string `json:"citycode"`
}
type StreetNumber struct {
Number string `json:"number"`
Location string `json:"location"`
Direction string `json:"direction"`
Distance string `json:"distance"`
Street string `json:"street"`
}
type One struct {
Location string `json:"location"`
Name string `json:"name"`
Id string `json:"id"`
}
type Two struct {
Name []string `json:"name"`
Type []string `json:"type"`
}

View File

@ -0,0 +1,473 @@
package coryCommon
import (
"bufio"
"context"
"fmt"
"io"
"log"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/tiger1103/gfast/v3/library/liberr"
"golang.org/x/exp/rand"
)
func UploadFileS(ctx context.Context, fileHeader []*ghttp.UploadFile, fileUrl string, typeStr string) (pathStr string, err error) {
for i := range fileHeader {
var str string
if typeStr == "1" {
str, err = UploadFile(ctx, fileHeader[i], fileUrl)
}
if typeStr == "2" {
str, err = UploadFileTwo(ctx, fileHeader[i], fileUrl)
}
if err != nil {
liberr.ErrIsNil(ctx, err)
return
}
pathStr = pathStr + ResourcePublicToFunc("/"+str, 0) + ","
}
pathStr = pathStr[0 : len(pathStr)-1]
return
}
// UploadFile [文件名称为原来的文件名称] 上传文件(流) 也可以将http图片上传 返回:resource/public/upload_file/2023-11-28/tmp_c7858f0fd98d74cbe2c095125871aaf19c749c209ae33f5f.png
func UploadFile(ctx context.Context, fileHeader *ghttp.UploadFile, fileUrl string) (str string, err error) {
// 获取上传的文件流
if fileHeader == nil {
log.Println("Failed to get file")
liberr.ErrIsNil(ctx, err, "Failed to get file")
return "", err
}
ynr := Ynr(fileUrl)
// 在当前目录创建一个新文件用于保存上传的数据
lj := "/" + ynr + fileHeader.Filename
destFilePath := filepath.Join(".", lj)
destFile, err := os.Create(destFilePath)
if err != nil {
log.Println("Failed to create destination file:", err)
liberr.ErrIsNil(ctx, err)
return "", err
}
defer destFile.Close()
// 创建一个内存缓冲区作为文件数据的临时存储
buffer := make([]byte, 4096)
// 记录已接收的数据量,用于计算上传进度
var totalReceived int64
// 获取文件上传的数据流
file, err := fileHeader.Open()
if err != nil {
log.Println("Failed to open file:", err)
liberr.ErrIsNil(ctx, err)
return "", err
}
defer file.Close()
// 循环读取文件流,直到读取完整个文件
for {
// 从文件流中读取数据到缓冲区
n, err := file.Read(buffer)
if err != nil && err != io.EOF {
log.Println("Failed to read file:", err)
liberr.ErrIsNil(ctx, err)
return "", err
}
// 如果缓冲区没有数据,则表示已读取完整个文件
if n == 0 {
break
}
// 将缓冲区的数据写入目标文件
_, err = destFile.Write(buffer[:n])
if err != nil {
log.Println("Failed to write file:", err)
liberr.ErrIsNil(ctx, err)
return "", err
}
// 更新已接收的数据量
totalReceived += int64(n)
}
//// 返回上传文件的路径给客户端
//uploadPath, err := filepath.Abs(destFilePath)
//if err != nil {
// log.Println("Failed to get absolute file path:", err)
// liberr.ErrIsNil(ctx, err)
// return "", err
//}
//统一路径斜杠为/
str = filepath.ToSlash(lj)
return str, err
}
func UploadUniqueFile(ctx context.Context, fileHeader *ghttp.UploadFile, fileUrl string) (str string, err error) {
// 获取上传的文件流
if fileHeader == nil {
return "", err
}
ynr := Ynr(fileUrl)
// 在当前目录创建一个新文件用于保存上传的数据
// 在文件名中添加一个唯一的标识符,例如当前的时间戳
lj := fmt.Sprintf("%s_%d_%s", ynr, time.Now().Unix(), fileHeader.Filename)
destFilePath := filepath.Join(".", lj)
destFile, err := os.Create(destFilePath)
if err != nil {
log.Println("Failed to create destination file:", err)
liberr.ErrIsNil(ctx, err)
return "", err
}
defer destFile.Close()
// 创建一个内存缓冲区作为文件数据的临时存储
buffer := make([]byte, 4096)
// 记录已接收的数据量,用于计算上传进度
var totalReceived int64
// 获取文件上传的数据流
file, err := fileHeader.Open()
if err != nil {
log.Println("Failed to open file:", err)
liberr.ErrIsNil(ctx, err)
return "", err
}
defer file.Close()
// 循环读取文件流,直到读取完整个文件
for {
// 从文件流中读取数据到缓冲区
n, err := file.Read(buffer)
if err != nil && err != io.EOF {
return "", err
}
// 如果缓冲区没有数据,则表示已读取完整个文件
if n == 0 {
break
}
// 将缓冲区的数据写入目标文件
_, err = destFile.Write(buffer[:n])
if err != nil {
return "", err
}
// 更新已接收的数据量
totalReceived += int64(n)
}
// 统一路径斜杠为/
str = filepath.ToSlash(lj)
return str, err
}
// UploadFileTwo [文件名称为时间戳] 上传文件(流) 也可以将http图片上传 返回:resource/public/upload_file/2023-11-28/tmp_c7858f0fd98d74cbe2c095125871aaf19c749c209ae33f5f.png
func UploadFileTwo(ctx context.Context, fileHeader *ghttp.UploadFile, fileUrl string) (str string, err error) {
// 获取上传的文件流
if fileHeader == nil {
log.Println("Failed to get file")
liberr.ErrIsNil(ctx, err, "Failed to get file")
return "", err
}
ynr := Ynr(fileUrl)
// 在当前目录创建一个新文件用于保存上传的数据
lj := /* "/" +*/ ynr + FileName("login") + filepath.Ext(fileHeader.Filename)
destFilePath := filepath.Join(".", lj)
destFile, err := os.Create(destFilePath)
if err != nil {
log.Println("Failed to create destination file:", err)
liberr.ErrIsNil(ctx, err)
return "", err
}
defer destFile.Close()
// 创建一个内存缓冲区作为文件数据的临时存储
buffer := make([]byte, 4096)
// 记录已接收的数据量,用于计算上传进度
var totalReceived int64
// 获取文件上传的数据流
file, err := fileHeader.Open()
if err != nil {
log.Println("Failed to open file:", err)
liberr.ErrIsNil(ctx, err)
return "", err
}
defer file.Close()
// 循环读取文件流,直到读取完整个文件
for {
// 从文件流中读取数据到缓冲区
n, err := file.Read(buffer)
if err != nil && err != io.EOF {
log.Println("Failed to read file:", err)
liberr.ErrIsNil(ctx, err)
return "", err
}
// 如果缓冲区没有数据,则表示已读取完整个文件
if n == 0 {
break
}
// 将缓冲区的数据写入目标文件
_, err = destFile.Write(buffer[:n])
if err != nil {
log.Println("Failed to write file:", err)
liberr.ErrIsNil(ctx, err)
return "", err
}
// 更新已接收的数据量
totalReceived += int64(n)
}
//// 返回上传文件的路径给客户端
//uploadPath, err := filepath.Abs(destFilePath)
//if err != nil {
// log.Println("Failed to get absolute file path:", err)
// liberr.ErrIsNil(ctx, err)
// return "", err
//}
//统一路径斜杠为/
str = filepath.ToSlash(lj)
return str, err
}
// Ynr 创建时间文件夹 传递相对路径/resource/public/ 返回相对路径resource/public/2023-11-12/
func Ynr(baseDir string) (str string) {
// 获取当前时间
currentTime := time.Now()
// 格式化为年月日的字符串格式
dateString := currentTime.Format("2006-01-02")
dateString = baseDir + dateString
// 在相对路径上添加文件夹
destFilePath := filepath.Join(".", dateString)
dateString = destFilePath
// 检查文件夹是否已存在
_, err := os.Stat(dateString)
if os.IsNotExist(err) {
// 文件夹不存在,创建文件夹
err := os.MkdirAll(dateString, os.ModePerm)
if err != nil {
fmt.Println("创建文件夹失败:", err)
return
}
// fmt.Println("文件夹创建成功:", dateString)
} else {
// fmt.Println("文件夹已存在:", dateString)
}
return dateString + "/"
}
// FileName 【前缀】+获取当前时间+随机数得到文件名
func FileName(prefix string) (uniqueFileName string) {
currentTime := time.Now()
timestamp := currentTime.UnixNano() / int64(time.Millisecond)
randomNum := rand.Intn(1000)
uniqueFileName = fmt.Sprintf("%d_%d", timestamp, randomNum)
if prefix != "" {
uniqueFileName = prefix + uniqueFileName
}
return uniqueFileName
}
// FileInfo 传参:相对路径 获取文件信息(文件名称、文件后缀、文件大小(字节))
func FileInfo(filePath string) (name string, ext string, size int64) {
newPath := ""
if strings.Contains(filePath, "wxfile") {
newPath = strings.ReplaceAll(filePath, "/wxfile", GetCWD()+"/resource/public")
} else {
newPath = strings.ReplaceAll(filePath, "/file", GetCWD()+"/resource/public")
}
newPath = strings.ReplaceAll(newPath, "\\", "/")
filePath = newPath
// filePath = "D:\\Cory\\go\\中煤\\zmkg-back\\resource\\public\\upload_file\\2023-08-31\\cv6kxb89pd984rt5ze.png"
// 获取文件的基本名称(包括扩展名)
fileName := filepath.Base(filePath)
// 分割文件名和扩展名
nameParts := strings.Split(fileName, ".")
fileNameWithoutExt := nameParts[0]
fileExt := nameParts[1]
// 获取文件大小
fileInfo, err := os.Stat(filePath)
if err != nil {
fmt.Println("无法获取文件信息:", err)
return
}
fileSize := fileInfo.Size()
// 文件名称、文件后缀、文件大小(字节)
name = fileNameWithoutExt
ext = fileExt
size = fileSize
return
}
// OutJson 创建txt写入数据jsonData数据、absPath 绝对路径(带文件名和后缀)
func OutJson(jsonData []byte, absPath string) (flag bool, err error) {
absPath = strings.ReplaceAll(absPath, "\\", "/")
absPath = strings.ReplaceAll(absPath, "/file", GetCWD()+"/resource/public")
flag = false
// 创建要写入的文件
file, err := os.Create(absPath)
if err != nil {
fmt.Println("无法创建文件:", err)
return
}
defer file.Close()
// 将 JSON 数据写入文件
_, err = file.Write(jsonData)
if err != nil {
fmt.Println("无法写入文件:", err)
return
}
return true, err
}
// PutJson 读取文件
func PutJson(filePath string) (jsonData string, err error) {
filePath = strings.ReplaceAll(filePath, "/file", GetCWD()+"/resource/public")
filePath = strings.ReplaceAll(filePath, "\\", "/")
// filePath := "relative/path/to/file.txt"
file, err := os.Open(filePath)
if err != nil {
fmt.Println("打开文件错误:", err)
return
}
defer file.Close()
scanner := bufio.NewScanner(file)
// 设置一个足够大的缓冲区
const maxScanTokenSize = 128 * 1024
buf := make([]byte, maxScanTokenSize)
scanner.Buffer(buf, maxScanTokenSize)
for scanner.Scan() {
line := scanner.Text()
jsonData = jsonData + line
}
if scanner.Err() != nil {
fmt.Println("读取文件错误:", scanner.Err())
err = scanner.Err()
return
}
return
}
/*
BatchFile 批量刪除文件
filePath 相对路径
*/
func BatchFile(filePath []string) {
for _, data := range filePath {
// if strings.Contains(data, "file") || strings.Contains(data, "wxfile"){
newPath := ""
if strings.Contains(data, "/file") {
newPath = strings.Replace(data, "/file", GetCWD()+"/resource/public", 1)
} else if strings.Contains(data, "/wxfile") {
newPath = strings.Replace(data, "/wxfile", GetCWD()+"/resource/public", 1)
}
os.Remove(strings.ReplaceAll(newPath, "\\", "/"))
}
}
// FlagImg 判斷是否是圖片
func FlagImg(filePath string) (flag bool) {
filePathLower := strings.ToLower(filePath)
isImage := strings.HasSuffix(filePathLower, ".png") || strings.HasSuffix(filePathLower, ".jpg") || strings.HasSuffix(filePathLower, ".jpeg") || strings.HasSuffix(filePathLower, ".gif") || strings.HasSuffix(filePathLower, ".bmp")
if isImage {
flag = true
} else {
flag = false
}
return flag
}
// CreateDirectory 判断文件夹是否存在,不存在则创建文件夹
func CreateDirectory(folderName string) error {
// 检查文件夹是否已经存在
_, err := os.Stat(folderName)
if err == nil {
return nil
}
// 创建文件夹
err = os.Mkdir(folderName, 0o755)
if err != nil {
return err
}
return nil
}
// FileToFunc file转resource/public
func FileToFunc(path string, num int) (rpath string) {
if num == 1 {
rpath = strings.Replace(path, GetCWD()+"/file/", "/resource/public/", 1)
} else if num == 2 {
rpath = strings.Replace(path, "/file/", GetCWD()+"/resource/public/", 1)
} else if num == 3 {
rpath = strings.Replace(path, "/file/", "/wxfile/", 1)
} else if num == 4 {
rpath = strings.Replace(path, "/wxfile/", GetCWD()+"/resource/public/", 1)
} else if num == 5 {
rpath = strings.Replace(path, "/wxfile/", "/file/", 1)
} else {
rpath = strings.Replace(path, "/file/", "/resource/public/", 1)
}
rpath = filepath.ToSlash(rpath)
return
}
// ResourcePublicToFunc resource/public转file
func ResourcePublicToFunc(path string, num int) (rpath string) {
if num == 1 {
rpath = strings.Replace(path, GetCWD()+"/resource/public/", "/file/", 1)
} else if num == 2 {
rpath = strings.Replace(path, "/resource/public/", GetCWD()+"/resource/public/", 1)
} else if num == 3 {
rpath = strings.Replace(path, "/resource/public/", "/wxfile/", 1)
} else {
rpath = strings.Replace(path, "/resource/public/", "/file/", 1)
}
rpath = filepath.ToSlash(rpath)
return
}
// FormatRestrictionFunc 判断文件后缀是否匹配
func FormatRestrictionFunc(path, suffix string) (flag bool) {
split := strings.Split(suffix, ",")
for _, data := range split {
extension := filepath.Ext(path)
if extension == data {
return true
}
}
return false
}
// URLCoding 对url的特俗符号进行编码不对/进行编码(定制编码,将"/file/networkDisk/completion/admin37/2.jpg"的"networkDisk/completion/admin37/2.jpg"进行编码)
func URLCoding(path string) (filenPathCoding string) {
s2 := path[6 : len(path)-1]
split := strings.Split(s2, "/")
p := ""
for i2 := range split {
p = p + url.PathEscape(split[i2]) + "/"
}
p = p[0 : len(p)-1]
filenPathCoding = strings.ReplaceAll(path, s2, p)
return
}

View File

@ -0,0 +1,199 @@
package coryCommon
import (
"fmt"
"github.com/fogleman/gg"
"github.com/nfnt/resize"
"image/color"
"log"
"os"
"path/filepath"
"strings"
)
func GetCWD() string {
cwd, _ := os.Getwd()
return filepath.ToSlash(cwd)
}
func MultiPicture(pictureList string, compere string, meetingDate string, site string, teamName string, labourserviceName string) {
//1、分割字符串
split := strings.Split(pictureList, ",")
//2、循环添加水印
for i := range split {
filePath := split[i]
newPath := strings.ReplaceAll(filePath, "/wxfile", GetCWD()+"/resource/public")
newPath = strings.ReplaceAll(newPath, "\\", "/")
WatermarkFunc(newPath, compere, meetingDate, site, teamName, labourserviceName)
}
}
// WatermarkFunc 给图片设置水印和logo
func WatermarkFunc(filePath string, compere string, meetingDate string, site string, teamName string, labourserviceName string) {
// 检查文件是否存在
_, err := os.Stat(filePath)
if err != nil {
if os.IsNotExist(err) {
fmt.Println("文件不存在")
} else {
fmt.Println("发生错误:", err)
}
return
}
//// 获取文件名和后缀
//fileName := filepath.Base(filePath)
//fileExt := filepath.Ext(filePath)
//1、加载图片
srcImage, err := gg.LoadImage(filePath)
if err != nil {
log.Fatalf("打开图片失败: %v", err)
}
//2、加载logo水印
dc := gg.NewContextForImage(srcImage)
dc.SetRGB(1, 1, 1)
logoImage, err := gg.LoadImage(GetCWD() + "/resource/cory/zmlogo.jpg")
if err != nil {
log.Fatalf("打开 logo 图片失败: %v", err)
}
logoWidth := 80.0
logoHeight := float64(logoImage.Bounds().Dy()) * (logoWidth / float64(logoImage.Bounds().Dx()))
logoImage = resize.Resize(uint(logoWidth), uint(logoHeight), logoImage, resize.Lanczos3)
x := float64(logoWidth) + 10.0
y := float64(dc.Height()/2) + 96.0
dc.DrawImageAnchored(logoImage, int(x), int(y), 1.0, 1.0)
//3、设置字体
fontPath := GetCWD() + "/resource/cory/msyh.ttc"
if err != nil {
log.Fatalf("加载字体失败: %v", err)
}
dc.SetRGB(0, 0, 0)
//4、创建矩形框 背景透明
boxText := teamName
dc.SetRGBA255(0, 99, 175, 100)
rectangleX := x
rectangleY := y - logoHeight + 1
rectangleWidth := len(boxText) * 8
rectangleHeight := logoHeight - 1
dc.DrawRectangle(rectangleX, rectangleY, float64(rectangleWidth), rectangleHeight)
dc.Fill()
textFunc(dc, boxText, fontPath, 18.0, rectangleX, rectangleY, float64(rectangleWidth), rectangleHeight, 1)
//5、添加文字水印
text := "开会宣讲人:" + compere + "\n \n" +
"开 会 时 间:" + meetingDate + "\n \n" +
//"天 气:多云转晴\n \n" +
"地 点:" + site + "\n \n"
textX := x - logoWidth
textY := y + 10
err = dc.LoadFontFace(fontPath, 12)
dc.DrawStringWrapped(text, textX, textY, 0.0, 0.0, float64(dc.Width())-textX, 1.2, gg.AlignLeft)
//6、创建矩形框 渐变透明
boxText = labourserviceName
width := len(boxText) * 8
height := 30
fromAlpha := 0.6
toAlpha := 0
for x := 0; x <= width; x++ {
alpha := fromAlpha - (fromAlpha-float64(toAlpha))*(float64(x)/float64(width))
rgba := color.RGBA{G: 99, B: 175, A: uint8(alpha * 255)}
dc.SetRGBA255(int(rgba.R), int(rgba.G), int(rgba.B), int(rgba.A))
dc.DrawLine(float64(x)+10, y+96, float64(x)+10, y+96+float64(height))
dc.StrokePreserve()
dc.Fill()
}
textFunc(dc, boxText, fontPath, 16.0, 10.0, y+96, float64(width), float64(height), 2)
//7、保存图片
err = dc.SavePNG(filePath)
if err != nil {
log.Fatalf("保存带水印的图片失败: %v", err)
}
}
func textFunc(dc *gg.Context, boxText string, fontPath string, boxTextSize, rectangleX, rectangleY, rectangleWidth, rectangleHeight float64, num int) {
err := dc.LoadFontFace(fontPath, boxTextSize)
if err != nil {
log.Fatalf("加载字体失败: %v", err)
}
dc.SetRGB(1, 1, 1)
boxTextWidth, boxTextHeight := dc.MeasureString(boxText)
boxTextX := 0.00
if num == 1 {
boxTextX = rectangleX + (rectangleWidth-boxTextWidth)/2
} else if num == 2 {
boxTextX = 10
} else {
log.Fatalf("对齐方式错误")
}
boxTextY := rectangleY + (rectangleHeight-boxTextHeight)/2 + boxTextHeight
dc.DrawStringAnchored(boxText, boxTextX, boxTextY, 0.0, 0.0)
}
// 绘制矩形框
func Test_draw_rect_text(im_path string, x, y, w, h float64) {
// Load image
//font_path := GetCWD() + "/resource/cory/msyh.ttc"
im, err := gg.LoadImage(im_path)
if err != nil {
log.Fatal(err)
}
// 2 method
dc := gg.NewContextForImage(im)
// Set color and line width
dc.SetHexColor("#FF0000")
dc.SetLineWidth(4)
// DrawRoundedRectangle 使用 DrawRoundedRectangle 方法在图像上绘制一个带有圆角的矩形。这里 x, y 是矩形左上角的坐标w, h 是矩形的宽度和高度,最后的 0 表示圆角的半径为0。
dc.DrawRoundedRectangle(x, y, w, h, 0)
// Store set
dc.Stroke()
dc.DrawRectangle(x, y, w, h)
dc.Clip()
// Save png image
dc.SavePNG(im_path)
}
type TestDrawRectTextEntity struct {
ImPath string `json:"im_path"`
Coordinates []*CoordinatesListEntity `json:"coordinates"`
}
type CoordinatesListEntity struct {
X float64 `json:"x"`
Y float64 `json:"y"`
W float64 `json:"w"`
H float64 `json:"h"`
}
// TestDrawRectTextFunc 同一文件多次绘制矩形框
func TestDrawRectTextFunc(entity *TestDrawRectTextEntity) {
if entity == nil {
return
}
if len(entity.Coordinates) == 0 {
return
}
// 加载图像
im, err := gg.LoadImage(entity.ImPath) // 加载图像一次
if err != nil {
log.Fatal(err)
}
// 创建上下文
dc := gg.NewContextForImage(im)
// 设置颜色和线宽
dc.SetHexColor("#FF0000")
dc.SetLineWidth(4)
//绘制和保存矩形
for _, zuobiao := range entity.Coordinates {
// 绘制矩形
dc.DrawRoundedRectangle(zuobiao.X, zuobiao.Y, zuobiao.W, zuobiao.H, 0)
}
dc.Stroke()
// 保存图像
dc.SavePNG(entity.ImPath)
}

View File

@ -0,0 +1,166 @@
package coryCommon
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
commonService "github.com/tiger1103/gfast/v3/internal/app/common/service"
"github.com/tiger1103/gfast/v3/internal/app/system/dao"
"github.com/tiger1103/gfast/v3/internal/app/system/model"
wxModel "github.com/tiger1103/gfast/v3/internal/app/wxApplet/model"
tool "github.com/tiger1103/gfast/v3/utility/coryUtils"
"strconv"
"strings"
"time"
)
// 微信小程序的消息订阅 https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-message-management/subscribe-message/sendMessage.html
type TokenEntity struct {
AccessToken string `json:"accessToken"`
ExpiresIn string `json:"expiresIn"`
}
func GetAccessToken() (token string, err error) {
var te *TokenEntity
appId, _ := g.Cfg().Get(gctx.New(), "wx.appId")
appSecret, _ := g.Cfg().Get(gctx.New(), "wx.appSecret")
key := "weChatAccessToken"
//从缓存捞取key
ctx := gctx.New()
get := commonService.Cache().Get(ctx, key)
if get != nil && get.String() != "" {
token = get.String()
return "", err
} else {
uri := "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + appId.String() + "&secret=" + appSecret.String()
response, err := g.Client().Get(gctx.New(), uri)
if err != nil {
return "", err
} else {
allString := response.ReadAllString()
err := json.Unmarshal([]byte(allString), &te)
if err != nil {
return "", err
} else {
//将token存储到redis中tiken默认时间为秒实际计算为2小时,(这里少100秒,防止token过期还存在redis中)
num, err := strconv.Atoi(te.ExpiresIn)
if err != nil {
err = errors.New("过期时间转换失败!")
return "", err
}
commonService.Cache().Set(ctx, key, te.AccessToken, time.Duration(num-100)*time.Second)
token = te.AccessToken
return token, err
}
}
}
}
type AppletSubscription struct {
Touser string `json:"touser"`
TemplateId string `json:"template_id"`
MiniprogramState string `json:"MiniprogramState"`
Data DataEntity `json:"data"`
}
type DataEntity struct {
Date1 ValueEntity `json:"date1"`
Thing2 ValueEntity `json:"thing2"`
Thing3 ValueEntity `json:"thing3"`
}
type ValueEntity struct {
Value string `json:"value"`
}
type MsgEntity struct {
ErrCode int `json:"errcode"`
ErrMsg int64 `json:"errmsg"`
MsgId string `json:"msgid"`
}
// Subscription 微信服务通知(消息订阅)
func Subscription(openid string) (msgEntity *MsgEntity, err error) {
//1、获取token
token, err := GetAccessToken()
if err != nil {
fmt.Println("获取微信凭证错误!")
return
}
//2、组装数据
//now := time.Now()
var entity = new(AppletSubscription)
entity.Touser = openid
entity.TemplateId = "EyBO6gWizF5HwUThYSSm_HuQWgfMrwEkVHPXeEq1Me8"
entity.MiniprogramState = "trial"
var dataEntity = DataEntity{}
dataEntity.Date1 = ValueEntity{Value: time.Now().Format("2006-01-02 15:04:05")}
dataEntity.Thing2 = ValueEntity{Value: "您今日还有打卡未完成!"}
dataEntity.Thing3 = ValueEntity{Value: "请登录中煤小程序进行打卡操作!"}
entity.Data = dataEntity
marshal, _ := json.Marshal(entity)
//3、发起请求
msgEntity = new(MsgEntity)
uri := "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=" + token
post, err := g.Client().Post(gctx.New(), uri, marshal)
postBytes, err := json.Marshal(post)
if err != nil {
fmt.Println("JSON marshaling error:", err)
return
}
if err := json.Unmarshal(postBytes, &msgEntity); err != nil {
fmt.Println("解析JSON错误:", err)
return nil, err
}
return
}
func ServiceNoticeFunc(ctx context.Context) {
//1、获取所有项目的打卡范围
var projectEntity []*model.SysProjectListRes
err := dao.SysProject.Ctx(ctx).
Where("status", 0).
Where("show_hidden", 1).
Fields("id,project_name,punch_range").Scan(&projectEntity)
if err != nil {
fmt.Println("获取项目失败!")
}
//2、遍历项目获取打卡范围
for _, pData := range projectEntity {
// 3、每个项目都有两个临时触发器
punchRange := pData.PunchRange
split := strings.Split(punchRange, ",")
for _, cfq := range split {
dn, err := tool.TimeStr(cfq)
if err != nil {
fmt.Println(err)
}
//创建临时定时器
if dn > 0 {
tempTimer := time.NewTimer(dn)
// 在新的 goroutine 中等待临时定时器触发
go func() {
<-tempTimer.C
//4、就获取当前项目下面的所有成员条件为subscription为1的数据 下发数据,并存储下发数据的状态
var openidList []*wxModel.BusConstructionUserListRes
dao.BusConstructionUser.Ctx(ctx).
Fields("openid").
Where("subscription", "1").
Where("status = 0").
Where("entry_date is not null and entry_date!='' and (leave_date is null or leave_date = '')").
Where("project_id", pData.Id).
Scan(&openidList)
for _, oi := range openidList {
Subscription(oi.Openid)
}
}()
}
}
}
}

View File

@ -0,0 +1,110 @@
package coryCommon
import (
"encoding/json"
"fmt"
"github.com/gogf/gf/v2/net/gclient"
"github.com/gogf/gf/v2/os/gctx"
)
var key = "00b60ebda96849e694cb570e3d4f5c89"
// WeatherRep 返回参数 免费天气查询 https://dev.qweather.com/docs/api/weather/weather-daily-forecast/
type WeatherRep struct {
Code string `json:"code"`
UpdateTime string `json:"updateTime"`
FxLink string `json:"fxLink"`
Daily []struct {
FxDate string `json:"fxDate"`
Sunrise string `json:"sunrise"`
Sunset string `json:"sunset"`
Moonrise string `json:"moonrise"`
Moonset string `json:"moonset"`
MoonPhase string `json:"moonPhase"`
MoonPhaseIcon string `json:"moonPhaseIcon"`
TempMax string `json:"tempMax"`
TempMin string `json:"tempMin"`
IconDay string `json:"iconDay"`
TextDay string `json:"textDay"`
IconNight string `json:"iconNight"`
TextNight string `json:"textNight"`
Wind360Day string `json:"wind360Day"`
WindDirDay string `json:"windDirDay"`
WindScaleDay string `json:"windScaleDay"`
WindSpeedDay string `json:"windSpeedDay"`
Wind360Night string `json:"wind360Night"`
WindDirNight string `json:"windDirNight"`
WindScaleNight string `json:"windScaleNight"`
WindSpeedNight string `json:"windSpeedNight"`
Humidity string `json:"humidity"`
Precip string `json:"precip"`
Pressure string `json:"pressure"`
Vis string `json:"vis"`
Cloud string `json:"cloud"`
UvIndex string `json:"uvIndex"`
} `json:"daily"`
Refer struct {
Sources []string `json:"sources"`
License []string `json:"license"`
} `json:"refer"`
}
// Weather 传递经纬度 location := "116.41,39.92"
func Weather(location string) (we string) {
//请求路径
//key := ""
requestURL := "https://devapi.qweather.com/v7/weather/3d?location=" + location + "&key=" + key
response, err := gclient.New().ContentJson().ContentType("application/x-www-form-urlencoded").Get(gctx.New(), requestURL)
if err != nil {
return
}
var dataInfo = ""
dataInfo = response.ReadAllString()
return dataInfo
}
type GridPointRes struct {
Code string `json:"code"`
UpdateTime string `json:"updateTime"`
FxLink string `json:"fxLink"`
Now struct {
ObsTime string `json:"obsTime"`
Temp string `json:"temp"`
Icon string `json:"icon"`
Text string `json:"text"`
Wind360 string `json:"wind360"`
WindDir string `json:"windDir"`
WindScale string `json:"windScale"`
WindSpeed string `json:"windSpeed"`
Humidity string `json:"humidity"`
Precip string `json:"precip"`
Pressure string `json:"pressure"`
Cloud string `json:"cloud"`
Dew string `json:"dew"`
} `json:"now"`
Refer struct {
Sources []string `json:"sources"`
License []string `json:"license"`
} `json:"refer"`
}
// 格点天气 GridPoint
func GridPoint(location string) (gp *GridPointRes, err error) {
//请求路径
//key := "00b60ebda96849e694cb570e3d4f5c89"
requestURL := "https://devapi.qweather.com/v7/grid-weather/now?location=" + location + "&key=" + key
response, err := gclient.New().ContentJson().ContentType("application/x-www-form-urlencoded").Get(gctx.New(), requestURL)
if err != nil {
return
}
var dataInfo = ""
dataInfo = response.ReadAllString()
gp = new(GridPointRes)
err = json.Unmarshal([]byte(dataInfo), &gp)
fmt.Println(gp.Now.Icon)
return
}

View File

@ -0,0 +1,621 @@
package coryCommon
import (
"archive/zip"
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/transform"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
)
//解压文件、文件夹复制
// FileZipFunc 对文件进行解压
func FileZipFunc(relativePath string, filenPath string, template string) (fileName string, err error) {
// 打开压缩文件
zipFile, err := zip.OpenReader(relativePath)
if err != nil {
fmt.Println("无法打开压缩文件只支持ZIP:", err)
return
}
var i = 0
var name = ""
//进入的压缩文件,判断压缩文件中的第一层是否包含文件,如果有就在当前随机产生一个文件夹,返回就返回到随机文件夹的位置
randomFolder := ""
// 遍历压缩文件中的文件头信息
for _, f := range zipFile.File {
//path, _ := gbkDecode(f.Name)
fmt.Println("头? ", f.Name)
path, _ := IsGB18030(f.Name)
// 判断是否为顶层文件夹
if !hasSlash(path) {
randomFolder = FileName("/randomFolder")
template = template + randomFolder
filenPath = filenPath + randomFolder
break
}
}
// 遍历压缩文件中的文件
for _, file := range zipFile.File {
fmt.Println("ti ", file.Name)
// 解决文件名编码问题
if i == 0 {
gb18030, _ := IsGB18030(file.Name)
if err != nil {
return "", err
}
name = gb18030
}
file.Name, err = IsGB18030(file.Name)
if err != nil {
return "", err
}
//对所有文件的名称进行空格替换
file.Name = strings.Replace(file.Name, " ", "", -1)
if err != nil {
return file.Name, err
}
// 获取文件的相对路径,根据原本路径来操作还是根据新路径来filenPath
extractedFilePath := ""
extractedFilePathTwo := ""
if filenPath != "" {
extractedFilePath = filepath.Join(FileToFunc(filenPath, 2), "/"+file.Name)
split := strings.Split(filenPath, "/")
extractedFilePathTwo = extractedFilePath + "/" + split[len(split)-1]
} else {
extractedFilePath = filepath.ToSlash(filepath.Join(GetCWD()+template+"/"+".", file.Name))
extractedFilePathTwo = extractedFilePath + "/"
}
//判断文件夹是否存在存在就退出i==0 是为了只判断最外层那一文件夹路径)
_, err := os.Stat(filepath.Dir(extractedFilePathTwo))
if err == nil && i == 0 {
zipFile.Close()
// 删除压缩文件
err = os.Remove(relativePath)
err = errors.New("当前文件夹已经存在,导入无效!")
return "", err
}
i = i + 1
// 检查是否为文件
if !file.FileInfo().IsDir() {
// 创建文件的目录结构
err = os.MkdirAll(filepath.Dir(extractedFilePath), os.ModePerm)
if err != nil {
fmt.Println("无法创建目录:", err)
return "", err
}
// 打开压缩文件中的文件
zippedFile, err := file.Open()
if err != nil {
fmt.Println("无法打开压缩文件中的文件:", err)
return "", err
}
defer zippedFile.Close()
// 创建目标文件
extractedFile, err := os.Create(extractedFilePath)
if err != nil {
fmt.Println("无法创建目标文件:", err)
return "", err
}
defer extractedFile.Close()
// 将压缩文件中的内容复制到目标文件
_, err = io.Copy(extractedFile, zippedFile)
if err != nil {
fmt.Println("无法解压缩文件:", err)
return "", err
}
}
}
zipFile.Close()
// 删除压缩文件
err = os.Remove(relativePath)
if err != nil {
fmt.Println("无法删除压缩文件:", err)
return
}
fileName = strings.Split(name, "/")[0]
if randomFolder != "" {
fileName = ""
} else {
fileName = "/" + fileName
}
if filenPath != "" {
return FileToFunc(filenPath, 2) + fileName, err
} else {
return GetCWD() + template + fileName, err
}
}
// IsGB18030 判断字符串是否是 GB18030 编码
func IsGB18030(name string) (string, error) {
// 创建 GB18030 解码器
decoder := simplifiedchinese.GB18030.NewDecoder()
// 使用 transform 解码数据
_, err := io.ReadAll(transform.NewReader(bytes.NewReader([]byte(name)), decoder))
if err == nil {
return name, nil
} else {
fileName, errName := simplifiedchinese.GB18030.NewDecoder().String(name)
return fileName, errName
}
}
// gbkDecode 解决文件名乱码
func gbkDecode(s string) (string, error) {
gbkDecoder := simplifiedchinese.GBK.NewDecoder()
decodedName, _, err := transform.String(gbkDecoder, s)
return decodedName, err
}
type DocumentListPublicRes struct {
Id int64 `json:"id"`
IdStr string `json:"idStr"`
Pid string `json:"pid"`
Name string `json:"name"`
FilenPath string `json:"filenPath"`
FilenPathCoding string `json:"filenPathCoding"`
Suffix string `json:"suffix"`
Type string `json:"type"`
CreateBy string `json:"createBy"`
CreatedAt *gtime.Time `json:"createdAt"`
IsDuplicate bool `json:"isDuplicate"`
ProjectId int64 `json:"projectId"`
}
// Traversal 遍历文件夹 ctx绝对路径上级文件夹可有可无表名存储位置,项目id,模板1or资料2
//
// one, err := coryCommon.Traversal(ctx, path, req.Pid, dao.DocumentCompletion.Table(), dataFolder, req.ProjectId, "2") //遍历解压后的文件,插入数据
// if err != nil {
// liberr.ErrIsNil(ctx, err)
// return
// }
// _, err = g.DB().Model(dao.DocumentCompletion.Table()).Ctx(ctx).Insert(one)
// liberr.ErrIsNil(ctx, err, "新增失败!")
func Traversal(ctx context.Context, root string, pidstr string, tableName string, templatePath string, projectId int64, num string) (dataOne []*DocumentListPublicRes, err error) {
template := strings.Replace(templatePath, "/resource/public", "/file", 1)
err = filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 获取相对路径
relativePath, err := filepath.Rel(root, path)
if err != nil {
return err
}
// 根目录下创建,还是指定文件夹下面创建?
p := ""
if pidstr != "" {
value, _ := g.DB().Model(tableName).Ctx(ctx).Where("id_str", pidstr).Fields("filen_path").Value()
split := strings.Split(root, "/")
p = value.String() + "/" + split[len(split)-1] + "/" + relativePath
} else {
p = template + "/" + relativePath
}
p = strings.ReplaceAll(p, "\\", "/")
// 获取当前项的深度
depth := strings.Count(relativePath, string(filepath.Separator))
// 判断父子关系并打印结果
if depth == 0 && info.IsDir() {
if relativePath == "." {
split := strings.Split(root, "/")
n := split[len(split)-1]
// 根目录下创建,还是指定文件夹下面创建?
p := ""
if pidstr != "" {
value, _ := g.DB().Model(tableName).Ctx(ctx).Where("id_str", pidstr).Fields("filen_path").Value()
p = value.String() + "/" + n
} else {
p = template + "/" + n
}
p = strings.ReplaceAll(p, "\\", "/")
template = template + "/" + n
var dataTwo = new(DocumentListPublicRes)
dataTwo.IdStr = SHA256(p)
if pidstr != "" {
dataTwo.Pid = pidstr
} else {
dataTwo.Pid = "0"
}
dataTwo.Name = n
dataTwo.FilenPath = p
////如果文件夹路径重复,就提示 解压文件夹的时候就已经判断了,这里就不需要了
//err := IsFolderExist(ctx, p)
//if err != nil {
// return err
//}
dataTwo.Type = "2"
dataOne = append(dataOne, dataTwo)
} else {
dir, n := filepath.Split(p)
dir = strings.TrimSuffix(dir, "/")
var dataTwo = new(DocumentListPublicRes)
dataTwo.IdStr = SHA256(p)
dataTwo.Pid = SHA256(dir)
dataTwo.Name = n
dataTwo.FilenPath = p
////如果文件夹路径重复,就提示
//err := IsFolderExist(ctx, p)
//if err != nil {
// return err
//}
dataTwo.Type = "2"
dataOne = append(dataOne, dataTwo)
}
} else if info.IsDir() {
// 子文件夹
dir, n := filepath.Split(p)
dir = strings.TrimSuffix(dir, "/")
var dataTwo = new(DocumentListPublicRes)
dataTwo.IdStr = SHA256(p)
dataTwo.Pid = SHA256(dir)
dataTwo.Name = n
dataTwo.FilenPath = p
dataTwo.Type = "2"
dataOne = append(dataOne, dataTwo)
} else {
dir, n := filepath.Split(p)
dir = strings.TrimSuffix(dir, "/")
var dataTwo = new(DocumentListPublicRes)
dataTwo.Pid = SHA256(dir)
lastDotIndex := strings.LastIndex(n, ".")
if lastDotIndex == -1 || lastDotIndex == 0 {
dataTwo.Name = strings.Split(n, ".")[0]
} else {
dataTwo.Name = n[:lastDotIndex]
}
dataTwo.Suffix = n[lastDotIndex:]
dataTwo.FilenPath = p
dataTwo.Type = "1"
//文件只能是这三种类型,其他类型进不来
s := n[lastDotIndex:]
if num == "1" { //资料有格式限制
if strings.EqualFold(s, ".xls") || strings.EqualFold(s, ".xlsx") || strings.EqualFold(s, ".docx") || strings.EqualFold(s, ".doc") || strings.EqualFold(s, ".pptx") || strings.EqualFold(s, ".ppt") {
dataOne = append(dataOne, dataTwo)
}
} else {
dataOne = append(dataOne, dataTwo)
}
}
return err
})
if err != nil {
//fmt.Println("遍历文件夹时发生错误:", err)
return nil, err
}
// 有项目id表示资料 无表模板
if projectId > 0 {
for i := range dataOne {
dataOne[i].ProjectId = projectId
}
}
return
}
func SHA256(str string) (hx string) {
// 创建 SHA-256 哈希对象
hash := sha256.New()
// 将字符串转换为字节数组并进行哈希计算
hash.Write([]byte(str))
// 计算 SHA-256 哈希值
hashedBytes := hash.Sum(nil)
// 将哈希值转换为十六进制字符串
hashStr := hex.EncodeToString(hashedBytes)
return hashStr
}
// CopyFile 文件复制
func CopyFile(src, dst string) error {
// 打开源文件
inputFile, err := os.Open(src)
if err != nil {
return err
}
defer inputFile.Close()
// 创建目标文件
outputFile, err := os.Create(dst)
if err != nil {
return err
}
defer outputFile.Close()
// 通过 Copy 函数实现拷贝功能
_, err = io.Copy(outputFile, inputFile)
if err != nil {
return err
}
// 确保文件内容被刷新到磁盘上
err = outputFile.Sync()
if err != nil {
return err
}
return nil
}
// CopyDirectory 文件夹复制
func CopyDirectory(src string, dest string) error {
// 检查源文件夹是否存在
_, err := os.Stat(src)
if err != nil {
return err
}
// 检查目标文件夹是否存在,不存在则创建
err = os.MkdirAll(dest, 0755)
if err != nil {
return err
}
// 遍历源文件夹
files, err := os.ReadDir(src)
if err != nil {
return err
}
for _, file := range files {
srcPath := src + "/" + file.Name()
destPath := dest + "/" + file.Name()
// 判断文件类型
if file.IsDir() {
// 如果是文件夹,则递归调用 copyDirectory 函数复制文件夹及其子文件
err = CopyDirectory(srcPath, destPath)
if err != nil {
return err
}
} else {
// 如果是文件,则复制文件到目标文件夹
inputFile, err := os.Open(srcPath)
if err != nil {
return err
}
defer inputFile.Close()
outputFile, err := os.Create(destPath)
if err != nil {
return err
}
defer outputFile.Close()
_, err = io.Copy(outputFile, inputFile)
if err != nil {
return err
}
}
}
return nil
}
// MoveFile 文件移动
func MoveFile(source, destination string) (err error) {
// 执行移动操作
err = os.Rename(source, destination)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("File moved successfully!")
}
return err
}
// MoveFolder 文件夹下面的文件及子文件全部移动到新文件夹下
func MoveFolder(srcPath, destPath string) error {
// 获取源文件夹下的所有文件和子文件夹
fileList := []string{}
err := filepath.Walk(srcPath, func(path string, info os.FileInfo, err error) error {
fileList = append(fileList, path)
return nil
})
if err != nil {
return err
}
// 移动每个文件和子文件夹
for _, file := range fileList {
// 获取相对路径
relPath, err := filepath.Rel(srcPath, file)
if err != nil {
return err
}
// 构建目标路径
destFile := filepath.Join(destPath, relPath)
// 判断是文件还是文件夹
if fileInfo, err := os.Stat(file); err == nil && fileInfo.IsDir() {
// 如果是文件夹,创建目标文件夹
err := os.MkdirAll(destFile, os.ModePerm)
if err != nil {
return err
}
} else {
// 如果是文件,复制文件
err := copyFile(file, destFile)
if err != nil {
return err
}
}
}
// 移动完成后删除源文件夹
return os.RemoveAll(srcPath)
}
func copyFile(src, dest string) error {
srcFile, err := os.Open(src)
if err != nil {
return err
}
defer srcFile.Close()
destFile, err := os.Create(dest)
if err != nil {
return err
}
defer destFile.Close()
_, err = io.Copy(destFile, srcFile)
return err
}
// FolderToZip 将给定文件夹压缩成压缩包存储到另外一个路径(参数:源数据、目标路径)
func FolderToZip(folderToZip, zipFile string) (err error) {
// 创建一个新的压缩包文件
newZipFile, err := os.Create(zipFile)
if err != nil {
return err
}
defer newZipFile.Close()
// 创建一个 zip.Writer 来向压缩包中写入内容
zipWriter := zip.NewWriter(newZipFile)
defer zipWriter.Close()
// 递归遍历文件夹并将其中的文件和目录添加到压缩包中
err = filepath.Walk(folderToZip, func(filePath string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 获取当前文件的相对路径
relativePath, err := filepath.Rel(folderToZip, filePath)
if err != nil {
return err
}
// 如果是目录,则创建一个目录项
if info.IsDir() {
_, err = zipWriter.Create(relativePath + "/")
if err != nil {
return err
}
return nil
}
// 如果是文件,则创建一个文件项并写入文件内容
fileData, err := ioutil.ReadFile(filePath)
if err != nil {
return err
}
file, err := zipWriter.Create(relativePath)
if err != nil {
return err
}
_, err = file.Write(fileData)
if err != nil {
return err
}
return nil
})
return
}
// zipDirFiles 压缩文件
func ZipDirFiles(src_dir string, zip_file_name string) error {
// 检查并创建目标目录
err := os.MkdirAll(filepath.Dir(zip_file_name), os.ModePerm)
if err != nil {
return err
}
// 删除空的zip而不是直接使用os.RemoveAll以提高安全性
err = os.Remove(zip_file_name)
if err != nil && !os.IsNotExist(err) {
return err
}
// 创建新的zip文件
zipfile, err := os.Create(zip_file_name)
if err != nil {
return err
}
defer zipfile.Close()
// 初始化zip写入器
archive := zip.NewWriter(zipfile)
defer archive.Close()
// 遍历源目录下的文件和子目录
return filepath.Walk(src_dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 计算相对于源目录的路径
relPath, _ := filepath.Rel(src_dir, path)
if path == src_dir {
// 如果是源目录本身,跳过
return nil
}
// 创建zip文件头
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
header.Name = filepath.ToSlash(relPath)
// 标记目录
if info.IsDir() {
header.Name += "/"
return nil
}
// 处理文件
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
writer, err := archive.CreateHeader(header)
if err != nil {
return err
}
_, err = io.Copy(writer, file)
return err
})
}
// 压缩后删除源文件
func DeleteFolderToZip(srcpath, destpathzip string) (err error) {
//srcpath := filepath.ToSlash(`D:\GiteeProject\gsproject\zmkg-back\resource\public\temporary\del`)
//destpathzip := filepath.ToSlash(`D:\GiteeProject\gsproject\zmkg-back\resource\public\temporary\yy.zip`)
err = ZipDirFiles(srcpath, destpathzip) // 文件压缩 压缩文件目录 和压缩文件zip
if err != nil {
return err
}
// 删除原文件
if err := os.RemoveAll(srcpath); err != nil {
if !os.IsNotExist(err) {
fmt.Printf("Error removing existing file %s: %v\n", srcpath, err)
return err
}
}
return err
}
// 判断字符串中是否包含斜杠
func hasSlash(s string) bool {
for _, c := range s {
if c == '/' || c == '\\' {
return true
}
}
return false
}

View File

@ -0,0 +1,278 @@
package fileUpload
import (
"bytes"
"fmt"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/tiger1103/gfast/v3/api/v1/common/globe"
"io"
"log"
"os"
"path"
"strconv"
"strings"
)
func InitUploadApi(group *ghttp.RouterGroup) {
group.POST("/source/upload", SourceUploadFunc)
//group.Bind(new(SourceUpload))
}
type SourceUpload struct {
}
type SourceUploadReq struct {
g.Meta `path:"source/upload" dc:"上传资源" method:"post" tags:"资源相关" `
}
type SourceUploadRes struct {
}
/*func (SourceUpload) UploadFile(ctx context.Context, req *SourceUploadReq) (res *SourceUploadRes, err error) {
err = startSaveFile(g.RequestFromCtx(ctx))
return
}*/
func SourceUploadFunc(request *ghttp.Request) {
projectId := request.Get("projectId")
fmt.Println("projectId", projectId)
startSaveFile(request)
}
func startSaveFile(request *ghttp.Request) error {
err, filename := Upload(request, globe.SOURCE)
fmt.Println("结束了")
fmt.Println(err)
if err != nil {
return err
}
fmt.Println(filename)
/* arr := strings.Split(filename, ".")
arr = arr[:len(arr)-1]
suffix := path.Ext(filename)
var SourceType = ""
switch suffix {
case globe.CLT:
SourceType = globe.TILESET
break
case globe.JCT:
SourceType = globe.TILESET
break
case globe.MBTILES:
SourceType = globe.LAYER
break
case globe.PAK:
//此时需要判断是地形还是正射
SourceType = globe.LAYER
break
}*/
//source := database.SOURCE{
// SourceID: tool.GetUuid(),
// SourceName: strings.Join(arr, "."),
// SourceType: SourceType,
// SourcePath: filename,
//}
//database.GetORMDBInstance().Model(&database.SOURCE{}).Create(&source)
return err
}
func Upload(r *ghttp.Request, dir string) (error, string) {
var contentLength int64
contentLength = r.Request.ContentLength
if contentLength <= 0 {
return globe.GetErrors("content_length error"), ""
}
content_type_, has_key := r.Request.Header["Content-Type"]
if !has_key {
return globe.GetErrors("Content-Type error"), ""
}
if len(content_type_) != 1 {
return globe.GetErrors("Content-Type count error"), ""
}
contentType := content_type_[0]
const BOUNDARY string = "; boundary="
loc := strings.Index(contentType, BOUNDARY)
if -1 == loc {
return globe.GetErrors("Content-Type error, no boundary"), ""
}
boundary := []byte(contentType[(loc + len(BOUNDARY)):])
readData := make([]byte, 1024*12)
var readTotal = 0
var des = ""
var filename = ""
for {
fileHeader, fileData, err := ParseFromHead(readData, readTotal, append(boundary, []byte("\r\n")...), r.Request.Body)
if err != nil {
return err, ""
}
filename = fileHeader.FileName
des = path.Join(dir, filename)
f, err := os.Create(des)
if err != nil {
return err, ""
}
f.Write(fileData)
fileData = nil
//需要反复搜索boundary
tempData, reachEnd, err := ReadToBoundary(boundary, r.Request.Body, f)
f.Close()
if err != nil {
return err, ""
}
if reachEnd {
break
} else {
copy(readData[0:], tempData)
readTotal = len(tempData)
continue
}
}
return nil, filename
}
// / 解析多个文件上传中,每个具体的文件的信息
type FileHeader struct {
ContentDisposition string
Name string
FileName string ///< 文件名
ContentType string
ContentLength int64
}
// / 解析描述文件信息的头部
// / @return FileHeader 文件名等信息的结构体
// / @return bool 解析成功还是失败
func ParseFileHeader(h []byte) (FileHeader, bool) {
arr := bytes.Split(h, []byte("\r\n"))
var out_header FileHeader
out_header.ContentLength = -1
const (
CONTENT_DISPOSITION = "Content-Disposition: "
NAME = "name=\""
FILENAME = "filename=\""
CONTENT_TYPE = "Content-Type: "
CONTENT_LENGTH = "Content-Length: "
)
for _, item := range arr {
if bytes.HasPrefix(item, []byte(CONTENT_DISPOSITION)) {
l := len(CONTENT_DISPOSITION)
arr1 := bytes.Split(item[l:], []byte("; "))
out_header.ContentDisposition = string(arr1[0])
if bytes.HasPrefix(arr1[1], []byte(NAME)) {
out_header.Name = string(arr1[1][len(NAME) : len(arr1[1])-1])
}
fmt.Println(arr1)
l = len(arr1[2])
if bytes.HasPrefix(arr1[2], []byte(FILENAME)) && arr1[2][l-1] == 0x22 {
out_header.FileName = string(arr1[2][len(FILENAME) : l-1])
}
} else if bytes.HasPrefix(item, []byte(CONTENT_TYPE)) {
l := len(CONTENT_TYPE)
out_header.ContentType = string(item[l:])
} else if bytes.HasPrefix(item, []byte(CONTENT_LENGTH)) {
l := len(CONTENT_LENGTH)
s := string(item[l:])
content_length, err := strconv.ParseInt(s, 10, 64)
if err != nil {
log.Printf("content length error:%s", string(item))
return out_header, false
} else {
out_header.ContentLength = content_length
}
} else {
log.Printf("unknown:%s\n", string(item))
}
}
if len(out_header.FileName) == 0 {
return out_header, false
}
return out_header, true
}
// / 从流中一直读到文件的末位
// / @return []byte 没有写到文件且又属于下一个文件的数据
// / @return bool 是否已经读到流的末位了
// / @return error 是否发生错误
func ReadToBoundary(boundary []byte, stream io.ReadCloser, target io.WriteCloser) ([]byte, bool, error) {
read_data := make([]byte, 1024*8)
read_data_len := 0
buf := make([]byte, 1024*4)
b_len := len(boundary)
reach_end := false
for !reach_end {
read_len, err := stream.Read(buf)
if err != nil {
if err != io.EOF && read_len <= 0 {
return nil, true, err
}
reach_end = true
}
//todo: 下面这一句很蠢,值得优化
copy(read_data[read_data_len:], buf[:read_len]) //追加到另一块buffer仅仅只是为了搜索方便
read_data_len += read_len
if read_data_len < b_len+4 {
continue
}
loc := bytes.Index(read_data[:read_data_len], boundary)
if loc >= 0 {
//找到了结束位置
target.Write(read_data[:loc-4])
return read_data[loc:read_data_len], reach_end, nil
}
target.Write(read_data[:read_data_len-b_len-4])
copy(read_data[0:], read_data[read_data_len-b_len-4:])
read_data_len = b_len + 4
}
target.Write(read_data[:read_data_len])
return nil, reach_end, nil
}
// / 解析表单的头部
// / @param read_data 已经从流中读到的数据
// / @param read_total 已经从流中读到的数据长度
// / @param boundary 表单的分割字符串
// / @param stream 输入流
// / @return FileHeader 文件名等信息头
// / []byte 已经从流中读到的部分
// / error 是否发生错误
func ParseFromHead(read_data []byte, readTotal int, boundary []byte, stream io.ReadCloser) (FileHeader, []byte, error) {
buf := make([]byte, 1024*4)
foundBoundary := false
boundaryLoc := -1
var file_header FileHeader
for {
read_len, err := stream.Read(buf)
fmt.Println("read_len", read_len)
if err != nil {
if err != io.EOF {
return file_header, nil, err
}
break
}
if readTotal+read_len > cap(read_data) {
return file_header, nil, fmt.Errorf("not found boundary")
}
copy(read_data[readTotal:], buf[:read_len])
readTotal += read_len
if !foundBoundary {
boundaryLoc = bytes.Index(read_data[:readTotal], boundary)
if -1 == boundaryLoc {
continue
}
foundBoundary = true
}
start_loc := boundaryLoc + len(boundary)
file_head_loc := bytes.Index(read_data[start_loc:readTotal], []byte("\r\n\r\n"))
if -1 == file_head_loc {
continue
}
file_head_loc += start_loc
ret := false
file_header, ret = ParseFileHeader(read_data[start_loc:file_head_loc])
if !ret {
return file_header, nil, fmt.Errorf("ParseFileHeader fail:%s", string(read_data[start_loc:file_head_loc]))
}
return file_header, read_data[file_head_loc+4 : readTotal], nil
}
return file_header, nil, fmt.Errorf("reach to stream EOF")
}

View File

@ -0,0 +1,96 @@
package globe
import (
"errors"
"github.com/gogf/gf/v2/net/ghttp"
"gorm.io/gorm"
"net/http"
"strconv"
)
const (
ALL = -1 //所有
ENABLE = 1
DISABLE = 0
DESC = "desc"
ASC = "asc"
PAGE = 1
PAGESIZE = 10
ONLINE = 1
OFFLINE = 0
PREFFIX = "yjearth4.0"
)
var IS_OFFLINE_VERSION = true //是否为单机版本
const SOURCE = "resource/public/clt/"
const (
TILESET = "tileset"
BIM = "bim"
LAYER = "layer"
TERRAIN = "terrain"
POINT = "point"
LINE = "line"
AREA = "area"
MODEL = "model"
KML = "kml"
GEOJSON = "geojson"
DIRECTORY = "directory"
SHP = "shp"
)
const (
PAK = ".pak"
MBTILES = ".mbtiles"
CLT = ".clt"
JCT = ".jct"
DOTGEOJSON = ".geojson"
DOTSHP = ".shp"
)
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 + "/" + PREFFIX
//}
//网络版时 有代理 不需要补全地址
return PREFFIX
}
/*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()
}

25
api/v1/common/req.go Normal file
View File

@ -0,0 +1,25 @@
/*
* @desc:公共接口相关
* @company:云南奇讯科技有限公司
* @Author: yixiaohu<yxh669@qq.com>
* @Date: 2022/3/30 9:28
*/
package common
// PageReq 公共请求参数
type PageReq struct {
DateRange []string `p:"dateRange"` //日期范围
PageNum int `p:"pageNum"` //当前页码
PageSize int `p:"pageSize"` //每页数
OrderBy string //排序方式
NotInPlan bool `p:"notInPlan"` //是否过滤周计划中的id
}
type Author struct {
Authorization string `p:"Authorization" in:"header" dc:"Bearer {{token}}"`
}
type Paging struct {
IsPaging string `json:"isPaging" dc:"是否开启分页功能 YES开启 NO不开启空字符串也不开启分页默认"` //是否开启分页功能 YES开启 NO不开启空字符串也不开启分页默认
}

21
api/v1/common/res.go Normal file
View File

@ -0,0 +1,21 @@
/*
* @desc:返回响应公共参数
* @company:云南奇讯科技有限公司
* @Author: yixiaohu<yxh669@qq.com>
* @Date: 2022/10/27 16:30
*/
package common
import "github.com/gogf/gf/v2/frame/g"
// EmptyRes 不响应任何数据
type EmptyRes struct {
g.Meta `mime:"application/json"`
}
// ListRes 列表公共返回
type ListRes struct {
CurrentPage int `json:"currentPage"`
Total interface{} `json:"total"`
}

254
api/v1/common/shp/shp.go Normal file
View File

@ -0,0 +1,254 @@
package shp
import (
"fmt"
"github.com/tomchavakis/turf-go"
"github.com/tiger1103/gfast/v3/api/v1/common/globe"
"github.com/tiger1103/gfast/v3/api/v1/common/tool"
"github.com/tiger1103/gfast/v3/api/v1/common/tool/shp"
"github.com/tomchavakis/geojson/geometry"
)
const (
DefaultColor = "#12f6f6"
DefaultWidth = "2"
)
type Point struct {
Lng float64 `json:"lng"`
Lat float64 `json:"lat"`
Alt float64 `json:"alt"` // 裝點更新 只更新這一個,更新立柱的高程時 這個字段不動
Width float64 `json:"width"`
Property
}
type Polyline struct {
Positions []Point `json:"positions"`
Width string `json:"width"`
Color string `json:"color"`
Alpha string `json:"alpha"`
Degree string `json:"degree"`
// Name string `json:"name"` // text
// Property string `json:"property"`
Range Box `json:"range"`
Property
}
type Polygon struct {
Positions []Point `json:"positions"`
Color string `json:"color"`
Range Box `json:"range"`
}
type Box struct {
MinX float64 `json:"min_x"`
MinY float64 `json:"min_y"`
MaxX float64 `json:"max_x"`
MaxY float64 `json:"max_y"`
}
type ShpObj struct {
Points []Point `json:"points"`
Polylines []Polyline `json:"polylines"`
Polygons []Polygon `json:"polygons"`
}
type Detail struct {
// Rotation []interfac e{} `json:"rotation"`
Position Point `json:"position"`
}
type Degree struct {
Position PointDegree `json:"position"`
}
type PointDegree struct {
Lng float64 `json:"lng"`
Lat float64 `json:"lat"`
Alt float64 `json:"alt"` // 裝點更新 只更新這一個,更新立柱的高程時 這個字段不動
Degree string `json:"degree"`
}
type Property struct {
Name string `json:"name"`
Beizhu string `json:"beizhu"`
Tishi string `json:"tishi"`
Height float64 `json:"height"` // 更新立柱的時 更新這個字段
Difference float64 `json:"difference"` // height - alt
SourceId string `json:"sourceId"`
}
/*读取shp数据*/
func ReadShp(file string) (error, *ShpObj) {
//if !globe.IS_OFFLINE_VERSION {
// file = globe.SOURCE + file
//}
if !tool.PathExists(file) {
return globe.GetErrors("资源不存在," + file), nil
}
shape, err := shp.Open(file)
if err != nil {
return err, nil
}
defer shape.Close()
obj := ShpObj{
Polygons: []Polygon{},
Polylines: []Polyline{},
Points: []Point{},
}
fields := shape.Fields()
for shape.Next() {
n, p := shape.Shape()
name := ""
beizhu := ""
tishi := ""
var O_LClr, O_LWidth, O_LAlpha /*, O_LType, O_SType, O_TType, O_Name, O_Comment*/ string
O_LClr = DefaultColor
O_LWidth = DefaultWidth
// Text := ""
for k, f := range fields {
val := shape.ReadAttribute(n, k)
bb := f.String()
// // 记录本次判断开始前的名字
// temp := name
switch bb {
// case "名称": // 方阵的名称
// if len(name) == 0 {
// name = val
// }
// case "TxtMemo": // 方阵的名称
// if len(name) == 0 {
// name = val
// }
case "name": // 方阵的名称
if len(name) == 0 {
name = val
}
// case "O_Name": // 方阵的名称
// if len(name) == 0 {
// name = val
// }
case "Text": // 方阵的名称
if len(name) == 0 {
name = val
}
case "备注": // 方阵的名称
beizhu = val
case "提示": // 方阵的名称
tishi = val
}
// 如果本次循环后名字被清空,则替换为原本的名字
// if name == "" {
// name = temp
// }
// fmt.Printf("\t%v: %v\n", f, val)
}
// fmt.Println(O_Name, O_Comment, O_LClr, O_LWidth, O_LAlpha, O_LType, O_SType, O_TType, Shape_Leng, Text, TxtMemo)
// fmt.Println("Text", Text)
// fmt.Println("O_Name", O_Name)
// fmt.Println("O_Comment", O_Comment)
// fmt.Println("O_LType", O_LType)
// fmt.Println("O_SType", O_SType)
// fmt.Println("O_TType", O_TType)
if p2, ok := p.(*shp.PolyLine); ok {
polyline := Polyline{}
polyline.Alpha = O_LAlpha
polyline.Color = O_LClr
polyline.Width = O_LWidth
polyline.Name = name
polyline.Range.MinX = p.BBox().MinX
polyline.Range.MinY = p.BBox().MinY
polyline.Range.MaxX = p.BBox().MaxX
polyline.Range.MaxY = p.BBox().MaxX
for _, po := range p2.Points {
point := Point{Lng: po.X, Lat: po.Y}
polyline.Positions = append(polyline.Positions, point)
}
obj.Polylines = append(obj.Polylines, polyline)
} else if p3, ok2 := p.(*shp.Polygon); ok2 {
polyline := Polyline{}
polyline.Alpha = O_LAlpha
polyline.Color = O_LClr
polyline.Width = O_LWidth
polyline.Name = name
polyline.Beizhu = beizhu
polyline.Tishi = tishi
// polyline.Property = Property
polyline.Range.MinX = p.BBox().MinX
polyline.Range.MinY = p.BBox().MinY
polyline.Range.MaxX = p.BBox().MaxX
polyline.Range.MaxY = p.BBox().MaxX
for _, po := range p3.Points {
point := Point{Lng: po.X, Lat: po.Y}
polyline.Positions = append(polyline.Positions, point)
}
obj.Polylines = append(obj.Polylines, polyline)
// fmt.Println(p3.Points)
} else if p3, ok3 := p.(*shp.Point); ok3 {
point := Point{Lng: p3.X, Lat: p3.Y}
point.Name = name
point.Tishi = tishi
point.Beizhu = beizhu
obj.Points = append(obj.Points, point)
} else if p3, ok2 := p.(*shp.PolygonZ); ok2 {
polyline := Polyline{}
polyline.Alpha = O_LAlpha
polyline.Color = O_LClr
polyline.Width = O_LWidth
polyline.Name = name
polyline.Beizhu = beizhu
polyline.Tishi = tishi
// polyline.Property = Property
polyline.Range.MinX = p.BBox().MinX
polyline.Range.MinY = p.BBox().MinY
polyline.Range.MaxX = p.BBox().MaxX
polyline.Range.MaxY = p.BBox().MaxX
for _, po := range p3.Points {
point := Point{Lng: po.X, Lat: po.Y}
polyline.Positions = append(polyline.Positions, point)
}
obj.Polylines = append(obj.Polylines, polyline)
// fmt.Println(p3.Points)
} else if p3, ok3 := p.(*shp.PointZ); ok3 {
point := Point{Lng: p3.X, Lat: p3.Y}
point.Name = name
point.Tishi = tishi
point.Beizhu = beizhu
obj.Points = append(obj.Points, point)
} else {
fmt.Println("其他类型")
}
}
return nil, &obj
}
/*判断点是否被区域包含*/
func PointInPolygon(point Point, positions []Point) (bool, error) {
if len(positions) < 3 {
return false, globe.GetErrors("坐标点数量不能小于3")
}
polygon := geometry.Polygon{}
var pts []geometry.Point
for _, position := range positions {
pts = append(pts, geometry.Point{Lat: position.Lat, Lng: point.Lng})
}
// pts = append(pts, pts[len(pts)-1])
LineString := geometry.LineString{}
LineString.Coordinates = pts
polygon.Coordinates = []geometry.LineString{LineString}
return turf.PointInPolygon(geometry.Point{Lat: point.Lat, Lng: point.Lng}, polygon)
}

View File

@ -0,0 +1,133 @@
package clt
import (
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/tiger1103/gfast/v3/api/v1/common/globe"
"github.com/tiger1103/gfast/v3/api/v1/common/tool"
"github.com/tiger1103/gfast/v3/database"
"github.com/tiger1103/gfast/v3/database/sqlite"
"io"
"net/http"
"os"
"path"
"strings"
)
func InitCltData(group *ghttp.RouterGroup) {
group.GET("/data/tileset/{source_id}/*.action", cltCallback)
group.GET("/data/bim/{source_id}/*.action", cltCallback)
}
func GetTile(sourceid, p string) []byte {
md5 := tool.Md5V(p)
tile := globe.Tile{}
database.GetSourceDB(sourceid).DB.Select("tile").Where("md5=?", md5).First(&tile)
// 创建一个字节缓冲区,并将压缩数据写入其中
buf := bytes.NewBuffer(tile.Tile)
// 创建一个gzip.Reader对象用于解压缩数据
reader, _ := gzip.NewReader(buf)
defer reader.Close()
// 读取解压缩后的数据
decompressedData, _ := io.ReadAll(reader)
return decompressedData
}
func cltCallback(request *ghttp.Request) {
sourceId := request.Get("source_id").String()
cltObj := database.GetSourceDB(sourceId)
if cltObj.DB == nil {
request.Response.WriteStatus(http.StatusNotFound)
return
}
argcs := strings.Split(request.RequestURI, "/")
argcs = argcs[7:]
md5 := tool.Md5V(strings.Join(argcs, "/"))
tile := globe.Tile{}
RowsAffected := cltObj.DB.Select("tile").Where("md5=?", md5).Find(&tile).RowsAffected
if RowsAffected == 0 {
request.Response.WriteStatus(http.StatusNotFound)
return
}
suffix := path.Ext(request.RequestURI)
if suffix == ".json" {
request.Response.Header().Set("content-type", "application/json")
} else {
request.Response.Header().Set("content-type", "application/octet-stream")
}
if cltObj.Gzip {
request.Response.Header().Set("Content-Encoding", "gzip")
}
globe.RenderData(request, tile.Tile)
}
type Info struct {
Params string `json:"params"`
}
type parseIsZip struct {
Zip bool `json:"zip"`
}
type IsJct struct {
Jct bool `json:"jct"`
}
func OpenClt(cltPath string, sourceID string) (error, *database.SourceObj) {
//if !globe.IS_OFFLINE_VERSION {
// //网络版事 需要拼接数据地址,方便服务器迁移
if !tool.PathExists(cltPath) {
getwd, err := os.Getwd()
if err != nil {
return err, nil
}
cltPath = path.Join(getwd, cltPath)
}
//}
fmt.Println(cltPath)
if tool.PathExists(cltPath) {
db, err := sqlite.OpenDB(cltPath)
if err != nil {
return err, nil
}
var obj database.SourceObj
obj.DB = db
var info []Info
db.Model(&Info{}).Find(&info)
p := parseIsZip{}
errs := json.Unmarshal([]byte(info[0].Params), &p)
if errs == nil {
obj.Gzip = p.Zip
}
suffix := path.Ext(cltPath)
if suffix == globe.CLT {
obj.Type = globe.TILESET
obj.Url = "/zm/api/v1/data/tileset/" + sourceID + "/tileset.json"
} else {
obj.Type = globe.BIM
obj.Url = "/zm/api/v1/data/bim/" + sourceID + "/tileset.json"
if len(info) < 2 {
//非jct资源
globe.CloseDB(db)
return globe.GetErrors("非jct资源"), nil
}
isjct := IsJct{}
errs := json.Unmarshal([]byte(info[1].Params), &isjct)
if errs != nil {
globe.CloseDB(db)
return globe.GetErrors("jct资源检测失败"), nil
}
}
database.SetSourceDB(sourceID, obj)
return err, &obj
}
fmt.Println("资源不存在:" + cltPath)
return globe.GetErrors("资源不存在:" + cltPath), nil
}

View File

@ -0,0 +1,123 @@
package mbt
import (
"github.com/gogf/gf/v2/net/ghttp"
"github.com/tiger1103/gfast/v3/api/v1/common/globe"
"github.com/tiger1103/gfast/v3/api/v1/common/tool"
"github.com/tiger1103/gfast/v3/database"
"github.com/tiger1103/gfast/v3/database/sqlite"
"gorm.io/gorm"
"math"
"net/http"
"os"
"path"
"strings"
)
func InitMbtData(group *ghttp.RouterGroup) {
group.GET("/data/mbt/{source_id}/{z}/{x}/{y}.*", mbtCallback)
}
func mbtCallback(request *ghttp.Request) {
sourceId := request.Get("source_id").String()
mbtobj := database.GetSourceDB(sourceId)
if mbtobj.DB == nil {
request.Response.WriteStatus(http.StatusNotFound)
return
}
z := request.Get("z").Int()
x := request.Get("x").Int()
y := request.Get("y").Int()
y = int(math.Pow(2, float64(z))) - 1 - y
tile := Tile{}
RowsAffected := mbtobj.DB.Model(&Tile{}).
Select("tile_data").
Where(&Tile{ZoomLevel: z, TileColumn: x, TileRow: y}).First(&tile).RowsAffected
if RowsAffected > 0 {
request.Response.Header().Set("content-type", mbtobj.ContentType)
globe.RenderData(request, tile.TileData)
return
} else {
request.Response.WriteStatus(http.StatusNotFound)
}
}
type Tile struct {
TileData []byte `json:"tile_data"`
ZoomLevel int `json:"zoom_level"`
TileColumn int `json:"tile_column"`
TileRow int `json:"tile_row"`
}
type Metadata struct {
Name string
Value string
}
func OpenMbt(mbtPath string, sourceID string) (error, *database.SourceObj) {
//if !globe.IS_OFFLINE_VERSION {
// //网络版事 需要拼接数据地址,方便服务器迁移
// mbtPath = path.Join(globe.SOURCE, mbtPath)
//}
getwd, err := os.Getwd()
if err != nil {
return err, nil
}
mbtPath = path.Join(getwd, mbtPath)
if !tool.PathExists(mbtPath) {
return globe.GetErrors("资源不存在," + mbtPath), nil
}
db, err := sqlite.OpenDB(mbtPath)
if err != nil {
return err, nil
}
var obj database.SourceObj
obj.DB = db
obj.Type = globe.LAYER
var meta []Metadata
db.Model(&Metadata{}).Find(&meta)
obj.Info.MinLevel, obj.Info.MaxLevel = startQueryLevel(db)
var format = "png"
for _, v := range meta {
if v.Name == "format" {
format = v.Value
}
if v.Name == "bounds" {
arr := strings.Split(v.Value, ",")
obj.Info.West = arr[0]
obj.Info.South = arr[1]
obj.Info.East = arr[2]
obj.Info.North = arr[3]
}
if v.Name == "profile" {
obj.Info.ProFile = v.Value
}
if v.Name == "description" {
//lsv下载的 自带投影,此时不需要加
if strings.Contains(v.Value, "LSV") {
obj.Info.TilingScheme = 0
} else {
obj.Info.TilingScheme = 1
}
}
}
obj.ContentType = "image/" + format
obj.Url = "/zm/api/v1/data/mbt/" + sourceID + "/{z}/{x}/{y}." + format
database.SetSourceDB(sourceID, obj)
return err, &obj
}
func startQueryLevel(db *gorm.DB) (min, max int) {
zoom_level := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22}
var existsLevels []int
for i := 0; i < len(zoom_level); i++ {
RowsAffected := db.Model(&Tile{}).Select("zoom_level").Where(&Tile{ZoomLevel: i}).Find(&Tile{}).RowsAffected
if RowsAffected > 0 {
existsLevels = append(existsLevels, i)
}
}
if len(existsLevels) > 0 {
min = existsLevels[0]
max = existsLevels[len(existsLevels)-1]
}
return
}

View File

@ -0,0 +1,148 @@
package pak
import (
"fmt"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/tiger1103/gfast/v3/api/v1/common/globe"
"github.com/tiger1103/gfast/v3/api/v1/common/tool"
"github.com/tiger1103/gfast/v3/database"
"github.com/tiger1103/gfast/v3/database/sqlite"
"math"
"net/http"
"os"
"path"
"strconv"
"strings"
)
const (
image = "image"
terrain = "terrain"
)
func InitPakData(group *ghttp.RouterGroup) {
//group.GET("/data/pak/{source_id}/{z}/{x}/{y}.*", pakCallback)
//group.GET("/data/pak/{source_id}/layer.json", pakCallback)
group.GET("/data/pak/{source_id}/*.action", pakCallback)
}
type Json struct {
Layerjson []byte `json:"layerjson"`
}
// 获取pak文件中的表名
func gettablename(x int, y int, z int) string {
if z < 10 {
return "blocks"
} else {
tx := math.Ceil(float64(x / 512))
ty := math.Ceil(float64(y / 512))
return "blocks_" + strconv.Itoa(z) + "_" + strconv.Itoa(int(tx)) + "_" + strconv.Itoa(int(ty))
}
}
type Tile struct {
Tile []byte `json:"tile"`
Z int `json:"z"`
X int `json:"x"`
Y int `json:"y"`
}
func pakCallback(request *ghttp.Request) {
sourceId := request.Get("source_id").String()
pakobj := database.GetSourceDB(sourceId)
if pakobj.DB == nil {
request.Response.WriteStatus(http.StatusNotFound)
return
}
suffix := path.Ext(request.RequestURI)
if suffix == ".json" {
json := Json{}
pakobj.DB.Model(&Info{}).First(&json)
request.Response.Header().Set("content-type", "application/json")
globe.RenderData(request, json.Layerjson)
return
} else {
uri := request.RequestURI
arr := strings.Split(uri, "/")
//z := request.Get("z").Int()
//x := request.Get("x").Int()
//y := request.Get("y").Int()
z, _ := strconv.Atoi(arr[7])
x, _ := strconv.Atoi(arr[8])
y, _ := strconv.Atoi(strings.Split(arr[9], ".")[0])
//y = int(math.Pow(2, float64(z))) - 1 - y
tile := Tile{}
RowsAffected := pakobj.DB.Table(gettablename(x, y, z)).Select("tile").Where(&Tile{Z: z, X: x, Y: y}).First(&tile).RowsAffected
if RowsAffected > 0 {
request.Response.Header().Set("content-type", pakobj.ContentType)
if pakobj.Gzip {
request.Response.Header().Set("Content-Encoding", "gzip")
}
globe.RenderData(request, tile.Tile)
return
} else {
request.Response.WriteStatus(http.StatusNotFound)
}
}
}
type Info struct {
Minx float64 `json:"minx"`
Miny float64 `json:"miny"`
Maxx float64 `json:"maxx"`
Maxy float64 `json:"maxy"`
Minlevel int `json:"minlevel"`
Maxlevel int `json:"maxlevel"`
Type string `json:"type"`
Zip int `json:"zip"`
//Layerjson []byte `json:"layerjson"`
Contenttype string `json:"contenttype"`
}
func OpenPak(pakPath string, sourceID string) (error, *database.SourceObj) {
//if !globe.IS_OFFLINE_VERSION {
// //网络版事 需要拼接数据地址,方便服务器迁移
// pakPath = path.Join(globe.SOURCE, pakPath)
//}
getwd, err := os.Getwd()
if err != nil {
return err, nil
}
pakPath = path.Join(getwd, pakPath)
if !tool.PathExists(pakPath) {
return globe.GetErrors("资源不存在," + pakPath), nil
}
fmt.Println("资源存在")
db, err := sqlite.OpenDB(pakPath)
if err != nil {
fmt.Println(err)
return err, nil
}
var obj database.SourceObj
obj.DB = db
info := Info{}
db.Model(&Info{}).First(&info)
if info.Type == image {
obj.Type = globe.LAYER
obj.ContentType = info.Contenttype
obj.Url = "/zm/api/v1/data/pak/" + sourceID + "/{z}/{x}/{y}." + strings.Split(obj.ContentType, "/")[1]
}
if info.Type == terrain {
obj.Type = globe.TERRAIN
obj.ContentType = "application/octet-stream"
obj.Url = "/zm/api/v1/data/pak/" + sourceID + "/"
}
if info.Zip > 0 {
obj.Gzip = true
}
obj.Info.MaxLevel = info.Maxlevel
obj.Info.MinLevel = info.Minlevel
obj.Info.West = strconv.FormatFloat(info.Minx, 'f', -1, 64)
obj.Info.South = strconv.FormatFloat(info.Miny, 'f', -1, 64)
obj.Info.East = strconv.FormatFloat(info.Maxx, 'f', -1, 64)
obj.Info.North = strconv.FormatFloat(info.Maxy, 'f', -1, 64)
database.SetSourceDB(sourceID, obj)
return err, &obj
}

View File

@ -0,0 +1,295 @@
package shp
import (
"context"
"fmt"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/tidwall/gjson"
shp2 "github.com/tiger1103/gfast/v3/api/v1/common/shp"
"io"
"net/http"
"regexp"
"strings"
)
const MaxNeighborsLen = 8 //最大邻居个数
const SOURCE = "static/source/"
const Gisfile = "gisfile/"
// 84的投影文件
const WGS84_PRJ = "GEOGCS[\"GCS_WGS_1984\",DATUM[\"D_WGS_1984\",SPHEROID[\"WGS_1984\",6378137.0,298.257223563]],PRIMEM[\"Greenwich\",0.0],UNIT[\"Degree\",0.0174532925199433]]"
func InitShp(group *ghttp.RouterGroup) {
group.Group("/shp", func(group *ghttp.RouterGroup) {
group.Bind(new(SHP))
})
}
type SHP struct {
}
type SHPLoadReq struct {
g.Meta `path:"load" summary:"cesium加载shp" method:"get" tags:"shp相关" `
//SourceID string `json:"source_id" dc:"资源id" v:"required"`
Path string `json:"path" dc:"路径" v:"required"`
}
type SHPLoadRes struct {
shp2.ShpObj
}
//func (receiver SHP) LoadSHP(ctx context.Context, req *SHPLoadReq) (res *SHPLoadRes, err error) {
// err, obj := shp2.ReadShp(req.Path)
// if err != nil {
// return nil, err
// }
// res = &SHPLoadRes{}
// res.Points = obj.Points
// res.Polylines = obj.Polylines
// res.Polygons = obj.Polygons
// return res, err
//}
type Range struct {
MinX float64 `json:"min_x"`
MinY float64 `json:"min_y"`
MaxX float64 `json:"max_x"`
MaxY float64 `json:"max_y"`
}
type Position struct {
X float64 `json:"x"`
Y float64 `json:"y"`
Z float64 `json:"z"`
Attr map[string]interface{} `json:"attr"`
}
type Text struct {
X float64 `json:"x"`
Y float64 `json:"y"`
Text string `json:"text"`
}
type Circle struct {
X float64 `json:"x"`
Y float64 `json:"y"`
Z float64 `json:"z"`
Radius float64 `json:"radius"`
}
type Polyline struct {
Positions []Position `json:"positions"`
Attr map[string]interface{} `json:"attr"`
Range Range `json:"range"`
}
type Polygon struct {
Positions []Position `json:"positions"`
Attr map[string]interface{} `json:"attr"`
Range Range `json:"range"`
}
type MultiPolygon struct {
Polygons []Polygon `json:"polygons"`
Attr map[string]interface{} `json:"attr"`
Range Range `json:"range"`
}
type MultiPolyline struct {
Polylines []Polyline `json:"polylines"`
Attr map[string]interface{} `json:"attr"`
Range Range `json:"range"`
}
type LayerData struct {
LayerName string `json:"layer_name"`
Proj4 string `json:"proj4"`
Texts []Text `json:"texts"`
Circles []Circle `json:"circles"`
Points []Position `json:"points"`
Polylines []Polyline `json:"polylines"`
Polygons []Polygon `json:"polygons"`
MultiPolygons []MultiPolygon `json:"multi_polygons"`
MultiPolylines []MultiPolyline `json:"multi_polylines"`
}
type Directory struct {
Name string `json:"name"`
IsDir bool `json:"is_dir"`
Data []LayerData `json:"data"`
Children []Directory `json:"children"`
}
type Response struct {
Code int `json:"code"`
Data Directory `json:"data"`
}
// JoinLoadSHP 原本的LoadSHP有问题直接调用远程的接口
func (receiver SHP) JoinLoadSHP(ctx context.Context, req *SHPLoadReq) (res *SHPLoadRes, err error) {
res = new(SHPLoadRes)
if req.Path[0] == '/' {
req.Path = req.Path[1:]
}
url := "http://192.168.1.177:8921/yjearth5/api/v1/vector/load?path=/project/zmkg/" + req.Path
reqs, err := http.Get(url)
if err != nil {
return nil, err
}
defer reqs.Body.Close()
body, err := io.ReadAll(reqs.Body)
if err != nil {
return nil, err
}
if gjson.Get(string(body), "message").String() == "资源不存在" {
return nil, fmt.Errorf("资源不存在")
}
var list shp2.ShpObj
processShapes(body, "data.children.0.data.0.polylines", &list)
processShapes(body, "data.children.0.data.0.polygons", &list)
processShapes(body, "data.children.0.data", &list)
if list.Polylines == nil {
list.Polylines = []shp2.Polyline{}
}
if list.Polygons == nil {
list.Polygons = []shp2.Polygon{}
}
if list.Points == nil {
list.Points = []shp2.Point{}
}
res.Polygons = list.Polygons
res.Polylines = list.Polylines
res.Points = list.Points
return res, err
}
func Adasda(path string) (res *SHPLoadRes) {
fmt.Println("加载shp文件", path)
res = new(SHPLoadRes)
if path[0] == '/' {
path = path[1:]
}
url := "http://192.168.1.177:8921/yjearth5/api/v1/vector/load?path=/project/zmkg/" + path
reqs, err := http.Get(url)
if err != nil {
fmt.Println("请求数据失败")
}
defer reqs.Body.Close()
body, err := io.ReadAll(reqs.Body)
if err != nil {
fmt.Errorf("读取资源失败")
}
if gjson.Get(string(body), "message").String() == "资源不存在" {
fmt.Errorf("资源不存在")
}
var list = shp2.ShpObj{
Polylines: []shp2.Polyline{},
Polygons: []shp2.Polygon{},
Points: []shp2.Point{},
}
processShapes(body, "data.children.0.data.0.polylines", &list)
processShapes(body, "data.children.0.data.0.polygons", &list)
processShapes(body, "data.children.0.data", &list)
res.Polygons = list.Polygons
res.Polylines = list.Polylines
res.Points = list.Points
return
}
func processShapes(data []byte, path string, shapes *shp2.ShpObj) {
gjson.GetBytes(data, path).ForEach(func(key, shape gjson.Result) bool {
// 面
var pointsList []shp2.Point
psGet := shape.Get("positions")
if psGet.Exists() {
psGet.ForEach(func(posKey, value gjson.Result) bool {
longitude := value.Get("x").Float()
latitude := value.Get("y").Float()
altitude := value.Get("z").Float()
pointsList = append(pointsList, shp2.Point{
Lng: longitude,
Lat: latitude,
Alt: altitude,
})
return true
})
if len(pointsList) > 0 {
shapes.Polylines = append(shapes.Polylines, shp2.Polyline{
Positions: pointsList,
Width: "5",
Color: "#f00",
})
}
if shape.Get("attr").Exists() {
aName := shape.Get("attr.NAME").String()
fmt.Println("!!! ", aName)
shapes.Polylines[len(shapes.Polylines)-1].Property = shp2.Property{
Name: aName,
}
}
if shape.Get("range").Exists() {
minX := shape.Get("range.min_x").Float()
minY := shape.Get("range.min_y").Float()
maxX := shape.Get("range.max_x").Float()
maxY := shape.Get("range.max_y").Float()
shapes.Polylines[len(shapes.Polylines)-1].Range = shp2.Box{
MinX: minX,
MinY: minY,
MaxX: maxX,
MaxY: maxY,
}
}
} else {
//fmt.Println("!!! ", shape.Get("points"))
// 点
var point []shp2.Point
shape.Get("points").ForEach(func(posKey, value gjson.Result) bool {
aName := value.Get("attr.NAME")
if value.Get("attr.NAME").Exists() {
//排除nc 和 空
isPureNumber, _ := regexp.MatchString(`^\d+$`, aName.String())
if strings.Contains(aName.String(), "NC") || strings.TrimSpace(aName.String()) == "" || isPureNumber {
return true
}
longitude := value.Get("x").Float()
latitude := value.Get("y").Float()
altitude := value.Get("z").Float()
point = append(point, shp2.Point{
Lng: longitude,
Lat: latitude,
Alt: altitude,
Property: shp2.Property{
Name: aName.String(),
},
})
}
return true
})
if len(point) > 0 {
shapes.Points = append(shapes.Points, point...)
}
}
return true
})
}

View File

@ -0,0 +1,85 @@
package source
import (
"context"
"encoding/json"
"fmt"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/os/gctx"
"github.com/tiger1103/gfast/v3/api/v1/common/globe"
"github.com/tiger1103/gfast/v3/api/v1/common/source/clt"
"github.com/tiger1103/gfast/v3/api/v1/common/source/pak"
"github.com/tiger1103/gfast/v3/database"
"github.com/tiger1103/gfast/v3/internal/app/system/dao"
"path"
)
func InitSource(group *ghttp.RouterGroup) {
ReadAllSourceFromDB()
group.Group("/data/service", func(group *ghttp.RouterGroup) {
group.Bind(new(LoadSource))
})
}
type LoadSource struct {
}
type LoadSourceReq struct {
g.Meta `path:"load-compact-service" summary:"引擎加载资源" method:"post" tags:"资源相关" `
SourceID string `json:"source_id" v:"required" dc:"资源id"`
}
type LoadSourceRes struct {
Type string `json:"type"`
Url string `json:"url"`
database.SourceInfo
}
func (LoadSource) LoadCompactService(ctx context.Context, req *LoadSourceReq) (res *LoadSourceRes, err error) {
obj := database.GetSourceDB(req.SourceID)
res = &LoadSourceRes{
Url: obj.Url,
Type: obj.Type,
}
res.North = obj.Info.North
res.West = obj.Info.West
res.East = obj.Info.East
res.South = obj.Info.South
res.ProFile = obj.Info.ProFile
res.TilingScheme = obj.Info.TilingScheme
res.MaxLevel = obj.Info.MaxLevel
res.MinLevel = obj.Info.MinLevel
return
}
func ReadAllSourceFromDB() {
var sources []database.SOURCE
var gfb []database.SOURCE
g.DB().Model(&database.SOURCE{})
ctx := gctx.New()
//模型
dao.QianqiMoxing.Ctx(ctx).Scan(&sources)
//光伏板
dao.QianqiGuangfuban.Ctx(ctx).Scan(&gfb)
sources = append(sources, gfb...)
for _, v := range sources {
suffix := path.Ext(v.SourcePath)
switch suffix {
case globe.CLT:
err, obj := clt.OpenClt(v.SourcePath, v.SourceID)
if err != nil {
fmt.Println(err)
}
marshal, _ := json.Marshal(obj)
fmt.Println(string(marshal), v.SourceID)
break
case globe.PAK:
err, obj := pak.OpenPak(v.SourcePath, v.SourceID)
if err != nil {
fmt.Println(err)
}
marshal, _ := json.Marshal(obj)
fmt.Println(string(marshal), v.SourceID)
break
}
}
}

View File

@ -0,0 +1,49 @@
package excel
import (
"errors"
"fmt"
"github.com/tiger1103/gfast/v3/api/v1/common/tool"
"github.com/xuri/excelize/v2"
)
type Sheet struct {
Name string `json:"name"`
Rows [][]string `json:"rows"`
}
func ReadXlsx(xlsx string) (err error, sheet []Sheet) {
if !tool.PathExists(xlsx) {
return errors.New("文件不存在:" + xlsx), sheet
}
f, err := excelize.OpenFile(xlsx)
if err != nil {
fmt.Println(err)
return err, sheet
}
defer func() {
// 关闭工作簿
if err := f.Close(); err != nil {
fmt.Println(err)
}
}()
list := f.GetSheetList()
// 获取 Sheet1 上所有单元格
for _, sheetName := range list {
result, err := f.GetRows(sheetName)
if err != nil {
fmt.Println(err)
continue
}
sheet = append(sheet, Sheet{sheetName, result})
//rows = append(rows, result...)
}
//for _, row := range rows {
// for _, colCell := range row {
// fmt.Print(colCell, "\t")
// }
// fmt.Println()
//}
return nil, sheet
}

View File

@ -0,0 +1,25 @@
package proj
import (
_ "embed"
"github.com/dop251/goja"
)
//go:embed proj4.js
var proj4 string
var CGCS2000_to_WGS84 func(degrees int, cscs2000 [][]string) string
var WGS84_to_CGCS2000 func(degrees int, wgs84 [][]string) string
func InitProj() {
vm := goja.New()
vm.RunString(proj4)
vm.ExportTo(vm.Get("CGCS2000_to_WGS84"), &CGCS2000_to_WGS84)
vm.ExportTo(vm.Get("WGS84_to_CGCS2000"), &WGS84_to_CGCS2000)
//var ss [][]string
//ss = append(ss, []string{
// "106.545463204423", "23.467020901621", "805.6832",
//})
//s := WGS84_to_CGCS2000(108, ss)
//fmt.Println(s)
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,2 @@
go:
enabled: true

View File

@ -0,0 +1,19 @@
language: go
sudo: false
go:
- 1.8.x
- 1.9.x
- master
os:
- linux
before_install:
- go get -t -v ./...
script:
- go test -race -coverprofile=coverage.txt -covermode=atomic
after_success:
- bash <(curl -s https://codecov.io/bash)

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014 Jonas Palm
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,87 @@
go-shp
======
[![Build Status](https://travis-ci.org/jonas-p/go-shp.svg?branch=master)](https://travis-ci.org/jonas-p/go-shp)
[![Build status](https://ci.appveyor.com/api/projects/status/b64sntax4kxlouxa?svg=true)](https://ci.appveyor.com/project/fawick/go-shp)
[![Go Report Card](https://goreportcard.com/badge/github.com/jonas-p/go-shp)](https://goreportcard.com/report/github.com/jonas-p/go-shp)
[![Codevov](https://codecov.io/gh/jonas-p/go-shp/branch/master/graphs/badge.svg)](https://codecov.io/gh/jonas-p/go-shp)
Go library for reading and writing ESRI Shapefiles. This is a pure Golang implementation based on the ESRI Shapefile technical description.
### Usage
#### Installation
go get github.com/jonas-p/go-shp
#### Importing
```go
import "github.com/jonas-p/go-shp"
```
### Examples
#### Reading a shapefile
```go
// open a shapefile for reading
shape, err := shp.Open("points.shp")
if err != nil { log.Fatal(err) }
defer shape.Close()
// fields from the attribute table (DBF)
fields := shape.Fields()
// loop through all features in the shapefile
for shape.Next() {
n, p := shape.Shape()
// print feature
fmt.Println(reflect.TypeOf(p).Elem(), p.BBox())
// print attributes
for k, f := range fields {
val := shape.ReadAttribute(n, k)
fmt.Printf("\t%v: %v\n", f, val)
}
fmt.Println()
}
```
#### Creating a shapefile
```go
// points to write
points := []shp.Point{
shp.Point{10.0, 10.0},
shp.Point{10.0, 15.0},
shp.Point{15.0, 15.0},
shp.Point{15.0, 10.0},
}
// fields to write
fields := []shp.Field{
// String attribute field with length 25
shp.StringField("NAME", 25),
}
// create and open a shapefile for writing points
shape, err := shp.Create("points.shp", shp.POINT)
if err != nil { log.Fatal(err) }
defer shape.Close()
// setup fields for attributes
shape.SetFields(fields)
// write points and attributes
for n, point := range points {
shape.Write(&point)
// write attribute for object n for field 0 (NAME)
shape.WriteAttribute(n, 0, "Point " + strconv.Itoa(n + 1))
}
```
### Resources
- [Documentation on godoc.org](http://godoc.org/github.com/jonas-p/go-shp)
- [ESRI Shapefile Technical Description](http://www.esri.com/library/whitepapers/pdfs/shapefile.pdf)

View File

@ -0,0 +1,26 @@
clone_folder: c:\go-shp
environment:
GOPATH: c:\gopath
branches:
only:
- master
init:
- ps: >-
$app = Get-WmiObject -Class Win32_Product -Filter "Vendor = 'http://golang.org'"
if ($app) {
$app.Uninstall()
}
install:
- rmdir c:\go /s /q
- appveyor DownloadFile https://storage.googleapis.com/golang/go1.9.windows-amd64.msi
- msiexec /i go1.9.windows-amd64.msi /q
- go version
- go env
build_script:
- go test ./...

View File

@ -0,0 +1,27 @@
package shp
import (
"fmt"
"io"
)
// errReader is a helper to perform multiple successive read from another reader
// and do the error checking only once afterwards. It will not perform any new
// reads in case there was an error encountered earlier.
type errReader struct {
io.Reader
e error
n int64
}
func (er *errReader) Read(p []byte) (n int, err error) {
if er.e != nil {
return 0, fmt.Errorf("unable to read after previous error: %v", er.e)
}
n, err = er.Reader.Read(p)
if n < len(p) && err != nil {
er.e = err
}
er.n += int64(n)
return n, er.e
}

View File

@ -0,0 +1,253 @@
package shp
import (
"encoding/binary"
"fmt"
"io"
"math"
"os"
"path/filepath"
"strings"
)
// Reader provides a interface for reading Shapefiles. Calls
// to the Next method will iterate through the objects in the
// Shapefile. After a call to Next the object will be available
// through the Shape method.
type Reader struct {
GeometryType ShapeType
bbox Box
err error
shp readSeekCloser
shape Shape
num int32
filename string
filelength int64
dbf readSeekCloser
dbfFields []Field
dbfNumRecords int32
dbfHeaderLength int16
dbfRecordLength int16
}
type readSeekCloser interface {
io.Reader
io.Seeker
io.Closer
}
// Open opens a Shapefile for reading.
func Open(filename string) (*Reader, error) {
ext := filepath.Ext(filename)
if strings.ToLower(ext) != ".shp" {
return nil, fmt.Errorf("Invalid file extension: %s", filename)
}
shp, err := os.Open(filename)
if err != nil {
return nil, err
}
s := &Reader{filename: strings.TrimSuffix(filename, ext), shp: shp}
return s, s.readHeaders()
}
// BBox returns the bounding box of the shapefile.
func (r *Reader) BBox() Box {
return r.bbox
}
// Read and parse headers in the Shapefile. This will
// fill out GeometryType, filelength and bbox.
func (r *Reader) readHeaders() error {
er := &errReader{Reader: r.shp}
// don't trust the the filelength in the header
r.filelength, _ = r.shp.Seek(0, io.SeekEnd)
var filelength int32
r.shp.Seek(24, 0)
// file length
binary.Read(er, binary.BigEndian, &filelength)
r.shp.Seek(32, 0)
binary.Read(er, binary.LittleEndian, &r.GeometryType)
r.bbox.MinX = readFloat64(er)
r.bbox.MinY = readFloat64(er)
r.bbox.MaxX = readFloat64(er)
r.bbox.MaxY = readFloat64(er)
r.shp.Seek(100, 0)
return er.e
}
func readFloat64(r io.Reader) float64 {
var bits uint64
binary.Read(r, binary.LittleEndian, &bits)
return math.Float64frombits(bits)
}
// Close closes the Shapefile.
func (r *Reader) Close() error {
if r.err == nil {
r.err = r.shp.Close()
if r.dbf != nil {
r.dbf.Close()
}
}
return r.err
}
// Shape returns the most recent feature that was read by
// a call to Next. It returns two values, the int is the
// object index starting from zero in the shapefile which
// can be used as row in ReadAttribute, and the Shape is the object.
func (r *Reader) Shape() (int, Shape) {
return int(r.num) - 1, r.shape
}
// Attribute returns value of the n-th attribute of the most recent feature
// that was read by a call to Next.
func (r *Reader) Attribute(n int) string {
return r.ReadAttribute(int(r.num)-1, n)
}
// newShape creates a new shape with a given type.
func newShape(shapetype ShapeType) (Shape, error) {
switch shapetype {
case NULL:
return new(Null), nil
case POINT:
return new(Point), nil
case POLYLINE:
return new(PolyLine), nil
case POLYGON:
return new(Polygon), nil
case MULTIPOINT:
return new(MultiPoint), nil
case POINTZ:
return new(PointZ), nil
case POLYLINEZ:
return new(PolyLineZ), nil
case POLYGONZ:
return new(PolygonZ), nil
case MULTIPOINTZ:
return new(MultiPointZ), nil
case POINTM:
return new(PointM), nil
case POLYLINEM:
return new(PolyLineM), nil
case POLYGONM:
return new(PolygonM), nil
case MULTIPOINTM:
return new(MultiPointM), nil
case MULTIPATCH:
return new(MultiPatch), nil
default:
return nil, fmt.Errorf("Unsupported shape type: %v", shapetype)
}
}
// Next reads in the next Shape in the Shapefile, which
// will then be available through the Shape method. It
// returns false when the reader has reached the end of the
// file or encounters an error.
func (r *Reader) Next() bool {
cur, _ := r.shp.Seek(0, io.SeekCurrent)
if cur >= r.filelength {
return false
}
var size int32
var shapetype ShapeType
er := &errReader{Reader: r.shp}
binary.Read(er, binary.BigEndian, &r.num)
binary.Read(er, binary.BigEndian, &size)
binary.Read(er, binary.LittleEndian, &shapetype)
if er.e != nil {
if er.e != io.EOF {
r.err = fmt.Errorf("Error when reading metadata of next shape: %v", er.e)
} else {
r.err = io.EOF
}
return false
}
var err error
r.shape, err = newShape(shapetype)
if err != nil {
r.err = fmt.Errorf("Error decoding shape type: %v", err)
return false
}
r.shape.read(er)
if er.e != nil {
r.err = fmt.Errorf("Error while reading next shape: %v", er.e)
return false
}
// move to next object
r.shp.Seek(int64(size)*2+cur+8, 0)
return true
}
// Opens DBF file using r.filename + "dbf". This method
// will parse the header and fill out all dbf* values int
// the f object.
func (r *Reader) openDbf() (err error) {
if r.dbf != nil {
return
}
r.dbf, err = os.Open(r.filename + ".dbf")
if err != nil {
return
}
// read header
r.dbf.Seek(4, io.SeekStart)
binary.Read(r.dbf, binary.LittleEndian, &r.dbfNumRecords)
binary.Read(r.dbf, binary.LittleEndian, &r.dbfHeaderLength)
binary.Read(r.dbf, binary.LittleEndian, &r.dbfRecordLength)
r.dbf.Seek(20, io.SeekCurrent) // skip padding
numFields := int(math.Floor(float64(r.dbfHeaderLength-33) / 32.0))
r.dbfFields = make([]Field, numFields)
binary.Read(r.dbf, binary.LittleEndian, &r.dbfFields)
return
}
// Fields returns a slice of Fields that are present in the
// DBF table.
func (r *Reader) Fields() []Field {
err := r.openDbf()
fmt.Println(err)
if err != nil {
return nil
} // make sure we have dbf file to read from
return r.dbfFields
}
// Err returns the last non-EOF error encountered.
func (r *Reader) Err() error {
if r.err == io.EOF {
return nil
}
return r.err
}
// AttributeCount returns number of records in the DBF table.
func (r *Reader) AttributeCount() int {
r.openDbf() // make sure we have a dbf file to read from
return int(r.dbfNumRecords)
}
// ReadAttribute returns the attribute value at row for field in
// the DBF table as a string. Both values starts at 0.
func (r *Reader) ReadAttribute(row int, field int) string {
r.openDbf() // make sure we have a dbf file to read from
seekTo := 1 + int64(r.dbfHeaderLength) + (int64(row) * int64(r.dbfRecordLength))
for n := 0; n < field; n++ {
seekTo += int64(r.dbfFields[n].Size)
}
r.dbf.Seek(seekTo, io.SeekStart)
buf := make([]byte, r.dbfFields[field].Size)
r.dbf.Read(buf)
return strings.Trim(string(buf[:]), " ")
}

View File

@ -0,0 +1,527 @@
package shp
import (
"bytes"
"io"
"io/ioutil"
"testing"
)
func pointsEqual(a, b []float64) bool {
if len(a) != len(b) {
return false
}
for k, v := range a {
if v != b[k] {
return false
}
}
return true
}
func getShapesFromFile(prefix string, t *testing.T) (shapes []Shape) {
filename := prefix + ".shp"
file, err := Open(filename)
if err != nil {
t.Fatal("Failed to open shapefile: " + filename + " (" + err.Error() + ")")
}
defer file.Close()
for file.Next() {
_, shape := file.Shape()
shapes = append(shapes, shape)
}
if file.Err() != nil {
t.Errorf("Error while getting shapes for %s: %v", prefix, file.Err())
}
return shapes
}
type shapeGetterFunc func(string, *testing.T) []Shape
type identityTestFunc func(*testing.T, [][]float64, []Shape)
func testPoint(t *testing.T, points [][]float64, shapes []Shape) {
for n, s := range shapes {
p, ok := s.(*Point)
if !ok {
t.Fatal("Failed to type assert.")
}
if !pointsEqual([]float64{p.X, p.Y}, points[n]) {
t.Error("Points did not match.")
}
}
}
func testPolyLine(t *testing.T, points [][]float64, shapes []Shape) {
for n, s := range shapes {
p, ok := s.(*PolyLine)
if !ok {
t.Fatal("Failed to type assert.")
}
for k, point := range p.Points {
if !pointsEqual(points[n*3+k], []float64{point.X, point.Y}) {
t.Error("Points did not match.")
}
}
}
}
func testPolygon(t *testing.T, points [][]float64, shapes []Shape) {
for n, s := range shapes {
p, ok := s.(*Polygon)
if !ok {
t.Fatal("Failed to type assert.")
}
for k, point := range p.Points {
if !pointsEqual(points[n*3+k], []float64{point.X, point.Y}) {
t.Error("Points did not match.")
}
}
}
}
func testMultiPoint(t *testing.T, points [][]float64, shapes []Shape) {
for n, s := range shapes {
p, ok := s.(*MultiPoint)
if !ok {
t.Fatal("Failed to type assert.")
}
for k, point := range p.Points {
if !pointsEqual(points[n*3+k], []float64{point.X, point.Y}) {
t.Error("Points did not match.")
}
}
}
}
func testPointZ(t *testing.T, points [][]float64, shapes []Shape) {
for n, s := range shapes {
p, ok := s.(*PointZ)
if !ok {
t.Fatal("Failed to type assert.")
}
if !pointsEqual([]float64{p.X, p.Y, p.Z}, points[n]) {
t.Error("Points did not match.")
}
}
}
func testPolyLineZ(t *testing.T, points [][]float64, shapes []Shape) {
for n, s := range shapes {
p, ok := s.(*PolyLineZ)
if !ok {
t.Fatal("Failed to type assert.")
}
for k, point := range p.Points {
if !pointsEqual(points[n*3+k], []float64{point.X, point.Y, p.ZArray[k]}) {
t.Error("Points did not match.")
}
}
}
}
func testPolygonZ(t *testing.T, points [][]float64, shapes []Shape) {
for n, s := range shapes {
p, ok := s.(*PolygonZ)
if !ok {
t.Fatal("Failed to type assert.")
}
for k, point := range p.Points {
if !pointsEqual(points[n*3+k], []float64{point.X, point.Y, p.ZArray[k]}) {
t.Error("Points did not match.")
}
}
}
}
func testMultiPointZ(t *testing.T, points [][]float64, shapes []Shape) {
for n, s := range shapes {
p, ok := s.(*MultiPointZ)
if !ok {
t.Fatal("Failed to type assert.")
}
for k, point := range p.Points {
if !pointsEqual(points[n*3+k], []float64{point.X, point.Y, p.ZArray[k]}) {
t.Error("Points did not match.")
}
}
}
}
func testPointM(t *testing.T, points [][]float64, shapes []Shape) {
for n, s := range shapes {
p, ok := s.(*PointM)
if !ok {
t.Fatal("Failed to type assert.")
}
if !pointsEqual([]float64{p.X, p.Y, p.M}, points[n]) {
t.Error("Points did not match.")
}
}
}
func testPolyLineM(t *testing.T, points [][]float64, shapes []Shape) {
for n, s := range shapes {
p, ok := s.(*PolyLineM)
if !ok {
t.Fatal("Failed to type assert.")
}
for k, point := range p.Points {
if !pointsEqual(points[n*3+k], []float64{point.X, point.Y, p.MArray[k]}) {
t.Error("Points did not match.")
}
}
}
}
func testPolygonM(t *testing.T, points [][]float64, shapes []Shape) {
for n, s := range shapes {
p, ok := s.(*PolygonM)
if !ok {
t.Fatal("Failed to type assert.")
}
for k, point := range p.Points {
if !pointsEqual(points[n*3+k], []float64{point.X, point.Y, p.MArray[k]}) {
t.Error("Points did not match.")
}
}
}
}
func testMultiPointM(t *testing.T, points [][]float64, shapes []Shape) {
for n, s := range shapes {
p, ok := s.(*MultiPointM)
if !ok {
t.Fatal("Failed to type assert.")
}
for k, point := range p.Points {
if !pointsEqual(points[n*3+k], []float64{point.X, point.Y, p.MArray[k]}) {
t.Error("Points did not match.")
}
}
}
}
func testMultiPatch(t *testing.T, points [][]float64, shapes []Shape) {
for n, s := range shapes {
p, ok := s.(*MultiPatch)
if !ok {
t.Fatal("Failed to type assert.")
}
for k, point := range p.Points {
if !pointsEqual(points[n*3+k], []float64{point.X, point.Y, p.ZArray[k]}) {
t.Error("Points did not match.")
}
}
}
}
func testshapeIdentity(t *testing.T, prefix string, getter shapeGetterFunc) {
shapes := getter(prefix, t)
d := dataForReadTests[prefix]
if len(shapes) != d.count {
t.Errorf("Number of shapes for %s read was wrong. Wanted %d, got %d.", prefix, d.count, len(shapes))
}
d.tester(t, d.points, shapes)
}
func TestReadBBox(t *testing.T) {
tests := []struct {
filename string
want Box
}{
{"test_files/multipatch.shp", Box{0, 0, 10, 10}},
{"test_files/multipoint.shp", Box{0, 5, 10, 10}},
{"test_files/multipointm.shp", Box{0, 5, 10, 10}},
{"test_files/multipointz.shp", Box{0, 5, 10, 10}},
{"test_files/point.shp", Box{0, 5, 10, 10}},
{"test_files/pointm.shp", Box{0, 5, 10, 10}},
{"test_files/pointz.shp", Box{0, 5, 10, 10}},
{"test_files/polygon.shp", Box{0, 0, 5, 5}},
{"test_files/polygonm.shp", Box{0, 0, 5, 5}},
{"test_files/polygonz.shp", Box{0, 0, 5, 5}},
{"test_files/polyline.shp", Box{0, 0, 25, 25}},
{"test_files/polylinem.shp", Box{0, 0, 25, 25}},
{"test_files/polylinez.shp", Box{0, 0, 25, 25}},
}
for _, tt := range tests {
r, err := Open(tt.filename)
if err != nil {
t.Fatalf("%v", err)
}
if got := r.BBox().MinX; got != tt.want.MinX {
t.Errorf("got MinX = %v, want %v", got, tt.want.MinX)
}
if got := r.BBox().MinY; got != tt.want.MinY {
t.Errorf("got MinY = %v, want %v", got, tt.want.MinY)
}
if got := r.BBox().MaxX; got != tt.want.MaxX {
t.Errorf("got MaxX = %v, want %v", got, tt.want.MaxX)
}
if got := r.BBox().MaxY; got != tt.want.MaxY {
t.Errorf("got MaxY = %v, want %v", got, tt.want.MaxY)
}
}
}
type testCaseData struct {
points [][]float64
tester identityTestFunc
count int
}
var dataForReadTests = map[string]testCaseData{
"test_files/polygonm": {
points: [][]float64{
{0, 0, 0},
{0, 5, 5},
{5, 5, 10},
{5, 0, 15},
{0, 0, 0},
},
tester: testPolygonM,
count: 1,
},
"test_files/multipointm": {
points: [][]float64{
{10, 10, 100},
{5, 5, 50},
{0, 10, 75},
},
tester: testMultiPointM,
count: 1,
},
"test_files/multipatch": {
points: [][]float64{
{0, 0, 0},
{10, 0, 0},
{10, 10, 0},
{0, 10, 0},
{0, 0, 0},
{0, 10, 0},
{0, 10, 10},
{0, 0, 10},
{0, 0, 0},
{0, 10, 0},
{10, 0, 0},
{10, 0, 10},
{10, 10, 10},
{10, 10, 0},
{10, 0, 0},
{0, 0, 0},
{0, 0, 10},
{10, 0, 10},
{10, 0, 0},
{0, 0, 0},
{10, 10, 0},
{10, 10, 10},
{0, 10, 10},
{0, 10, 0},
{10, 10, 0},
{0, 0, 10},
{0, 10, 10},
{10, 10, 10},
{10, 0, 10},
{0, 0, 10},
},
tester: testMultiPatch,
count: 1,
},
"test_files/point": {
points: [][]float64{
{10, 10},
{5, 5},
{0, 10},
},
tester: testPoint,
count: 3,
},
"test_files/polyline": {
points: [][]float64{
{0, 0},
{5, 5},
{10, 10},
{15, 15},
{20, 20},
{25, 25},
},
tester: testPolyLine,
count: 2,
},
"test_files/polygon": {
points: [][]float64{
{0, 0},
{0, 5},
{5, 5},
{5, 0},
{0, 0},
},
tester: testPolygon,
count: 1,
},
"test_files/multipoint": {
points: [][]float64{
{10, 10},
{5, 5},
{0, 10},
},
tester: testMultiPoint,
count: 1,
},
"test_files/pointz": {
points: [][]float64{
{10, 10, 100},
{5, 5, 50},
{0, 10, 75},
},
tester: testPointZ,
count: 3,
},
"test_files/polylinez": {
points: [][]float64{
{0, 0, 0},
{5, 5, 5},
{10, 10, 10},
{15, 15, 15},
{20, 20, 20},
{25, 25, 25},
},
tester: testPolyLineZ,
count: 2,
},
"test_files/polygonz": {
points: [][]float64{
{0, 0, 0},
{0, 5, 5},
{5, 5, 10},
{5, 0, 15},
{0, 0, 0},
},
tester: testPolygonZ,
count: 1,
},
"test_files/multipointz": {
points: [][]float64{
{10, 10, 100},
{5, 5, 50},
{0, 10, 75},
},
tester: testMultiPointZ,
count: 1,
},
"test_files/pointm": {
points: [][]float64{
{10, 10, 100},
{5, 5, 50},
{0, 10, 75},
},
tester: testPointM,
count: 3,
},
"test_files/polylinem": {
points: [][]float64{
{0, 0, 0},
{5, 5, 5},
{10, 10, 10},
{15, 15, 15},
{20, 20, 20},
{25, 25, 25},
},
tester: testPolyLineM,
count: 2,
},
}
func TestReadPoint(t *testing.T) {
testshapeIdentity(t, "test_files/point", getShapesFromFile)
}
func TestReadPolyLine(t *testing.T) {
testshapeIdentity(t, "test_files/polyline", getShapesFromFile)
}
func TestReadPolygon(t *testing.T) {
testshapeIdentity(t, "test_files/polygon", getShapesFromFile)
}
func TestReadMultiPoint(t *testing.T) {
testshapeIdentity(t, "test_files/multipoint", getShapesFromFile)
}
func TestReadPointZ(t *testing.T) {
testshapeIdentity(t, "test_files/pointz", getShapesFromFile)
}
func TestReadPolyLineZ(t *testing.T) {
testshapeIdentity(t, "test_files/polylinez", getShapesFromFile)
}
func TestReadPolygonZ(t *testing.T) {
testshapeIdentity(t, "test_files/polygonz", getShapesFromFile)
}
func TestReadMultiPointZ(t *testing.T) {
testshapeIdentity(t, "test_files/multipointz", getShapesFromFile)
}
func TestReadPointM(t *testing.T) {
testshapeIdentity(t, "test_files/pointm", getShapesFromFile)
}
func TestReadPolyLineM(t *testing.T) {
testshapeIdentity(t, "test_files/polylinem", getShapesFromFile)
}
func TestReadPolygonM(t *testing.T) {
testshapeIdentity(t, "test_files/polygonm", getShapesFromFile)
}
func TestReadMultiPointM(t *testing.T) {
testshapeIdentity(t, "test_files/multipointm", getShapesFromFile)
}
func TestReadMultiPatch(t *testing.T) {
testshapeIdentity(t, "test_files/multipatch", getShapesFromFile)
}
func newReadSeekCloser(b []byte) readSeekCloser {
return struct {
io.Closer
io.ReadSeeker
}{
ioutil.NopCloser(nil),
bytes.NewReader(b),
}
}
func TestReadInvalidShapeType(t *testing.T) {
record := []byte{
0, 0, 0, 0,
0, 0, 0, 0,
255, 255, 255, 255, // shape type
}
tests := []struct {
r interface {
Next() bool
Err() error
}
name string
}{
{&Reader{shp: newReadSeekCloser(record), filelength: int64(len(record))}, "reader"},
{&seqReader{shp: newReadSeekCloser(record), filelength: int64(len(record))}, "seqReader"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.r.Next() {
t.Fatal("read unsupported shape type without stopping")
}
if test.r.Err() == nil {
t.Fatal("read unsupported shape type without error")
}
})
}
}

View File

@ -0,0 +1,235 @@
package shp
import (
"encoding/binary"
"fmt"
"io"
"io/ioutil"
"math"
"strings"
)
// SequentialReader is the interface that allows reading shapes and attributes one after another. It also embeds io.Closer.
type SequentialReader interface {
// Close() frees the resources allocated by the SequentialReader.
io.Closer
// Next() tries to advance the reading by one shape and one attribute row
// and returns true if the read operation could be performed without any
// error.
Next() bool
// Shape returns the index and the last read shape. If the SequentialReader
// encountered any errors, nil is returned for the Shape.
Shape() (int, Shape)
// Attribute returns the value of the n-th attribute in the current row. If
// the SequentialReader encountered any errors, the empty string is
// returned.
Attribute(n int) string
// Fields returns the fields of the database. If the SequentialReader
// encountered any errors, nil is returned.
Fields() []Field
// Err returns the last non-EOF error encountered.
Err() error
}
// Attributes returns all attributes of the shape that sr was last advanced to.
func Attributes(sr SequentialReader) []string {
if sr.Err() != nil {
return nil
}
s := make([]string, len(sr.Fields()))
for i := range s {
s[i] = sr.Attribute(i)
}
return s
}
// AttributeCount returns the number of fields of the database.
func AttributeCount(sr SequentialReader) int {
return len(sr.Fields())
}
// seqReader implements SequentialReader based on external io.ReadCloser
// instances
type seqReader struct {
shp, dbf io.ReadCloser
err error
geometryType ShapeType
bbox Box
shape Shape
num int32
filelength int64
dbfFields []Field
dbfNumRecords int32
dbfHeaderLength int16
dbfRecordLength int16
dbfRow []byte
}
// Read and parse headers in the Shapefile. This will fill out GeometryType,
// filelength and bbox.
func (sr *seqReader) readHeaders() {
// contrary to Reader.readHeaders we cannot seek with the ReadCloser, so we
// need to trust the filelength in the header
er := &errReader{Reader: sr.shp}
// shp headers
io.CopyN(ioutil.Discard, er, 24)
var l int32
binary.Read(er, binary.BigEndian, &l)
sr.filelength = int64(l) * 2
io.CopyN(ioutil.Discard, er, 4)
binary.Read(er, binary.LittleEndian, &sr.geometryType)
sr.bbox.MinX = readFloat64(er)
sr.bbox.MinY = readFloat64(er)
sr.bbox.MaxX = readFloat64(er)
sr.bbox.MaxY = readFloat64(er)
io.CopyN(ioutil.Discard, er, 32) // skip four float64: Zmin, Zmax, Mmin, Max
if er.e != nil {
sr.err = fmt.Errorf("Error when reading SHP header: %v", er.e)
return
}
// dbf header
er = &errReader{Reader: sr.dbf}
if sr.dbf == nil {
return
}
io.CopyN(ioutil.Discard, er, 4)
binary.Read(er, binary.LittleEndian, &sr.dbfNumRecords)
binary.Read(er, binary.LittleEndian, &sr.dbfHeaderLength)
binary.Read(er, binary.LittleEndian, &sr.dbfRecordLength)
io.CopyN(ioutil.Discard, er, 20) // skip padding
numFields := int(math.Floor(float64(sr.dbfHeaderLength-33) / 32.0))
sr.dbfFields = make([]Field, numFields)
binary.Read(er, binary.LittleEndian, &sr.dbfFields)
buf := make([]byte, 1)
er.Read(buf[:])
if er.e != nil {
sr.err = fmt.Errorf("Error when reading DBF header: %v", er.e)
return
}
if buf[0] != 0x0d {
sr.err = fmt.Errorf("Field descriptor array terminator not found")
return
}
sr.dbfRow = make([]byte, sr.dbfRecordLength)
}
// Next implements a method of interface SequentialReader for seqReader.
func (sr *seqReader) Next() bool {
if sr.err != nil {
return false
}
var num, size int32
var shapetype ShapeType
// read shape
er := &errReader{Reader: sr.shp}
binary.Read(er, binary.BigEndian, &num)
binary.Read(er, binary.BigEndian, &size)
binary.Read(er, binary.LittleEndian, &shapetype)
if er.e != nil {
if er.e != io.EOF {
sr.err = fmt.Errorf("Error when reading shapefile header: %v", er.e)
} else {
sr.err = io.EOF
}
return false
}
sr.num = num
var err error
sr.shape, err = newShape(shapetype)
if err != nil {
sr.err = fmt.Errorf("Error decoding shape type: %v", err)
return false
}
sr.shape.read(er)
switch {
case er.e == io.EOF:
// io.EOF means end-of-file was reached gracefully after all
// shape-internal reads succeeded, so it's not a reason stop
// iterating over all shapes.
er.e = nil
case er.e != nil:
sr.err = fmt.Errorf("Error while reading next shape: %v", er.e)
return false
}
skipBytes := int64(size)*2 + 8 - er.n
_, ce := io.CopyN(ioutil.Discard, er, skipBytes)
if er.e != nil {
sr.err = er.e
return false
}
if ce != nil {
sr.err = fmt.Errorf("Error when discarding bytes on sequential read: %v", ce)
return false
}
if _, err := io.ReadFull(sr.dbf, sr.dbfRow); err != nil {
sr.err = fmt.Errorf("Error when reading DBF row: %v", err)
return false
}
if sr.dbfRow[0] != 0x20 && sr.dbfRow[0] != 0x2a {
sr.err = fmt.Errorf("Attribute row %d starts with incorrect deletion indicator", num)
}
return sr.err == nil
}
// Shape implements a method of interface SequentialReader for seqReader.
func (sr *seqReader) Shape() (int, Shape) {
return int(sr.num) - 1, sr.shape
}
// Attribute implements a method of interface SequentialReader for seqReader.
func (sr *seqReader) Attribute(n int) string {
if sr.err != nil {
return ""
}
start := 1
f := 0
for ; f < n; f++ {
start += int(sr.dbfFields[f].Size)
}
s := string(sr.dbfRow[start : start+int(sr.dbfFields[f].Size)])
return strings.Trim(s, " ")
}
// Err returns the first non-EOF error that was encountered.
func (sr *seqReader) Err() error {
if sr.err == io.EOF {
return nil
}
return sr.err
}
// Close closes the seqReader and free all the allocated resources.
func (sr *seqReader) Close() error {
if err := sr.shp.Close(); err != nil {
return err
}
if err := sr.dbf.Close(); err != nil {
return err
}
return nil
}
// Fields returns a slice of the fields that are present in the DBF table.
func (sr *seqReader) Fields() []Field {
return sr.dbfFields
}
// SequentialReaderFromExt returns a new SequentialReader that interprets shp
// as a source of shapes whose attributes can be retrieved from dbf.
func SequentialReaderFromExt(shp, dbf io.ReadCloser) SequentialReader {
sr := &seqReader{shp: shp, dbf: dbf}
sr.readHeaders()
return sr
}

View File

@ -0,0 +1,43 @@
package shp
import (
"os"
"testing"
)
func openFile(name string, t *testing.T) *os.File {
f, err := os.Open(name)
if err != nil {
t.Fatalf("Failed to open %s: %v", name, err)
}
return f
}
func getShapesSequentially(prefix string, t *testing.T) (shapes []Shape) {
shp := openFile(prefix+".shp", t)
dbf := openFile(prefix+".dbf", t)
sr := SequentialReaderFromExt(shp, dbf)
if err := sr.Err(); err != nil {
t.Fatalf("Error when iterating over the shapefile header: %v", err)
}
for sr.Next() {
_, shape := sr.Shape()
shapes = append(shapes, shape)
}
if err := sr.Err(); err != nil {
t.Errorf("Error when iterating over the shapes: %v", err)
}
if err := sr.Close(); err != nil {
t.Errorf("Could not close sequential reader: %v", err)
}
return shapes
}
func TestSequentialReader(t *testing.T) {
for prefix := range dataForReadTests {
t.Logf("Testing sequential read for %s", prefix)
testshapeIdentity(t, prefix, getShapesSequentially)
}
}

View File

@ -0,0 +1,612 @@
package shp
import (
"encoding/binary"
"io"
"strings"
)
//go:generate stringer -type=ShapeType
// ShapeType is a identifier for the the type of shapes.
type ShapeType int32
// These are the possible shape types.
const (
NULL ShapeType = 0
POINT ShapeType = 1
POLYLINE ShapeType = 3
POLYGON ShapeType = 5
MULTIPOINT ShapeType = 8
POINTZ ShapeType = 11
POLYLINEZ ShapeType = 13
POLYGONZ ShapeType = 15
MULTIPOINTZ ShapeType = 18
POINTM ShapeType = 21
POLYLINEM ShapeType = 23
POLYGONM ShapeType = 25
MULTIPOINTM ShapeType = 28
MULTIPATCH ShapeType = 31
)
// Box structure made up from four coordinates. This type
// is used to represent bounding boxes
type Box struct {
MinX, MinY, MaxX, MaxY float64
}
// Extend extends the box with coordinates from the provided
// box. This method calls Box.ExtendWithPoint twice with
// {MinX, MinY} and {MaxX, MaxY}
func (b *Box) Extend(box Box) {
b.ExtendWithPoint(Point{box.MinX, box.MinY})
b.ExtendWithPoint(Point{box.MaxX, box.MaxY})
}
// ExtendWithPoint extends box with coordinates from point
// if they are outside the range of the current box.
func (b *Box) ExtendWithPoint(p Point) {
if p.X < b.MinX {
b.MinX = p.X
}
if p.Y < b.MinY {
b.MinY = p.Y
}
if p.X > b.MaxX {
b.MaxX = p.X
}
if p.Y > b.MaxY {
b.MaxY = p.Y
}
}
// BBoxFromPoints returns the bounding box calculated
// from points.
func BBoxFromPoints(points []Point) (box Box) {
for k, p := range points {
if k == 0 {
box = Box{p.X, p.Y, p.X, p.Y}
} else {
box.ExtendWithPoint(p)
}
}
return
}
// Shape interface
type Shape interface {
BBox() Box
read(io.Reader)
write(io.Writer)
}
// Null is an empty shape.
type Null struct {
}
// BBox Returns an empty BBox at the geometry origin.
func (n Null) BBox() Box {
return Box{0.0, 0.0, 0.0, 0.0}
}
func (n *Null) read(file io.Reader) {
binary.Read(file, binary.LittleEndian, n)
}
func (n *Null) write(file io.Writer) {
binary.Write(file, binary.LittleEndian, n)
}
// Point is the shape that consists of single a geometry point.
type Point struct {
X, Y float64
}
// BBox returns the bounding box of the Point feature, i.e. an empty area at
// the point location itself.
func (p Point) BBox() Box {
return Box{p.X, p.Y, p.X, p.Y}
}
func (p *Point) read(file io.Reader) {
binary.Read(file, binary.LittleEndian, p)
}
func (p *Point) write(file io.Writer) {
binary.Write(file, binary.LittleEndian, p)
}
func flatten(points [][]Point) []Point {
n, i := 0, 0
for _, v := range points {
n += len(v)
}
r := make([]Point, n)
for _, v := range points {
for _, p := range v {
r[i] = p
i++
}
}
return r
}
// PolyLine is a shape type that consists of an ordered set of vertices that
// consists of one or more parts. A part is a connected sequence of two ore
// more points. Parts may or may not be connected to another and may or may not
// intersect each other.
type PolyLine struct {
Box
NumParts int32
NumPoints int32
Parts []int32
Points []Point
}
// NewPolyLine returns a pointer a new PolyLine created
// with the provided points. The inner slice should be
// the points that the parent part consists of.
func NewPolyLine(parts [][]Point) *PolyLine {
points := flatten(parts)
p := &PolyLine{}
p.NumParts = int32(len(parts))
p.NumPoints = int32(len(points))
p.Parts = make([]int32, len(parts))
var marker int32
for i, part := range parts {
p.Parts[i] = marker
marker += int32(len(part))
}
p.Points = points
p.Box = p.BBox()
return p
}
// BBox returns the bounding box of the PolyLine feature
func (p PolyLine) BBox() Box {
return BBoxFromPoints(p.Points)
}
func (p *PolyLine) read(file io.Reader) {
binary.Read(file, binary.LittleEndian, &p.Box)
binary.Read(file, binary.LittleEndian, &p.NumParts)
binary.Read(file, binary.LittleEndian, &p.NumPoints)
p.Parts = make([]int32, p.NumParts)
p.Points = make([]Point, p.NumPoints)
binary.Read(file, binary.LittleEndian, &p.Parts)
binary.Read(file, binary.LittleEndian, &p.Points)
}
func (p *PolyLine) write(file io.Writer) {
binary.Write(file, binary.LittleEndian, p.Box)
binary.Write(file, binary.LittleEndian, p.NumParts)
binary.Write(file, binary.LittleEndian, p.NumPoints)
binary.Write(file, binary.LittleEndian, p.Parts)
binary.Write(file, binary.LittleEndian, p.Points)
}
// Polygon is identical to the PolyLine struct. However the parts must form
// rings that may not intersect.
type Polygon PolyLine
// BBox returns the bounding box of the Polygon feature
func (p Polygon) BBox() Box {
return BBoxFromPoints(p.Points)
}
func (p *Polygon) read(file io.Reader) {
binary.Read(file, binary.LittleEndian, &p.Box)
binary.Read(file, binary.LittleEndian, &p.NumParts)
binary.Read(file, binary.LittleEndian, &p.NumPoints)
p.Parts = make([]int32, p.NumParts)
p.Points = make([]Point, p.NumPoints)
binary.Read(file, binary.LittleEndian, &p.Parts)
binary.Read(file, binary.LittleEndian, &p.Points)
}
func (p *Polygon) write(file io.Writer) {
binary.Write(file, binary.LittleEndian, p.Box)
binary.Write(file, binary.LittleEndian, p.NumParts)
binary.Write(file, binary.LittleEndian, p.NumPoints)
binary.Write(file, binary.LittleEndian, p.Parts)
binary.Write(file, binary.LittleEndian, p.Points)
}
// MultiPoint is the shape that consists of multiple points.
type MultiPoint struct {
Box Box
NumPoints int32
Points []Point
}
// BBox returns the bounding box of the MultiPoint feature
func (p MultiPoint) BBox() Box {
return BBoxFromPoints(p.Points)
}
func (p *MultiPoint) read(file io.Reader) {
binary.Read(file, binary.LittleEndian, &p.Box)
binary.Read(file, binary.LittleEndian, &p.NumPoints)
p.Points = make([]Point, p.NumPoints)
binary.Read(file, binary.LittleEndian, &p.Points)
}
func (p *MultiPoint) write(file io.Writer) {
binary.Write(file, binary.LittleEndian, p.Box)
binary.Write(file, binary.LittleEndian, p.NumPoints)
binary.Write(file, binary.LittleEndian, p.Points)
}
// PointZ is a triplet of double precision coordinates plus a measure.
type PointZ struct {
X float64
Y float64
Z float64
M float64
}
// BBox eturns the bounding box of the PointZ feature which is an zero-sized area
// at the X and Y coordinates of the feature.
func (p PointZ) BBox() Box {
return Box{p.X, p.Y, p.X, p.Y}
}
func (p *PointZ) read(file io.Reader) {
binary.Read(file, binary.LittleEndian, p)
}
func (p *PointZ) write(file io.Writer) {
binary.Write(file, binary.LittleEndian, p)
}
// PolyLineZ is a shape which consists of one or more parts. A part is a
// connected sequence of two or more points. Parts may or may not be connected
// and may or may not intersect one another.
type PolyLineZ struct {
Box Box
NumParts int32
NumPoints int32
Parts []int32
Points []Point
ZRange [2]float64
ZArray []float64
MRange [2]float64
MArray []float64
}
// BBox eturns the bounding box of the PolyLineZ feature.
func (p PolyLineZ) BBox() Box {
return BBoxFromPoints(p.Points)
}
func (p *PolyLineZ) read(file io.Reader) {
binary.Read(file, binary.LittleEndian, &p.Box)
binary.Read(file, binary.LittleEndian, &p.NumParts)
binary.Read(file, binary.LittleEndian, &p.NumPoints)
p.Parts = make([]int32, p.NumParts)
p.Points = make([]Point, p.NumPoints)
p.ZArray = make([]float64, p.NumPoints)
p.MArray = make([]float64, p.NumPoints)
binary.Read(file, binary.LittleEndian, &p.Parts)
binary.Read(file, binary.LittleEndian, &p.Points)
binary.Read(file, binary.LittleEndian, &p.ZRange)
binary.Read(file, binary.LittleEndian, &p.ZArray)
binary.Read(file, binary.LittleEndian, &p.MRange)
binary.Read(file, binary.LittleEndian, &p.MArray)
}
func (p *PolyLineZ) write(file io.Writer) {
binary.Write(file, binary.LittleEndian, p.Box)
binary.Write(file, binary.LittleEndian, p.NumParts)
binary.Write(file, binary.LittleEndian, p.NumPoints)
binary.Write(file, binary.LittleEndian, p.Parts)
binary.Write(file, binary.LittleEndian, p.Points)
binary.Write(file, binary.LittleEndian, p.ZRange)
binary.Write(file, binary.LittleEndian, p.ZArray)
binary.Write(file, binary.LittleEndian, p.MRange)
binary.Write(file, binary.LittleEndian, p.MArray)
}
// PolygonZ structure is identical to the PolyLineZ structure.
type PolygonZ PolyLineZ
// BBox returns the bounding box of the PolygonZ feature
func (p PolygonZ) BBox() Box {
return BBoxFromPoints(p.Points)
}
func (p *PolygonZ) read(file io.Reader) {
binary.Read(file, binary.LittleEndian, &p.Box)
binary.Read(file, binary.LittleEndian, &p.NumParts)
binary.Read(file, binary.LittleEndian, &p.NumPoints)
p.Parts = make([]int32, p.NumParts)
p.Points = make([]Point, p.NumPoints)
p.ZArray = make([]float64, p.NumPoints)
p.MArray = make([]float64, p.NumPoints)
binary.Read(file, binary.LittleEndian, &p.Parts)
binary.Read(file, binary.LittleEndian, &p.Points)
binary.Read(file, binary.LittleEndian, &p.ZRange)
binary.Read(file, binary.LittleEndian, &p.ZArray)
binary.Read(file, binary.LittleEndian, &p.MRange)
binary.Read(file, binary.LittleEndian, &p.MArray)
}
func (p *PolygonZ) write(file io.Writer) {
binary.Write(file, binary.LittleEndian, p.Box)
binary.Write(file, binary.LittleEndian, p.NumParts)
binary.Write(file, binary.LittleEndian, p.NumPoints)
binary.Write(file, binary.LittleEndian, p.Parts)
binary.Write(file, binary.LittleEndian, p.Points)
binary.Write(file, binary.LittleEndian, p.ZRange)
binary.Write(file, binary.LittleEndian, p.ZArray)
binary.Write(file, binary.LittleEndian, p.MRange)
binary.Write(file, binary.LittleEndian, p.MArray)
}
// MultiPointZ consists of one ore more PointZ.
type MultiPointZ struct {
Box Box
NumPoints int32
Points []Point
ZRange [2]float64
ZArray []float64
MRange [2]float64
MArray []float64
}
// BBox eturns the bounding box of the MultiPointZ feature.
func (p MultiPointZ) BBox() Box {
return BBoxFromPoints(p.Points)
}
func (p *MultiPointZ) read(file io.Reader) {
binary.Read(file, binary.LittleEndian, &p.Box)
binary.Read(file, binary.LittleEndian, &p.NumPoints)
p.Points = make([]Point, p.NumPoints)
p.ZArray = make([]float64, p.NumPoints)
p.MArray = make([]float64, p.NumPoints)
binary.Read(file, binary.LittleEndian, &p.Points)
binary.Read(file, binary.LittleEndian, &p.ZRange)
binary.Read(file, binary.LittleEndian, &p.ZArray)
binary.Read(file, binary.LittleEndian, &p.MRange)
binary.Read(file, binary.LittleEndian, &p.MArray)
}
func (p *MultiPointZ) write(file io.Writer) {
binary.Write(file, binary.LittleEndian, p.Box)
binary.Write(file, binary.LittleEndian, p.NumPoints)
binary.Write(file, binary.LittleEndian, p.Points)
binary.Write(file, binary.LittleEndian, p.ZRange)
binary.Write(file, binary.LittleEndian, p.ZArray)
binary.Write(file, binary.LittleEndian, p.MRange)
binary.Write(file, binary.LittleEndian, p.MArray)
}
// PointM is a point with a measure.
type PointM struct {
X float64
Y float64
M float64
}
// BBox returns the bounding box of the PointM feature which is a zero-sized
// area at the X- and Y-coordinates of the point.
func (p PointM) BBox() Box {
return Box{p.X, p.Y, p.X, p.Y}
}
func (p *PointM) read(file io.Reader) {
binary.Read(file, binary.LittleEndian, p)
}
func (p *PointM) write(file io.Writer) {
binary.Write(file, binary.LittleEndian, p)
}
// PolyLineM is the polyline in which each point also has a measure.
type PolyLineM struct {
Box Box
NumParts int32
NumPoints int32
Parts []int32
Points []Point
MRange [2]float64
MArray []float64
}
// BBox returns the bounding box of the PolyLineM feature.
func (p PolyLineM) BBox() Box {
return BBoxFromPoints(p.Points)
}
func (p *PolyLineM) read(file io.Reader) {
binary.Read(file, binary.LittleEndian, &p.Box)
binary.Read(file, binary.LittleEndian, &p.NumParts)
binary.Read(file, binary.LittleEndian, &p.NumPoints)
p.Parts = make([]int32, p.NumParts)
p.Points = make([]Point, p.NumPoints)
p.MArray = make([]float64, p.NumPoints)
binary.Read(file, binary.LittleEndian, &p.Parts)
binary.Read(file, binary.LittleEndian, &p.Points)
binary.Read(file, binary.LittleEndian, &p.MRange)
binary.Read(file, binary.LittleEndian, &p.MArray)
}
func (p *PolyLineM) write(file io.Writer) {
binary.Write(file, binary.LittleEndian, p.Box)
binary.Write(file, binary.LittleEndian, p.NumParts)
binary.Write(file, binary.LittleEndian, p.NumPoints)
binary.Write(file, binary.LittleEndian, p.Parts)
binary.Write(file, binary.LittleEndian, p.Points)
binary.Write(file, binary.LittleEndian, p.MRange)
binary.Write(file, binary.LittleEndian, p.MArray)
}
// PolygonM structure is identical to the PolyLineZ structure.
type PolygonM PolyLineZ
// BBox returns the bounding box of the PolygonM feature.
func (p PolygonM) BBox() Box {
return BBoxFromPoints(p.Points)
}
func (p *PolygonM) read(file io.Reader) {
binary.Read(file, binary.LittleEndian, &p.Box)
binary.Read(file, binary.LittleEndian, &p.NumParts)
binary.Read(file, binary.LittleEndian, &p.NumPoints)
p.Parts = make([]int32, p.NumParts)
p.Points = make([]Point, p.NumPoints)
p.MArray = make([]float64, p.NumPoints)
binary.Read(file, binary.LittleEndian, &p.Parts)
binary.Read(file, binary.LittleEndian, &p.Points)
binary.Read(file, binary.LittleEndian, &p.MRange)
binary.Read(file, binary.LittleEndian, &p.MArray)
}
func (p *PolygonM) write(file io.Writer) {
binary.Write(file, binary.LittleEndian, p.Box)
binary.Write(file, binary.LittleEndian, p.NumParts)
binary.Write(file, binary.LittleEndian, p.NumPoints)
binary.Write(file, binary.LittleEndian, p.Parts)
binary.Write(file, binary.LittleEndian, p.Points)
binary.Write(file, binary.LittleEndian, p.MRange)
binary.Write(file, binary.LittleEndian, p.MArray)
}
// MultiPointM is the collection of multiple points with measures.
type MultiPointM struct {
Box Box
NumPoints int32
Points []Point
MRange [2]float64
MArray []float64
}
// BBox eturns the bounding box of the MultiPointM feature
func (p MultiPointM) BBox() Box {
return BBoxFromPoints(p.Points)
}
func (p *MultiPointM) read(file io.Reader) {
binary.Read(file, binary.LittleEndian, &p.Box)
binary.Read(file, binary.LittleEndian, &p.NumPoints)
p.Points = make([]Point, p.NumPoints)
p.MArray = make([]float64, p.NumPoints)
binary.Read(file, binary.LittleEndian, &p.Points)
binary.Read(file, binary.LittleEndian, &p.MRange)
binary.Read(file, binary.LittleEndian, &p.MArray)
}
func (p *MultiPointM) write(file io.Writer) {
binary.Write(file, binary.LittleEndian, p.Box)
binary.Write(file, binary.LittleEndian, p.NumPoints)
binary.Write(file, binary.LittleEndian, p.Points)
binary.Write(file, binary.LittleEndian, p.MRange)
binary.Write(file, binary.LittleEndian, p.MArray)
}
// MultiPatch consists of a number of surfaces patches. Each surface path
// descries a surface. The surface patches of a MultiPatch are referred to as
// its parts, and the type of part controls how the order of vertices of an
// MultiPatch part is interpreted.
type MultiPatch struct {
Box Box
NumParts int32
NumPoints int32
Parts []int32
PartTypes []int32
Points []Point
ZRange [2]float64
ZArray []float64
MRange [2]float64
MArray []float64
}
// BBox returns the bounding box of the MultiPatch feature
func (p MultiPatch) BBox() Box {
return BBoxFromPoints(p.Points)
}
func (p *MultiPatch) read(file io.Reader) {
binary.Read(file, binary.LittleEndian, &p.Box)
binary.Read(file, binary.LittleEndian, &p.NumParts)
binary.Read(file, binary.LittleEndian, &p.NumPoints)
p.Parts = make([]int32, p.NumParts)
p.PartTypes = make([]int32, p.NumParts)
p.Points = make([]Point, p.NumPoints)
p.ZArray = make([]float64, p.NumPoints)
p.MArray = make([]float64, p.NumPoints)
binary.Read(file, binary.LittleEndian, &p.Parts)
binary.Read(file, binary.LittleEndian, &p.PartTypes)
binary.Read(file, binary.LittleEndian, &p.Points)
binary.Read(file, binary.LittleEndian, &p.ZRange)
binary.Read(file, binary.LittleEndian, &p.ZArray)
binary.Read(file, binary.LittleEndian, &p.MRange)
binary.Read(file, binary.LittleEndian, &p.MArray)
}
func (p *MultiPatch) write(file io.Writer) {
binary.Write(file, binary.LittleEndian, p.Box)
binary.Write(file, binary.LittleEndian, p.NumParts)
binary.Write(file, binary.LittleEndian, p.NumPoints)
binary.Write(file, binary.LittleEndian, p.Parts)
binary.Write(file, binary.LittleEndian, p.PartTypes)
binary.Write(file, binary.LittleEndian, p.Points)
binary.Write(file, binary.LittleEndian, p.ZRange)
binary.Write(file, binary.LittleEndian, p.ZArray)
binary.Write(file, binary.LittleEndian, p.MRange)
binary.Write(file, binary.LittleEndian, p.MArray)
}
// Field representation of a field object in the DBF file
type Field struct {
Name [11]byte
Fieldtype byte
Addr [4]byte // not used
Size uint8
Precision uint8
Padding [14]byte
}
// Returns a string representation of the Field. Currently
// this only returns field name.
func (f Field) String() string {
return strings.TrimRight(string(f.Name[:]), "\x00")
}
// StringField returns a Field that can be used in SetFields to initialize the
// DBF file.
func StringField(name string, length uint8) Field {
// TODO: Error checking
field := Field{Fieldtype: 'C', Size: length}
copy(field.Name[:], []byte(name))
return field
}
// NumberField returns a Field that can be used in SetFields to initialize the
// DBF file.
func NumberField(name string, length uint8) Field {
field := Field{Fieldtype: 'N', Size: length}
copy(field.Name[:], []byte(name))
return field
}
// FloatField returns a Field that can be used in SetFields to initialize the
// DBF file. Used to store floating points with precision in the DBF.
func FloatField(name string, length uint8, precision uint8) Field {
field := Field{Fieldtype: 'F', Size: length, Precision: precision}
copy(field.Name[:], []byte(name))
return field
}
// DateField feturns a Field that can be used in SetFields to initialize the
// DBF file. Used to store Date strings formatted as YYYYMMDD. Data wise this
// is the same as a StringField with length 8.
func DateField(name string) Field {
field := Field{Fieldtype: 'D', Size: 8}
copy(field.Name[:], []byte(name))
return field
}

View File

@ -0,0 +1,22 @@
package shp
import "testing"
func TestBoxExtend(t *testing.T) {
a := Box{-124.763068, 45.543541, -116.915989, 49.002494}
b := Box{-92.888114, 42.49192, -86.805415, 47.080621}
a.Extend(b)
c := Box{-124.763068, 42.49192, -86.805415, 49.002494}
if a.MinX != c.MinX {
t.Errorf("a.MinX = %v, want %v", a.MinX, c.MinX)
}
if a.MinY != c.MinY {
t.Errorf("a.MinY = %v, want %v", a.MinY, c.MinY)
}
if a.MaxX != c.MaxX {
t.Errorf("a.MaxX = %v, want %v", a.MaxX, c.MaxX)
}
if a.MaxY != c.MaxY {
t.Errorf("a.MaxY = %v, want %v", a.MaxY, c.MaxY)
}
}

View File

@ -0,0 +1,31 @@
// Code generated by "stringer -type=ShapeType"; DO NOT EDIT.
package shp
import "strconv"
const _ShapeType_name = "NULLPOINTPOLYLINEPOLYGONMULTIPOINTPOINTZPOLYLINEZPOLYGONZMULTIPOINTZPOINTMPOLYLINEMPOLYGONMMULTIPOINTMMULTIPATCH"
var _ShapeType_map = map[ShapeType]string{
0: _ShapeType_name[0:4],
1: _ShapeType_name[4:9],
3: _ShapeType_name[9:17],
5: _ShapeType_name[17:24],
8: _ShapeType_name[24:34],
11: _ShapeType_name[34:40],
13: _ShapeType_name[40:49],
15: _ShapeType_name[49:57],
18: _ShapeType_name[57:68],
21: _ShapeType_name[68:74],
23: _ShapeType_name[74:83],
25: _ShapeType_name[83:91],
28: _ShapeType_name[91:102],
31: _ShapeType_name[102:112],
}
func (i ShapeType) String() string {
if str, ok := _ShapeType_map[i]; ok {
return str
}
return "ShapeType(" + strconv.FormatInt(int64(i), 10) + ")"
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,345 @@
package shp
import (
"encoding/binary"
"errors"
"fmt"
"io"
"math"
"os"
"path/filepath"
"strconv"
"strings"
)
// Writer is the type that is used to write a new shapefile.
type Writer struct {
filename string
shp writeSeekCloser
shx writeSeekCloser
GeometryType ShapeType
num int32
bbox Box
dbf writeSeekCloser
dbfFields []Field
dbfHeaderLength int16
dbfRecordLength int16
}
type writeSeekCloser interface {
io.Writer
io.Seeker
io.Closer
}
// Create returns a point to new Writer and the first error that was
// encountered. In case an error occurred the returned Writer point will be nil
// This also creates a corresponding SHX file. It is important to use Close()
// when done because that method writes all the headers for each file (SHP, SHX
// and DBF).
// If filename does not end on ".shp" already, it will be treated as the basename
// for the file and the ".shp" extension will be appended to that name.
func Create(filename string, t ShapeType) (*Writer, error) {
if strings.HasSuffix(strings.ToLower(filename), ".shp") {
filename = filename[0 : len(filename)-4]
}
shp, err := os.Create(filename + ".shp")
if err != nil {
return nil, err
}
shx, err := os.Create(filename + ".shx")
if err != nil {
return nil, err
}
shp.Seek(100, io.SeekStart)
shx.Seek(100, io.SeekStart)
w := &Writer{
filename: filename,
shp: shp,
shx: shx,
GeometryType: t,
}
return w, nil
}
// Append returns a Writer pointer that will append to the given shapefile and
// the first error that was encounted during creation of that Writer. The
// shapefile must have a valid index file.
func Append(filename string) (*Writer, error) {
shp, err := os.OpenFile(filename, os.O_RDWR, 0666)
if err != nil {
return nil, err
}
ext := filepath.Ext(filename)
basename := filename[:len(filename)-len(ext)]
w := &Writer{
filename: basename,
shp: shp,
}
_, err = shp.Seek(32, io.SeekStart)
if err != nil {
return nil, fmt.Errorf("cannot seek to SHP geometry type: %v", err)
}
err = binary.Read(shp, binary.LittleEndian, &w.GeometryType)
if err != nil {
return nil, fmt.Errorf("cannot read geometry type: %v", err)
}
er := &errReader{Reader: shp}
w.bbox.MinX = readFloat64(er)
w.bbox.MinY = readFloat64(er)
w.bbox.MaxX = readFloat64(er)
w.bbox.MaxY = readFloat64(er)
if er.e != nil {
return nil, fmt.Errorf("cannot read bounding box: %v", er.e)
}
shx, err := os.OpenFile(basename+".shx", os.O_RDWR, 0666)
if os.IsNotExist(err) {
// TODO allow index file to not exist, in that case just
// read through all the shapes and create it on the fly
}
if err != nil {
return nil, fmt.Errorf("cannot open shapefile index: %v", err)
}
_, err = shx.Seek(-8, io.SeekEnd)
if err != nil {
return nil, fmt.Errorf("cannot seek to last shape index: %v", err)
}
var offset int32
err = binary.Read(shx, binary.BigEndian, &offset)
if err != nil {
return nil, fmt.Errorf("cannot read last shape index: %v", err)
}
offset = offset * 2
_, err = shp.Seek(int64(offset), io.SeekStart)
if err != nil {
return nil, fmt.Errorf("cannot seek to last shape: %v", err)
}
err = binary.Read(shp, binary.BigEndian, &w.num)
if err != nil {
return nil, fmt.Errorf("cannot read number of last shape: %v", err)
}
_, err = shp.Seek(0, io.SeekEnd)
if err != nil {
return nil, fmt.Errorf("cannot seek to SHP end: %v", err)
}
_, err = shx.Seek(0, io.SeekEnd)
if err != nil {
return nil, fmt.Errorf("cannot seek to SHX end: %v", err)
}
w.shx = shx
dbf, err := os.Open(basename + ".dbf")
if os.IsNotExist(err) {
return w, nil // it's okay if the DBF does not exist
}
if err != nil {
return nil, fmt.Errorf("cannot open DBF: %v", err)
}
_, err = dbf.Seek(8, io.SeekStart)
if err != nil {
return nil, fmt.Errorf("cannot seek in DBF: %v", err)
}
err = binary.Read(dbf, binary.LittleEndian, &w.dbfHeaderLength)
if err != nil {
return nil, fmt.Errorf("cannot read header length from DBF: %v", err)
}
err = binary.Read(dbf, binary.LittleEndian, &w.dbfRecordLength)
if err != nil {
return nil, fmt.Errorf("cannot read record length from DBF: %v", err)
}
_, err = dbf.Seek(20, io.SeekCurrent) // skip padding
if err != nil {
return nil, fmt.Errorf("cannot seek in DBF: %v", err)
}
numFields := int(math.Floor(float64(w.dbfHeaderLength-33) / 32.0))
w.dbfFields = make([]Field, numFields)
err = binary.Read(dbf, binary.LittleEndian, &w.dbfFields)
if err != nil {
return nil, fmt.Errorf("cannot read number of fields from DBF: %v", err)
}
_, err = dbf.Seek(0, io.SeekEnd) // skip padding
if err != nil {
return nil, fmt.Errorf("cannot seek to DBF end: %v", err)
}
w.dbf = dbf
return w, nil
}
// Write shape to the Shapefile. This also creates
// a record in the SHX file and DBF file (if it is
// initialized). Returns the index of the written object
// which can be used in WriteAttribute.
func (w *Writer) Write(shape Shape) int32 {
// increate bbox
if w.num == 0 {
w.bbox = shape.BBox()
} else {
w.bbox.Extend(shape.BBox())
}
w.num++
binary.Write(w.shp, binary.BigEndian, w.num)
w.shp.Seek(4, io.SeekCurrent)
start, _ := w.shp.Seek(0, io.SeekCurrent)
binary.Write(w.shp, binary.LittleEndian, w.GeometryType)
shape.write(w.shp)
finish, _ := w.shp.Seek(0, io.SeekCurrent)
length := int32(math.Floor((float64(finish) - float64(start)) / 2.0))
w.shp.Seek(start-4, io.SeekStart)
binary.Write(w.shp, binary.BigEndian, length)
w.shp.Seek(finish, io.SeekStart)
// write shx
binary.Write(w.shx, binary.BigEndian, int32((start-8)/2))
binary.Write(w.shx, binary.BigEndian, length)
// write empty record to dbf
if w.dbf != nil {
w.writeEmptyRecord()
}
return w.num - 1
}
// Close closes the Writer. This must be used at the end of
// the transaction because it writes the correct headers
// to the SHP/SHX and DBF files before closing.
func (w *Writer) Close() {
w.writeHeader(w.shx)
w.writeHeader(w.shp)
w.shp.Close()
w.shx.Close()
if w.dbf == nil {
w.SetFields([]Field{})
}
w.writeDbfHeader(w.dbf)
w.dbf.Close()
}
// writeHeader wrires SHP/SHX headers to ws.
func (w *Writer) writeHeader(ws io.WriteSeeker) {
filelength, _ := ws.Seek(0, io.SeekEnd)
if filelength == 0 {
filelength = 100
}
ws.Seek(0, io.SeekStart)
// file code
binary.Write(ws, binary.BigEndian, []int32{9994, 0, 0, 0, 0, 0})
// file length
binary.Write(ws, binary.BigEndian, int32(filelength/2))
// version and shape type
binary.Write(ws, binary.LittleEndian, []int32{1000, int32(w.GeometryType)})
// bounding box
binary.Write(ws, binary.LittleEndian, w.bbox)
// elevation, measure
binary.Write(ws, binary.LittleEndian, []float64{0.0, 0.0, 0.0, 0.0})
}
// writeDbfHeader writes a DBF header to ws.
func (w *Writer) writeDbfHeader(ws io.WriteSeeker) {
ws.Seek(0, 0)
// version, year (YEAR-1990), month, day
binary.Write(ws, binary.LittleEndian, []byte{3, 24, 5, 3})
// number of records
binary.Write(ws, binary.LittleEndian, w.num)
// header length, record length
binary.Write(ws, binary.LittleEndian, []int16{w.dbfHeaderLength, w.dbfRecordLength})
// padding
binary.Write(ws, binary.LittleEndian, make([]byte, 20))
for _, field := range w.dbfFields {
binary.Write(ws, binary.LittleEndian, field)
}
// end with return
ws.Write([]byte("\r"))
}
// SetFields sets field values in the DBF. This initializes the DBF file and
// should be used prior to writing any attributes.
func (w *Writer) SetFields(fields []Field) error {
if w.dbf != nil {
return errors.New("Cannot set fields in existing dbf")
}
var err error
w.dbf, err = os.Create(w.filename + ".dbf")
if err != nil {
return fmt.Errorf("Failed to open %s.dbf: %v", w.filename, err)
}
w.dbfFields = fields
// calculate record length
w.dbfRecordLength = int16(1)
for _, field := range w.dbfFields {
w.dbfRecordLength += int16(field.Size)
}
// header lengh
w.dbfHeaderLength = int16(len(w.dbfFields)*32 + 33)
// fill header space with empty bytes for now
buf := make([]byte, w.dbfHeaderLength)
binary.Write(w.dbf, binary.LittleEndian, buf)
// write empty records
for n := int32(0); n < w.num; n++ {
w.writeEmptyRecord()
}
return nil
}
// Writes an empty record to the end of the DBF. This
// works by seeking to the end of the file and writing
// dbfRecordLength number of bytes. The first byte is a
// space that indicates a new record.
func (w *Writer) writeEmptyRecord() {
w.dbf.Seek(0, io.SeekEnd)
buf := make([]byte, w.dbfRecordLength)
buf[0] = ' '
binary.Write(w.dbf, binary.LittleEndian, buf)
}
// WriteAttribute writes value for field into the given row in the DBF. Row
// number should be the same as the order the Shape was written to the
// Shapefile. The field value corresponds to the field in the slice used in
// SetFields.
func (w *Writer) WriteAttribute(row int, field int, value interface{}) error {
var buf []byte
switch v := value.(type) {
case int:
buf = []byte(strconv.Itoa(v))
case float64:
precision := w.dbfFields[field].Precision
buf = []byte(strconv.FormatFloat(v, 'f', int(precision), 64))
case string:
buf = []byte(v)
default:
return fmt.Errorf("Unsupported value type: %T", v)
}
if w.dbf == nil {
return errors.New("Initialize DBF by using SetFields first")
}
if sz := int(w.dbfFields[field].Size); len(buf) > sz {
return fmt.Errorf("Unable to write field %v: %q exceeds field length %v", field, buf, sz)
}
seekTo := 1 + int64(w.dbfHeaderLength) + (int64(row) * int64(w.dbfRecordLength))
for n := 0; n < field; n++ {
seekTo += int64(w.dbfFields[n].Size)
}
w.dbf.Seek(seekTo, io.SeekStart)
return binary.Write(w.dbf, binary.LittleEndian, buf)
}
// BBox returns the bounding box of the Writer.
func (w *Writer) BBox() Box {
return w.bbox
}

View File

@ -0,0 +1,209 @@
package shp
import (
"bytes"
"io"
"os"
"reflect"
"testing"
)
var filenamePrefix = "test_files/write_"
func removeShapefile(filename string) {
os.Remove(filename + ".shp")
os.Remove(filename + ".shx")
os.Remove(filename + ".dbf")
}
func pointsToFloats(points []Point) [][]float64 {
floats := make([][]float64, len(points))
for k, v := range points {
floats[k] = make([]float64, 2)
floats[k][0] = v.X
floats[k][1] = v.Y
}
return floats
}
func TestAppend(t *testing.T) {
filename := filenamePrefix + "point"
defer removeShapefile(filename)
points := [][]float64{
{0.0, 0.0},
{5.0, 5.0},
{10.0, 10.0},
}
shape, err := Create(filename+".shp", POINT)
if err != nil {
t.Fatal(err)
}
for _, p := range points {
shape.Write(&Point{p[0], p[1]})
}
wantNum := shape.num
shape.Close()
newPoints := [][]float64{
{15.0, 15.0},
{20.0, 20.0},
{25.0, 25.0},
}
shape, err = Append(filename + ".shp")
if err != nil {
t.Fatal(err)
}
if shape.GeometryType != POINT {
t.Fatalf("wanted geo type %d, got %d", POINT, shape.GeometryType)
}
if shape.num != wantNum {
t.Fatalf("wrong 'num', wanted type %d, got %d", wantNum, shape.num)
}
for _, p := range newPoints {
shape.Write(&Point{p[0], p[1]})
}
points = append(points, newPoints...)
shapes := getShapesFromFile(filename, t)
if len(shapes) != len(points) {
t.Error("Number of shapes read was wrong")
}
testPoint(t, points, shapes)
}
func TestWritePoint(t *testing.T) {
filename := filenamePrefix + "point"
defer removeShapefile(filename)
points := [][]float64{
{0.0, 0.0},
{5.0, 5.0},
{10.0, 10.0},
}
shape, err := Create(filename+".shp", POINT)
if err != nil {
t.Fatal(err)
}
for _, p := range points {
shape.Write(&Point{p[0], p[1]})
}
shape.Close()
shapes := getShapesFromFile(filename, t)
if len(shapes) != len(points) {
t.Error("Number of shapes read was wrong")
}
testPoint(t, points, shapes)
}
func TestWritePolyLine(t *testing.T) {
filename := filenamePrefix + "polyline"
defer removeShapefile(filename)
points := [][]Point{
{Point{0.0, 0.0}, Point{5.0, 5.0}},
{Point{10.0, 10.0}, Point{15.0, 15.0}},
}
shape, err := Create(filename+".shp", POLYLINE)
if err != nil {
t.Log(shape, err)
}
l := NewPolyLine(points)
lWant := &PolyLine{
Box: Box{MinX: 0, MinY: 0, MaxX: 15, MaxY: 15},
NumParts: 2,
NumPoints: 4,
Parts: []int32{0, 2},
Points: []Point{{X: 0, Y: 0},
{X: 5, Y: 5},
{X: 10, Y: 10},
{X: 15, Y: 15},
},
}
if !reflect.DeepEqual(l, lWant) {
t.Errorf("incorrect NewLine: have: %+v; want: %+v", l, lWant)
}
shape.Write(l)
shape.Close()
shapes := getShapesFromFile(filename, t)
if len(shapes) != 1 {
t.Error("Number of shapes read was wrong")
}
testPolyLine(t, pointsToFloats(flatten(points)), shapes)
}
type seekTracker struct {
io.Writer
offset int64
}
func (s *seekTracker) Seek(offset int64, whence int) (int64, error) {
s.offset = offset
return s.offset, nil
}
func (s *seekTracker) Close() error {
return nil
}
func TestWriteAttribute(t *testing.T) {
buf := new(bytes.Buffer)
s := &seekTracker{Writer: buf}
w := Writer{
dbf: s,
dbfFields: []Field{
StringField("A_STRING", 6),
FloatField("A_FLOAT", 8, 4),
NumberField("AN_INT", 4),
},
dbfRecordLength: 100,
}
tests := []struct {
name string
row int
field int
data interface{}
wantOffset int64
wantData string
}{
{"string-0", 0, 0, "test", 1, "test"},
{"string-0-overflow-1", 0, 0, "overflo", 0, ""},
{"string-0-overflow-n", 0, 0, "overflowing", 0, ""},
{"string-3", 3, 0, "things", 301, "things"},
{"float-0", 0, 1, 123.44, 7, "123.4400"},
{"float-0-overflow-1", 0, 1, 1234.0, 0, ""},
{"float-0-overflow-n", 0, 1, 123456789.0, 0, ""},
{"int-0", 0, 2, 4242, 15, "4242"},
{"int-0-overflow-1", 0, 2, 42424, 0, ""},
{"int-0-overflow-n", 0, 2, 42424343, 0, ""},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
buf.Reset()
s.offset = 0
err := w.WriteAttribute(test.row, test.field, test.data)
if buf.String() != test.wantData {
t.Errorf("got data: %v, want: %v", buf.String(), test.wantData)
}
if s.offset != test.wantOffset {
t.Errorf("got seek offset: %v, want: %v", s.offset, test.wantOffset)
}
if err == nil && test.wantData == "" {
t.Error("got no data and no error")
}
})
}
}

View File

@ -0,0 +1,151 @@
package shp
import (
"archive/zip"
"fmt"
"io"
"path"
"strings"
)
// ZipReader provides an interface for reading Shapefiles that are compressed in a ZIP archive.
type ZipReader struct {
sr SequentialReader
z *zip.ReadCloser
}
// openFromZIP is convenience function for opening the file called name that is
// compressed in z for reading.
func openFromZIP(z *zip.ReadCloser, name string) (io.ReadCloser, error) {
for _, f := range z.File {
if f.Name == name {
return f.Open()
}
}
return nil, fmt.Errorf("No such file in archive: %s", name)
}
// OpenZip opens a ZIP file that contains a single shapefile.
func OpenZip(zipFilePath string) (*ZipReader, error) {
z, err := zip.OpenReader(zipFilePath)
if err != nil {
return nil, err
}
zr := &ZipReader{
z: z,
}
shapeFiles := shapesInZip(z)
if len(shapeFiles) == 0 {
return nil, fmt.Errorf("archive does not contain a .shp file")
}
if len(shapeFiles) > 1 {
return nil, fmt.Errorf("archive does contain multiple .shp files")
}
shp, err := openFromZIP(zr.z, shapeFiles[0].Name)
if err != nil {
return nil, err
}
withoutExt := strings.TrimSuffix(shapeFiles[0].Name, ".shp")
// dbf is optional, so no error checking here
dbf, _ := openFromZIP(zr.z, withoutExt+".dbf")
zr.sr = SequentialReaderFromExt(shp, dbf)
return zr, nil
}
// ShapesInZip returns a string-slice with the names (i.e. relatives paths in
// archive file tree) of all shapes that are in the ZIP archive at zipFilePath.
func ShapesInZip(zipFilePath string) ([]string, error) {
var names []string
z, err := zip.OpenReader(zipFilePath)
if err != nil {
return nil, err
}
shapeFiles := shapesInZip(z)
for i := range shapeFiles {
names = append(names, shapeFiles[i].Name)
}
return names, nil
}
func shapesInZip(z *zip.ReadCloser) []*zip.File {
var shapeFiles []*zip.File
for _, f := range z.File {
if strings.HasSuffix(f.Name, ".shp") {
shapeFiles = append(shapeFiles, f)
}
}
return shapeFiles
}
// OpenShapeFromZip opens a shape file that is contained in a ZIP archive. The
// parameter name is name of the shape file.
// The name of the shapefile must be a relative path: it must not start with a
// drive letter (e.g. C:) or leading slash, and only forward slashes are
// allowed. These rules are the same as in
// https://golang.org/pkg/archive/zip/#FileHeader.
func OpenShapeFromZip(zipFilePath string, name string) (*ZipReader, error) {
z, err := zip.OpenReader(zipFilePath)
if err != nil {
return nil, err
}
zr := &ZipReader{
z: z,
}
shp, err := openFromZIP(zr.z, name)
if err != nil {
return nil, err
}
// dbf is optional, so no error checking here
prefix := strings.TrimSuffix(name, path.Ext(name))
dbf, _ := openFromZIP(zr.z, prefix+".dbf")
zr.sr = SequentialReaderFromExt(shp, dbf)
return zr, nil
}
// Close closes the ZipReader and frees the allocated resources.
func (zr *ZipReader) Close() error {
s := ""
err := zr.sr.Close()
if err != nil {
s += err.Error() + ". "
}
err = zr.z.Close()
if err != nil {
s += err.Error() + ". "
}
if s != "" {
return fmt.Errorf(s)
}
return nil
}
// Next reads the next shape in the shapefile and the next row in the DBF. Call
// Shape() and Attribute() to access the values.
func (zr *ZipReader) Next() bool {
return zr.sr.Next()
}
// Shape returns the shape that was last read as well as the current index.
func (zr *ZipReader) Shape() (int, Shape) {
return zr.sr.Shape()
}
// Attribute returns the n-th field of the last row that was read. If there
// were any errors before, the empty string is returned.
func (zr *ZipReader) Attribute(n int) string {
return zr.sr.Attribute(n)
}
// Fields returns a slice of Fields that are present in the
// DBF table.
func (zr *ZipReader) Fields() []Field {
return zr.sr.Fields()
}
// Err returns the last non-EOF error that was encountered by this ZipReader.
func (zr *ZipReader) Err() error {
return zr.sr.Err()
}

View File

@ -0,0 +1,236 @@
package shp
import (
"archive/zip"
"io"
"io/ioutil"
"net/http"
"os"
"path"
"path/filepath"
"testing"
)
func compressFileToZIP(zw *zip.Writer, src, tgt string, t *testing.T) {
r, err := os.Open(src)
if err != nil {
t.Fatalf("Could not open for compression %s: %v", src, err)
}
w, err := zw.Create(tgt)
if err != nil {
t.Fatalf("Could not start to compress %s: %v", tgt, err)
}
_, err = io.Copy(w, r)
if err != nil {
t.Fatalf("Could not compress contents for %s: %v", tgt, err)
}
}
// createTempZIP packs the SHP, SHX, and DBF into a ZIP in a temporary
// directory
func createTempZIP(prefix string, t *testing.T) (dir, filename string) {
dir, err := ioutil.TempDir("", "go-shp-test")
if err != nil {
t.Fatalf("Could not create temporary directory: %v", err)
}
base := filepath.Base(prefix)
zipName := base + ".zip"
w, err := os.Create(filepath.Join(dir, zipName))
if err != nil {
t.Fatalf("Could not create temporary zip file: %v", err)
}
zw := zip.NewWriter(w)
for _, suffix := range []string{".shp", ".shx", ".dbf"} {
compressFileToZIP(zw, prefix+suffix, base+suffix, t)
}
if err := zw.Close(); err != nil {
t.Fatalf("Could not close the written zip: %v", err)
}
return dir, zipName
}
func getShapesZipped(prefix string, t *testing.T) (shapes []Shape) {
dir, filename := createTempZIP(prefix, t)
defer os.RemoveAll(dir)
zr, err := OpenZip(filepath.Join(dir, filename))
if err != nil {
t.Errorf("Error when opening zip file: %v", err)
}
for zr.Next() {
_, shape := zr.Shape()
shapes = append(shapes, shape)
}
if err := zr.Err(); err != nil {
t.Errorf("Error when iterating over the shapes: %v", err)
}
if err := zr.Close(); err != nil {
t.Errorf("Could not close zipreader: %v", err)
}
return shapes
}
func TestZipReader(t *testing.T) {
for prefix := range dataForReadTests {
t.Logf("Testing zipped reading for %s", prefix)
testshapeIdentity(t, prefix, getShapesZipped)
}
}
func unzipToTempDir(t *testing.T, p string) string {
td, err := ioutil.TempDir("", "")
if err != nil {
t.Fatalf("%v", err)
}
zip, err := zip.OpenReader(p)
if err != nil {
t.Fatalf("%v", err)
}
defer zip.Close()
for _, f := range zip.File {
_, fn := path.Split(f.Name)
pn := filepath.Join(td, fn)
t.Logf("Uncompress: %s -> %s", f.Name, pn)
w, err := os.Create(pn)
if err != nil {
t.Fatalf("Cannot unzip %s: %v", p, err)
}
defer w.Close()
r, err := f.Open()
if err != nil {
t.Fatalf("Cannot unzip %s: %v", p, err)
}
defer r.Close()
_, err = io.Copy(w, r)
if err != nil {
t.Fatalf("Cannot unzip %s: %v", p, err)
}
}
return td
}
// TestZipReaderAttributes reads the same shapesfile twice, first directly from
// the Shp with a Reader, and, second, from a zip. It compares the fields as
// well as the shapes and the attributes. For this test, the Shapes are
// considered to be equal if their bounding boxes are equal.
func TestZipReaderAttribute(t *testing.T) {
b := "ne_110m_admin_0_countries"
skipOrDownloadNaturalEarth(t, b+".zip")
d := unzipToTempDir(t, b+".zip")
defer os.RemoveAll(d)
lr, err := Open(filepath.Join(d, b+".shp"))
if err != nil {
t.Fatal(err)
}
defer lr.Close()
zr, err := OpenZip(b + ".zip")
if os.IsNotExist(err) {
t.Skipf("Skipping test, as Natural Earth dataset wasn't found")
}
if err != nil {
t.Fatal(err)
}
defer zr.Close()
fsl := lr.Fields()
fsz := zr.Fields()
if len(fsl) != len(fsz) {
t.Fatalf("Number of attributes do not match: Wanted %d, got %d", len(fsl), len(fsz))
}
for i := range fsl {
if fsl[i] != fsz[i] {
t.Fatalf("Attribute %d (%s) does not match (%s)", i, fsl[i], fsz[i])
}
}
for zr.Next() && lr.Next() {
ln, ls := lr.Shape()
zn, zs := zr.Shape()
if ln != zn {
t.Fatalf("Sequence number wrong: Wanted %d, got %d", ln, zn)
}
if ls.BBox() != zs.BBox() {
t.Fatalf("Bounding boxes for shape #%d do not match", ln+1)
}
for i := range fsl {
la := lr.Attribute(i)
za := zr.Attribute(i)
if la != za {
t.Fatalf("Shape %d: Attribute %d (%s) are unequal: '%s' vs '%s'",
ln+1, i, fsl[i].String(), la, za)
}
}
}
if lr.Err() != nil {
t.Logf("Reader error: %v / ZipReader error: %v", lr.Err(), zr.Err())
t.FailNow()
}
}
func skipOrDownloadNaturalEarth(t *testing.T, p string) {
if _, err := os.Stat(p); os.IsNotExist(err) {
dl := false
for _, a := range os.Args {
if a == "download" {
dl = true
break
}
}
u := "http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/110m/cultural/ne_110m_admin_0_countries.zip"
if !dl {
t.Skipf("Skipped, as %s does not exist. Consider calling tests with '-args download` "+
"or download manually from '%s'", p, u)
} else {
t.Logf("Downloading %s", u)
w, err := os.Create(p)
if err != nil {
t.Fatalf("Could not create %q: %v", p, err)
}
defer w.Close()
resp, err := http.Get(u)
if err != nil {
t.Fatalf("Could not download %q: %v", u, err)
}
defer resp.Body.Close()
_, err = io.Copy(w, resp.Body)
if err != nil {
t.Fatalf("Could not download %q: %v", u, err)
}
t.Logf("Download complete")
}
}
}
func TestNaturalEarthZip(t *testing.T) {
type metaShape struct {
Attributes map[string]string
Shape
}
p := "ne_110m_admin_0_countries.zip"
skipOrDownloadNaturalEarth(t, p)
zr, err := OpenZip(p)
if err != nil {
t.Fatal(err)
}
defer zr.Close()
fs := zr.Fields()
if len(fs) != 63 {
t.Fatalf("Expected 63 columns in Natural Earth dataset, got %d", len(fs))
}
var metas []metaShape
for zr.Next() {
m := metaShape{
Attributes: make(map[string]string),
}
_, m.Shape = zr.Shape()
for n := range fs {
m.Attributes[fs[n].String()] = zr.Attribute(n)
}
metas = append(metas, m)
}
if zr.Err() != nil {
t.Fatal(zr.Err())
}
for _, m := range metas {
t.Log(m.Attributes["name"])
}
}

308
api/v1/common/tool/tool.go Normal file
View File

@ -0,0 +1,308 @@
package tool
import (
"bytes"
"crypto/md5"
"encoding/base64"
"encoding/hex"
"fmt"
"io"
"io/ioutil"
"log"
"math/rand"
"net/http"
"os"
"os/exec"
"path/filepath"
"reflect"
"regexp"
"runtime"
"sort"
"strconv"
"strings"
"time"
)
/*
判断文件或文件夹是否存在
如果返回的错误为nil,说明文件或文件夹存在
如果返回的错误类型使用os.IsNotExist()判断为true,说明文件或文件夹不存在
如果返回的错误为其它类型,则不确定是否在存在
*/
func PathExists(path string) bool {
_, err := os.Stat(path)
if err == nil {
return true
}
if os.IsNotExist(err) {
return false
}
return false
}
/*
检查字符串是否在某一数组中
*/
func IsContainStr(items []string, item string) bool {
for _, eachItem := range items {
if eachItem == item {
return true
}
}
return false
}
// 调用os.MkdirAll递归创建文件夹
func CreateDir(dirPath string) {
if !isExist(dirPath) {
fmt.Println("路径不存在,创建路径", dirPath)
_ = os.MkdirAll(dirPath, os.ModePerm)
}
}
// 判断所给路径文件/文件夹是否存在(返回true是存在)
func isExist(path string) bool {
_, err := os.Stat(path) //os.Stat获取文件信息
if err != nil {
if os.IsExist(err) {
return true
}
return false
}
return true
}
/*获取uuid*/
func GetUuid() string {
str := GetRandstring(32)
time.Sleep(time.Nanosecond)
return strings.ToLower(Md5V(str))
}
// #取得随机字符串:使用字符串拼接
func GetRandstring(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()
}
// 获取当前执行程序所在的绝对路径
func GetCurrentAbPathByExecutable() string {
exePath, err := os.Executable()
if err != nil {
log.Fatal(err)
}
res, _ := filepath.EvalSymlinks(filepath.Dir(exePath))
return res
}
/*结构体转map且首字母小写*/
func Struct2Map(obj interface{}) map[string]interface{} {
t := reflect.TypeOf(obj)
v := reflect.ValueOf(obj)
var data = make(map[string]interface{})
for i := 0; i < t.NumField(); i++ {
data[strings.ToLower(string(t.Field(i).Name[0]))+t.Field(i).Name[1:]] = v.Field(i).Interface()
//data[t.Field(i).Name] = v.Field(i).Interface()
}
return data
}
func PortInUse(port int) int {
checkStatement := fmt.Sprintf("lsof -i:%d ", port)
output, _ := exec.Command("sh", "-c", checkStatement).CombinedOutput()
fmt.Println(output)
if len(output) > 0 {
return 1
}
return -1
}
/*检测端口是否在使用中*/
// 传入查询的端口号
// 返回端口号对应的进程PID若没有找到相关进程返回-1
func portInUse(portNumber int) int {
res := -1
var outBytes bytes.Buffer
sysType := runtime.GOOS
fmt.Println(sysType)
var cmdStr = ""
if sysType == "linux" {
cmdStr = fmt.Sprintf("lsof -i:%d ", portNumber)
}
//
if sysType == "windows" {
cmdStr = fmt.Sprintf("netstat -ano -p tcp | findstr %d", portNumber)
}
//checkStatement := fmt.Sprintf("lsof -i:%d ", portNumber)
fmt.Println(cmdStr)
cmd := exec.Command("cmd", "/c", cmdStr)
cmd.Stdout = &outBytes
cmd.Run()
resStr := outBytes.String()
fmt.Println(resStr)
r := regexp.MustCompile(`\s\d+\s`).FindAllString(resStr, -1)
if len(r) > 0 {
pid, err := strconv.Atoi(strings.TrimSpace(r[0]))
if err != nil {
res = -1
} else {
res = pid
}
}
return res
}
func GetUnUsePort(port int) int {
isInUse := PortInUse(port)
if isInUse != -1 {
fmt.Println("端口:" + strconv.Itoa(port) + " 被占用")
port++
port = GetUnUsePort(port)
return port
} else {
return port
}
}
// WeekIntervalTime 获取某周的开始和结束时间,week为0本周,-1上周1下周以此类推
func WeekIntervalTime(week int) (startTime, endTime string) {
now := time.Now()
offset := int(time.Monday - now.Weekday())
//周日做特殊判断 因为time.Monday = 0
if offset > 0 {
offset = -6
}
year, month, day := now.Date()
thisWeek := time.Date(year, month, day, 0, 0, 0, 0, time.Local)
startTime = thisWeek.AddDate(0, 0, offset+7*week).Format("2006-01-02") + " 00:00:00"
endTime = thisWeek.AddDate(0, 0, offset+6+7*week).Format("2006-01-02") + " 23:59:59"
return startTime, endTime
}
// GetCurrentTime 获取当前系统时间
func GetCurrentTime() string {
return time.Now().Format("2006-01-02 15:04:05")
}
/*删除文件或文件夹*/
func RemoveFile(path string) {
if isExist(path) {
err := os.Remove(path)
if err != nil {
return
}
}
}
// 检查参数
func Md5V(str string) string {
h := md5.New()
h.Write([]byte(str))
return hex.EncodeToString(h.Sum(nil))
}
// download file会将url下载到本地文件它会在下载时写入而不是将整个文件加载到内存中。
func DownloadFile(filepath string, url string) 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(filepath)
if err != nil {
return err
}
defer out.Close()
// Write the body to file
_, err = io.Copy(out, resp.Body)
return err
}
// 通用排序
// 结构体排序必须重写数组Len() Swap() Less()函数
type body_wrapper struct {
Bodys []interface{}
by func(p, q *interface{}) bool //内部Less()函数会用到
}
type SortBodyBy func(p, q *interface{}) bool //定义一个函数类型
// 数组长度Len()
func (acw body_wrapper) Len() int {
return len(acw.Bodys)
}
// 元素交换
func (acw body_wrapper) Swap(i, j int) {
acw.Bodys[i], acw.Bodys[j] = acw.Bodys[j], acw.Bodys[i]
}
// 比较函数使用外部传入的by比较函数
func (acw body_wrapper) Less(i, j int) bool {
return acw.by(&acw.Bodys[i], &acw.Bodys[j])
}
// 自定义排序字段参考SortBodyByCreateTime中的传入函数
func SortBody(bodys []interface{}, by SortBodyBy) {
sort.Sort(body_wrapper{bodys, by})
}
// 格式化时间
type LocalTime time.Time
func (t *LocalTime) MarshalJSON() ([]byte, error) {
tTime := time.Time(*t)
return []byte(fmt.Sprintf("\"%v\"", tTime.Format("2006-01-02 15:04:05"))), nil
}
// 写入文件,保存
func WriteFile(path string, base64_image_content string) (error, string) {
//b, _ := regexp.MatchString(`^data:\s*image\/(\w+);base64,`, base64_image_content)
//if !b {
// return errors.New(""), ""
//}
base64_image_content = "data:image/png;base64," + base64_image_content
re, _ := regexp.Compile(`^data:\s*image\/(\w+);base64,`)
allData := re.FindAllSubmatch([]byte(base64_image_content), 2)
fileType := string(allData[0][1]) //png jpeg 后缀获取
base64Str := re.ReplaceAllString(base64_image_content, "")
//date := time.Now().Format("2006-01-02")
//if ok := IsFileExist(path + "/" + date); !ok {
// os.Mkdir(path+"/"+date, 0666)
//}
curFileStr := strconv.FormatInt(time.Now().UnixNano(), 10)
r := rand.New(rand.NewSource(time.Now().UnixNano()))
n := r.Intn(99999)
var filename = curFileStr + strconv.Itoa(n) + "." + fileType
var file = path + "/" + filename
byte, _ := base64.StdEncoding.DecodeString(base64Str)
err := ioutil.WriteFile(file, byte, 0666)
if err != nil {
log.Println(err)
}
return err, filename
}
func UploadsFile() {
}

View File

@ -0,0 +1,26 @@
package turf
import (
_ "embed"
"github.com/dop251/goja"
)
//go:embed turf.min.js
var turf_min string
//go:embed turf.js
var turf string
var Tin func(wgs84 [][]string) [][]string
var BooleanPointInPolygon func(point []float64, polygon [][]float64) bool
//// 初始化
//func init() {
// InitTurfjs()
//}
func InitTurfjs() {
vm := goja.New()
vm.RunString(turf_min + turf)
vm.ExportTo(vm.Get("Tin"), &Tin)
vm.ExportTo(vm.Get("BooleanPointInPolygon"), &BooleanPointInPolygon)
}

View File

@ -0,0 +1,26 @@
function Tin(points=[]) {
let arr = []
points.forEach(p=>{
arr.push(turf.point( [parseFloat(p[0]), parseFloat(p[1])]))
})
var tin = turf.tin(turf.featureCollection(arr));
let polylines=[]
tin.features.forEach((feature, index) => {
feature.geometry.coordinates.forEach((coordinate,) => {
polylines.push([
coordinate[0],
coordinate[1],
coordinate[2],
])
})
})
return polylines
return JSON.stringify(polylines)
}
function BooleanPointInPolygon(point,polygon=[]) {
var pt = turf.point([point[0],point[1]]);
var poly = turf.polygon([polygon]);
var scaledPoly = turf.transformScale(poly, 2);
return turf.booleanPointInPolygon(pt, poly)
}

91
api/v1/common/tool/turf/turf.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,52 @@
package tool
import (
"math"
)
var a = float64(6378137)
var b = 6356752.3142
var asqr = a * a
var bsqr = b * b
var e = math.Sqrt((asqr - bsqr) / asqr)
var eprime = math.Sqrt((asqr - bsqr) / bsqr)
func Xyz2Wgs84(X, Y, Z float64) (lng, lat, height float64) {
var p = math.Sqrt(X*X + Y*Y)
var theta = math.Atan((Z * a) / (p * b))
var sintheta = math.Sin(theta)
var costheta = math.Cos(theta)
var num = Z + eprime*eprime*b*sintheta*sintheta*sintheta
var denom = p - e*e*a*costheta*costheta*costheta
//Now calculate LLA
var latitude = math.Atan(num / denom)
var longitude = math.Atan(Y / X)
var N = getN(latitude)
var altitude = (p / math.Cos(latitude)) - N
if X < 0 && Y < 0 {
longitude = longitude - math.Pi
}
if X < 0 && Y > 0 {
longitude = longitude + math.Pi
}
return radiansToDegrees(longitude), radiansToDegrees(latitude), altitude
}
func getN(latitude float64) float64 {
var sinlatitude = math.Sin(latitude)
var denom = math.Sqrt(1 - e*e*sinlatitude*sinlatitude)
var N = a / denom
return N
}
func radiansToDegrees(radians float64) float64 {
return radians * 180 / math.Pi
}
//106.54959740614493 23.47200769358978

View File

@ -0,0 +1,113 @@
package zip
import (
"archive/zip"
"fmt"
"io"
"os"
"path"
)
func ZipFiles(filename string, files []string) error {
fmt.Println("start zip file......")
//创建输出文件目录
newZipFile, err := os.Create(filename)
if err != nil {
return err
}
defer newZipFile.Close()
//创建空的zip档案可以理解为打开zip文件准备写入
zipWriter := zip.NewWriter(newZipFile)
defer zipWriter.Close()
// Add files to zip
for _, file := range files {
if err = AddFileToZip(zipWriter, file); err != nil {
return err
}
}
return nil
}
func AddFileToZip(zipWriter *zip.Writer, filename string) error {
//打开要压缩的文件
fileToZip, err := os.Open(filename)
if err != nil {
return err
}
defer fileToZip.Close()
//获取文件的描述
info, err := fileToZip.Stat()
if err != nil {
return err
}
//FileInfoHeader返回一个根据fi填写了部分字段的Header可以理解成是将fileinfo转换成zip格式的文件信息
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
header.Name = filename
/*
预定义压缩算法。
archive/zip包中预定义的有两种压缩方式。一个是仅把文件写入到zip中。不做压缩。一种是压缩文件然后写入到zip中。默认的Store模式。就是只保存不压缩的模式。
Store unit16 = 0 //仅存储文件
Deflate unit16 = 8 //压缩文件
*/
header.Method = zip.Store
//创建压缩包头部信息
writer, err := zipWriter.CreateHeader(header)
if err != nil {
return err
}
//将源复制到目标将fileToZip 写入writer 是按默认的缓冲区32k循环操作的不会将内容一次性全写入内存中,这样就能解决大文件的问题
_, err = io.Copy(writer, fileToZip)
return err
}
// Decompressor 解压
func Decompressor(zipFilePath string, targetDir string, filename string) error {
reader, err := zip.OpenReader(zipFilePath)
if nil != err {
fmt.Println(err)
return err
}
defer reader.Close()
_ = os.MkdirAll(targetDir, 0777)
names := []string{}
for _, f := range reader.File {
err := func() error {
if f.FileInfo().IsDir() {
_ = os.MkdirAll(path.Join(targetDir, f.Name), f.Mode())
return nil
}
suffix := path.Ext(f.Name)
//fmt.Println(f.Name)
//fmt.Println(path.Join(targetDir, f.Name))
writeFile, err := os.OpenFile(path.Join(targetDir, filename+suffix), os.O_WRONLY|os.O_CREATE, f.Mode())
if nil != err {
return err
}
defer writeFile.Close()
readFile, err := f.Open()
if nil != err {
return err
}
defer readFile.Close()
n, err := io.Copy(writeFile, readFile)
if nil != err {
return err
}
if false {
names = append(names, f.Name)
fmt.Printf("解压文件: %s 大小: %v", f.Name, n)
}
return nil
}()
if nil != err {
return err
}
}
return nil
}

View File

@ -0,0 +1,83 @@
// ==========================================================================
// GFast自动生成api操作代码。
// 生成日期2024-05-28 15:09:13
// 生成路径: api/v1/system/app_menus.go
// 生成人gfast
// desc:app菜单相关参数
// company:云南奇讯科技有限公司
// ==========================================================================
package system
import (
"github.com/gogf/gf/v2/frame/g"
commonApi "github.com/tiger1103/gfast/v3/api/v1/common"
"github.com/tiger1103/gfast/v3/internal/app/system/model"
)
// AppMenusSearchReq 分页请求参数
type AppMenusSearchReq struct {
g.Meta `path:"/list" tags:"app菜单" method:"get" summary:"app菜单列表"`
MenuId string `p:"menuId"` //
MenuName string `p:"menuName"` //菜单名
CreatedAt string `p:"createdAt" v:"createdAt@datetime#需为YYYY-MM-DD hh:mm:ss格式"` //
commonApi.PageReq
commonApi.Author
}
// AppMenusSearchRes 列表返回结果
type AppMenusSearchRes struct {
g.Meta `mime:"application/json"`
commonApi.ListRes
List []*model.AppMenusListRes `json:"list"`
}
// AppMenusAddReq 添加操作请求参数
type AppMenusAddReq struct {
g.Meta `path:"/add" tags:"app菜单" method:"post" summary:"app菜单添加"`
commonApi.Author
MenuName string `p:"menuName" v:"required#菜单名不能为空"`
}
// AppMenusAddRes 添加操作返回结果
type AppMenusAddRes struct {
commonApi.EmptyRes
}
// AppMenusEditReq 修改操作请求参数
type AppMenusEditReq struct {
g.Meta `path:"/edit" tags:"app菜单" method:"put" summary:"app菜单修改"`
commonApi.Author
MenuId uint `p:"menuId" v:"required#主键ID不能为空"`
MenuName string `p:"menuName" v:"required#菜单名不能为空"`
}
// AppMenusEditRes 修改操作返回结果
type AppMenusEditRes struct {
commonApi.EmptyRes
}
// AppMenusGetReq 获取一条数据请求
type AppMenusGetReq struct {
g.Meta `path:"/get" tags:"app菜单" method:"get" summary:"获取app菜单信息"`
commonApi.Author
MenuId uint `p:"menuId" v:"required#主键必须"` //通过主键获取
}
// AppMenusGetRes 获取一条数据结果
type AppMenusGetRes struct {
g.Meta `mime:"application/json"`
*model.AppMenusInfoRes
}
// AppMenusDeleteReq 删除数据请求
type AppMenusDeleteReq struct {
g.Meta `path:"/delete" tags:"app菜单" method:"delete" summary:"删除app菜单"`
commonApi.Author
MenuIds []uint `p:"menuIds" v:"required#主键必须"` //通过主键删除
}
// AppMenusDeleteRes 删除数据返回
type AppMenusDeleteRes struct {
commonApi.EmptyRes
}

View File

@ -0,0 +1,86 @@
// ==========================================================================
// GFast自动生成api操作代码。
// 生成日期2024-05-28 15:11:14
// 生成路径: api/v1/system/app_role_menus.go
// 生成人gfast
// desc:app角色绑定菜单相关参数
// company:云南奇讯科技有限公司
// ==========================================================================
package system
import (
"github.com/gogf/gf/v2/frame/g"
commonApi "github.com/tiger1103/gfast/v3/api/v1/common"
"github.com/tiger1103/gfast/v3/internal/app/system/model"
)
// AppRoleMenusSearchReq 分页请求参数
type AppRoleMenusSearchReq struct {
g.Meta `path:"/list" tags:"app角色绑定菜单" method:"get" summary:"app角色绑定菜单列表"`
Id string `p:"id"` //
RoleId string `p:"roleId" v:"roleId@integer#角色ID需为整数"` //角色ID
MenuId string `p:"menuId" v:"menuId@integer#菜单ID需为整数"` //菜单ID
CreatedAt string `p:"createdAt" v:"createdAt@datetime#需为YYYY-MM-DD hh:mm:ss格式"` //
commonApi.PageReq
commonApi.Author
}
// AppRoleMenusSearchRes 列表返回结果
type AppRoleMenusSearchRes struct {
g.Meta `mime:"application/json"`
commonApi.ListRes
List []*model.AppRoleMenusListRes `json:"list"`
}
// AppRoleMenusAddReq 添加操作请求参数
type AppRoleMenusAddReq struct {
g.Meta `path:"/add" tags:"app角色绑定菜单" method:"post" summary:"app角色绑定菜单添加"`
commonApi.Author
RoleId int `p:"roleId" `
MenuId int `p:"menuId" `
}
// AppRoleMenusAddRes 添加操作返回结果
type AppRoleMenusAddRes struct {
commonApi.EmptyRes
}
// AppRoleMenusEditReq 修改操作请求参数
type AppRoleMenusEditReq struct {
g.Meta `path:"/edit" tags:"app角色绑定菜单" method:"put" summary:"app角色绑定菜单修改"`
commonApi.Author
Id uint `p:"id" v:"required#主键ID不能为空"`
RoleId int `p:"roleId" `
MenuId int `p:"menuId" `
}
// AppRoleMenusEditRes 修改操作返回结果
type AppRoleMenusEditRes struct {
commonApi.EmptyRes
}
// AppRoleMenusGetReq 获取一条数据请求
type AppRoleMenusGetReq struct {
g.Meta `path:"/get" tags:"app角色绑定菜单" method:"get" summary:"获取app角色绑定菜单信息"`
commonApi.Author
Id uint `p:"id" v:"required#主键必须"` //通过主键获取
}
// AppRoleMenusGetRes 获取一条数据结果
type AppRoleMenusGetRes struct {
g.Meta `mime:"application/json"`
*model.AppRoleMenusInfoRes
}
// AppRoleMenusDeleteReq 删除数据请求
type AppRoleMenusDeleteReq struct {
g.Meta `path:"/delete" tags:"app角色绑定菜单" method:"delete" summary:"删除app角色绑定菜单"`
commonApi.Author
Ids []uint `p:"ids" v:"required#主键必须"` //通过主键删除
}
// AppRoleMenusDeleteRes 删除数据返回
type AppRoleMenusDeleteRes struct {
commonApi.EmptyRes
}

108
api/v1/system/app_roles.go Normal file
View File

@ -0,0 +1,108 @@
// ==========================================================================
// GFast自动生成api操作代码。
// 生成日期2024-05-28 15:11:36
// 生成路径: api/v1/system/app_roles.go
// 生成人gfast
// desc:app角色相关参数
// company:云南奇讯科技有限公司
// ==========================================================================
package system
import (
"github.com/gogf/gf/v2/frame/g"
commonApi "github.com/tiger1103/gfast/v3/api/v1/common"
"github.com/tiger1103/gfast/v3/internal/app/system/model"
)
// AppRolesSearchReq 分页请求参数
type AppRolesSearchReq struct {
g.Meta `path:"/list" tags:"app角色" method:"get" summary:"app角色列表"`
RoleId string `p:"roleId"` //
RoleName string `p:"roleName"` // 角色名
CreatedAt string `p:"createdAt" v:"createdAt@datetime#需为YYYY-MM-DD hh:mm:ss格式"` //
commonApi.PageReq
commonApi.Author
}
// AppRolesSearchRes 列表返回结果
type AppRolesSearchRes struct {
g.Meta `mime:"application/json"`
commonApi.ListRes
List []*model.AppRolesListRes `json:"list"`
}
// AppRolesAddReq 添加操作请求参数
type AppRolesAddReq struct {
g.Meta `path:"/add" tags:"app角色" method:"post" summary:"app角色添加"`
commonApi.Author
RoleName string `p:"roleName" v:"required#角色名不能为空"`
}
// AppRolesAddRes 添加操作返回结果
type AppRolesAddRes struct {
commonApi.EmptyRes
}
// AppRolesAddMenuReq 添加角色时绑定菜单
type AppRolesAddMenuReq struct {
g.Meta `path:"/addMenu" tags:"app角色" method:"post" summary:"app添加角色的同时绑定菜单"`
RoleName string `p:"roleName" v:"required#角色名不能为空" dc:"角色名"` // 角色名字
MenuIds []int `p:"menuIds" v:"required#菜单ID不能为空" dc:"菜单ID 数组"` // 菜单ID
commonApi.Author
}
type AppRolesAddMenuRes struct {
commonApi.EmptyRes
}
// AppRolesEditReq 修改操作请求参数
type AppRolesEditReq struct {
g.Meta `path:"/edit" tags:"app角色" method:"put" summary:"app角色修改"`
commonApi.Author
RoleId uint `p:"roleId" v:"required#主键ID不能为空"`
RoleName string `p:"roleName" v:"required#角色名不能为空"`
MenuIds []int `p:"menuIds" dc:"菜单列表"` // 菜单列表
}
// AppRolesEditRes 修改操作返回结果
type AppRolesEditRes struct {
commonApi.EmptyRes
}
// AppRolesGetReq 获取一条数据请求
type AppRolesGetReq struct {
g.Meta `path:"/get" tags:"app角色" method:"get" summary:"获取app角色信息"`
commonApi.Author
RoleId uint `p:"roleId" v:"required#主键必须"` // 通过主键获取
}
// AppRolesGetRes 获取一条数据结果
type AppRolesGetRes struct {
g.Meta `mime:"application/json"`
*model.AppRolesInfoRes
}
// AppRolesDeleteReq 删除数据请求
type AppRolesDeleteReq struct {
g.Meta `path:"/delete" tags:"app角色" method:"delete" summary:"删除app角色"`
commonApi.Author
RoleIds []uint `p:"roleIds" v:"required#主键必须"` // 通过主键删除
}
// AppRolesDeleteRes 删除数据返回
type AppRolesDeleteRes struct {
commonApi.EmptyRes
}
// 获取一个角色的菜单
type AppRolesGetMenuReq struct {
g.Meta `path:"/getMenu" tags:"app角色" method:"get" summary:"获取角色的菜单"`
commonApi.Author
RoleId uint `p:"roleId" v:"required#角色ID不能为空"`
}
type AppRolesGetMenuRes struct {
g.Meta `mime:"application/json"`
List model.AppRoleDetails
}

View File

@ -0,0 +1,86 @@
// ==========================================================================
// GFast自动生成api操作代码。
// 生成日期2024-05-28 15:11:37
// 生成路径: api/v1/system/app_user_disable_menus.go
// 生成人gfast
// desc:app用户禁用菜单关联相关参数
// company:云南奇讯科技有限公司
// ==========================================================================
package system
import (
"github.com/gogf/gf/v2/frame/g"
commonApi "github.com/tiger1103/gfast/v3/api/v1/common"
"github.com/tiger1103/gfast/v3/internal/app/system/model"
)
// AppUserDisableMenusSearchReq 分页请求参数
type AppUserDisableMenusSearchReq struct {
g.Meta `path:"/list" tags:"app用户禁用菜单关联" method:"get" summary:"app用户禁用菜单关联列表"`
UserDisabledMenuId string `p:"userDisabledMenuId"` //
UserId string `p:"userId" v:"userId@integer#用户ID需为整数"` //用户ID
MenuId string `p:"menuId" v:"menuId@integer#菜单ID需为整数"` //菜单ID
CreatedAt string `p:"createdAt" v:"createdAt@datetime#需为YYYY-MM-DD hh:mm:ss格式"` //
commonApi.PageReq
commonApi.Author
}
// AppUserDisableMenusSearchRes 列表返回结果
type AppUserDisableMenusSearchRes struct {
g.Meta `mime:"application/json"`
commonApi.ListRes
List []*model.AppUserDisableMenusListRes `json:"list"`
}
// AppUserDisableMenusAddReq 添加操作请求参数
type AppUserDisableMenusAddReq struct {
g.Meta `path:"/add" tags:"app用户禁用菜单关联" method:"post" summary:"app用户禁用菜单关联添加"`
commonApi.Author
UserId int `p:"userId" `
MenuId int `p:"menuId" `
}
// AppUserDisableMenusAddRes 添加操作返回结果
type AppUserDisableMenusAddRes struct {
commonApi.EmptyRes
}
// AppUserDisableMenusEditReq 修改操作请求参数
type AppUserDisableMenusEditReq struct {
g.Meta `path:"/edit" tags:"app用户禁用菜单关联" method:"put" summary:"app用户禁用菜单关联修改"`
commonApi.Author
UserDisabledMenuId uint `p:"userDisabledMenuId" v:"required#主键ID不能为空"`
UserId int `p:"userId" `
MenuId int `p:"menuId" `
}
// AppUserDisableMenusEditRes 修改操作返回结果
type AppUserDisableMenusEditRes struct {
commonApi.EmptyRes
}
// AppUserDisableMenusGetReq 获取一条数据请求
type AppUserDisableMenusGetReq struct {
g.Meta `path:"/get" tags:"app用户禁用菜单关联" method:"get" summary:"获取app用户禁用菜单关联信息"`
commonApi.Author
UserDisabledMenuId uint `p:"userDisabledMenuId" v:"required#主键必须"` //通过主键获取
}
// AppUserDisableMenusGetRes 获取一条数据结果
type AppUserDisableMenusGetRes struct {
g.Meta `mime:"application/json"`
*model.AppUserDisableMenusInfoRes
}
// AppUserDisableMenusDeleteReq 删除数据请求
type AppUserDisableMenusDeleteReq struct {
g.Meta `path:"/delete" tags:"app用户禁用菜单关联" method:"delete" summary:"删除app用户禁用菜单关联"`
commonApi.Author
UserDisabledMenuIds []uint `p:"userDisabledMenuIds" v:"required#主键必须"` //通过主键删除
}
// AppUserDisableMenusDeleteRes 删除数据返回
type AppUserDisableMenusDeleteRes struct {
commonApi.EmptyRes
}

Some files were not shown because too many files have changed in this diff Show More