diff --git a/api/viid_client.go b/api/viid_client.go new file mode 100644 index 0000000..61ac7d4 --- /dev/null +++ b/api/viid_client.go @@ -0,0 +1,209 @@ +package api + +import ( + "ZhenTuLocalPassiveAdapter/config" + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +// VIID Request/Response Structures + +type DeviceIdObject struct { + DeviceID string `json:"DeviceID"` +} + +type RegisterRequest struct { + RegisterObject DeviceIdObject `json:"RegisterObject"` +} + +type KeepaliveRequest struct { + KeepaliveObject DeviceIdObject `json:"KeepaliveObject"` +} + +type UnRegisterRequest struct { + UnRegisterObject DeviceIdObject `json:"UnRegisterObject"` +} + +type VIIDBaseResponse struct { + ResponseStatusObject ResponseStatusObject `json:"ResponseStatusObject"` +} + +type ResponseStatusObject struct { + ID string `json:"Id"` + RequestURL string `json:"RequestURL"` + StatusCode string `json:"StatusCode"` + StatusString string `json:"StatusString"` + LocalTime string `json:"LocalTime"` +} + +type SystemTimeResponse struct { + ResponseStatusObject struct { + LocalTime string `json:"LocalTime"` // yyyyMMddHHmmss + } `json:"ResponseStatusObject"` +} + +// VIID Client Methods + +func ProxyRequest(ctx context.Context, method, path string, body io.Reader, header http.Header) (*http.Response, error) { + url := fmt.Sprintf("%s%s", config.Config.Viid.ServerUrl, path) + req, err := http.NewRequestWithContext(ctx, method, url, body) + if err != nil { + return nil, err + } + + // Copy headers + for k, v := range header { + req.Header[k] = v + } + + return GetAPIClient().Do(req) +} + +// Upload Proxy Structures + +type UploadConfig struct { + TaskID int64 `json:"taskId"` + FaceUploadURL string `json:"faceUploadUrl"` + ThumbnailUploadURL string `json:"thumbnailUploadUrl"` + SourceUploadURL string `json:"sourceUploadUrl"` + ExpiresAt time.Time `json:"expiresAt"` +} + +type ProxyResponse struct { + Code int `json:"code"` + Message string `json:"message"` + Success bool `json:"success"` + Data *UploadConfig `json:"data"` // Data can be UploadConfig or string depending on endpoint +} + +type FacePositionInfo struct { + LeftTopX int `json:"leftTopX"` + LeftTopY int `json:"leftTopY"` + RightBtmX int `json:"rightBtmX"` + RightBtmY int `json:"rightBtmY"` + ImgWidth int `json:"imgWidth"` + ImgHeight int `json:"imgHeight"` + ShotTime string `json:"shotTime"` // yyyyMMddHHmmss +} + +type SubmitResultRequest struct { + TaskID int64 `json:"taskId"` + FacePosition FacePositionInfo `json:"facePosition"` +} + +type SubmitFailureRequest struct { + TaskID int64 `json:"taskId"` + ErrorCode string `json:"errorCode"` + ErrorMessage string `json:"errorMessage"` +} + +// Upload Proxy Methods + +func GetUploadConfig(ctx context.Context, scenicId int64, deviceNo string) (*UploadConfig, error) { + url := fmt.Sprintf("%s/proxy/VIID/upload-config?scenicId=%d&deviceNo=%s", config.Config.Viid.ServerUrl, scenicId, deviceNo) + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, err + } + + resp, err := GetAPIClient().Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result ProxyResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, err + } + + if !result.Success { + return nil, fmt.Errorf("get upload config failed: %s", result.Message) + } + + return result.Data, nil +} + +func SubmitResult(ctx context.Context, taskID int64, facePos FacePositionInfo) error { + url := fmt.Sprintf("%s/proxy/VIID/submit-result", config.Config.Viid.ServerUrl) + reqData := SubmitResultRequest{ + TaskID: taskID, + FacePosition: facePos, + } + + jsonData, _ := json.Marshal(reqData) + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + + resp, err := GetAPIClient().Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + var result struct { + Code int `json:"code"` + Message string `json:"message"` + Success bool `json:"success"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return err + } + + if !result.Success { + return fmt.Errorf("submit result failed: %s", result.Message) + } + + return nil +} + +func SubmitFailure(ctx context.Context, taskID int64, errorCode, errorMessage string) error { + url := fmt.Sprintf("%s/proxy/VIID/submit-failure", config.Config.Viid.ServerUrl) + reqData := SubmitFailureRequest{ + TaskID: taskID, + ErrorCode: errorCode, + ErrorMessage: errorMessage, + } + + jsonData, _ := json.Marshal(reqData) + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + + resp, err := GetAPIClient().Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} + +func UploadFileToOSS(ctx context.Context, uploadUrl string, data []byte, contentType string) error { + req, err := http.NewRequestWithContext(ctx, "PUT", uploadUrl, bytes.NewReader(data)) + if err != nil { + return err + } + req.Header.Set("Content-Type", contentType) + + resp, err := GetUploadClient().Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + return fmt.Errorf("oss upload failed: %d", resp.StatusCode) + } + return nil +} diff --git a/api/viid_upload.go b/api/viid_upload.go new file mode 100644 index 0000000..f45ccc9 --- /dev/null +++ b/api/viid_upload.go @@ -0,0 +1,70 @@ +package api + +import ( + "ZhenTuLocalPassiveAdapter/logger" + "context" + "fmt" + "go.uber.org/zap" + "golang.org/x/sync/errgroup" +) + +// UploadFaceData implements the coordinated upload flow: Get Config -> Upload -> Notify +func UploadFaceData(ctx context.Context, scenicId int64, deviceNo string, faceImg, thumbImg, srcImg []byte, facePos FacePositionInfo) error { + // 1. Get Upload Config + uploadConfig, err := GetUploadConfig(ctx, scenicId, deviceNo) + if err != nil { + logger.Error("获取上传配置失败", zap.String("deviceNo", deviceNo), zap.Error(err)) + return err + } + + logger.Info("获取到VIID任务ID", zap.Int64("taskId", uploadConfig.TaskID)) + + // 2. Parallel Upload to OSS + g, subCtx := errgroup.WithContext(ctx) + + // Upload Face Image + g.Go(func() error { + if len(faceImg) > 0 { + if err := UploadFileToOSS(subCtx, uploadConfig.FaceUploadURL, faceImg, "image/jpeg"); err != nil { + return fmt.Errorf("upload face image failed: %w", err) + } + } + return nil + }) + + // Upload Thumbnail Image + g.Go(func() error { + if len(thumbImg) > 0 { + if err := UploadFileToOSS(subCtx, uploadConfig.ThumbnailUploadURL, thumbImg, "image/jpeg"); err != nil { + return fmt.Errorf("upload thumbnail image failed: %w", err) + } + } + return nil + }) + + // Upload Source Image + g.Go(func() error { + if len(srcImg) > 0 { + if err := UploadFileToOSS(subCtx, uploadConfig.SourceUploadURL, srcImg, "image/jpeg"); err != nil { + return fmt.Errorf("upload source image failed: %w", err) + } + } + return nil + }) + + if err := g.Wait(); err != nil { + // Report failure + SubmitFailure(ctx, uploadConfig.TaskID, "UPLOAD_FAILED", err.Error()) + logger.Error("文件上传失败", zap.Int64("taskId", uploadConfig.TaskID), zap.Error(err)) + return err + } + + // 3. Submit Result + if err := SubmitResult(ctx, uploadConfig.TaskID, facePos); err != nil { + logger.Error("提交结果失败", zap.Int64("taskId", uploadConfig.TaskID), zap.Error(err)) + return err + } + + logger.Info("VIID任务完成", zap.Int64("taskId", uploadConfig.TaskID)) + return nil +} diff --git a/config.yaml b/config.yaml index 6f914cc..de441bf 100644 --- a/config.yaml +++ b/config.yaml @@ -1,24 +1,27 @@ api: -# baseUrl: "https://zhentuai.com/vpt/v1/scenic/3946669713328836608" - baseUrl: "http://127.0.0.1:8030/vpt/v1/scenic/3946669713328836608" + baseUrl: "https://zhentuai.com/vpt/v1/scenic/3975985126059413504" record: storage: path: "/root/opt/" type: "s3" s3: region: us-east-1 - endpoint: http://192.168.55.101:9000 + endpoint: http://127.0.0.1:9000 bucket: opt prefix: - akId: 5vzfDiMztKO6VLvygoeX - akSec: Ot77u2kdVTm8zfQgExFrsm7xlGecxsiR6jk1idXM - duration: 60 + akId: Idi2MBaWH2F0LFIWGdDY + akSec: Idi2MBaWH2F0LFIWGdDY + duration: 30 devices: - - deviceNo: "34020000001322200001" - name: "192.168.55.201" - path: "/root/opt/34020000001322200001/" + - deviceNo: "44020000001322500001" + name: "ppda-010268-zymyj" fileName: timeSplit: "_" dateSeparator: "" fileExt: "ts" - unFinExt: "ts" \ No newline at end of file + unFinExt: "ts" +viid: + enabled: true + serverUrl: "http://127.0.0.1:18083" + scenicId: 3975985126059413504 + port: 8080 diff --git a/config/dto.go b/config/dto.go index e43dd07..3918d95 100644 --- a/config/dto.go +++ b/config/dto.go @@ -36,9 +36,17 @@ type FileNameConfig struct { UnfinishedFileExt string `mapstructure:"unFinExt"` } +type ViidConfig struct { + Enabled bool `mapstructure:"enabled"` + ServerUrl string `mapstructure:"serverUrl"` + ScenicId int64 `mapstructure:"scenicId"` + Port int `mapstructure:"port"` +} + type MainConfig struct { Api ApiConfig `mapstructure:"api"` Record RecordConfig `mapstructure:"record"` Devices []DeviceMapping `mapstructure:"devices"` FileName FileNameConfig `mapstructure:"fileName"` + Viid ViidConfig `mapstructure:"viid"` } diff --git a/go.mod b/go.mod index 20b4e88..ab6d79c 100644 --- a/go.mod +++ b/go.mod @@ -10,10 +10,12 @@ require ( github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2 github.com/spf13/viper v1.20.0 go.opentelemetry.io/otel v1.35.0 - go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 go.opentelemetry.io/otel/sdk v1.35.0 - go.opentelemetry.io/otel/sdk/metric v1.35.0 + go.uber.org/zap v1.27.0 + golang.org/x/sync v0.16.0 + gopkg.in/natefinch/lumberjack.v2 v2.2.1 ) require ( @@ -26,35 +28,58 @@ require ( github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 // indirect github.com/aws/smithy-go v1.22.3 // indirect + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/gin-gonic/gin v1.11.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.27.0 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect - github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.54.0 // indirect github.com/sagikazarmark/locafero v0.8.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.14.0 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect + go.uber.org/mock v0.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect - golang.org/x/net v0.35.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/text v0.23.0 // indirect + golang.org/x/arch v0.20.0 // indirect + golang.org/x/crypto v0.40.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.27.0 // indirect + golang.org/x/tools v0.34.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect google.golang.org/grpc v1.71.0 // indirect - google.golang.org/protobuf v1.36.5 // indirect - gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + google.golang.org/protobuf v1.36.9 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 124a638..db408f0 100644 --- a/go.sum +++ b/go.sum @@ -22,35 +22,79 @@ github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2 h1:jIiopHEV22b4yQP2q36Y0OmwLbsxN github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2/go.mod h1:U5SNqwhXB3Xe6F47kXvWihPl/ilGaEDe8HD/50Z9wxc= github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k= github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= +github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= +github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= +github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/sagikazarmark/locafero v0.8.0 h1:mXaMVw7IqxNBxfv3LdWt9MDmcWDQ1fagDH918lOdVaQ= @@ -65,10 +109,21 @@ github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY= github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= @@ -77,10 +132,6 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0f go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 h1:PB3Zrjs1sG1GBX51SXyTSoOTqcDglmsk7nT6tkKPb/k= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 h1:T0Ec2E+3YZf5bgTNQVet8iTDW7oIk03tXHq+wkwIDnE= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0/go.mod h1:30v2gqH+vYGJsesLWFov8u47EpYTcIQcBjKpI6pJThg= go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= @@ -93,16 +144,37 @@ go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= +golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= @@ -111,10 +183,13 @@ google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 2245047..283a417 100644 --- a/main.go +++ b/main.go @@ -8,12 +8,17 @@ import ( "ZhenTuLocalPassiveAdapter/logger" "ZhenTuLocalPassiveAdapter/telemetry" "context" + "fmt" + "os" + "os/signal" + "syscall" + "time" + + "github.com/gin-gonic/gin" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.uber.org/zap" - "os" - "time" ) var tracer = otel.Tracer("vpt") @@ -65,6 +70,60 @@ func startTask(device config.DeviceMapping, task dto.Task) { zap.String("deviceNo", task.DeviceNo)) } +func runTaskLoop(ctx context.Context) { + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + // 执行任务 + tasks, err := api.SyncTask() + if err == nil { + for _, task := range tasks { + logger.Info("开始处理任务", + zap.String("taskID", task.TaskID), + zap.String("deviceNo", task.DeviceNo), + zap.String("startTime", task.StartTime.Format("2006-01-02 15:04:05")), + zap.String("endTime", task.EndTime.Format("2006-01-02 15:04:05"))) + // 处理任务 + for _, device := range config.Config.Devices { + if device.DeviceNo == task.DeviceNo { + // 处理任务 + go startTask(device, task) + break // 提前返回,避免不必要的循环 + } + } + } + } else { + logger.Error("同步任务失败", zap.Error(err)) + } + } + } +} + +func startViidServer() { + if !config.Config.Viid.Enabled { + return + } + + gin.SetMode(gin.ReleaseMode) + r := gin.Default() + + // Register Routes + api.RegisterVIIDRoutes(r) + + addr := fmt.Sprintf(":%d", config.Config.Viid.Port) + logger.Info("VIID Server starting", zap.String("addr", addr)) + go func() { + if err := r.Run(addr); err != nil { + logger.Error("VIID Server failed", zap.Error(err)) + } + }() +} + func main() { // 初始化日志 err := logger.Init() @@ -85,6 +144,7 @@ func main() { return } defer shutdown(ctx) + if config.Config.Record.Storage.Type == "local" { _, err = os.Stat(config.Config.Record.Storage.Path) if err != nil { @@ -96,30 +156,24 @@ func main() { } else { logger.Info("录像文件夹配置为OSS") } - // 每两秒定时执行 - for { - // 执行任务 - tasks, err := api.SyncTask() - if err == nil { - for _, task := range tasks { - logger.Info("开始处理任务", - zap.String("taskID", task.TaskID), - zap.String("deviceNo", task.DeviceNo), - zap.String("startTime", task.StartTime.Format("2006-01-02 15:04:05")), - zap.String("endTime", task.EndTime.Format("2006-01-02 15:04:05"))) - // 处理任务 - for _, device := range config.Config.Devices { - if device.DeviceNo == task.DeviceNo { - // 处理任务 - go startTask(device, task) - break // 提前返回,避免不必要的循环 - } - } - } - } else { - logger.Error("同步任务失败", zap.Error(err)) - } - // 等待两秒 - <-time.After(2 * time.Second) - } + + // Start VIID Server + startViidServer() + + // Context for graceful shutdown + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Handle Signals + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + // Start Task Loop + go runTaskLoop(ctx) + + // Wait for signal + <-sigChan + logger.Info("Received shutdown signal") + cancel() + logger.Info("Shutdown complete") } diff --git a/model/custom_time.go b/model/custom_time.go new file mode 100644 index 0000000..280c60f --- /dev/null +++ b/model/custom_time.go @@ -0,0 +1,104 @@ +package model + +import ( + "database/sql/driver" + "encoding/json" + "fmt" + "time" +) + +// CustomTime 自定义时间类型,用于JSON序列化为YYYY-MM-DD HH:mm:ss格式 +type CustomTime struct { + time.Time +} + +// MarshalJSON 自定义JSON序列化 +func (ct CustomTime) MarshalJSON() ([]byte, error) { + if ct.Time.IsZero() { + return json.Marshal("") + } + return json.Marshal(ct.Time.Format("2006-01-02 15:04:05")) +} + +// UnmarshalJSON 自定义JSON反序列化 +// 支持格式: +// 1. 数字时间戳:10位秒级(1732435200)或13位毫秒级(1732435200000) +// 2. 字符串格式:秒级(2024-11-24 12:00:00)或毫秒级(2024-11-24 12:00:00.000) +// 3. 空字符串:转换为零值时间 +func (ct *CustomTime) UnmarshalJSON(data []byte) error { + // 1. 尝试解析为数字时间戳 + var timestamp int64 + if err := json.Unmarshal(data, ×tamp); err == nil { + if timestamp == 0 { + ct.Time = time.Time{} + return nil + } + // 根据位数判断精度:13位为毫秒级,10位为秒级 + if timestamp > 9999999999 { + ct.Time = time.UnixMilli(timestamp) + } else { + ct.Time = time.Unix(timestamp, 0) + } + return nil + } + + // 2. 解析字符串格式 + var str string + if err := json.Unmarshal(data, &str); err != nil { + return err + } + if str == "" { + ct.Time = time.Time{} + return nil + } + + // 3. 使用本地时区解析时间,避免时区问题 + // 尝试多种格式(按精度从高到低) + formats := []string{ + "2006-01-02 15:04:05.000", // 毫秒格式 + "2006-01-02 15:04:05", // 秒格式(现有) + } + + var lastErr error + for _, format := range formats { + t, err := time.ParseInLocation(format, str, time.Local) + if err == nil { + ct.Time = t + return nil + } + lastErr = err + } + + return lastErr +} + +// Value 实现driver.Valuer接口,用于写入数据库 +func (ct CustomTime) Value() (driver.Value, error) { + if ct.Time.IsZero() { + return nil, nil + } + return ct.Time, nil +} + +// Scan 实现sql.Scanner接口,用于从数据库读取 +func (ct *CustomTime) Scan(value interface{}) error { + if value == nil { + ct.Time = time.Time{} + return nil + } + if t, ok := value.(time.Time); ok { + ct.Time = t + return nil + } + return fmt.Errorf("无法扫描 %T 到 CustomTime", value) +} + +// NewCustomTime 创建一个新的 CustomTime 实例 +func NewCustomTime(t time.Time) CustomTime { + return CustomTime{Time: t} +} + +// NowCustomTime 返回当前时间的 CustomTime 实例 +func NowCustomTime() CustomTime { + return CustomTime{Time: time.Now()} +} diff --git a/util/image.go b/util/image.go new file mode 100644 index 0000000..a686018 --- /dev/null +++ b/util/image.go @@ -0,0 +1,96 @@ +package util + +import ( + "bytes" + "fmt" + "image" + "image/draw" + "image/jpeg" + _ "image/png" // Register PNG decoder just in case +) + +// GenerateThumbnail creates a thumbnail by cropping the original image. +// The thumbnail size is 1/2 of the original dimensions. +// The crop is centered on the provided coordinates (centerX, centerY), constrained to image bounds. +func GenerateThumbnail(srcData []byte, centerX, centerY int) ([]byte, error) { + // 1. Decode image + img, _, err := image.Decode(bytes.NewReader(srcData)) + if err != nil { + return nil, fmt.Errorf("decode image failed: %v", err) + } + + bounds := img.Bounds() + origW := bounds.Dx() + origH := bounds.Dy() + + // 2. Calculate Target Dimensions (1/2 of original) + targetW := origW / 2 + targetH := origH / 2 + + if targetW == 0 || targetH == 0 { + return nil, fmt.Errorf("image too small to generate half-size thumbnail") + } + + // 3. Calculate Crop Origin (Top-Left) + // cropX = centerX - targetW / 2 + cropX := centerX - targetW/2 + cropY := centerY - targetH/2 + + // 4. Constrain to Bounds + if cropX < 0 { + cropX = 0 + } + if cropY < 0 { + cropY = 0 + } + if cropX+targetW > origW { + cropX = origW - targetW + } + if cropY+targetH > origH { + cropY = origH - targetH + } + + // Double check after adjustment + if cropX < 0 { + cropX = 0 + } + if cropY < 0 { + cropY = 0 + } + + // 5. Perform Crop + cropRect := image.Rect(cropX, cropY, cropX+targetW, cropY+targetH) + + var dst image.Image + + // Try to use SubImage if supported for performance + type SubImager interface { + SubImage(r image.Rectangle) image.Image + } + + if sub, ok := img.(SubImager); ok { + dst = sub.SubImage(cropRect) + } else { + // Fallback: create new image and draw + rgba := image.NewRGBA(image.Rect(0, 0, targetW, targetH)) + draw.Draw(rgba, rgba.Bounds(), img, cropRect.Min, draw.Src) + dst = rgba + } + + // 6. Encode to JPEG + var buf bytes.Buffer + err = jpeg.Encode(&buf, dst, &jpeg.Options{Quality: 85}) + if err != nil { + return nil, fmt.Errorf("encode thumbnail failed: %v", err) + } + + return buf.Bytes(), nil +} + +// Helper to process using LeftTop and RightBottom coordinates +func GenerateThumbnailFromCoords(srcData []byte, ltX, ltY, rbX, rbY int) ([]byte, error) { + // Calculate center based on the provided coordinates + centerX := (ltX + rbX) / 2 + centerY := (ltY + rbY) / 2 + return GenerateThumbnail(srcData, centerX, centerY) +}