提交 9b11dbc1 authored 作者: mooncake's avatar mooncake

utils

上级 76f3cb6b
SHELL := /bin/bash SHELL := /bin/bash
PROJECT_NAME := "olog" PROJECT_NAME := "opkg"
PKG := "$(PROJECT_NAME)" PKG := "$(PROJECT_NAME)"
PKG_LIST := $(shell go list ${PKG}/... | grep -v /vendor/ | grep -v /api/ | grep -v /cmd/) PKG_LIST := $(shell go list ${PKG}/... | grep -v /vendor/ | grep -v /api/ | grep -v /cmd/)
......
...@@ -2,10 +2,11 @@ package config ...@@ -2,10 +2,11 @@ package config
import ( import (
"fmt" "fmt"
"olog/utils/fileutils"
"os" "os"
"path/filepath" "path/filepath"
"gitlab.wanzhuangkj.com/tush/opkg/xutils/fileutils"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
......
module olog module gitlab.wanzhuangkj.com/tush/opkg
go 1.24.3 go 1.24.3
require ( require (
github.com/bwmarrin/snowflake v0.3.0
github.com/duke-git/lancet/v2 v2.3.8
github.com/gin-gonic/gin v1.11.0
github.com/huandu/xstrings v1.5.0
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/uptrace/opentelemetry-go-extra/otelgorm v0.3.2
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.0
google.golang.org/grpc v1.76.0 google.golang.org/grpc v1.76.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.6.0
gorm.io/driver/postgres v1.6.0
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.1
gorm.io/plugin/dbresolver v1.6.2
) )
require ( require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-logr/logr v1.4.3 // 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-sql-driver/mysql v1.8.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // 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/mattn/go-sqlite3 v1.14.22 // 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/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.uber.org/mock v0.5.0 // indirect
go.uber.org/multierr v1.10.0 // indirect go.uber.org/multierr v1.10.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect golang.org/x/arch v0.20.0 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/exp v0.0.0-20221208152030-732eee02a75a // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.16.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/protobuf v1.36.9 // indirect
) )
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
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/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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/duke-git/lancet/v2 v2.3.8 h1:dlkqn6Nj2LRWFuObNxttkMHxrFeaV6T26JR8jbEVbPg=
github.com/duke-git/lancet/v2 v2.3.8/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc=
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.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/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/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
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-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
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/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/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
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/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
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.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/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/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.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
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=
github.com/uptrace/opentelemetry-go-extra/otelgorm v0.3.2 h1:Jjn3zoRz13f8b1bR6LrXWglx93Sbh4kYfwgmPju3E2k=
github.com/uptrace/opentelemetry-go-extra/otelgorm v0.3.2/go.mod h1:wocb5pNrj/sjhWB9J5jctnC0K2eisSdz/nJJBNFHo+A=
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 h1:ZjUj9BLYf9PEqBn8W/OapxhPjVRdC6CsXTdULHsyk5c=
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2/go.mod h1:O8bHQfyinKwTXKkiKNGmLQS7vRsqRxIQTFZpYpHK3IQ=
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.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 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/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.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 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/exp v0.0.0-20221208152030-732eee02a75a h1:4iLhBPcpqFmylhnkbY3W0ONLUYYkDAW9xMFLfxgsvCw=
golang.org/x/exp v0.0.0-20221208152030-732eee02a75a/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
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.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
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.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 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 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 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
gorm.io/plugin/dbresolver v1.6.2 h1:F4b85TenghUeITqe3+epPSUtHH7RIk3fXr5l83DF8Pc=
gorm.io/plugin/dbresolver v1.6.2/go.mod h1:tctw63jdrOezFR9HmrKnPkmig3m5Edem9fdxk9bQSzM=
package entity
import (
"bytes"
"fmt"
"github.com/gin-gonic/gin"
ctxutils "gitlab.wanzhuangkj.com/tush/opkg/xutils/ctxutils"
)
type CopyHttpReq struct {
Method string `json:"method"`
URL string `json:"url"`
Headers map[string][]string `json:"headers"`
Body string `json:"body"`
}
type CopyHttpRsp struct {
Headers map[string][]string `json:"headers"`
Body string `json:"body"`
}
func SetCopyReq(c *gin.Context, buf *bytes.Buffer) {
copyHttpReq := &CopyHttpReq{}
copyHttpReq.Method = c.Request.Method
copyHttpReq.URL = c.Request.URL.String()
copyHttpReq.Headers = c.Request.Header
copyHttpReq.Body = buf.String()
c.Set(ctxutils.KeyReqBody, copyHttpReq)
}
func GetCopyReq(c *gin.Context) *CopyHttpReq {
if val, isExist := c.Get(ctxutils.KeyReqBody); isExist {
if req, ok := val.(*CopyHttpReq); ok {
return req
}
}
return nil
}
func SetCopyRsp(c *gin.Context, buf *bytes.Buffer) {
copyHttpRsp := &CopyHttpRsp{}
copyHttpRsp.Headers = c.Writer.Header()
copyHttpRsp.Body = buf.String()
c.Set(ctxutils.KeyRspBody, copyHttpRsp)
}
func GetCopyRsp(c *gin.Context) *CopyHttpRsp {
if val, isExist := c.Get(ctxutils.KeyRspBody); isExist {
if rsp, ok := val.(*CopyHttpRsp); ok {
return rsp
}
}
return nil
}
func SetCopyApiReq(c *gin.Context, buf *bytes.Buffer) {
copyHttpReq := &CopyHttpReq{}
copyHttpReq.Method = c.Request.Method
copyHttpReq.URL = c.Request.URL.String()
copyHttpReq.Headers = c.Request.Header
copyHttpReq.Body = buf.String()
c.Set(fmt.Sprintf(ctxutils.KeyOApiReqBody, ctxutils.GinTraceID(c)), copyHttpReq)
}
func GetCopyApiReq(c *gin.Context) *CopyHttpReq {
if val, isExist := c.Get(fmt.Sprintf(ctxutils.KeyOApiReqBody, ctxutils.GinTraceID(c))); isExist {
if req, ok := val.(*CopyHttpReq); ok {
return req
}
}
return nil
}
func SetCopyApiRsp(c *gin.Context, buf *bytes.Buffer) {
copyHttpRsp := &CopyHttpRsp{}
copyHttpRsp.Headers = c.Writer.Header()
copyHttpRsp.Body = buf.String()
c.Set(fmt.Sprintf(ctxutils.KeyOApiRspBody, ctxutils.GinTraceID(c)), copyHttpRsp)
}
func GetCopyApiRsp(c *gin.Context) *CopyHttpRsp {
if val, isExist := c.Get(fmt.Sprintf(ctxutils.KeyOApiRspBody, ctxutils.GinTraceID(c))); isExist {
if rsp, ok := val.(*CopyHttpRsp); ok {
return rsp
}
}
return nil
}
package httpContentType
import (
"net/http"
"strings"
)
func IsMedia(header http.Header) bool {
ct := header.Get("Content-Type")
isMedia := false
if strings.Contains(ct, "image") {
isMedia = true
}
return isMedia
}
// Package httpcli is http request client, which only supports return json format.
package httpcli
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/httpcli/entity"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/httpcli/httpContentType"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/xerrors/xerror"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/logger"
ctxutils "gitlab.wanzhuangkj.com/tush/opkg/xutils/ctxutils"
retryutils "gitlab.wanzhuangkj.com/tush/opkg/xutils/retryutils"
)
const defaultTimeout = 30 * time.Second
type Request struct {
cli *http.Client
url string
method string
headers map[string]string
params map[string]interface{} // parameters after URL
reqBody []byte // Body data
reqBodyJSON interface{} // 传入对象,会自动json marshal
timeout time.Duration // Client timeout
retryCount int
request *http.Request
response *Response
err error
omitLog bool
}
// Response HTTP response
type Response struct {
body []byte
StatusCode int
Status string
Header http.Header
}
type Client struct {
client *http.Client
}
// eg
// NewClient(&http.Client{
// Transport: &http.Transport{
// MaxIdleConnsPerHost: 10,
// MaxIdleConns: 20, // 最大空闲连接数
// IdleConnTimeout: 90 * time.Second, // 空闲连接超时时间
// DisableCompression: true, // 禁用压缩(按需)
// },
// }),
func NewClient(cli *http.Client) *Client {
return &Client{
client: cli,
}
}
func (x *Client) NewRequest() *Request {
return &Request{
cli: x.client,
response: &Response{},
}
}
// ----------------------------------- Request way 1 -----------------------------------
//
// New create a new Request
func New() *Request {
return &Request{
response: &Response{},
}
}
// Reset set all fields to default value, use at pool
func (x *Request) Reset() {
x.params = nil
x.reqBody = nil
x.reqBodyJSON = nil
x.timeout = 0
x.headers = nil
x.response = nil
x.method = ""
x.err = nil
}
// SetURL set URL
func (x *Request) SetURL(path string) *Request {
x.url = path
return x
}
// SetParams parameters after setting the URL
func (x *Request) SetParams(params map[string]interface{}) *Request {
if x.params == nil {
x.params = params
} else {
for k, v := range params {
x.params[k] = v
}
}
return x
}
// SetParam parameters after setting the URL
func (x *Request) SetParam(k string, v interface{}) *Request {
if x.params == nil {
x.params = make(map[string]interface{})
}
x.params[k] = v
return x
}
// SetBody set body data, support string and []byte, if it is not string, it will be json marshal.
func (x *Request) SetBody(body interface{}) *Request {
switch v := body.(type) {
case string:
x.reqBody = []byte(v)
case []byte:
x.reqBody = v
default:
x.reqBodyJSON = body
}
return x
}
// SetTimeout set timeout
func (x *Request) SetTimeout(t time.Duration) *Request {
x.timeout = t
return x
}
func (x *Request) SetRetry(count int) *Request {
x.retryCount = count
return x
}
func (x *Request) SetMethod(method string) *Request {
x.method = method
return x
}
func (x *Request) OmitLog() *Request {
x.omitLog = true
return x
}
// SetContentType set ContentType
// 1. ‌文本类‌
// text/html; charset=UTF-8
// ‌text/plain‌:纯文本(无格式)‌
// ‌text/css‌:CSS 样式表‌
// 表单数据
// application/x-www-form-urlencoded
// multipart/form-data
// JSON 数据
// application/json; charset=UTF-8
// 二进制数据
// application/octet-stream
// image/jpeg‌、‌image/png
// XML 数据
// text/xml application/xml‌
func (x *Request) SetContentType(a string) *Request {
x.SetHeader("Content-Type", a)
return x
}
// SetHeader set the value of the request header
func (x *Request) SetHeader(k, v string) *Request {
if x.headers == nil {
x.headers = make(map[string]string)
}
x.headers[k] = v
return x
}
// SetHeaders set the value of Request Headers
func (x *Request) SetHeaders(headers map[string]string) *Request {
if x.headers == nil {
x.headers = make(map[string]string)
}
for k, v := range headers {
x.headers[k] = v
}
return x
}
// GET send a GET request
func (x *Request) GET(ctx context.Context) *Request {
x.method = http.MethodGet
x.pull(ctx)
return x
}
// DELETE send a DELETE request
func (x *Request) DELETE(ctx context.Context) *Request {
x.method = http.MethodDelete
x.pull(ctx)
return x
}
// POST send a POST request
func (x *Request) POST(ctx context.Context) *Request {
x.method = http.MethodPost
x.push(ctx)
return x
}
// PUT send a PUT request
func (x *Request) PUT(ctx context.Context) *Request {
x.method = http.MethodPut
x.push(ctx)
return x
}
// PATCH send PATCH requests
func (x *Request) PATCH(ctx context.Context) *Request {
x.method = http.MethodPatch
x.push(ctx)
return x
}
// Do a request
func (x *Request) Do(ctx context.Context, method string, data interface{}) *Request {
x.method = method
switch method {
case http.MethodGet:
if data != nil {
if params, ok := data.(map[string]interface{}); ok { //nolint
x.SetParams(params)
} else {
x.err = errors.New("params is not a map[string]interface{}")
return x
}
}
x.pull(ctx)
case http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete:
if data != nil {
x.SetBody(data)
}
x.push(ctx)
default:
x.err = errors.New("unknow method " + method)
}
return x
}
func (x *Request) pull(ctx context.Context) {
val := ""
if len(x.params) > 0 {
values := url.Values{}
for k, v := range x.params {
values.Add(k, fmt.Sprintf("%v", v))
}
val += values.Encode()
}
if val != "" {
if strings.Contains(x.url, "?") {
x.url += "&" + val
} else {
x.url += "?" + val
}
}
x.send(ctx)
}
func (x *Request) push(ctx context.Context) {
if x.reqBodyJSON != nil {
body, err := json.Marshal(x.reqBodyJSON)
if err != nil {
x.err = err
return
}
x.reqBody = body
}
x.send(ctx)
}
func (x *Request) send(ctx context.Context) {
bodyBuf := bytes.NewBuffer(x.reqBody)
x.request, x.err = http.NewRequest(x.method, x.url, bodyBuf)
if x.err != nil {
return
}
if x.reqBody != nil {
if c := ctxutils.CtxGin(ctx); c != nil {
entity.SetCopyApiReq(c, bodyBuf)
}
}
if x.headers != nil {
for k, v := range x.headers {
x.request.Header.Add(k, v)
}
}
x.request.Header.Add(ctxutils.HeaderXRequestIDKey, ctxutils.CtxTraceID(ctx))
if x.timeout < 1 {
x.timeout = defaultTimeout
}
ctx, cancel := context.WithTimeout(ctx, x.timeout)
defer cancel()
x.request = x.request.WithContext(ctx)
if !x.omitLog {
logger.Info("[httpcli] req",
logger.String("method", x.method),
logger.String("url", x.url),
logger.Any("header", x.request.Header),
logger.String("body", bodyBuf.String()),
ctxutils.CtxTraceIDField(ctx))
}
st := time.Now()
if x.retryCount > 0 {
x.err = retryutils.Retry(func() error {
x.pushDo(ctx)
return x.err
},
retryutils.RetryTimes(uint(x.retryCount)),
retryutils.RetryWithLinearBackoff(8*time.Second),
retryutils.Context(ctx),
)
} else {
x.pushDo(ctx)
}
body := x.RespBodyString()
if httpContentType.IsMedia(x.response.Header) {
body = string(getResponseBody(x.Response().BodyBuf(), 16))
}
if !x.omitLog {
logger.Info("[httpcli] rsp",
logger.String("cost", time.Since(st).String()),
logger.Int("statusCode", x.response.StatusCode),
logger.String("status", x.response.Status),
logger.Any("header", x.Response().Header),
logger.String("body", body),
ctxutils.CtxTraceIDField(ctx))
}
}
// If there is sensitive information in the body, you can use WithIgnoreRoutes set the route to ignore logging
func getResponseBody(buf *bytes.Buffer, maxLen int) []byte {
l := buf.Len()
if l == 0 {
return []byte("")
} else if l > maxLen {
l = maxLen
}
body := make([]byte, l)
n, _ := buf.Read(body)
if n == 0 {
return emptyBody
} else if n < maxLen {
if isLastByteNewline(body) {
return body[:n-1]
}
return body[:n]
}
return append(body[:maxLen-len(contentMark)], contentMark...)
}
var (
emptyBody = []byte("")
contentMark = []byte(" ...... ")
)
func isLastByteNewline(data []byte) bool {
if len(data) == 0 {
return false
}
return data[len(data)-1] == '\n'
}
func (x *Request) pushDo(ctx context.Context) {
client := x.cli
if client == nil {
client = &http.Client{Timeout: x.timeout}
}
var rsp *http.Response
rsp, x.err = client.Do(x.request)
if x.err != nil {
x.err = xerror.Wrap(x.err, "httpcli do url["+x.url+"] failed")
return
}
if rsp != nil {
x.response.Status = rsp.Status
x.response.Header = rsp.Header
x.response.StatusCode = rsp.StatusCode
if rsp.Body != nil {
defer rsp.Body.Close()
x.response.body, x.err = io.ReadAll(rsp.Body)
if x.err != nil {
x.err = xerror.Wrap(x.err, "read response body failed")
return
}
if len(x.response.body) > 0 {
if c := ctxutils.CtxGin(ctx); c != nil {
entity.SetCopyApiRsp(c, x.response.BodyBuf())
}
}
}
if rsp.StatusCode != http.StatusOK {
x.err = errors.New(rsp.Status)
return
}
}
}
// ----------------------------------- Response -----------------------------------
func (x *Request) Err() error {
return x.err
}
// RespBodyString returns the body data of the HttpResponse
func (x *Request) RespBodyString() string {
if len(x.response.body) == 0 {
return ""
}
return bytes.NewBuffer(x.response.body).String()
}
func (x *Request) Response() *Response {
return x.response
}
func (x *Response) BodyBuf() *bytes.Buffer {
return bytes.NewBuffer(x.body)
}
func (x *Request) ReadRspBody() ([]byte, error) {
if x.err != nil {
return []byte{}, x.err
}
if x.response.body == nil {
return []byte{}, errors.New("nil")
}
return x.response.body, nil
}
// BindJSON parses the response's body as JSON
func (x *Request) BindJSON(v interface{}) *Request {
if v == nil {
return x
}
if x.err != nil {
return x
}
if err := json.Unmarshal(x.response.body, v); err != nil {
x.err = err
}
return x
}
// ----------------------------------- Request way 2 -----------------------------------
// Option set options.
type Option func(*options)
type options struct {
params map[string]interface{}
headers map[string]string
timeout time.Duration
}
func (o *options) apply(opts ...Option) {
for _, opt := range opts {
opt(o)
}
}
func defaultOptions() *options {
return &options{}
}
// WithParams set params
func WithParams(params map[string]interface{}) Option {
return func(o *options) {
o.params = params
}
}
// WithHeaders set headers
func WithHeaders(headers map[string]string) Option {
return func(o *options) {
o.headers = headers
}
}
// WithTimeout set timeout
func WithTimeout(t time.Duration) Option {
return func(o *options) {
o.timeout = t
}
}
// StdResult standard return data
type StdResult struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data,omitempty"`
}
// StdResult standard return data
type StdResult2 struct {
Code int `json:"code"`
Msg string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
// KV string:interface{}
type KV = map[string]interface{}
package olog package logger
type Config struct { type Config struct {
Format string `yaml:"format" json:"format" mapstructure:"format"` Format string `yaml:"format" json:"format" mapstructure:"format"`
......
package olog package logger
import ( import (
"fmt" "fmt"
......
package olog package logger
import ( import (
"testing" "testing"
......
package olog package logger
import ( import (
"encoding/json" "encoding/json"
......
package olog package logger
import ( import (
"fmt" "fmt"
......
package olog package logger
import ( import (
"strings" "strings"
......
package olog package logger
import ( import (
"strings" "strings"
......
package olog package logger
import ( import (
"fmt" "fmt"
......
package olog package logger
import ( import (
"errors" "errors"
......
// Package sgorm is a library encapsulated on gorm.io/gorm
package sgorm
import (
"reflect"
"time"
"github.com/huandu/xstrings"
"gorm.io/gorm"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/sgorm/dbclose"
)
type DB = gorm.DB
var ErrRecordNotFound = gorm.ErrRecordNotFound
const (
// DBDriverMysql mysql driver
DBDriverMysql = "mysql"
// DBDriverPostgresql postgresql driver
DBDriverPostgresql = "postgresql"
// DBDriverTidb tidb driver
DBDriverTidb = "tidb"
// DBDriverSqlite sqlite driver
DBDriverSqlite = "sqlite"
)
// Model embedded structs, add `gorm: "embedded"` when defining table structs
type Model struct {
ID uint64 `gorm:"column:id;AUTO_INCREMENT;primary_key" json:"id"`
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;index" json:"-"`
}
// Model2 embedded structs, json tag named is snake case
type Model2 struct {
ID uint64 `gorm:"column:id;AUTO_INCREMENT;primary_key" json:"id"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;index" json:"-"`
}
// KV map type
type KV = map[string]interface{}
// GetTableName get table name
func GetTableName(object interface{}) string {
tableName := ""
typeof := reflect.TypeOf(object)
switch typeof.Kind() {
case reflect.Ptr:
tableName = typeof.Elem().Name()
case reflect.Struct:
tableName = typeof.Name()
default:
return tableName
}
return xstrings.ToSnakeCase(tableName)
}
// CloseDB close db
func CloseDB(db *gorm.DB) error {
return dbclose.Close(db)
}
package sgorm
import (
"testing"
"github.com/stretchr/testify/assert"
)
type userExample struct {
Model `gorm:"embedded"`
Name string `gorm:"type:varchar(40);unique_index;not null" json:"name"`
Age int `gorm:"not null" json:"age"`
Gender string `gorm:"type:varchar(10);not null" json:"gender"`
}
func TestGetTableName(t *testing.T) {
name := GetTableName(&userExample{})
assert.NotEmpty(t, name)
name = GetTableName(userExample{})
assert.NotEmpty(t, name)
name = GetTableName("table")
assert.Empty(t, name)
}
// Package dbclose provides a function to close gorm db.
package dbclose
import (
"context"
"database/sql"
"time"
"gorm.io/gorm"
)
// Close close gorm db
func Close(db *gorm.DB) error {
if db == nil {
return nil
}
sqlDB, err := db.DB()
if err != nil {
return err
}
checkInUse(sqlDB, time.Second*5)
return sqlDB.Close()
}
func checkInUse(sqlDB *sql.DB, duration time.Duration) {
ctx, _ := context.WithTimeout(context.Background(), duration) //nolint
for {
select {
case <-time.After(time.Millisecond * 250):
if v := sqlDB.Stats().InUse; v == 0 {
return
}
case <-ctx.Done():
return
}
}
}
package dbclose
import (
"database/sql"
"testing"
"time"
"gorm.io/gorm"
)
func TestCloseDB(t *testing.T) {
sqlDB := new(sql.DB)
checkInUse(sqlDB, time.Millisecond*100)
checkInUse(sqlDB, time.Millisecond*600)
db := new(gorm.DB)
defer func() { recover() }()
_ = Close(db)
}
// Package glog provides a gorm logger implementation based on zap.
package glog
import (
"context"
"runtime"
"strconv"
"strings"
"time"
"go.uber.org/zap"
"gorm.io/gorm/logger"
)
const (
FieldFileLine = "file"
)
type gormLogger struct {
gLog *zap.Logger
requestIDKey string
logLevel logger.LogLevel
}
// NewCustomGormLogger custom gorm logger
func NewCustomGormLogger(l *zap.Logger, requestIDKey string, logLevel logger.LogLevel) logger.Interface {
if l == nil {
l, _ = zap.NewProduction()
}
if requestIDKey == "" {
requestIDKey = "X-Request-ID"
}
if logLevel == 0 {
logLevel = logger.Info
}
return &gormLogger{
gLog: l,
requestIDKey: requestIDKey,
logLevel: logLevel,
}
}
// LogMode log mode
func (l *gormLogger) LogMode(level logger.LogLevel) logger.Interface {
l.logLevel = level
return l
}
// Info print info
func (l *gormLogger) Info(ctx context.Context, msg string, data ...interface{}) {
if l.logLevel >= logger.Info {
msg = strings.ReplaceAll(msg, "%v", "")
l.gLog.Info(msg, zap.Any("data", data), zap.String("line", FileWithLineNum()), requestIDField(ctx, l.requestIDKey))
}
}
// Warn print warn messages
func (l *gormLogger) Warn(ctx context.Context, msg string, data ...interface{}) {
if l.logLevel >= logger.Warn {
msg = strings.ReplaceAll(msg, "%v", "")
l.gLog.Warn(msg, zap.Any("data", data), zap.String("line", FileWithLineNum()), requestIDField(ctx, l.requestIDKey))
}
}
// Error print error messages
func (l *gormLogger) Error(ctx context.Context, msg string, data ...interface{}) {
if l.logLevel >= logger.Error {
msg = strings.ReplaceAll(msg, "%v", "")
l.gLog.Warn(msg, zap.Any("data", data), zap.String("line", FileWithLineNum()), requestIDField(ctx, l.requestIDKey))
}
}
// Trace print sql message
func (l *gormLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {
if l.logLevel <= logger.Silent {
return
}
cost := time.Since(begin).String()
sql, rows := fc()
var rowsField zap.Field
if rows == -1 {
rowsField = zap.String("rows", "-")
} else {
rowsField = zap.Int64("rows", rows)
}
var fileLineField zap.Field
fileLine := FileWithLineNum()
ss := strings.Split(fileLine, "/internal/")
if len(ss) == 2 {
fileLineField = zap.String(FieldFileLine, ss[1])
} else {
fileLineField = zap.String(FieldFileLine, fileLine)
}
sql = clearSql(sql)
if err != nil {
l.gLog.Warn("[gorm]",
zap.Error(err),
zap.String("sql", sql),
rowsField,
zap.String("cost", cost),
fileLineField,
requestIDField(ctx, l.requestIDKey),
)
return
}
if l.logLevel >= logger.Info {
l.gLog.Info("[gorm]",
zap.String("sql", sql),
rowsField,
zap.String("cost", cost),
fileLineField,
requestIDField(ctx, l.requestIDKey),
)
return
}
if l.logLevel >= logger.Warn {
l.gLog.Warn("[gorm]",
zap.String("sql", sql),
rowsField,
zap.String("cost", cost),
fileLineField,
requestIDField(ctx, l.requestIDKey),
)
}
}
func clearSql(sql string) string {
sql = strings.ReplaceAll(sql, "\n", "")
sql = strings.ReplaceAll(sql, "\r", "")
sql = strings.ReplaceAll(sql, "\t", "")
return sql
}
func requestIDField(ctx context.Context, requestIDKey string) zap.Field {
if requestIDKey == "" {
return zap.Skip()
}
var field zap.Field
if requestIDKey != "" {
if v, ok := ctx.Value(requestIDKey).(string); ok {
field = zap.String(requestIDKey, v)
} else {
field = zap.Skip()
}
}
return field
}
func FileWithLineNum() string {
for i := 2; i < 15; i++ {
_, file, line, ok := runtime.Caller(i)
if ok && !strings.Contains(file, "common/odao") && (!strings.Contains(file, "gorm.io/gorm@") || strings.HasSuffix(file, "_test.go")) {
return file + ":" + strconv.FormatInt(int64(line), 10)
}
}
return ""
}
package glog
import (
"context"
"errors"
"testing"
"time"
"github.com/stretchr/testify/assert"
"go.uber.org/zap"
"gorm.io/gorm/logger"
)
func TestNewCustomGormLogger(t *testing.T) {
zapLog, _ := zap.NewDevelopment()
l := NewCustomGormLogger(zapLog, "request_id", logger.Info)
l.LogMode(logger.Info)
ctx := context.WithValue(context.Background(), "request_id", "123")
l.Info(ctx, "info", "foo")
l.Warn(ctx, "warn", "bar")
l.Error(ctx, "error", "foo bar")
l.LogMode(logger.Silent)
l.Trace(ctx, time.Now(), nil, nil)
l.LogMode(logger.Info)
l.Trace(ctx, time.Now(), func() (string, int64) {
return "sql statement", 1
}, nil)
l.Trace(ctx, time.Now(), func() (string, int64) {
return "sql statement", -1
}, nil)
l.Trace(ctx, time.Now(), func() (string, int64) {
return "sql statement", 0
}, logger.ErrRecordNotFound)
l.Trace(ctx, time.Now(), func() (string, int64) {
return "sql statement", 0
}, errors.New("Error 1054: Unknown column 'test_column'"))
l.LogMode(logger.Warn)
l.Trace(ctx, time.Now(), func() (string, int64) {
return "sql statement", 0
}, logger.ErrRecordNotFound)
}
func Test_requestIDField(t *testing.T) {
ctx := context.WithValue(context.Background(), "request_id", "123")
field := requestIDField(ctx, "")
assert.Equal(t, zap.Skip(), field)
field = requestIDField(ctx, "your request id key")
assert.Equal(t, zap.Skip(), field)
field = requestIDField(ctx, "request_id")
assert.Equal(t, zap.String("request_id", "123"), field)
}
// Package mysql provides a gorm driver for mysql.
package mysql
import (
"database/sql"
"log"
"os"
"github.com/uptrace/opentelemetry-go-extra/otelgorm"
mysqlDriver "gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"gorm.io/plugin/dbresolver"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/sgorm/dbclose"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/sgorm/glog"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/xerrors/xerror"
)
// Init mysql
func Init(dsn string, opts ...Option) (*gorm.DB, error) {
o := defaultOptions()
o.apply(opts...)
sqlDB, err := sql.Open("mysql", dsn)
if err != nil {
return nil, xerror.New(err.Error())
}
sqlDB.SetMaxIdleConns(o.maxIdleConns) // set the maximum number of connections in the idle connection pool
sqlDB.SetMaxOpenConns(o.maxOpenConns) // set the maximum number of open database connections
sqlDB.SetConnMaxLifetime(o.connMaxLifetime) // set the maximum time a connection can be reused
db, err := gorm.Open(mysqlDriver.New(mysqlDriver.Config{Conn: sqlDB}), gormConfig(o))
if err != nil {
return nil, xerror.New(err.Error())
}
db.Set("gorm:table_options", "CHARSET=utf8mb4") // automatic appending of table suffixes when creating tables
// register trace plugin
if o.enableTrace {
err = db.Use(otelgorm.NewPlugin())
if err != nil {
return nil, xerror.Errorf("using gorm opentelemetry, err: %v", err)
}
}
if err = sqlDB.Ping(); err != nil {
return nil, xerror.Errorf("ping db failed, err: %v", err)
}
// register read-write separation plugin
if len(o.slavesDsn) > 0 {
err = db.Use(rwSeparationPlugin(o))
if err != nil {
return nil, xerror.New(err.Error())
}
}
// register plugins
for _, plugin := range o.plugins {
err = db.Use(plugin)
if err != nil {
return nil, xerror.New(err.Error())
}
}
return db, nil
}
// InitTidb init tidb
func InitTidb(dsn string, opts ...Option) (*gorm.DB, error) {
return Init(dsn, opts...)
}
// gorm setting
func gormConfig(o *options) *gorm.Config {
config := &gorm.Config{
// disable foreign key constraints, not recommended for production environments
DisableForeignKeyConstraintWhenMigrating: o.disableForeignKey,
// removing the plural of an epithet
NamingStrategy: schema.NamingStrategy{SingularTable: true},
}
// print SQL
if o.isLog {
if o.gLog == nil {
config.Logger = logger.Default.LogMode(o.logLevel)
} else {
config.Logger = glog.NewCustomGormLogger(o.gLog, o.requestIDKey, o.logLevel)
}
} else {
config.Logger = logger.Default.LogMode(logger.Silent)
}
// print only slow queries
if o.slowThreshold > 0 {
config.Logger = logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // use the standard output asWriter
logger.Config{
SlowThreshold: o.slowThreshold,
Colorful: true,
LogLevel: logger.Warn, // set the logging level, only above the specified level will output the slow query log
},
)
}
return config
}
func rwSeparationPlugin(o *options) gorm.Plugin {
slaves := []gorm.Dialector{}
for _, dsn := range o.slavesDsn {
slaves = append(slaves, mysqlDriver.New(mysqlDriver.Config{
DSN: dsn,
}))
}
masters := []gorm.Dialector{}
for _, dsn := range o.mastersDsn {
masters = append(masters, mysqlDriver.New(mysqlDriver.Config{
DSN: dsn,
}))
}
return dbresolver.Register(dbresolver.Config{
Sources: masters,
Replicas: slaves,
Policy: dbresolver.RandomPolicy{},
})
}
// Close close gorm db
func Close(db *gorm.DB) error {
return dbclose.Close(db)
}
package mysql
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestInitMysql(t *testing.T) {
dsn := "root:123456@(192.168.3.37:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
db, err := Init(dsn, WithEnableTrace())
if err != nil {
// ignore test error about not being able to connect to real mysql
t.Logf(fmt.Sprintf("connect to mysql failed, err=%v, dsn=%s", err, dsn))
return
}
defer Close(db)
t.Logf("%+v", db.Name())
}
func TestInitTidb(t *testing.T) {
dsn := "root:123456@(192.168.3.37:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
db, err := InitTidb(dsn)
if err != nil {
// ignore test error about not being able to connect to real tidb
t.Logf(fmt.Sprintf("connect to mysql failed, err=%v, dsn=%s", err, dsn))
return
}
defer Close(db)
t.Logf("%+v", db.Name())
}
func Test_gormConfig(t *testing.T) {
o := defaultOptions()
o.apply(
WithLogging(nil),
WithLogging(nil, 4),
WithSlowThreshold(time.Millisecond*100),
WithEnableTrace(),
WithMaxIdleConns(5),
WithMaxOpenConns(50),
WithConnMaxLifetime(time.Minute*3),
WithEnableForeignKey(),
WithLogRequestIDKey("request_id"),
WithRWSeparation([]string{
"root:123456@(192.168.3.37:3306)/slave1",
"root:123456@(192.168.3.37:3306)/slave2"},
"root:123456@(192.168.3.37:3306)/master"),
WithGormPlugin(nil),
)
c := gormConfig(o)
assert.NotNil(t, c)
err := rwSeparationPlugin(o)
assert.NotNil(t, err)
}
package mysql
import (
"time"
"go.uber.org/zap"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// Option set the mysql options.
type Option func(*options)
type options struct {
isLog bool
slowThreshold time.Duration
maxIdleConns int
maxOpenConns int
connMaxLifetime time.Duration
disableForeignKey bool
enableTrace bool
preparedStatement bool
requestIDKey string
gLog *zap.Logger
logLevel logger.LogLevel
slavesDsn []string
mastersDsn []string
plugins []gorm.Plugin
}
func (o *options) apply(opts ...Option) {
for _, opt := range opts {
opt(o)
}
}
// default settings
func defaultOptions() *options {
return &options{
isLog: false, // whether to output logs, default off
slowThreshold: time.Duration(0), // if greater than 0, only print logs that are longer than the threshold, higher priority than isLog
maxIdleConns: 3, // set the maximum number of connections in the idle connection pool
maxOpenConns: 50, // set the maximum number of open database connections
connMaxLifetime: 30 * time.Minute, // sets the maximum amount of time a connection can be reused
disableForeignKey: true, // disables the use of foreign keys, true is recommended for production environments, enabled by default
enableTrace: false, // whether to enable link tracing, default is off
requestIDKey: "", // request id key
gLog: nil, // custom logger
logLevel: logger.Info, // default logLevel
}
}
// WithLogging set log sql, If l=nil, the gorm log library will be used
func WithLogging(l *zap.Logger, level ...logger.LogLevel) Option {
return func(o *options) {
o.isLog = true
o.gLog = l
if len(level) > 0 {
o.logLevel = level[0]
}
o.logLevel = logger.Info
}
}
// WithSlowThreshold Set sql values greater than the threshold
func WithSlowThreshold(d time.Duration) Option {
return func(o *options) {
o.slowThreshold = d
}
}
// WithMaxIdleConns set max idle conns
func WithMaxIdleConns(size int) Option {
return func(o *options) {
o.maxIdleConns = size
}
}
// WithMaxOpenConns set max open conns
func WithMaxOpenConns(size int) Option {
return func(o *options) {
o.maxOpenConns = size
}
}
// WithConnMaxLifetime set conn max lifetime
func WithConnMaxLifetime(t time.Duration) Option {
return func(o *options) {
o.connMaxLifetime = t
}
}
// WithEnableForeignKey use foreign keys
func WithEnableForeignKey() Option {
return func(o *options) {
o.disableForeignKey = false
}
}
// WithPrepareStatement use prepared statement
func WithPrepareStatement() Option {
return func(o *options) {
o.preparedStatement = true
}
}
// WithEnableTrace use trace
func WithEnableTrace() Option {
return func(o *options) {
o.enableTrace = true
}
}
// WithLogRequestIDKey log request id
func WithLogRequestIDKey(key string) Option {
return func(o *options) {
if key == "" {
key = "request_id"
}
o.requestIDKey = key
}
}
// WithRWSeparation setting read-write separation
func WithRWSeparation(slavesDsn []string, mastersDsn ...string) Option {
return func(o *options) {
o.slavesDsn = slavesDsn
o.mastersDsn = mastersDsn
}
}
// WithGormPlugin setting gorm plugin
func WithGormPlugin(plugins ...gorm.Plugin) Option {
return func(o *options) {
o.plugins = plugins
}
}
package postgresql
import (
"time"
"go.uber.org/zap"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// Option set the mysql options.
type Option func(*options)
type options struct {
isLog bool
slowThreshold time.Duration
maxIdleConns int
maxOpenConns int
connMaxLifetime time.Duration
disableForeignKey bool
enableTrace bool
requestIDKey string
gLog *zap.Logger
logLevel logger.LogLevel
plugins []gorm.Plugin
}
func (o *options) apply(opts ...Option) {
for _, opt := range opts {
opt(o)
}
}
// default settings
func defaultOptions() *options {
return &options{
isLog: false, // whether to output logs, default off
slowThreshold: time.Duration(0), // if greater than 0, only print logs that are longer than the threshold, higher priority than isLog
maxIdleConns: 3, // set the maximum number of connections in the idle connection pool
maxOpenConns: 50, // set the maximum number of open database connections
connMaxLifetime: 30 * time.Minute, // sets the maximum amount of time a connection can be reused
disableForeignKey: true, // disables the use of foreign keys, true is recommended for production environments, enabled by default
enableTrace: false, // whether to enable link tracing, default is off
requestIDKey: "request_id", // request id key
gLog: nil, // custom logger
logLevel: logger.Info, // default logLevel
}
}
// WithLogging set log sql, If l=nil, the gorm log library will be used
func WithLogging(l *zap.Logger, level ...logger.LogLevel) Option {
return func(o *options) {
o.isLog = true
o.gLog = l
if len(level) > 0 {
o.logLevel = level[0]
}
o.logLevel = logger.Info
}
}
// WithSlowThreshold Set sql values greater than the threshold
func WithSlowThreshold(d time.Duration) Option {
return func(o *options) {
o.slowThreshold = d
}
}
// WithMaxIdleConns set max idle conns
func WithMaxIdleConns(size int) Option {
return func(o *options) {
o.maxIdleConns = size
}
}
// WithMaxOpenConns set max open conns
func WithMaxOpenConns(size int) Option {
return func(o *options) {
o.maxOpenConns = size
}
}
// WithConnMaxLifetime set conn max lifetime
func WithConnMaxLifetime(t time.Duration) Option {
return func(o *options) {
o.connMaxLifetime = t
}
}
// WithEnableForeignKey use foreign keys
func WithEnableForeignKey() Option {
return func(o *options) {
o.disableForeignKey = false
}
}
// WithEnableTrace use trace
func WithEnableTrace() Option {
return func(o *options) {
o.enableTrace = true
}
}
// WithLogRequestIDKey log request id
func WithLogRequestIDKey(key string) Option {
return func(o *options) {
if key == "" {
key = "request_id"
}
o.requestIDKey = key
}
}
// WithGormPlugin setting gorm plugin
func WithGormPlugin(plugins ...gorm.Plugin) Option {
return func(o *options) {
o.plugins = plugins
}
}
// Package postgresql provides a gorm driver for postgresql.
package postgresql
import (
"fmt"
"log"
"os"
"github.com/uptrace/opentelemetry-go-extra/otelgorm"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/sgorm/dbclose"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/sgorm/glog"
)
// Init postgresql
func Init(dsn string, opts ...Option) (*gorm.DB, error) {
o := defaultOptions()
o.apply(opts...)
db, err := gorm.Open(postgres.Open(dsn), gormConfig(o))
if err != nil {
return nil, err
}
// register trace plugin
if o.enableTrace {
err = db.Use(otelgorm.NewPlugin())
if err != nil {
return nil, fmt.Errorf("using gorm opentelemetry, err: %v", err)
}
}
// register plugins
for _, plugin := range o.plugins {
err = db.Use(plugin)
if err != nil {
return nil, err
}
}
return db, nil
}
// gorm setting
func gormConfig(o *options) *gorm.Config {
config := &gorm.Config{
// disable foreign key constraints, not recommended for production environments
DisableForeignKeyConstraintWhenMigrating: o.disableForeignKey,
// removing the plural of an epithet
NamingStrategy: schema.NamingStrategy{SingularTable: true},
}
// print SQL
if o.isLog {
if o.gLog == nil {
config.Logger = logger.Default.LogMode(o.logLevel)
} else {
config.Logger = glog.NewCustomGormLogger(o.gLog, o.requestIDKey, o.logLevel)
}
} else {
config.Logger = logger.Default.LogMode(logger.Silent)
}
// print only slow queries
if o.slowThreshold > 0 {
config.Logger = logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // use the standard output asWriter
logger.Config{
SlowThreshold: o.slowThreshold,
Colorful: true,
LogLevel: logger.Warn, // set the logging level, only above the specified level will output the slow query log
},
)
}
return config
}
// Close close gorm db
func Close(db *gorm.DB) error {
return dbclose.Close(db)
}
package postgresql
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestInit(t *testing.T) {
dsn := "host=192.168.3.37 user=root password=123456 dbname=account port=5432 sslmode=disable TimeZone=Asia/Shanghai"
db, err := Init(dsn, WithEnableTrace())
if err != nil {
// ignore test error about not being able to connect to real postgresql
t.Logf(fmt.Sprintf("connect to postgresql failed, err=%v, dsn=%s", err, dsn))
return
}
defer Close(db)
t.Logf("%+v", db.Name())
}
func Test_gormConfig(t *testing.T) {
o := defaultOptions()
o.apply(
WithLogging(nil),
WithLogging(nil, 4),
WithSlowThreshold(time.Millisecond*100),
WithEnableTrace(),
WithMaxIdleConns(5),
WithMaxOpenConns(50),
WithConnMaxLifetime(time.Minute*3),
WithEnableForeignKey(),
WithLogRequestIDKey("request_id"),
WithGormPlugin(nil),
)
c := gormConfig(o)
assert.NotNil(t, c)
}
package query
import "strings"
var defaultMaxSize = 1000
// SetMaxSize change the default maximum number of pages per page
func SetMaxSize(max int) {
if max < 10 {
max = 10
}
defaultMaxSize = max
}
// Page info
type Page struct {
pageIndex int // page number, starting from page 0
pageSize int // number per page
sort string // sort fields, default is id backwards, you can add - sign before the field to indicate reverse order, no - sign to indicate ascending order, multiple fields separated by comma
}
// Page get page value
func (p *Page) Page() int {
return p.pageIndex
}
// Limit number per page
func (p *Page) Limit() int {
return p.pageSize
}
// Size number per page
func (p *Page) Size() int {
return p.pageSize
}
// Sort get sort field
func (p *Page) Sort() string {
return p.sort
}
// Offset get offset value
func (p *Page) Offset() int {
return p.pageIndex * p.pageSize
}
// DefaultPage default page, number 20 per page, sorted by id backwards
func DefaultPage(page int) *Page {
if page < 0 {
page = 0
}
return &Page{
pageIndex: page,
pageSize: 20,
sort: "id DESC",
}
}
// NewPage custom page, starting from page 0.
// the parameter columnNames indicates a sort field, if empty means id descending,
// if there are multiple column names, separated by a comma,
// a '-' sign in front of each column name indicates descending order, otherwise ascending order.
func NewPage(pageIndex int, pageSize int, columnNames string) *Page {
if pageIndex < 0 {
pageIndex = 1
}
if pageSize > defaultMaxSize || pageSize < 1 {
pageSize = defaultMaxSize
}
return &Page{
pageIndex: pageIndex - 1,
pageSize: pageSize,
sort: getSort(columnNames),
}
}
// convert to mysql sort, each column name preceded by a '-' sign, indicating descending order, otherwise ascending order, example:
//
// columnNames="name" means sort by name in ascending order,
// columnNames="-name" means sort by name descending,
// columnNames="name,age" means sort by name in ascending order, otherwise sort by age in ascending order,
// columnNames="-name,-age" means sort by name descending before sorting by age descending.
func getSort(columnNames string) string {
columnNames = strings.TrimSpace(columnNames)
if columnNames == "" {
return "id DESC"
}
names := strings.Split(columnNames, ",")
strs := make([]string, 0, len(names))
for _, name := range names {
if name[0] == '-' && len(name) > 1 {
strs = append(strs, name[1:]+" DESC")
} else {
strs = append(strs, name+" ASC")
}
}
return strings.Join(strs, ", ")
}
// Package query is a library of custom condition queries, support for complex conditional paging queries.
package query
import (
"fmt"
"strings"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/xerrors/xerror"
"gitlab.wanzhuangkj.com/tush/opkg/xutils/xtime"
)
const (
// Eq equal
Eq = "eq"
// Neq not equal
Neq = "neq"
// Gt greater than
Gt = "gt"
// Gte greater than or equal
Gte = "gte"
// Lt less than
Lt = "lt"
// Lte less than or equal
Lte = "lte"
// Like fuzzy lookup
Like = "like"
// In include
In = "in"
// AND logic and
AND string = "and"
// OR logic or
OR string = "or"
)
var expMap = map[string]string{
Eq: " = ",
Neq: " <> ",
Gt: " > ",
Gte: " >= ",
Lt: " < ",
Lte: " <= ",
Like: " LIKE ",
In: " IN ",
"=": " = ",
"!=": " <> ",
">": " > ",
">=": " >= ",
"<": " < ",
"<=": " <= ",
}
var logicMap = map[string]string{
AND: " AND ",
OR: " OR ",
"&": " AND ",
"&&": " AND ",
"|": " OR ",
"||": " OR ",
"AND": " AND ",
"OR": " OR ",
}
type IPagination interface {
GetOffset() int
GetLimit() int
GetPageIndex() int
GetPageSize() int
}
type Pagination struct {
PageIndex int `json:"pageIndex" form:"pageIndex" binding:"" example:"1"` // 1-based
PageSize int `json:"pageSize" form:"pageSize" binding:"" example:"10"` // 1-based
Sort string `json:"sort,omitempty" form:"sort" binding:"" example:"-id" query:"type:order"` //排序字段,负号表示降序,多个字段排序中间用,(英文逗号)分隔
Unscope bool `json:"-" form:"-" query:"-"`
}
func (x Pagination) Unscoped() bool {
return x.Unscope
}
type CreatedAtPeriod struct {
CreatedAtBegin xtime.DateTime `json:"createdAtBegin" form:"createdAtBegin" gorm:"column:created_at;type:datetime;comment:创建时间" query:"type:gte;column:created_at" example:"2025-01-01 12:20:26"` //开始日期起始 (>=)
CreatedAtEnd xtime.DateTime `json:"createdAtEnd" form:"createdAtEnd" gorm:"column:created_at;type:datetime;comment:创建时间" query:"type:lt;column:created_at" example:"2025-02-01 12:20:26"` //结束日期截止 (<)
}
func (x Pagination) GetOffset() int {
if x.PageIndex <= 0 {
x.PageIndex = 1
}
return (x.PageIndex - 1) * x.GetPageSize()
}
func (x Pagination) GetPageSize() int {
if x.PageSize <= 0 {
x.PageSize = 10
}
return x.PageSize
}
func (x Pagination) GetPageIndex() int {
if x.PageIndex <= 0 {
x.PageIndex = 1
}
return x.PageIndex
}
func (x Pagination) GetLimit() int {
return x.GetPageSize()
}
func (x Pagination) GetSort() string {
if x.Sort == "" {
x.Sort = "id desc"
}
return x.Sort
}
// Params query parameters
type Params struct {
PageIndex int `json:"pageIndex" form:"pageIndex" binding:"gte=1" validate:"required" example:"1"` // 1-based
PageSize int `json:"pageSize" form:"pageSize" binding:"gte=1" validate:"required" example:"10"` // 1-based
Sort string `json:"sort,omitempty" form:"sort" binding:"" validate:"required" example:"id"`
Columns []Column `json:"columns,omitempty" form:"columns"` // not required
}
// Column query info
type Column struct {
Name string `json:"name" form:"name" validate:"required" example:"id"` // column name eg id
Exp string `json:"exp" form:"exp" validate:"required" example:"="` // expressions, default value is "=", support =, !=, >, >=, <, <=, like, in
Value interface{} `json:"value" form:"value" validate:"required"` // column value eg 102
Logic string `json:"logic" form:"logic" example:"and"` // logical type, defaults to and when the value is null, with &(and), ||(or)
}
func (c *Column) checkValid() error {
if c.Name == "" {
return xerror.New("field 'name' cannot be empty")
}
if c.Value == nil {
return xerror.New("field 'value' cannot be nil")
}
return nil
}
// converting ExpType to sql expressions and LogicType to sql using characters
func (c *Column) convert() error {
if c.Exp == "" {
c.Exp = Eq
}
if v, ok := expMap[strings.ToLower(c.Exp)]; ok { //nolint
c.Exp = v
if c.Exp == " LIKE " {
c.Value = fmt.Sprintf("%%%v%%", c.Value)
}
if c.Exp == " IN " {
val, ok := c.Value.(string)
if !ok {
return xerror.Newf("invalid value type '%s'", c.Value)
}
iVal := []interface{}{}
ss := strings.Split(val, ",")
for _, s := range ss {
iVal = append(iVal, s)
}
c.Value = iVal
}
} else {
return xerror.Newf("unknown exp type '%s'", c.Exp)
}
if c.Logic == "" {
c.Logic = AND
}
if v, ok := logicMap[strings.ToLower(c.Logic)]; ok { //nolint
c.Logic = v
} else {
return xerror.Newf("unknown logic type '%s'", c.Logic)
}
return nil
}
// ConvertToPage converted to page
func (p *Params) ConvertToPage() (order string, limit int, offset int) { //nolint
page := NewPage(p.PageIndex, p.PageSize, p.Sort)
order = page.sort
limit = page.pageSize
offset = page.pageIndex * page.pageSize
return //nolint
}
// ConvertToGormConditions conversion to gorm-compliant parameters based on the Columns parameter
// ignore the logical type of the last column, whether it is a one-column or multi-column query
func (p *Params) ConvertToGormConditions() (string, []interface{}, error) {
str := ""
args := []interface{}{}
l := len(p.Columns)
if l == 0 {
return "", nil, nil
}
isUseIN := true
if l == 1 {
isUseIN = false
}
field := p.Columns[0].Name
for i, column := range p.Columns {
if err := column.checkValid(); err != nil {
return "", nil, err
}
err := column.convert()
if err != nil {
return "", nil, err
}
symbol := "?"
if column.Exp == " IN " {
symbol = "(?)"
}
if i == l-1 { // ignore the logical type of the last column
str += column.Name + column.Exp + symbol
} else {
str += column.Name + column.Exp + symbol + column.Logic
}
args = append(args, column.Value)
// when multiple columns are the same, determine whether the use of IN
if isUseIN {
if field != column.Name {
isUseIN = false
continue
}
if column.Exp != expMap[Eq] {
isUseIN = false
}
}
}
if isUseIN {
str = field + " IN (?)"
args = []interface{}{args}
}
return str, args, nil
}
// Conditions query conditions
type Conditions struct {
Columns []Column `json:"columns" form:"columns" binding:"min=1"` // columns info
Sort string `json:"sort,omitempty" form:"sort" binding:"" validate:"required" example:"id"`
}
// CheckValid check valid
func (c *Conditions) CheckValid() error {
if len(c.Columns) == 0 {
return xerror.New("field 'columns' cannot be empty")
}
for _, column := range c.Columns {
err := column.checkValid()
if err != nil {
return err
}
if column.Exp != "" {
if _, ok := expMap[column.Exp]; !ok {
return xerror.Newf("unknown exp type '%s'", column.Exp)
}
}
if column.Logic != "" {
if _, ok := logicMap[column.Logic]; !ok {
return xerror.Newf("unknown logic type '%s'", column.Logic)
}
}
}
return nil
}
// ConvertToGorm conversion to gorm-compliant parameters based on the Columns parameter
// ignore the logical type of the last column, whether it is a one-column or multi-column query
func (c *Conditions) ConvertToGorm() (string, []interface{}, error) {
p := &Params{Columns: c.Columns}
return p.ConvertToGormConditions()
}
package query
import (
"reflect"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestPage(t *testing.T) {
page := DefaultPage(-1)
t.Log(page.Page(), page.Limit(), page.Sort(), page.Offset())
SetMaxSize(1)
page = NewPage(-1, 100, "id")
t.Log(page.Page(), page.Limit(), page.Sort(), page.Offset())
}
func TestParams_ConvertToPage(t *testing.T) {
p := &Params{
PageIndex: 1,
PageSize: 50,
Sort: "age,-name",
}
order, limit, offset := p.ConvertToPage()
t.Logf("order=%s, limit=%d, offset=%d", order, limit, offset)
}
func TestParams_ConvertToGormConditions(t *testing.T) {
type args struct {
columns []Column
}
tests := []struct {
name string
args args
want string
want1 []interface{}
wantErr bool
}{
// --------------------------- only 1 column query ------------------------------
{
name: "1 column eq",
args: args{
columns: []Column{
{
Name: "name",
Value: "ZhangSan",
},
},
},
want: "name = ?",
want1: []interface{}{"ZhangSan"},
wantErr: false,
},
{
name: "1 column neq",
args: args{
columns: []Column{
{
Name: "name",
Value: "ZhangSan",
//Exp: "neq",
Exp: "!=",
},
},
},
want: "name <> ?",
want1: []interface{}{"ZhangSan"},
wantErr: false,
},
{
name: "1 column gt",
args: args{
columns: []Column{
{
Name: "age",
Value: 20,
//Exp: Gt,
Exp: ">",
},
},
},
want: "age > ?",
want1: []interface{}{20},
wantErr: false,
},
{
name: "1 column gte",
args: args{
columns: []Column{
{
Name: "age",
Value: 20,
//Exp: Gte,
Exp: ">=",
},
},
},
want: "age >= ?",
want1: []interface{}{20},
wantErr: false,
},
{
name: "1 column lt",
args: args{
columns: []Column{
{
Name: "age",
Value: 20,
//Exp: Lt,
Exp: "<",
},
},
},
want: "age < ?",
want1: []interface{}{20},
wantErr: false,
},
{
name: "1 column lte",
args: args{
columns: []Column{
{
Name: "age",
Value: 20,
//Exp: Lte,
Exp: "<=",
},
},
},
want: "age <= ?",
want1: []interface{}{20},
wantErr: false,
},
{
name: "1 column like",
args: args{
columns: []Column{
{
Name: "name",
Value: "Li",
Exp: Like,
},
},
},
want: "name LIKE ?",
want1: []interface{}{"%Li%"},
wantErr: false,
},
{
name: "1 column IN",
args: args{
columns: []Column{
{
Name: "name",
Value: "ab,cd,ef",
Exp: In,
},
},
},
want: "name IN (?)",
want1: []interface{}{[]interface{}{"ab", "cd", "ef"}},
wantErr: false,
},
// --------------------------- query 2 columns ------------------------------
{
name: "2 columns eq and",
args: args{
columns: []Column{
{
Name: "name",
Value: "ZhangSan",
},
{
Name: "gender",
Value: "male",
},
},
},
want: "name = ? AND gender = ?",
want1: []interface{}{"ZhangSan", "male"},
wantErr: false,
},
{
name: "2 columns neq and",
args: args{
columns: []Column{
{
Name: "name",
Value: "ZhangSan",
//Exp: Neq,
Exp: "!=",
},
{
Name: "name",
Value: "LiSi",
//Exp: Neq,
Exp: "!=",
},
},
},
want: "name <> ? AND name <> ?",
want1: []interface{}{"ZhangSan", "LiSi"},
wantErr: false,
},
{
name: "2 columns gt and",
args: args{
columns: []Column{
{
Name: "gender",
Value: "male",
},
{
Name: "age",
Value: 20,
//Exp: Gt,
Exp: ">",
},
},
},
want: "gender = ? AND age > ?",
want1: []interface{}{"male", 20},
wantErr: false,
},
{
name: "2 columns gte and",
args: args{
columns: []Column{
{
Name: "gender",
Value: "male",
},
{
Name: "age",
Value: 20,
//Exp: Gte,
Exp: ">=",
},
},
},
want: "gender = ? AND age >= ?",
want1: []interface{}{"male", 20},
wantErr: false,
},
{
name: "2 columns lt and",
args: args{
columns: []Column{
{
Name: "gender",
Value: "female",
},
{
Name: "age",
Value: 20,
//Exp: Lt,
Exp: "<",
},
},
},
want: "gender = ? AND age < ?",
want1: []interface{}{"female", 20},
wantErr: false,
},
{
name: "2 columns lte and",
args: args{
columns: []Column{
{
Name: "gender",
Value: "female",
},
{
Name: "age",
Value: 20,
//Exp: Lte,
Exp: "<=",
},
},
},
want: "gender = ? AND age <= ?",
want1: []interface{}{"female", 20},
wantErr: false,
},
{
name: "2 columns range and",
args: args{
columns: []Column{
{
Name: "age",
Value: 10,
//Exp: Gte,
Exp: ">=",
},
{
Name: "age",
Value: 20,
//Exp: Lte,
Exp: "<=",
},
},
},
want: "age >= ? AND age <= ?",
want1: []interface{}{10, 20},
wantErr: false,
},
{
name: "2 columns eq or",
args: args{
columns: []Column{
{
Name: "name",
Value: "LiSi",
//Logic: OR,
Logic: "||",
},
{
Name: "gender",
Value: "female",
},
},
},
want: "name = ? OR gender = ?",
want1: []interface{}{"LiSi", "female"},
wantErr: false,
},
{
name: "2 columns neq or",
args: args{
columns: []Column{
{
Name: "name",
Value: "LiSi",
//Logic: OR,
Logic: "||",
},
{
Name: "gender",
Value: "male",
//Exp: Neq,
Exp: "!=",
},
},
},
want: "name = ? OR gender <> ?",
want1: []interface{}{"LiSi", "male"},
wantErr: false,
},
{
name: "2 columns eq and in",
args: args{
columns: []Column{
{
Name: "gender",
Value: "male",
//Logic: "&",
},
{
Name: "name",
Value: "LiSi,ZhangSan,WangWu",
Exp: In,
},
},
},
want: "gender = ? AND name IN (?)",
want1: []interface{}{"male", []interface{}{"LiSi", "ZhangSan", "WangWu"}},
wantErr: false,
},
// ------------------------------ IN -------------------------------------------------
{
name: "3 columns eq and",
args: args{
columns: []Column{
{
Name: "name",
Value: "LiSi",
},
{
Name: "name",
Value: "ZhangSan",
},
{
Name: "name",
Value: "WangWu",
},
},
},
want: "name IN (?)",
want1: []interface{}{[]interface{}{"LiSi", "ZhangSan", "WangWu"}},
wantErr: false,
},
// ---------------------------- error ----------------------------------------------
{
name: "exp type err",
args: args{
columns: []Column{
{
Name: "gender",
Value: "male",
Exp: "xxxxxx",
},
},
},
want: "",
want1: nil,
wantErr: true,
},
{
name: "logic type err",
args: args{
columns: []Column{
{
Name: "gender",
Value: "male",
Logic: "xxxxxx",
},
},
},
want: "",
want1: nil,
wantErr: true,
},
{
name: "empty",
args: args{
columns: nil,
},
want: "",
want1: nil,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
params := &Params{
Columns: tt.args.columns,
}
got, got1, err := params.ConvertToGormConditions()
if (err != nil) != tt.wantErr {
t.Errorf("ConvertToGormConditions() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("ConvertToGormConditions() got = %v, want %v", got, tt.want)
}
if !reflect.DeepEqual(got1, tt.want1) {
t.Errorf("ConvertToGormConditions() got1 = %v, want %v", got1, tt.want1)
}
got = strings.Replace(got, "?", "%v", -1)
t.Logf(got, got1...)
})
}
}
func TestConditions_ConvertToGorm(t *testing.T) {
c := Conditions{
Columns: []Column{
{
Name: "name",
Value: "ZhangSan",
},
{
Name: "gender",
Value: "male",
},
}}
str, values, err := c.ConvertToGorm()
if err != nil {
t.Error(err)
}
assert.Equal(t, "name = ? AND gender = ?", str)
assert.Equal(t, len(values), 2)
}
func TestConditions_checkValid(t *testing.T) {
// empty error
c := Conditions{}
err := c.CheckValid()
assert.Error(t, err)
// value is empty error
c = Conditions{
Columns: []Column{
{
Name: "foo",
Value: nil,
},
},
}
err = c.CheckValid()
assert.Error(t, err)
// exp error
c = Conditions{
Columns: []Column{
{
Name: "foo",
Value: "bar",
Exp: "unknown-exp",
},
},
}
err = c.CheckValid()
assert.Error(t, err)
// logic error
c = Conditions{
Columns: []Column{
{
Name: "foo",
Value: "bar",
Logic: "unknown-logic",
},
},
}
err = c.CheckValid()
assert.Error(t, err)
// success
c = Conditions{
Columns: []Column{
{
Name: "name",
Value: "ZhangSan",
},
{
Name: "gender",
Value: "male",
},
}}
err = c.CheckValid()
assert.NoError(t, err)
}
package sqlite
import (
"time"
"go.uber.org/zap"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// Option set the mysql options.
type Option func(*options)
type options struct {
isLog bool
slowThreshold time.Duration
maxIdleConns int
maxOpenConns int
connMaxLifetime time.Duration
disableForeignKey bool
enableTrace bool
requestIDKey string
gLog *zap.Logger
logLevel logger.LogLevel
plugins []gorm.Plugin
}
func (o *options) apply(opts ...Option) {
for _, opt := range opts {
opt(o)
}
}
// default settings
func defaultOptions() *options {
return &options{
isLog: false, // whether to output logs, default off
slowThreshold: time.Duration(0), // if greater than 0, only print logs that are longer than the threshold, higher priority than isLog
maxIdleConns: 3, // set the maximum number of connections in the idle connection pool
maxOpenConns: 50, // set the maximum number of open database connections
connMaxLifetime: 30 * time.Minute, // sets the maximum amount of time a connection can be reused
disableForeignKey: true, // disables the use of foreign keys, true is recommended for production environments, enabled by default
enableTrace: false, // whether to enable link tracing, default is off
requestIDKey: "", // request id key
gLog: nil, // custom logger
logLevel: logger.Info, // default logLevel
}
}
// WithLogging set log sql, If l=nil, the gorm log library will be used
func WithLogging(l *zap.Logger, level ...logger.LogLevel) Option {
return func(o *options) {
o.isLog = true
o.gLog = l
if len(level) > 0 {
o.logLevel = level[0]
}
o.logLevel = logger.Info
}
}
// WithSlowThreshold Set sql values greater than the threshold
func WithSlowThreshold(d time.Duration) Option {
return func(o *options) {
o.slowThreshold = d
}
}
// WithMaxIdleConns set max idle conns
func WithMaxIdleConns(size int) Option {
return func(o *options) {
o.maxIdleConns = size
}
}
// WithMaxOpenConns set max open conns
func WithMaxOpenConns(size int) Option {
return func(o *options) {
o.maxOpenConns = size
}
}
// WithConnMaxLifetime set conn max lifetime
func WithConnMaxLifetime(t time.Duration) Option {
return func(o *options) {
o.connMaxLifetime = t
}
}
// WithEnableForeignKey use foreign keys
func WithEnableForeignKey() Option {
return func(o *options) {
o.disableForeignKey = false
}
}
// WithEnableTrace use trace
func WithEnableTrace() Option {
return func(o *options) {
o.enableTrace = true
}
}
// WithLogRequestIDKey log request id
func WithLogRequestIDKey(key string) Option {
return func(o *options) {
if key == "" {
key = "request_id"
}
o.requestIDKey = key
}
}
// WithGormPlugin setting gorm plugin
func WithGormPlugin(plugins ...gorm.Plugin) Option {
return func(o *options) {
o.plugins = plugins
}
}
// Package sqlite provides a gorm driver for sqlite.
package sqlite
import (
"fmt"
"log"
"os"
"github.com/uptrace/opentelemetry-go-extra/otelgorm"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/sgorm/dbclose"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/sgorm/glog"
)
// Init sqlite
func Init(dbFile string, opts ...Option) (*gorm.DB, error) {
o := defaultOptions()
o.apply(opts...)
dsn := fmt.Sprintf("%s?_journal=WAL&_vacuum=incremental", dbFile)
db, err := gorm.Open(sqlite.Open(dsn), gormConfig(o))
if err != nil {
return nil, err
}
db.Set("gorm:auto_increment", true)
// register trace plugin
if o.enableTrace {
err = db.Use(otelgorm.NewPlugin())
if err != nil {
return nil, fmt.Errorf("using gorm opentelemetry, err: %v", err)
}
}
// register plugins
for _, plugin := range o.plugins {
err = db.Use(plugin)
if err != nil {
return nil, err
}
}
return db, nil
}
// gorm setting
func gormConfig(o *options) *gorm.Config {
config := &gorm.Config{
// disable foreign key constraints, not recommended for production environments
DisableForeignKeyConstraintWhenMigrating: o.disableForeignKey,
// removing the plural of an epithet
NamingStrategy: schema.NamingStrategy{SingularTable: true},
}
// print SQL
if o.isLog {
if o.gLog == nil {
config.Logger = logger.Default.LogMode(o.logLevel)
} else {
config.Logger = glog.NewCustomGormLogger(o.gLog, o.requestIDKey, o.logLevel)
}
} else {
config.Logger = logger.Default.LogMode(logger.Silent)
}
// print only slow queries
if o.slowThreshold > 0 {
config.Logger = logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // use the standard output asWriter
logger.Config{
SlowThreshold: o.slowThreshold,
Colorful: true,
LogLevel: logger.Warn, // set the logging level, only above the specified level will output the slow query log
},
)
}
return config
}
// Close close gorm db
func Close(db *gorm.DB) error {
return dbclose.Close(db)
}
package sqlite
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestInit(t *testing.T) {
dbFile := "test_sqlite.db"
db, err := Init(dbFile)
if err != nil {
// ignore test error about not being able to connect to real sqlite
t.Logf(fmt.Sprintf("connect to sqlite failed, err=%v, dbFile=%s", err, dbFile))
return
}
defer Close(db)
t.Logf("%+v", db.Name())
}
func Test_gormConfig(t *testing.T) {
o := defaultOptions()
o.apply(
WithLogging(nil),
WithLogging(nil, 4),
WithSlowThreshold(time.Millisecond*100),
WithEnableTrace(),
WithMaxIdleConns(5),
WithMaxOpenConns(50),
WithConnMaxLifetime(time.Minute*3),
WithEnableForeignKey(),
WithLogRequestIDKey("request_id"),
WithGormPlugin(nil),
)
c := gormConfig(o)
assert.NotNil(t, c)
}
package xcode
// Code 通用错误代码接口定义。
type Code interface {
// Code 错误码。
Code() int
// Message 错误码简短信息。
Message() string
// Detail 错误码详细信息。
Detail() any
}
// ================================================================================================================
// 公共错误码定义。
// 保留内部错误码: code < 1000。
// ================================================================================================================
var (
CodeDefault = 0
CodeOK = 200
CodeValidationFailed = 501
CodeInvalidParameter = 503
CodeMissingConfiguration = 507
CodeNotAuthorized = 401
CodeUnknown = 604
)
package xcode
import "fmt"
// localCode 内部错误码实现。
type localCode struct {
code int // Error code, usually an integer.
message string // Brief message for this error code.
detail any // As type of interface, it is mainly designed as an extension field for error code.
}
// Code 错误码。
func (c localCode) Code() int {
return c.code
}
// Message 错误码简短信息。
func (c localCode) Message() string {
return c.message
}
// Detail 错误码详细信息。
func (c localCode) Detail() any {
return c.detail
}
// String 错误码字符串。
func (c localCode) String() string {
if c.detail != nil {
return fmt.Sprintf(`%d:%s %v`, c.code, c.message, c.detail)
}
if c.message != "" {
return fmt.Sprintf(`%d:%s`, c.code, c.message)
}
return fmt.Sprintf(`%d`, c.code)
}
package xerror
// IIs Is 接口。
type IIs interface {
Error() string
Is(target error) bool
}
// IEqual Equal 接口。
type IEqual interface {
Error() string
Equal(target error) bool
}
// ICode Code 接口。
type ICode interface {
Error() string
Code() int
}
// IStack Stack 接口。
type IStack interface {
Error() string
Stack() string
}
// ICause Cause 接口。
type ICause interface {
Error() string
Cause() error
}
// ICurrent Current 接口。
type ICurrent interface {
Error() string
Current() error
}
// IUnwrap Unwrap 接口。
type IUnwrap interface {
Error() string
Unwrap() error
}
const (
// stackFilterKeyForX 过滤 G 模块路径堆栈。
stackFilterKeyForX = "xmall/common/intern/"
// separatorSpace 空间分隔符。
separatorSpace = ", "
)
var (
// IsUsingBriefStack 简短错误堆栈的开关。(默认为true)
IsUsingBriefStack bool
)
func init() {
IsUsingBriefStack = true
}
package xerror
import (
"fmt"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/xerrors/xcode"
)
// New 用于创建一个自定义文本错误信息的 error 对象,并包含堆栈信息。
func New(text string) error {
return &Error{
stack: callers(),
text: text,
code: xcode.CodeDefault,
}
}
// NewC add code
func NewC(code int, text string) error {
return &Error{
stack: callers(),
text: text,
code: code,
}
}
func Join(errs ...error) error {
if len(errs) == 0 {
return nil
}
var es []error
for _, e := range errs {
if e != nil {
es = append(es, e)
}
}
if len(es) == 0 {
return nil
}
if len(es) == 1 {
return es[0]
}
err := es[0]
for i := 1; i < len(es); i++ {
err = Wrap(err, es[i].Error())
}
return err
}
// Newf 用于创建一个自定义文本错误带参数信息的 error 对象,并包含堆栈信息。
func Newf(format string, args ...any) error {
return &Error{
stack: callers(),
text: fmt.Sprintf(format, args...),
code: xcode.CodeDefault,
}
}
func Errorf(format string, args ...any) error {
return &Error{
stack: callers(),
text: fmt.Sprintf(format, args...),
code: xcode.CodeDefault,
}
}
func NewCf(code int, format string, args ...any) error {
return &Error{
stack: callers(),
text: fmt.Sprintf(format, args...),
code: code,
}
}
// NewSkip 用于创建一个自定义错误信息的 error 对象,并且忽略部分堆栈信息(按照当前调用方法位置往上忽略)。高级功能,一般开发者很少用得到。
// 参数 `skip` 指定堆栈跳过的层数。
func NewSkip(skip int, text string) error {
return &Error{
stack: callers(skip),
text: text,
code: xcode.CodeDefault,
}
}
func NewSkipC(code, skip int, text string) error {
return &Error{
stack: callers(skip),
text: text,
code: code,
}
}
// NewSkipf 用于创建一个自定义错误信息的error对象,并且忽略部分堆栈信息(按照当前调用方法位置往上忽略)。
// 参数 `skip` 指定堆栈跳过的层数。
func NewSkipf(skip int, format string, args ...any) error {
return &Error{
stack: callers(skip),
text: fmt.Sprintf(format, args...),
code: xcode.CodeDefault,
}
}
func NewSkipCf(code, skip int, format string, args ...any) error {
return &Error{
stack: callers(skip),
text: fmt.Sprintf(format, args...),
code: code,
}
}
// Wrap 用于包裹其他错误 error 对象,构造成多级的错误信息,包含堆栈信息。
// 注:它不会丢失包装错误的错误码,因为它从中继承了错误码。
func Wrap(err error, text string) error {
if err == nil {
return nil
}
return &Error{
error: err,
stack: callers(),
text: text,
code: Code(err),
}
}
// Wrapf 用于包裹其他错误 error 对象,构造成多级的错误信息,包含堆栈信息。
func Wrapf(err error, format string, args ...any) error {
if err == nil {
return nil
}
return &Error{
error: err,
stack: callers(),
text: fmt.Sprintf(format, args...),
code: Code(err),
}
}
// WrapSkip 用于包裹其他错误 error 对象,构造成多级的错误信息,包含堆栈信息,并且忽略部分堆栈信息(按照当前调用方法位置往上忽略)。
func WrapSkip(skip int, err error, text string) error {
if err == nil {
return nil
}
return &Error{
error: err,
stack: callers(skip),
text: text,
code: Code(err),
}
}
// WrapSkipf 用于包裹其他错误 error 对象,构造成多级的错误信息,包含堆栈信息,并且忽略部分堆栈信息(按照当前调用方法位置往上忽略)。
func WrapSkipf(skip int, err error, format string, args ...any) error {
if err == nil {
return nil
}
return &Error{
error: err,
stack: callers(skip),
text: fmt.Sprintf(format, args...),
code: Code(err),
}
}
type CodeI interface {
Code() int
}
func GetCodeOrDefault(err error, defaultCode int) int {
if err == nil {
return defaultCode
}
if code, ok := err.(CodeI); ok {
return code.Code()
}
return defaultCode
}
package xerror
import (
"fmt"
"strings"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/xerrors/xcode"
)
// NewCode 用于创建一个自定义错误信息的 error 对象,并包含堆栈信息,并增加错误码对象的输入。
func NewCode(code int, text ...string) error {
return &Error{
stack: callers(),
text: strings.Join(text, separatorSpace),
code: code,
}
}
// NewCodef 用于创建一个自定义错误信息的 error 对象,并包含堆栈信息,并增加错误码对象的输入。
func NewCodef(code int, format string, args ...any) error {
return &Error{
stack: callers(),
text: fmt.Sprintf(format, args...),
code: code,
}
}
// NewCodeSkip 用于创建一个自定义错误信息的 error 对象,并包含堆栈信息,并增加错误码对象的输入。并且忽略部分堆栈信息(按照当前调用方法位置往上忽略)。
func NewCodeSkip(code int, skip int, text ...string) error {
return &Error{
stack: callers(skip),
text: strings.Join(text, separatorSpace),
code: code,
}
}
// NewCodeSkipf 用于创建一个自定义错误信息的 error 对象,并包含堆栈信息,并增加错误码对象的输入。并且忽略部分堆栈信息(按照当前调用方法位置往上忽略)。
func NewCodeSkipf(code int, skip int, format string, args ...any) error {
return &Error{
stack: callers(skip),
text: fmt.Sprintf(format, args...),
code: code,
}
}
// WrapCode 用 error 和文本包裹错误。
func WrapCode(code int, err error, text ...string) error {
if err == nil {
return nil
}
return &Error{
error: err,
stack: callers(),
text: strings.Join(text, separatorSpace),
code: code,
}
}
// WrapCodef 用 error 和格式指定符包裹错误。
func WrapCodef(code int, err error, format string, args ...any) error {
if err == nil {
return nil
}
return &Error{
error: err,
stack: callers(),
text: fmt.Sprintf(format, args...),
code: code,
}
}
// WrapCodeSkip 用 error 和文本包裹错误,并且忽略部分堆栈信息(按照当前调用方法位置往上忽略)。
func WrapCodeSkip(code int, skip int, err error, text ...string) error {
if err == nil {
return nil
}
return &Error{
error: err,
stack: callers(skip),
text: strings.Join(text, separatorSpace),
code: code,
}
}
// WrapCodeSkipf 用 error 和格式指定符包裹错误,并且忽略部分堆栈信息(按照当前调用方法位置往上忽略)。
func WrapCodeSkipf(code int, skip int, err error, format string, args ...any) error {
if err == nil {
return nil
}
return &Error{
error: err,
stack: callers(skip),
text: fmt.Sprintf(format, args...),
code: code,
}
}
// Code 获取 error 中的错误码接口。
func Code(err error) int {
if err == nil {
return xcode.CodeDefault
}
if e, ok := err.(ICode); ok {
return e.Code()
}
if e, ok := err.(IUnwrap); ok {
return Code(e.Unwrap())
}
return xcode.CodeDefault
}
// HasCode 检查并报告 `err` 在其链接错误中是否具有 `code`。
func HasCode(err error, code int) bool {
if err == nil {
return false
}
if e, ok := err.(ICode); ok {
return code == e.Code()
}
if e, ok := err.(IUnwrap); ok {
return HasCode(e.Unwrap(), code)
}
return false
}
package xerror
// Option 自定义的错误创建。
type Option struct {
Error error // 包装错误(如果有)。
Stack bool // 是否将堆栈信息记录到错误中。
Text string // 错误文本,由 New* 函数创建
Code int // 如有必要,错误码。
}
// NewOption 用于自定义配置的错误对象创建。
func NewOption(option Option) error {
err := &Error{
error: option.Error,
text: option.Text,
code: option.Code,
}
if option.Stack {
err.stack = callers()
}
return err
}
package xerror
import (
"runtime"
"github.com/pkg/errors"
)
// stack 代表堆栈程序计数器。
type stack []uintptr
const (
// maxStackDepth 标记最大堆栈深度的错误后轨迹。
maxStackDepth = 64
// maxStackDepth = 10
)
// Cause 获取根错误 error。
// 如果 `err` 是 nil, 则返回 nil。
func Cause(err error) error {
if err == nil {
return nil
}
if e, ok := err.(ICause); ok {
return e.Cause()
}
if e, ok := err.(IUnwrap); ok {
return Cause(e.Unwrap())
}
return err
}
// Stack 获取堆栈信息。
// 如果 `err` 是 nil, 则返回空字符串。
// 如果 `err` 不支持堆栈,它将直接返回错误字符串。
func Stack(err error) string {
if err == nil {
return ""
}
if e, ok := err.(IStack); ok {
return e.Stack()
}
return err.Error()
}
// Current 获取当前 error。
// 如果当前错误是 nil, 则返回 nil。
func Current(err error) error {
if err == nil {
return nil
}
if e, ok := err.(ICurrent); ok {
return e.Current()
}
return err
}
// Unwrap 获取下一层 error。
// 如果当前级别错误或下一个级别错误为 nil,则返回 nil。
func Unwrap(err error) error {
if err == nil {
return nil
}
if e, ok := err.(IUnwrap); ok {
return e.Unwrap()
}
return nil
}
// HasStack 判断错误是否带堆栈,实现 `xerror.IStack` 接口。
func HasStack(err error) bool {
_, ok := err.(IStack)
return ok
}
func Equal(err, target error) bool {
if err == target {
return true
}
if err.Error() == target.Error() {
return true
}
if e, ok := err.(IEqual); ok {
return e.Equal(target)
}
if e, ok := target.(IEqual); ok {
return e.Equal(err)
}
return Is(err, target)
}
func IsNot(err, target error) bool {
return !Is(err, target)
}
func Is(err, target error) bool {
if err == nil && target == nil {
return true
}
if err != nil && target == nil {
return false
}
if err == nil && target != nil {
return false
}
if err.Error() == target.Error() {
return true
}
if errors.Is(err, target) {
return true
}
if e, ok := err.(IIs); ok {
return e.Is(target)
}
return false
}
func HasError(err, target error) bool {
return Is(err, target)
}
func callers(skip ...int) stack {
var (
pcs [maxStackDepth]uintptr
n = 3
)
if len(skip) > 0 {
n += skip[0]
}
return pcs[:runtime.Callers(n, pcs[:])]
}
package xerror
import (
"fmt"
"runtime"
"strings"
"github.com/pkg/errors"
)
// Error 自定义错误对象。
type Error struct {
error error // 包装错误。
stack stack // 堆栈数组,当创建或包装此错误时记录堆栈信息。
text string // 创建错误时自定义错误文本。
code int // 如有必要,错误码。
}
const (
// stackFilterKeyLocal 过滤当前错误模块路径的键。
stackFilterKeyLocal = "/xerrors/xerror"
)
var (
// goRootForFilter 用于开发环境中的堆栈过滤。
goRootForFilter = runtime.GOROOT()
)
func init() {
if goRootForFilter != "" {
goRootForFilter = strings.ReplaceAll(goRootForFilter, "\\", "/")
}
}
// Error 实现错误的接口,它将所有错误返回为字符串。
func (err *Error) Error() string {
if err == nil {
return ""
}
errStr := err.text
if err.error != nil {
if errStr != "" {
errStr += ": "
}
errStr += err.error.Error()
}
return errStr
}
// Cause 获取根错误 error。
func (err *Error) Cause() error {
if err == nil {
return nil
}
loop := err
for loop != nil {
if loop.error != nil {
if e, ok := loop.error.(*Error); ok {
// 内部自定义错误。
loop = e
} else if e, ok := loop.error.(ICause); ok {
// 实现 ApiCause 接口的其他错误。
return e.Cause()
} else {
return loop.error
}
} else {
// return loop
//
// 参考案例 https://github.com/pkg/errors。
return errors.New(loop.text)
}
}
return nil
}
// Current 获取当前 error。
// 如果当前错误是 nil, 则返回 nil。
func (err *Error) Current() error {
if err == nil {
return nil
}
return &Error{
error: nil,
stack: err.stack,
text: err.text,
code: err.code,
}
}
// Unwrap 获取下一层 error。
func (err *Error) Unwrap() error {
if err == nil {
return nil
}
return err.error
}
// Equal 错误对象比较。
// 如果它们的 `code` 和 `text` 都相同,则认为错误相同。
func (err *Error) Equal(target error) bool {
if err.Error() == target.Error() {
return true
}
if errors.Is(target, err) {
return true
}
if err.text != fmt.Sprintf(`%-s`, target) {
return false
}
return true
}
// Is 当前错误 `err` 的链接错误中是否包含错误 `target`。
func (err *Error) Is(target error) bool {
if Equal(err, target) {
return true
}
nextErr := err.Unwrap()
if nextErr == nil {
return false
}
if Equal(nextErr, target) {
return true
}
if e, ok := nextErr.(IIs); ok {
return e.Is(target)
}
return false
}
package xerror
import "gitlab.wanzhuangkj.com/tush/opkg/pkg/xerrors/xcode"
// Code 获取错误码。
// 如果没有错误代码,则返回 `xcode.CodeDefault`。
func (err *Error) Code() int {
if err == nil {
return xcode.CodeDefault
}
if err.code == xcode.CodeDefault {
return Code(err.Unwrap())
}
return err.code
}
// SetCode 使用指定 `code` 更新内部 `code` 。
func (err *Error) SetCode(code int) {
if err == nil {
return
}
err.code = code
}
package xerror
import (
"fmt"
"io"
)
// Format 根据 `fmt.Formatter` 接口格式化。
//
// %v, %s : 打印所有错误字符串;
// %-v, %-s : 打印当前级别错误字符串;
// %+s : 打印完整堆栈错误列表;
// %+v : 打印错误字符串和完整堆栈错误列表
func (err *Error) Format(s fmt.State, verb rune) {
switch verb {
case 's', 'v':
switch {
case s.Flag('-'):
if err.text != "" {
_, _ = io.WriteString(s, err.text)
} else {
_, _ = io.WriteString(s, err.Error())
}
case s.Flag('+'):
if verb == 's' {
_, _ = io.WriteString(s, err.Stack())
} else {
_, _ = io.WriteString(s, err.Error()+"\n"+err.Stack())
}
default:
_, _ = io.WriteString(s, err.Error())
}
}
}
package xerror
// MarshalJSON 实现 json.Marshal 接口。
// 注:这里不要使用指针作为其接收器。
func (err Error) MarshalJSON() ([]byte, error) {
return []byte(`"` + err.Error() + `"`), nil
}
package xerror
import (
"bytes"
"container/list"
"fmt"
"runtime"
"strings"
)
// stackInfo 管理特定错误的堆栈信息。
type stackInfo struct {
Index int // Index 整个错误堆栈中当前错误的索引。
Message string // Error 信息字符串。
Lines *list.List // Lines 按顺序包含当前错误堆栈的所有错误堆栈行。
}
// stackLine 管理堆栈的每一行信息。
type stackLine struct {
Function string // Function 包含其完整包路径。
FileLine string // FileLine 函数的源文件名及其行号。
}
// Stack 以字符串形式返回错误堆栈信息。
func (err *Error) Stack() string {
if err == nil {
return ""
}
var (
loop = err
index = 1
infos []*stackInfo
)
for loop != nil {
info := &stackInfo{
Index: index,
Message: fmt.Sprintf("%-v", loop),
}
index++
infos = append(infos, info)
loopLinesOfStackInfo(loop.stack, info)
if loop.error != nil {
if e, ok := loop.error.(*Error); ok {
loop = e
} else {
infos = append(infos, &stackInfo{
Index: index,
Message: loop.error.Error(),
})
index++
break
}
} else {
break
}
}
filterLinesOfStackInfos(infos)
return formatStackInfos(infos)
}
// filterLinesOfStackInfos 从顶部错误中删除存在于后续堆栈中的重复行。
func filterLinesOfStackInfos(infos []*stackInfo) {
var (
ok bool
set = make(map[string]struct{})
info *stackInfo
line *stackLine
removes []*list.Element
)
for i := len(infos) - 1; i >= 0; i-- {
info = infos[i]
if info.Lines == nil {
continue
}
for n, e := 0, info.Lines.Front(); n < info.Lines.Len(); n, e = n+1, e.Next() {
line = e.Value.(*stackLine)
if _, ok = set[line.FileLine]; ok {
removes = append(removes, e)
} else {
set[line.FileLine] = struct{}{}
}
}
if len(removes) > 0 {
for _, e := range removes {
info.Lines.Remove(e)
}
}
removes = removes[:0]
}
}
// formatStackInfos 格式化并以字符串形式返回错误堆栈信息。
func formatStackInfos(infos []*stackInfo) string {
var buffer = bytes.NewBuffer(nil)
for i, info := range infos {
buffer.WriteString(fmt.Sprintf("%d. %s\n", i+1, info.Message))
if info.Lines != nil && info.Lines.Len() > 0 {
formatStackLines(buffer, info.Lines)
}
}
return buffer.String()
}
// formatStackLines 格式化并将错误堆栈行作为字符串返回。
func formatStackLines(buffer *bytes.Buffer, lines *list.List) string {
var (
line *stackLine
space = " "
length = lines.Len()
)
for i, e := 0, lines.Front(); i < length; i, e = i+1, e.Next() {
line = e.Value.(*stackLine)
// Graceful indent.
if i >= 9 {
space = " "
}
buffer.WriteString(fmt.Sprintf(
" %d. %s%s\n %s\n",
i+1, space, line.Function, line.FileLine,
))
}
return buffer.String()
}
// loopLinesOfStackInfo 迭代堆栈信息行并生成堆栈行信息。
func loopLinesOfStackInfo(st stack, info *stackInfo) {
if st == nil {
return
}
for _, p := range st {
if fn := runtime.FuncForPC(p - 1); fn != nil {
file, line := fn.FileLine(p - 1)
if IsUsingBriefStack {
// 过滤整个 X 包堆栈路径。
if strings.Contains(file, stackFilterKeyForX) {
continue
}
} else {
// 包路径堆栈过滤。
if strings.Contains(file, stackFilterKeyLocal) {
continue
}
}
// 避免堆栈字符串如 `autogenerated`
if strings.Contains(file, "<") {
continue
}
// 忽略 GO ROOT 路径。
if goRootForFilter != "" &&
len(file) >= len(goRootForFilter) &&
file[0:len(goRootForFilter)] == goRootForFilter {
continue
}
if info.Lines == nil {
info.Lines = list.New()
}
info.Lines.PushBack(&stackLine{
Function: fn.Name(),
FileLine: fmt.Sprintf(`%s:%d`, file, line),
})
}
}
}
package xerror_test
import (
"testing"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/xerrors/xerror"
"github.com/pkg/errors"
)
var (
// Wrap* 函数基准测试的基本错误。
baseError = errors.New("test")
)
func Benchmark_New(b *testing.B) {
for i := 0; i < b.N; i++ {
xerror.New("test")
}
}
func Benchmark_Newf(b *testing.B) {
for i := 0; i < b.N; i++ {
xerror.Newf("%s", "test")
}
}
func Benchmark_Wrap(b *testing.B) {
for i := 0; i < b.N; i++ {
xerror.Wrap(baseError, "test")
}
}
func Benchmark_Wrapf(b *testing.B) {
for i := 0; i < b.N; i++ {
xerror.Wrapf(baseError, "%s", "test")
}
}
func Benchmark_NewSkip(b *testing.B) {
for i := 0; i < b.N; i++ {
xerror.NewSkip(1, "test")
}
}
func Benchmark_NewSkipf(b *testing.B) {
for i := 0; i < b.N; i++ {
xerror.NewSkipf(1, "%s", "test")
}
}
func Benchmark_NewCode(b *testing.B) {
for i := 0; i < b.N; i++ {
xerror.NewCode(500, "test")
}
}
func Benchmark_NewCodef(b *testing.B) {
for i := 0; i < b.N; i++ {
xerror.NewCodef(500, "%s", "test")
}
}
func Benchmark_NewCodeSkip(b *testing.B) {
for i := 0; i < b.N; i++ {
xerror.NewCodeSkip(1, 500, "test")
}
}
func Benchmark_NewCodeSkipf(b *testing.B) {
for i := 0; i < b.N; i++ {
xerror.NewCodeSkipf(1, 500, "%s", "test")
}
}
func Benchmark_WrapCode(b *testing.B) {
for i := 0; i < b.N; i++ {
xerror.WrapCode(500, baseError, "test")
}
}
func Benchmark_WrapCodef(b *testing.B) {
for i := 0; i < b.N; i++ {
xerror.WrapCodef(500, baseError, "test")
}
}
package xerror_test
import (
"fmt"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/xerrors/xcode"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/xerrors/xerror"
"github.com/pkg/errors"
)
func ExampleNewCode() {
err := xerror.NewCode(10000, "My Error")
fmt.Println(err.Error())
fmt.Println(xerror.Code(err))
// Output:
// My Error
// 10000
}
func ExampleNewCodef() {
err := xerror.NewCodef(10000, "It's %s", "My Error")
fmt.Println(err.Error())
fmt.Println(xerror.Code(err))
// Output:
// It's My Error
// 10000
}
func ExampleWrapCode() {
err1 := errors.New("permission denied")
err2 := xerror.WrapCode(10000, err1, "Custom Error")
fmt.Println(err2.Error())
fmt.Println(xerror.Code(err2))
// Output:
// Custom Error: permission denied
// 10000
}
func ExampleWrapCodef() {
err1 := errors.New("permission denied")
err2 := xerror.WrapCodef(10000, err1, "It's %s", "Custom Error")
fmt.Println(err2.Error())
fmt.Println(xerror.Code(err2))
// Output:
// It's Custom Error: permission denied
// 10000
}
func ExampleEqual() {
err1 := errors.New("permission denied")
err2 := xerror.New("permission denied")
err3 := xerror.NewCode(xcode.CodeNotAuthorized, "permission denied")
fmt.Println(xerror.Equal(err1, err2))
fmt.Println(xerror.Equal(err2, err3))
// Output:
// true
// false
}
func ExampleIs() {
err1 := errors.New("permission denied")
err2 := xerror.Wrap(err1, "operation failed")
fmt.Println(xerror.Is(err1, err1))
fmt.Println(xerror.Is(err2, err2))
fmt.Println(xerror.Is(err2, err1))
fmt.Println(xerror.Is(err1, err2))
// Output:
// false
// true
// true
// false
}
package xerror_test
import (
"encoding/json"
"fmt"
"testing"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/xerrors/xcode"
"github.com/pkg/errors"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/xerrors/xerror"
"gorm.io/gorm"
"github.com/stretchr/testify/assert"
)
func nilError() error {
return nil
}
func Test_Nil(t *testing.T) {
assert.NotNil(t, xerror.New(""))
assert.Nil(t, xerror.Wrap(nilError(), "test"))
}
func Test_New(t *testing.T) {
err1 := xerror.New("1")
assert.NotNil(t, err1)
assert.Equal(t, err1.Error(), "1")
err2 := xerror.Newf("%d", 1)
assert.NotNil(t, err2)
assert.Equal(t, err2.Error(), "1")
err3 := xerror.NewSkipf(1, "%d", 1)
assert.NotNil(t, err3)
assert.Equal(t, err3.Error(), "1")
}
func Test_Wrap(t *testing.T) {
err1 := errors.New("1")
err1 = xerror.Wrap(err1, "2")
err1 = xerror.Wrap(err1, "3")
assert.NotNil(t, err1)
assert.Equal(t, err1.Error(), "3: 2: 1")
err2 := xerror.New("1")
err2 = xerror.Wrap(err2, "")
assert.NotNil(t, err2)
assert.Equal(t, err2.Error(), "1")
}
func Test_Wrapf(t *testing.T) {
err1 := errors.New("1")
err1 = xerror.Wrapf(err1, "%d", 2)
err1 = xerror.Wrapf(err1, "%d", 3)
assert.NotNil(t, err1)
assert.Equal(t, err1.Error(), "3: 2: 1")
err2 := xerror.New("1")
err2 = xerror.Wrapf(err2, "")
assert.NotNil(t, err2, nil)
assert.Equal(t, err2.Error(), "1")
}
func Test_WrapSkip(t *testing.T) {
err1 := errors.New("1")
err1 = xerror.WrapSkip(1, err1, "2")
err1 = xerror.WrapSkip(1, err1, "3")
assert.NotNil(t, err1, nil)
assert.Equal(t, err1.Error(), "3: 2: 1")
err2 := xerror.New("1")
err2 = xerror.WrapSkip(1, err2, "")
assert.NotNil(t, err2, nil)
assert.Equal(t, err2.Error(), "1")
}
func Test_WrapSkipf(t *testing.T) {
err1 := errors.New("1")
err1 = xerror.WrapSkipf(1, err1, "2")
err1 = xerror.WrapSkipf(1, err1, "3")
assert.NotNil(t, err1, nil)
assert.Equal(t, err1.Error(), "3: 2: 1")
err2 := xerror.New("1")
err2 = xerror.WrapSkipf(1, err2, "")
assert.NotNil(t, err2, nil)
assert.Equal(t, err2.Error(), "1")
}
func Test_Cause(t *testing.T) {
err := errors.New("1")
assert.Equal(t, xerror.Cause(err), err)
err1 := errors.New("1")
err1 = xerror.Wrap(err1, "2")
err1 = xerror.Wrap(err1, "3")
assert.Equal(t, xerror.Cause(err1).Error(), "1")
err2 := xerror.New("1")
assert.Equal(t, xerror.Cause(err2).Error(), "1")
err3 := xerror.New("1")
err3 = xerror.Wrap(err3, "2")
err3 = xerror.Wrap(err3, "3")
assert.Equal(t, xerror.Cause(err3).Error(), "1")
}
func Test_Format(t *testing.T) {
err1 := errors.New("1")
err1 = xerror.Wrap(err1, "2")
err1 = xerror.Wrap(err1, "3")
assert.NotNil(t, err1)
assert.Equal(t, fmt.Sprintf("%s", err1), "3: 2: 1")
assert.Equal(t, fmt.Sprintf("%v", err1), "3: 2: 1")
err2 := xerror.New("1")
err2 = xerror.Wrap(err2, "2")
err2 = xerror.Wrap(err2, "3")
assert.NotNil(t, err2, nil)
assert.Equal(t, fmt.Sprintf("%-s", err2), "3")
assert.Equal(t, fmt.Sprintf("%-v", err2), "3")
}
func Test_Stack(t *testing.T) {
err := errors.New("1")
assert.Equal(t, fmt.Sprintf("%+v", err), "1")
err1 := errors.New("1")
err1 = xerror.Wrap(err1, "2")
err1 = xerror.Wrap(err1, "3")
assert.NotNil(t, err1, nil)
// fmt.Printf("%+v", err1)
err2 := xerror.New("1")
assert.NotNil(t, fmt.Sprintf("%+v", err2), "1")
// fmt.Printf("%+v", err2)
err3 := xerror.New("1")
err3 = xerror.Wrap(err3, "2")
err3 = xerror.Wrap(err3, "3")
assert.NotNil(t, err3, nil)
// fmt.Printf("%+v", err3)
}
func Test_Current(t *testing.T) {
err := errors.New("1")
err = xerror.Wrap(err, "2")
err = xerror.Wrap(err, "3")
assert.Equal(t, err.Error(), "3: 2: 1")
assert.Equal(t, xerror.Current(err).Error(), "3")
}
func Test_Unwrap(t *testing.T) {
err := errors.New("1")
err = xerror.Wrap(err, "2")
err = xerror.Wrap(err, "3")
assert.Equal(t, err.Error(), "3: 2: 1")
err = xerror.Unwrap(err)
assert.Equal(t, err.Error(), "2: 1")
err = xerror.Unwrap(err)
assert.Equal(t, err.Error(), "1")
err = xerror.Unwrap(err)
assert.Nil(t, err)
}
func Test_Code(t *testing.T) {
err1 := errors.New("123")
assert.Equal(t, xerror.Code(err1), -1)
assert.Equal(t, err1.Error(), "123")
err2 := xerror.NewCode(xcode.CodeUnknown, "123")
assert.Equal(t, xerror.Code(err2), xcode.CodeUnknown)
assert.Equal(t, err2.Error(), "123")
err3 := xerror.NewCodef(1, "%s", "123")
assert.Equal(t, xerror.Code(err3), 1)
assert.Equal(t, err3.Error(), "123")
err4 := xerror.NewCodeSkip(1, 0, "123")
assert.Equal(t, xerror.Code(err4), 1)
assert.Equal(t, err4.Error(), "123")
err5 := xerror.NewCodeSkipf(1, 0, "%s", "123")
assert.Equal(t, xerror.Code(err5), 1)
assert.Equal(t, err5.Error(), "123")
err6 := errors.New("1")
err6 = xerror.Wrap(err6, "2")
err6 = xerror.WrapCode(1, err6, "3")
assert.Equal(t, xerror.Code(err6), 1)
assert.Equal(t, err6.Error(), "3: 2: 1")
err7 := errors.New("1")
err7 = xerror.Wrap(err7, "2")
err7 = xerror.WrapCodef(1, err7, "%s", "3")
assert.Equal(t, xerror.Code(err7), 1)
assert.Equal(t, err7.Error(), "3: 2: 1")
err8 := errors.New("1")
err8 = xerror.Wrap(err8, "2")
err8 = xerror.WrapCodeSkip(1, 100, err8, "3")
assert.Equal(t, xerror.Code(err8), 1)
assert.Equal(t, err8.Error(), "3: 2: 1")
err9 := errors.New("1")
err9 = xerror.Wrap(err9, "2")
err9 = xerror.WrapCodeSkipf(1, 100, err9, "%s", "3")
assert.Equal(t, xerror.Code(err9), 1)
assert.Equal(t, err9.Error(), "3: 2: 1")
}
func Test_SetCode(t *testing.T) {
err := xerror.New("123")
assert.Equal(t, xerror.Code(err), -1)
assert.Equal(t, err.Error(), "123")
err.(*xerror.Error).SetCode(xcode.CodeValidationFailed)
assert.Equal(t, xerror.Code(err), xcode.CodeValidationFailed)
assert.Equal(t, err.Error(), "123")
}
func Test_Json(t *testing.T) {
err := xerror.Wrap(xerror.New("1"), "2")
b, e := json.Marshal(err)
assert.Equal(t, e, nil)
assert.Equal(t, string(b), `"2: 1"`)
}
func Test_HasStack(t *testing.T) {
err1 := errors.New("1")
err2 := xerror.New("1")
assert.Equal(t, xerror.HasStack(err1), false)
assert.Equal(t, xerror.HasStack(err2), true)
}
func Test_Equal(t *testing.T) {
err1 := errors.New("1")
err2 := errors.New("1")
err3 := xerror.New("1")
err4 := xerror.New("4")
assert.Equal(t, xerror.Equal(err1, err2), false)
assert.Equal(t, xerror.Equal(err1, err3), true)
assert.Equal(t, xerror.Equal(err2, err3), true)
assert.Equal(t, xerror.Equal(err3, err4), false)
assert.Equal(t, xerror.Equal(err1, err4), false)
}
func Test_Is(t *testing.T) {
err1 := errors.New("1")
err2 := xerror.Wrap(err1, "2")
err2 = xerror.Wrap(err2, "3")
assert.Equal(t, xerror.Is(err2, err1), true)
err3 := xerror.Wrap(gorm.ErrRecordNotFound, "3")
assert.Equal(t, xerror.Is(err3, gorm.ErrRecordNotFound), true)
err4 := xerror.New(gorm.ErrRecordNotFound.Error())
assert.Equal(t, xerror.Is(err4, gorm.ErrRecordNotFound), true)
err5 := xerror.WrapCode(1, gorm.ErrRecordNotFound, "exes")
assert.Equal(t, xerror.Is(err5, gorm.ErrRecordNotFound), true)
t.Log((err5).(xerror.ICode))
}
func Test_HashError(t *testing.T) {
err1 := errors.New("1")
err2 := xerror.Wrap(err1, "2")
err2 = xerror.Wrap(err2, "3")
assert.Equal(t, xerror.HasError(err2, err1), true)
}
func Test_HashCode(t *testing.T) {
err1 := errors.New("1")
err2 := xerror.WrapCode(2, err1, "2")
err3 := xerror.Wrap(err2, "3")
err4 := xerror.Wrap(err3, "4")
assert.Equal(t, xerror.HasCode(err1, xcode.CodeDefault), false)
assert.Equal(t, xerror.HasCode(err2, xcode.CodeNotAuthorized), true)
assert.Equal(t, xerror.HasCode(err3, xcode.CodeNotAuthorized), true)
assert.Equal(t, xerror.HasCode(err4, xcode.CodeNotAuthorized), true)
}
package ctxutils
import (
"context"
"errors"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/sgorm/query"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
const (
GinContextKey = "ginContext"
HeaderXTimestampKey = "Timestamp"
KeyReqBody = "reqBody"
KeyRspBody = "rspBody"
KeyRspCode = "rspCode"
KeyOApiReqBody = "oApiReqBody%s"
KeyOApiRspBody = "oApiRspBody%s"
KeyAppName = "appName"
KeyUser = "user"
KeyApiStartTime = "apiStartTime"
KeyClientIP = "clientIP"
KeyUID = "uid"
KeyUType = "uType"
KeyCompanyID = "companyID"
KeyCompanyName = "companyName"
KeyShopID = "shopID"
KeyUName = "uname"
KeyToken = "token"
KeyCost = "X-Request-Cost"
KeyRspBodyMax = "rsp-body-max"
KeyApiType = "apiType" // enums: page
KeyPagination = "pagination"
)
var (
HeaderXRequestIDKey = "X-Request-ID"
ContextTraceIDKey = "X-Request-ID"
)
var (
ErrorGinContextNotFound = errors.New("gin context not found")
)
func Set(c *gin.Context, key string, val any) {
c.Set(key, val)
}
func GinTraceID(c *gin.Context) string {
if v, isExist := c.Get(ContextTraceIDKey); isExist {
if requestID, ok := v.(string); ok {
return requestID
}
}
return ""
}
func CtxTraceID(ctx context.Context) string {
if v := ctx.Value(ContextTraceIDKey); v != nil {
if traceID, ok := v.(string); ok {
return traceID
}
}
return ""
}
func GinTraceIDField(c *gin.Context) zap.Field {
return zap.String(ContextTraceIDKey, GinTraceID(c))
}
func CtxTraceIDField(ctx context.Context) zap.Field {
return zap.String(ContextTraceIDKey, CtxTraceID(ctx))
}
// func NewEmptyCtx() context.Context {
// return context.WithValue(context.Background(), ContextTraceIDKey, GenerateTid())
// }
func NewCtx(ctx context.Context) context.Context {
return context.WithValue(context.Background(), ContextTraceIDKey, ctx.Value(ContextTraceIDKey))
}
func GetClientIP(ctx context.Context) string {
ip := ""
ipVal := ctx.Value(KeyClientIP)
if ipVal != nil {
if str, ok := ipVal.(string); ok {
ip = str
}
}
return ip
}
func WrapCtx(c *gin.Context) context.Context {
ctx := context.WithValue(c.Request.Context(), ContextTraceIDKey, c.GetString(ContextTraceIDKey))
for k, v := range c.Keys {
ctx = context.WithValue(ctx, k, v)
}
ctx = context.WithValue(ctx, GinContextKey, c) //nolint
return ctx
}
func CtxGin(c context.Context) *gin.Context {
if str, ok := c.Value(GinContextKey).(*gin.Context); ok {
return str
}
return nil
}
type IPagination interface {
GetPageIndex() int
GetPageSize() int
}
func SetPage(ctx context.Context, in IPagination) {
if c := CtxGin(ctx); c != nil {
c.Set(KeyApiType, "page")
pagination := query.Pagination{
PageIndex: in.GetPageIndex(),
PageSize: in.GetPageSize(),
}
c.Set(KeyPagination, pagination)
}
}
package idutils
import (
"context"
"errors"
"fmt"
"net/http"
"time"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/httpcli"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/logger"
"gitlab.wanzhuangkj.com/tush/opkg/xutils/sf"
)
type IDGeneration struct {
Type string `yaml:"type" json:"type" mapstructure:"type"` // snowflake or leaf
Leaf Leaf `yaml:"leaf" json:"leaf" mapstructure:"leaf"`
Snowflake Snowflake `yaml:"snowflake" json:"snowflake" mapstructure:"snowflake"`
}
type Leaf struct {
Addr string `yaml:"addr" json:"addr" mapstructure:"addr"` // eg http://leaf.qitu
ShowLog bool `yaml:"showLog" json:"showLog" mapstructure:"showLog"`
RetryCount int `yaml:"retryCount" json:"retryCount" mapstructure:"retryCount"`
}
type Snowflake struct {
Node int `yaml:"node" json:"node" mapstructure:"node"`
}
var (
idGenerateType = "snowflake" // 默认snowflake方式生成
leafRetryCount = 3
leafShowLog = false
leafAddr string = "http://leaf.qitu"
leafApi = &client{
cli: httpcli.NewClient(
&http.Client{
Transport: &http.Transport{
MaxIdleConnsPerHost: 4,
MaxIdleConns: 4,
IdleConnTimeout: 90 * time.Second,
},
}),
}
)
type client struct {
cli *httpcli.Client
}
func Init(cfg IDGeneration) error {
if cfg.Type != "" {
if cfg.Type == "leaf" {
if cfg.Leaf.Addr != "" {
leafAddr = cfg.Leaf.Addr
}
if cfg.Leaf.ShowLog {
leafShowLog = true
}
if cfg.Leaf.RetryCount > 0 {
leafRetryCount = cfg.Leaf.RetryCount
}
}
}
logger.Infof("[dxsf] type: %s", cfg.Type)
return nil
}
func GenerateID(ctx context.Context, bizTag string) (sf.ID, error) {
if idGenerateType == "snowflake" {
return generateSFID(ctx, bizTag)
} else {
return generateApiID(ctx, bizTag)
}
}
func generateSFID(_ context.Context, _ string) (sf.ID, error) {
return sf.GenerateID(), nil
}
func GenerateSFID() sf.ID {
return sf.GenerateID()
}
func generateApiID(ctx context.Context, bizTag string) (sf.ID, error) {
url := fmt.Sprintf(`%s/api/leaf?biz_tag=%s`, leafAddr, bizTag)
var id sf.ID
reply := &IDReply{
Data: &id,
}
req := leafApi.cli.NewRequest()
if !leafShowLog {
req = req.OmitLog()
}
if err := req.SetRetry(leafRetryCount).SetContentType("application/json").SetURL(url).GET(ctx).BindJSON(reply).Err(); err != nil {
logger.Error("调用leaf失败", logger.String("bizTag", bizTag), logger.Err(err))
return sf.ID(0), fmt.Errorf("调用leaf失败, %w", err)
}
if reply.Code == 1 {
return id, nil
}
return sf.ID(0), errors.New(reply.Msg)
}
type IDReply struct {
Code int `json:"code"`
Data any `json:"data"`
Msg string `json:"msg"`
}
package retryutils
import (
"context"
"errors"
"fmt"
"math"
"math/rand"
"time"
)
const (
DefaultRetryTimes = 5
DefaultRetryLinearInterval = time.Second * 3
)
type RetryConfig struct {
context context.Context
retryTimes uint
backoffStrategy BackoffStrategy
}
type RetryFunc func() error
type Option func(*RetryConfig)
func RetryTimes(n uint) Option {
return func(rc *RetryConfig) {
rc.retryTimes = n
}
}
func RetryWithCustomBackoff(backoffStrategy BackoffStrategy) Option {
if backoffStrategy == nil {
panic("backoffStrategy不能为空")
}
return func(rc *RetryConfig) {
rc.backoffStrategy = backoffStrategy
}
}
func RetryWithLinearBackoff(interval time.Duration) Option {
if interval <= 0 {
panic("retry间隔必须大于0")
}
return func(rc *RetryConfig) {
rc.backoffStrategy = &linear{
interval: interval,
}
}
}
func RetryWithExponentialWithJitterBackoff(interval time.Duration, base uint64, maxJitter time.Duration) Option {
if interval <= 0 {
panic("retry间隔必须大于0")
}
if maxJitter < 0 {
panic("maxJitter必须大于0")
}
if base%2 == 0 {
return func(rc *RetryConfig) {
rc.backoffStrategy = &shiftExponentialWithJitter{
interval: interval,
maxJitter: maxJitter,
shifter: uint64(math.Log2(float64(base))),
}
}
}
return func(rc *RetryConfig) {
rc.backoffStrategy = &exponentialWithJitter{
interval: interval,
base: time.Duration(base),
maxJitter: maxJitter,
}
}
}
func Context(ctx context.Context) Option {
return func(rc *RetryConfig) {
rc.context = ctx
}
}
func Retry(retryFunc RetryFunc, opts ...Option) error {
config := &RetryConfig{
retryTimes: DefaultRetryTimes,
context: context.TODO(),
}
for _, opt := range opts {
opt(config)
}
if config.backoffStrategy == nil {
config.backoffStrategy = &linear{
interval: DefaultRetryLinearInterval,
}
}
var (
i uint
err error
)
for i < config.retryTimes {
if err = retryFunc(); err != nil {
after := time.After(config.backoffStrategy.CalculateInterval())
select {
case <-after:
case <-config.context.Done():
return errors.New("retry is cancelled")
}
} else {
return nil
}
i++
}
// funcPath := runtime.FuncForPC(reflect.ValueOf(retryFunc).Pointer()).Name()
// lastSlash := strings.LastIndex(funcPath, "/")
// funcName := funcPath[lastSlash+1:]
return fmt.Errorf("retry[%d] faild, %s", config.retryTimes, err.Error())
}
type BackoffStrategy interface {
CalculateInterval() time.Duration
}
type linear struct {
interval time.Duration
}
func (l linear) CalculateInterval() time.Duration {
return l.interval
}
type exponentialWithJitter struct {
base time.Duration
interval time.Duration
maxJitter time.Duration
}
func (e *exponentialWithJitter) CalculateInterval() time.Duration {
current := e.interval
e.interval = e.interval * e.base
return current + jitter(e.maxJitter)
}
type shiftExponentialWithJitter struct {
interval time.Duration
maxJitter time.Duration
shifter uint64
}
func (e shiftExponentialWithJitter) CalculateInterval() time.Duration {
current := e.interval
e.interval = e.interval << e.shifter
return current + jitter(e.maxJitter)
}
func jitter(maxJitter time.Duration) time.Duration {
if maxJitter == 0 {
return 0
}
return time.Duration(rand.Int63n(int64(maxJitter)) + 1)
}
package sf
import (
"fmt"
"strconv"
"github.com/bwmarrin/snowflake"
"github.com/duke-git/lancet/v2/xerror"
)
var (
node *snowflake.Node
)
type ID snowflake.ID
func (f ID) MarshalJSON() ([]byte, error) {
buff := make([]byte, 0, 22)
buff = append(buff, '"')
buff = strconv.AppendInt(buff, int64(f), 10)
buff = append(buff, '"')
return buff, nil
}
func (f *ID) UnmarshalParam(src string) error {
return f.unmarshal([]byte(src))
}
func (f *ID) UnmarshalJSON(b []byte) error {
return f.unmarshal(b)
}
func (f *ID) unmarshal(b []byte) error {
if len(b) == 0 {
return nil
}
if len(b) == 1 {
i, err := strconv.ParseInt(string(b), 10, 64)
if err != nil {
return err
}
*f = ID(i)
return nil
}
if len(b) == 2 {
if b[0] == '"' && b[1] == '"' {
*f = ID(0)
}
i, err := strconv.ParseInt(string(b), 10, 64)
if err != nil {
return err
}
*f = ID(i)
return nil
}
if b[0] == '"' && b[len(b)-1] == '"' {
i, err := strconv.ParseInt(string(b[1:len(b)-1]), 10, 64)
if err != nil {
return err
}
*f = ID(i)
return nil
}
i, err := strconv.ParseInt(string(b), 10, 64)
if err != nil {
return err
}
*f = ID(i)
return nil
}
type JSONSyntaxError struct{ original []byte }
func (j JSONSyntaxError) Error() string {
return fmt.Sprintf("invalid xsf ID %q", string(j.original))
}
// n [1,1023]
func Init(n int) error {
nod, e := snowflake.NewNode(int64(n))
if e != nil {
return e
}
node = nod
return nil
}
func (x ID) String() string {
return snowflake.ID(x).String()
}
func (x ID) Int64() int64 {
return snowflake.ID(x).Int64()
}
func (x ID) Int() int {
return int(snowflake.ID(x).Int64())
}
func GenerateID() ID {
return ID(node.Generate())
}
func ParseInt64(val int64) ID {
return ID(snowflake.ParseInt64(val))
}
func ParseString(val string) (ID, error) {
v, err := snowflake.ParseString(val)
if err != nil {
return 0, xerror.New(err.Error())
}
return ID(v), nil
}
package xtime
import (
"database/sql/driver"
"encoding/json"
"fmt"
"strings"
"time"
"gopkg.in/yaml.v3"
)
// Date 自定义日期类型,基于 time.Time,但只关心年月日
type Date time.Time
// 使用常量确保格式一致性,便于维护
const (
dateFormat = "2006-01-02" // 标准ISO格式
)
// Scan 改进:增强健壮性,统一解析路径,优化错误信息
func (d *Date) Scan(value interface{}) error {
if value == nil {
*d = Date(time.Time{}) // 明确处理数据库NULL值
return nil
}
switch v := value.(type) {
case []byte:
if len(v) == 0 {
*d = Date(time.Time{})
return nil
}
return d.parseString(string(v))
case string:
if v == "" {
*d = Date(time.Time{})
return nil
}
return d.parseString(v)
case time.Time:
// 提取日期部分,忽略时间组件,同时保留原时区信息
y, m, day := v.Date()
*d = Date(time.Date(y, m, day, 0, 0, 0, 0, v.Location()))
return nil
default:
// 更清晰的错误信息,指导使用者
return fmt.Errorf("Date.Scan: 不支持的扫描类型 %T, 期望: string, []byte 或 time.Time", value)
}
}
// Value 改进:更精确的零值处理和返回类型
func (d Date) Value() (driver.Value, error) {
t := time.Time(d)
if t.IsZero() {
return nil, nil // 零值对应数据库NULL
}
// 返回格式化的日期字符串,确保数据库接收明确格式
return t.Format(dateFormat), nil
}
// MarshalJSON 改进:使用更安全的JSON编码方式
func (d Date) MarshalJSON() ([]byte, error) {
t := time.Time(d)
if t.IsZero() {
return []byte("null"), nil
}
// 使用标准JSON编码器,避免手动拼接可能导致的转义错误
return json.Marshal(t.Format(dateFormat))
}
// UnmarshalJSON 改进:增强格式兼容性和错误处理
func (d *Date) UnmarshalJSON(data []byte) error {
str := strings.TrimSpace(string(data))
// 处理显式null和空字符串
if str == "null" || str == `""` || str == "" {
*d = Date(time.Time{})
return nil
}
// 安全地去除JSON字符串的引号
var dateStr string
if len(str) >= 2 && str[0] == '"' && str[len(str)-1] == '"' {
dateStr = str[1 : len(str)-1]
} else {
// 如果不是引号包裹的字符串,尝试直接解析
dateStr = str
}
return d.parseString(dateStr)
}
// MarshalYAML 改进:符合yaml.v3接口标准
func (d Date) MarshalYAML() (interface{}, error) {
t := time.Time(d)
if t.IsZero() {
return nil, nil
}
return t.Format(dateFormat), nil
}
// UnmarshalYAML 改进:使用正确的yaml.v3 Node接口
func (d *Date) UnmarshalYAML(value *yaml.Node) error {
if value == nil || value.Kind != yaml.ScalarNode {
return fmt.Errorf("Date.UnmarshalYAML: 日期必须为标量值(字符串)")
}
str := strings.TrimSpace(value.Value)
if str == "" || str == "null" || str == "~" {
*d = Date(time.Time{})
return nil
}
return d.parseString(str)
}
// String 返回日期字符串表示
func (d Date) String() string {
t := time.Time(d)
if t.IsZero() {
return "" // 零值返回空字符串,符合Go惯例
}
return t.Format(dateFormat)
}
// Time 返回time.Time类型(去除时间部分)
func (d Date) Time() time.Time {
t := time.Time(d)
if t.IsZero() {
return time.Time{}
}
// 确保只返回日期部分,时间设为00:00:00
y, m, day := t.Date()
return time.Date(y, m, day, 0, 0, 0, 0, t.Location())
}
// parseString 内部解析方法改进:支持多种格式,优化错误处理
func (d *Date) parseString(s string) error {
s = strings.TrimSpace(s)
if s == "" {
*d = Date(time.Time{})
return nil
}
// 定义支持的日期格式(按优先级排序)
formats := []string{
"2006-01-02", // 标准ISO格式(优先)
"2006/01/02", // 斜杠格式
"20060102", // 紧凑格式
"02-01-2006", // 日-月-年格式
"02/01/2006", // 日/月/年格式
"January 2, 2006", // 英文全写格式
"Jan 2, 2006", // 英文缩写格式
}
var firstErr error
for _, format := range formats {
parsed, err := time.Parse(format, s)
if err == nil {
// 成功解析,提取日期部分
y, m, day := parsed.Date()
*d = Date(time.Date(y, m, day, 0, 0, 0, 0, parsed.Location()))
return nil
}
if firstErr == nil {
firstErr = err
}
}
return fmt.Errorf("Date.parseString: 无法解析日期 %q, 支持的格式示例: 2006-01-02", s)
}
// Equal 改进:精确的日期比较(只比较年月日)
func (d Date) Equal(other Date) bool {
t1 := time.Time(d).UTC().Truncate(24 * time.Hour)
t2 := time.Time(other).UTC().Truncate(24 * time.Hour)
return t1.Equal(t2)
}
// Before 检查当前日期是否在另一个日期之前
func (d Date) Before(other Date) bool {
if d.Equal(other) {
return false
}
return d.Time().Before(other.Time())
}
// After 检查当前日期是否在另一个日期之后
func (d Date) After(other Date) bool {
if d.Equal(other) {
return false
}
return d.Time().After(other.Time())
}
// IsZero 检查是否为零值
func (d Date) IsZero() bool {
return time.Time(d).IsZero()
}
// AddDays 添加指定天数
func (d Date) AddDays(days int) Date {
t := d.Time().AddDate(0, 0, days)
return Date(t)
}
// DaysBetween 计算两个日期之间的天数差(考虑自然日)
func (d Date) DaysBetween(other Date) int {
// 将两个日期都规范到UTC的零点,消除时间和时区影响
t1 := d.Time().UTC().Truncate(24 * time.Hour)
t2 := other.Time().UTC().Truncate(24 * time.Hour)
// 计算天数差(使用正确的顺序)
hours := t1.Sub(t2).Hours()
days := int(hours / 24)
return days
}
// Today 返回当前日期(忽略时间部分)
func Today() Date {
now := time.Now()
y, m, day := now.Date()
return Date(time.Date(y, m, day, 0, 0, 0, 0, now.Location()))
}
// ParseDate 从字符串解析日期
func ParseDate(s string) (Date, error) {
var d Date
err := d.parseString(s)
return d, err
}
// MustParseDate 从字符串解析日期,解析失败时panic
func MustParseDate(s string) Date {
d, err := ParseDate(s)
if err != nil {
panic(fmt.Sprintf("MustParseDate 解析失败: %q, 错误: %v", s, err))
}
return d
}
// 新增便捷方法
// Format 使用自定义格式格式化日期
func (d Date) Format(layout string) string {
return d.Time().Format(layout)
}
// Weekday 返回星期几
func (d Date) Weekday() time.Weekday {
return d.Time().Weekday()
}
// Year 返回年份
func (d Date) Year() int {
return d.Time().Year()
}
// Month 返回月份
func (d Date) Month() time.Month {
return d.Time().Month()
}
// Day 返回日期
func (d Date) Day() int {
return d.Time().Day()
}
// IsWeekend 检查是否为周末
func (d Date) IsWeekend() bool {
weekday := d.Weekday()
return weekday == time.Saturday || weekday == time.Sunday
}
// DaysSince 计算从该日期到现在的天数
func (d Date) DaysSince() int {
return Today().DaysBetween(d)
}
// MarshalCSV 实现CSV序列化方法
func (d Date) MarshalCSV() (string, error) {
t := time.Time(d)
if t.IsZero() {
return "", nil
}
return t.Format("2006-01-02"), nil
}
// UnmarshalCSV 实现CSV反序列化方法
func (d *Date) UnmarshalCSV(csv string) error {
if csv == "" {
*d = Date(time.Time{})
return nil
}
// 支持多种日期格式
formats := []string{
"2006-01-02",
"2006/01/02",
"02/01/2006",
"2006-1-2",
}
var t time.Time
var err error
for _, format := range formats {
t, err = time.Parse(format, csv)
if err == nil {
*d = Date(t)
return nil
}
}
return fmt.Errorf("无法解析日期格式: %s", csv)
}
package xtime
import (
"encoding/json"
"testing"
"time"
"gopkg.in/yaml.v3"
)
// TestDate_Scan 测试数据库扫描功能
func TestDate_Scan(t *testing.T) {
tests := []struct {
name string
input interface{}
want string
wantErr bool
}{
{
name: "有效时间类型",
input: time.Date(2023, 10, 15, 15, 30, 0, 0, time.UTC),
want: "2023-10-15",
wantErr: false,
},
{
name: "有效字节切片",
input: []byte("2023-10-15"),
want: "2023-10-15",
wantErr: false,
},
{
name: "有效字符串",
input: "2023-10-15",
want: "2023-10-15",
wantErr: false,
},
{
name: "空值处理",
input: nil,
want: "",
wantErr: false,
},
{
name: "空字符串",
input: "",
want: "",
wantErr: false,
},
{
name: "无效类型",
input: 123, // 数字类型不支持
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var d Date
err := d.Scan(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("Scan() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && d.String() != tt.want {
t.Errorf("Scan() = %v, want %v", d.String(), tt.want)
}
})
}
}
// TestDate_Value 测试数据库值生成
func TestDate_Value(t *testing.T) {
tests := []struct {
name string
input Date
want interface{}
wantErr bool
}{
{
name: "有效日期",
input: Date(time.Date(2023, 10, 15, 0, 0, 0, 0, time.UTC)),
want: "2023-10-15",
wantErr: false,
},
{
name: "零值日期",
input: Date(time.Time{}),
want: nil,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.input.Value()
if (err != nil) != tt.wantErr {
t.Errorf("Value() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && got != tt.want {
t.Errorf("Value() = %v, want %v", got, tt.want)
}
})
}
}
// TestDate_JSON 测试JSON序列化和反序列化[3](@ref)
func TestDate_JSON(t *testing.T) {
tests := []struct {
name string
input string
want string
wantErr bool
}{
{
name: "有效日期JSON",
input: `"2023-10-15"`,
want: "2023-10-15",
wantErr: false,
},
{
name: "空值JSON",
input: "null",
want: "",
wantErr: false,
},
{
name: "空字符串JSON",
input: `""`,
want: "",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 测试反序列化
var d Date
err := json.Unmarshal([]byte(tt.input), &d)
if (err != nil) != tt.wantErr {
t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && d.String() != tt.want {
t.Errorf("UnmarshalJSON() = %v, want %v", d.String(), tt.want)
return
}
// 测试序列化(仅对有效用例)
if !tt.wantErr && tt.want != "" {
bytes, err := json.Marshal(d)
if err != nil {
t.Errorf("MarshalJSON() error = %v", err)
return
}
expectedJSON := `"` + tt.want + `"`
if string(bytes) != expectedJSON {
t.Errorf("MarshalJSON() = %s, want %s", string(bytes), expectedJSON)
}
}
})
}
}
// TestDate_YAML 测试YAML序列化和反序列化
func TestDate_YAML(t *testing.T) {
tests := []struct {
name string
input string
want string
wantErr bool
}{
{
name: "有效日期YAML",
input: "2023-10-15",
want: "2023-10-15",
wantErr: false,
},
{
name: "空字符串YAML",
input: "",
want: "",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 创建YAML节点进行测试[4](@ref)
node := &yaml.Node{
Kind: yaml.ScalarNode,
Value: tt.input,
}
var d Date
err := d.UnmarshalYAML(node)
if (err != nil) != tt.wantErr {
t.Errorf("UnmarshalYAML() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && d.String() != tt.want {
t.Errorf("UnmarshalYAML() = %v, want %v", d.String(), tt.want)
}
})
}
}
// TestDate_String 测试字符串表示
func TestDate_String(t *testing.T) {
tests := []struct {
name string
input Date
want string
}{
{
name: "有效日期",
input: Date(time.Date(2023, 10, 15, 0, 0, 0, 0, time.UTC)),
want: "2023-10-15",
},
{
name: "零值日期",
input: Date(time.Time{}),
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.input.String(); got != tt.want {
t.Errorf("String() = %v, want %v", got, tt.want)
}
})
}
}
// TestParseDate 测试日期解析函数[6](@ref)
func TestParseDate(t *testing.T) {
tests := []struct {
name string
input string
want string
wantErr bool
}{
{
name: "有效日期解析",
input: "2023-10-15",
want: "2023-10-15",
wantErr: false,
},
{
name: "斜杠格式日期",
input: "2023/10/15",
want: "2023-10-15",
wantErr: false,
},
{
name: "紧凑格式日期",
input: "20231015",
want: "2023-10-15",
wantErr: false,
},
{
name: "无效日期格式",
input: "invalid-date",
want: "",
wantErr: true,
},
{
name: "空字符串",
input: "",
want: "",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseDate(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("ParseDate() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && got.String() != tt.want {
t.Errorf("ParseDate() = %v, want %v", got.String(), tt.want)
}
})
}
}
// TestDate_Comparison 测试日期比较方法[7,8](@ref)
func TestDate_Comparison(t *testing.T) {
date1 := MustParseDate("2023-10-15")
date2 := MustParseDate("2023-10-16")
date3 := MustParseDate("2023-10-15") // 与date1相同
t.Run("After方法", func(t *testing.T) {
if !date2.After(date1) {
t.Error("date2应该在date1之后")
}
if date1.After(date2) {
t.Error("date1不应该在date2之后")
}
})
t.Run("Before方法", func(t *testing.T) {
if !date1.Before(date2) {
t.Error("date1应该在date2之前")
}
if date2.Before(date1) {
t.Error("date2不应该在date1之前")
}
})
t.Run("Equal方法", func(t *testing.T) {
if !date1.Equal(date3) {
t.Error("date1应该等于date3")
}
if date1.Equal(date2) {
t.Error("date1不应该等于date2")
}
})
}
// TestDate_Arithmetic 测试日期算术运算
func TestDate_Arithmetic(t *testing.T) {
baseDate := MustParseDate("2023-10-15")
t.Run("AddDays方法", func(t *testing.T) {
result := baseDate.AddDays(5)
expected := MustParseDate("2023-10-20")
if !result.Equal(expected) {
t.Errorf("AddDays(5) = %v, want %v", result, expected)
}
result = baseDate.AddDays(-3)
expected = MustParseDate("2023-10-12")
if !result.Equal(expected) {
t.Errorf("AddDays(-3) = %v, want %v", result, expected)
}
})
t.Run("DaysBetween方法", func(t *testing.T) {
date1 := MustParseDate("2023-10-15")
date2 := MustParseDate("2023-10-20")
days := date2.DaysBetween(date1)
if days != 5 {
t.Errorf("DaysBetween() = %v, want 5", days)
}
days = date1.DaysBetween(date2)
if days != -5 {
t.Errorf("DaysBetween() = %v, want -5", days)
}
})
}
// TestDate_Components 测试日期组件获取
func TestDate_Components(t *testing.T) {
date := MustParseDate("2023-10-15")
t.Run("Year方法", func(t *testing.T) {
if year := date.Year(); year != 2023 {
t.Errorf("Year() = %v, want 2023", year)
}
})
t.Run("Month方法", func(t *testing.T) {
if month := date.Month(); month != time.October {
t.Errorf("Month() = %v, want October", month)
}
})
t.Run("Day方法", func(t *testing.T) {
if day := date.Day(); day != 15 {
t.Errorf("Day() = %v, want 15", day)
}
})
t.Run("Weekday方法", func(t *testing.T) {
// 2023-10-15是星期日
if weekday := date.Weekday(); weekday != time.Sunday {
t.Errorf("Weekday() = %v, want Sunday", weekday)
}
})
}
// TestDate_ZeroValue 测试零值处理
func TestDate_ZeroValue(t *testing.T) {
var zeroDate Date
t.Run("IsZero方法", func(t *testing.T) {
if !zeroDate.IsZero() {
t.Error("IsZero()应该返回true对于零值")
}
validDate := MustParseDate("2023-10-15")
if validDate.IsZero() {
t.Error("IsZero()应该返回false对于有效日期")
}
})
t.Run("零值的字符串表示", func(t *testing.T) {
if zeroDate.String() != "" {
t.Errorf("零值的String()应该返回空字符串, 得到: %s", zeroDate.String())
}
})
t.Run("零值的JSON序列化", func(t *testing.T) {
bytes, err := json.Marshal(zeroDate)
if err != nil {
t.Errorf("零值JSON序列化错误: %v", err)
}
if string(bytes) != "null" {
t.Errorf("零值JSON应该序列化为null, 得到: %s", string(bytes))
}
})
}
// TestToday 测试Today函数[7,8](@ref)
func TestToday(t *testing.T) {
today := Today()
now := time.Now()
expected := Date(time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()))
if !today.Equal(expected) {
t.Errorf("Today() = %v, want %v", today, expected)
}
}
// TestMustParseDate 测试MustParseDate函数
func TestMustParseDate(t *testing.T) {
t.Run("有效日期", func(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Error("有效日期不应该引起panic")
}
}()
date := MustParseDate("2023-10-15")
if date.String() != "2023-10-15" {
t.Errorf("MustParseDate() = %v, want 2023-10-15", date.String())
}
})
t.Run("无效日期应该panic", func(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Error("无效日期应该引起panic")
}
}()
_ = MustParseDate("invalid-date")
})
}
// BenchmarkDate_Parse 性能测试:日期解析[3](@ref)
func BenchmarkDate_Parse(b *testing.B) {
testCases := []string{
"2023-10-15",
"2023/10/15",
"20231015",
"",
}
for _, tc := range testCases {
b.Run(tc, func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = ParseDate(tc)
}
})
}
}
// BenchmarkDate_JSONMarshal 性能测试:JSON序列化
func BenchmarkDate_JSONMarshal(b *testing.B) {
date := MustParseDate("2023-10-15")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = json.Marshal(date)
}
}
// BenchmarkDate_JSONUnmarshal 性能测试:JSON反序列化
func BenchmarkDate_JSONUnmarshal(b *testing.B) {
testCases := []struct {
name string
data []byte
}{
{"有效日期", []byte(`"2023-10-15"`)},
{"空值", []byte(`null`)},
}
for _, tc := range testCases {
b.Run(tc.name, func(b *testing.B) {
var date Date
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = json.Unmarshal(tc.data, &date)
}
})
}
}
// BenchmarkDate_Value 性能测试:数据库值生成
func BenchmarkDate_Value(b *testing.B) {
date := MustParseDate("2023-10-15")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = date.Value()
}
}
// TestDate_EdgeCases 测试边界情况[4](@ref)
func TestDate_EdgeCases(t *testing.T) {
t.Run("闰年测试", func(t *testing.T) {
leapDate := MustParseDate("2020-02-29")
if leapDate.String() != "2020-02-29" {
t.Error("闰年日期处理错误")
}
})
t.Run("时区处理", func(t *testing.T) {
utcTime := time.Date(2023, 10, 15, 22, 0, 0, 0, time.UTC)
cstTime := time.Date(2023, 10, 16, 6, 0, 0, 0, time.FixedZone("CST", 8*3600))
utcDate := Date(utcTime)
cstDate := Date(cstTime)
if !utcDate.Equal(cstDate) {
t.Error("不同时区的同一天应该相等")
}
})
t.Run("月份边界", func(t *testing.T) {
lastDay := MustParseDate("2023-01-31")
nextMonth := lastDay.AddDays(1)
if nextMonth.String() != "2023-02-01" {
t.Errorf("月份边界处理错误: %v", nextMonth)
}
})
}
// TestDate_Integration 集成测试[1](@ref)
func TestDate_Integration(t *testing.T) {
t.Run("完整序列化循环", func(t *testing.T) {
original := MustParseDate("2023-10-15")
// JSON序列化 -> 反序列化
jsonData, err := json.Marshal(original)
if err != nil {
t.Fatalf("JSON序列化失败: %v", err)
}
var fromJSON Date
err = json.Unmarshal(jsonData, &fromJSON)
if err != nil {
t.Fatalf("JSON反序列化失败: %v", err)
}
if !original.Equal(fromJSON) {
t.Error("JSON序列化循环失败")
}
// 数据库值循环
dbValue, err := original.Value()
if err != nil {
t.Fatalf("Value()失败: %v", err)
}
var fromDB Date
err = fromDB.Scan(dbValue)
if err != nil {
t.Fatalf("Scan()失败: %v", err)
}
if !original.Equal(fromDB) {
t.Error("数据库循环失败")
}
})
}
package xtime
import (
"database/sql/driver"
"encoding/json"
"fmt"
"regexp"
"strconv"
"strings"
"time"
"gopkg.in/yaml.v3"
)
const (
Layout_YYYYMMMDDHHMMSS = "20060102150405"
Layout_DateTimeWithMS = "2006-01-02 15:04:05.000"
Layout_DateTime = "2006-01-02 15:04:05"
Layout_Date = "2006-01-02"
Layout_Time = "15:04:05"
Layout_YYYY = "2006"
Layout_YYYYMM = "2006-01"
Layout_YYYYMMDD = "2006-01-02"
Layout_YYYYMMDD2 = "2006/01/02"
Layout_YYYYMMDD3 = "20060102"
Layout_YYYYMMDDHHmmSS = "2006-01-02 15:04:05"
)
var (
LocBeiJing, _ = time.LoadLocation("Asia/Shanghai")
)
// DateTime 定义基于 time.Time 的自定义日期时间类型
type DateTime time.Time
// 定义支持的时间格式常量(按优先级排序)
const (
DateTimeFormat = "2006-01-02 15:04:05" // 标准日期时间格式
DateFormat = "2006-01-02" // 仅日期格式
RFC3339Format = time.RFC3339 // RFC3339 标准格式
CompactDateTimeFormat = "20060102150405" // 紧凑格式
)
// MarshalJSON 自定义JSON序列化
func (dt DateTime) MarshalJSON() ([]byte, error) {
t := time.Time(dt)
if t.IsZero() {
return []byte("null"), nil
}
// 使用标准JSON编码确保引号正确处理
return json.Marshal(t.Format(DateTimeFormat))
}
// UnmarshalJSON 自定义JSON反序列化,支持多种格式
func (dt *DateTime) UnmarshalJSON(data []byte) error {
str := strings.TrimSpace(string(data))
// 处理空值和null
if str == "null" || str == `""` || str == "" {
*dt = DateTime(time.Time{})
return nil
}
// 移除JSON字符串的引号
if len(str) >= 2 && str[0] == '"' && str[len(str)-1] == '"' {
str = str[1 : len(str)-1]
}
return dt.parseString(str)
}
// MarshalYAML 自定义YAML序列化
func (dt DateTime) MarshalYAML() (interface{}, error) {
t := time.Time(dt)
if t.IsZero() {
return nil, nil
}
return t.Format(DateTimeFormat), nil
}
// UnmarshalYAML 自定义YAML反序列化
func (dt *DateTime) UnmarshalYAML(value *yaml.Node) error {
if value == nil || value.Kind != yaml.ScalarNode {
return fmt.Errorf("DateTime.UnmarshalYAML: 日期时间必须为字符串值")
}
str := strings.TrimSpace(value.Value)
if str == "" || str == "null" {
*dt = DateTime(time.Time{})
return nil
}
return dt.parseString(str)
}
// String 返回日期时间的字符串表示
func (dt DateTime) String() string {
t := time.Time(dt)
if t.IsZero() {
return ""
}
return t.Format(DateTimeFormat)
}
// parseString 内部解析方法,支持多种时间格式
func (dt *DateTime) parseString(s string) error {
s = strings.TrimSpace(s)
if s == "" {
*dt = DateTime(time.Time{})
return nil
}
// 支持的时间格式列表(按优先级排序)
formats := []string{
DateTimeFormat, // "2006-01-02 15:04:05"
DateFormat, // "2006-01-02"
RFC3339Format, // RFC3339格式
"2006/01/02 15:04:05", // 斜杠分隔格式
CompactDateTimeFormat, // 紧凑格式
time.RFC1123, // HTTP日期格式
}
var firstErr error
for _, format := range formats {
parsed, err := time.Parse(format, s)
if err == nil {
*dt = DateTime(parsed)
return nil
}
if firstErr == nil {
firstErr = err
}
}
return fmt.Errorf("DateTime.parseString: 无法解析日期时间 %q, 支持的格式示例: %s", s, DateTimeFormat)
}
// Time 转换为标准的 time.Time
func (dt DateTime) Time() time.Time {
return time.Time(dt)
}
// IsZero 检查是否为零值
func (dt DateTime) IsZero() bool {
return time.Time(dt).IsZero()
}
// IsNotZero 检查是否非零值
func (dt DateTime) IsNotZero() bool {
return !dt.IsZero()
}
// Format 使用自定义格式格式化日期时间
func (dt DateTime) Format(layout string) string {
return time.Time(dt).Format(layout)
}
// Value 实现 driver.Valuer 接口,用于数据库存储
func (dt DateTime) Value() (driver.Value, error) {
t := time.Time(dt)
if t.IsZero() {
return nil, nil
}
return t, nil
}
// Scan 实现 sql.Scanner 接口,用于从数据库读取
func (dt *DateTime) Scan(value interface{}) error {
if value == nil {
*dt = DateTime(time.Time{})
return nil
}
switch v := value.(type) {
case time.Time:
*dt = DateTime(v)
return nil
case []byte:
if len(v) == 0 {
*dt = DateTime(time.Time{})
return nil
}
return dt.parseString(string(v))
case string:
if v == "" {
*dt = DateTime(time.Time{})
return nil
}
return dt.parseString(v)
default:
return fmt.Errorf("DateTime.Scan: 不支持的扫描类型 %T", value)
}
}
// After 检查当前时间是否在另一个时间之后
func (dt DateTime) After(other DateTime) bool {
return time.Time(dt).After(time.Time(other))
}
// Before 检查当前时间是否在另一个时间之前
func (dt DateTime) Before(other DateTime) bool {
return time.Time(dt).Before(time.Time(other))
}
// Equal 检查两个时间是否相等
func (dt DateTime) Equal(other DateTime) bool {
return time.Time(dt).Equal(time.Time(other))
}
// Add 添加时间间隔
func (dt DateTime) Add(duration time.Duration) DateTime {
return DateTime(time.Time(dt).Add(duration))
}
// Unix 返回Unix时间戳
func (dt DateTime) Unix() int64 {
return time.Time(dt).Unix()
}
// UnixNano 返回纳秒级Unix时间戳
func (dt DateTime) UnixNano() int64 {
return time.Time(dt).UnixNano()
}
// 构造函数
// NewDateTime 从 time.Time 创建 DateTime
func NewDateTime(t time.Time) DateTime {
return DateTime(t)
}
// Now 返回当前日期时间
func Now() DateTime {
return DateTime(time.Now())
}
// NowPtr 返回当前日期时间的指针
func NowPtr() *DateTime {
dt := DateTime(time.Now())
return &dt
}
// ParseDateTime 从字符串解析日期时间
func ParseDateTime(s string) (DateTime, error) {
var dt DateTime
err := dt.parseString(s)
return dt, err
}
// MustParseDateTime 从字符串解析日期时间,解析失败时panic
func MustParseDateTime(s string) DateTime {
dt, err := ParseDateTime(s)
if err != nil {
panic(fmt.Sprintf("MustParseDateTime 解析失败: %q, 错误: %v", s, err))
}
return dt
}
func BeginOfDay(t time.Time) DateTime {
y, m, d := t.Date()
begin := time.Date(y, m, d, 0, 0, 0, 0, t.Location())
return DateTime(begin)
}
func EndOfDay(t time.Time) DateTime {
y, m, d := t.Date()
end := time.Date(y, m, d, 23, 59, 59, int(time.Second-time.Nanosecond), t.Location())
return DateTime(end)
}
func TodayBegin() DateTime {
t := time.Now()
y, m, d := t.Date()
begin := time.Date(y, m, d, 0, 0, 0, 0, t.Location())
return DateTime(begin)
}
func TodayEnd() DateTime {
t := time.Now()
y, m, d := t.Date()
end := time.Date(y, m, d, 23, 59, 59, int(time.Second-time.Nanosecond), t.Location())
return DateTime(end)
}
var durationRegex = regexp.MustCompile(`(\d+)([a-zA-Z]+)`)
func ParseExtendedDuration(s string) (time.Duration, error) {
matches := durationRegex.FindAllStringSubmatch(s, -1)
if len(matches) == 0 {
return 0, fmt.Errorf("invalid format: %q", s)
}
// 验证完整匹配
var matched string
for _, m := range matches {
matched += m[0]
}
if matched != s {
return 0, fmt.Errorf("invalid characters in duration: %q", s)
}
var total time.Duration
for _, match := range matches {
numStr := match[1]
unit := match[2]
// unit := strings.ToUpper(match[2])
num, err := strconv.ParseInt(numStr, 10, 64)
if err != nil {
return 0, fmt.Errorf("invalid number %q: %v", numStr, err)
}
var d time.Duration
switch unit {
case "d", "D":
d = time.Duration(num) * 24 * time.Hour
case "w", "W":
d = time.Duration(num) * 7 * 24 * time.Hour
case "M":
d = time.Duration(num) * 30 * 24 * time.Hour
case "y", "Y":
d = time.Duration(num) * 365 * 24 * time.Hour
default:
// 回退到标准库解析
if dur, err := time.ParseDuration(match[0]); err == nil {
d = dur
} else {
return 0, fmt.Errorf("unknown unit %q", unit)
}
}
total += d
}
return total, nil
}
func (x DateTime) FormatYYYYMMDDHHmmSS() string {
return x.Time().Format(Layout_YYYYMMDDHHmmSS)
}
func (x DateTime) Date() DateTime {
y, m, d := x.Time().Date()
trimmedTime := time.Date(y, m, d, 0, 0, 0, 0, x.Time().Location())
return NewDateTime(trimmedTime)
}
func (x DateTime) WeekStart() DateTime {
return x.WeekEnd().AddDate(0, 0, -6)
}
func (x DateTime) WeekEnd() DateTime {
weekday := int(x.Time().Weekday())
if weekday == 0 {
weekday = 7
}
return x.AddDate(0, 0, +7-weekday)
}
func (t0 DateTime) MonthStart() DateTime {
year, month, _ := t0.Time().Date()
t1 := time.Date(year, month, 1, 0, 0, 0, 0, t0.Time().Location())
return DateTime(t1)
}
func (t0 DateTime) MonthEnd() DateTime {
year, month, _ := t0.Time().Date()
t1 := time.Date(year, month+1, 0, 0, 0, 0, 0, t0.Time().Location())
return DateTime(t1)
}
// func (t DateTime) AddDate(y, m, d int) DateTime {
// return DateTime(t.Time().AddDate(y, m, d))
// }
// AddDate 添加年、月、日,正确处理月份边界溢出
func (dt DateTime) AddDate(years, months, days int) DateTime {
t := time.Time(dt)
y, m, d := t.Date()
h, min, s := t.Clock()
nsec := t.Nanosecond()
loc := t.Location()
// 计算新月份和新年
newMonth := int(m) + months
newYear := y + years + (newMonth-1)/12
newMonth = (newMonth-1)%12 + 1
if newMonth <= 0 {
newMonth += 12
newYear--
}
// 计算新月的最大天数
maxDay := daysInMonth(newYear, time.Month(newMonth))
newDay := d
if d > maxDay {
newDay = maxDay
}
// 创建新时间,年月日部分
newTime := time.Date(newYear, time.Month(newMonth), newDay, h, min, s, nsec, loc)
// 然后加减天数
newTime = newTime.AddDate(0, 0, days)
return DateTime(newTime)
}
// WeekDay 1-7 周一到周日
func (x DateTime) WeekDay() int {
return ((int(x.Time().Weekday()) + 6) % 7) + 1
}
// 1-31 1号到31号
// func (x DateTime) Day() int {
// return int(x.Time().Day())
// }
// 1-12 1月到12月
func (x DateTime) Month() int {
return int(x.Time().Month())
}
func daysInMonth(year int, month time.Month) int {
// 下个月的第0天是本月最后一天
return time.Date(year, month+1, 0, 0, 0, 0, 0, time.UTC).Day()
}
// 转为北京时间
func (x DateTime) LocalBeiJing() DateTime {
return DateTime(x.Time().In(LocBeiJing))
}
func (x DateTime) Local() DateTime {
return DateTime(x.Time().In(time.Local))
}
func (x DateTime) Quarter() int {
return (int(x.Time().Month())-1)/3 + 1
}
func ParseYYYYMMDD(value string) DateTime {
t, err := time.Parse(Layout_YYYYMMDD, value)
if err != nil {
return DateTime{}
}
return DateTime(t)
}
func ParseYYYYMMDDHHmmSS(value string) DateTime {
t, err := time.Parse(Layout_YYYYMMDDHHmmSS, value)
if err != nil {
return DateTime{}
}
return DateTime(t)
}
func IsSameDay(t1, t2 DateTime) bool {
t1Local := t1.Local()
t2Local := t2.Local()
return t1Local.Year() == t2Local.Year() &&
t1Local.Month() == t2Local.Month() &&
t1Local.Day() == t2Local.Day()
}
// 是否同一周
func IsSameWeek(t0, t1 DateTime) bool {
y1, w1 := t0.Time().Local().ISOWeek()
y2, w2 := t1.Time().Local().ISOWeek()
return y1 == y2 && w1 == w2
}
func NotSameWeek(t0, t1 DateTime) bool {
return !IsSameWeek(t0, t1)
}
// 是否同一个月
func IsSameMonth(t0, t1 DateTime) bool {
return t0.Month() == t1.Month()
}
func NotSameMonth(t0, t1 DateTime) bool {
return !IsSameMonth(t0, t1)
}
// 是否同一季度
func IsSameQuarter(t0, t1 DateTime) bool {
return t0.Quarter() == t1.Quarter()
}
func NotSameSeason(t0, t1 DateTime) bool {
return !IsSameQuarter(t0, t1)
}
// 是否同一年
func IsSameYear(t0, t1 DateTime) bool {
return t0.Time().Year() == t1.Time().Year()
}
func NotSameYear(t0, t1 DateTime) bool {
return !IsSameYear(t0, t1)
}
func DayBeforeYesterday() DateTime {
return DateTime(time.Now().AddDate(0, 0, -2))
}
func Yesterday() DateTime {
return DateTime(time.Now().AddDate(0, 0, -1))
}
func TodayDateTime() DateTime {
return DateTime(time.Now())
}
func (t0 DateTime) PreviousQuarter(n int) DateTime {
month := t0.Month()
quarterStartMonth := time.Month((int(month)-1)/3*3 + 1)
currentQuarterStart := time.Date(
t0.Year(), quarterStartMonth, 1,
0, 0, 0, 0, t0.Time().Location(),
)
return DateTime(currentQuarterStart.AddDate(0, -3*n, 0))
}
func (t0 DateTime) FormatYYYYQT() string {
month := t0.Month()
quarter := (int(month)-1)/3 + 1
return fmt.Sprintf("%s_%d", t0.Format(Layout_YYYY), quarter)
}
func DaysBetween(t0, t1 DateTime) int {
t2 := time.Date(t0.Year(), t0.Time().Month(), t0.Day(), 0, 0, 0, 0, t0.Time().Location())
t3 := time.Date(t1.Year(), t1.Time().Month(), t1.Day(), 0, 0, 0, 0, t1.Time().Location())
diff := t2.Sub(t3)
days := int(diff.Hours() / 24)
return days
}
// 时间组件获取方法
func (dt DateTime) Hour() int {
return time.Time(dt).Hour()
}
func (dt DateTime) Minute() int {
return time.Time(dt).Minute()
}
func (dt DateTime) Second() int {
return time.Time(dt).Second()
}
func (dt DateTime) Year() int {
return time.Time(dt).Year()
}
// func (dt DateTime) Month() time.Month {
// return time.Time(dt).Month()
// }
func (dt DateTime) Day() int {
return time.Time(dt).Day()
}
// 月份常量(Go语言time包内置,直接使用)
// time.January (1月), time.February (2月), time.March (3月)
// time.April (4月), time.May (5月), time.June (6月)
// time.July (7月), time.August (8月), time.September (9月)
// time.October (10月), time.November (11月), time.December (12月)
// MarshalCSV 实现CSV序列化方法
func (d DateTime) MarshalCSV() (string, error) {
t := time.Time(d)
if t.IsZero() {
return "", nil
}
return t.Format("2006-01-02 15:04:05"), nil
}
// UnmarshalCSV 实现CSV反序列化方法
func (d *DateTime) UnmarshalCSV(csv string) error {
if csv == "" {
*d = DateTime(time.Time{})
return nil
}
// 支持多种时间格式
formats := []string{
"2006-01-02 15:04:05",
"2006-01-02",
time.RFC3339,
time.RFC3339Nano,
}
var t time.Time
var err error
for _, format := range formats {
t, err = time.Parse(format, csv)
if err == nil {
*d = DateTime(t)
return nil
}
}
return fmt.Errorf("无法解析时间格式: %s", csv)
}
package xtime
import (
"encoding/json"
"testing"
"time"
"gopkg.in/yaml.v3"
)
// TestDateTime_Scan 测试数据库扫描功能
func TestDateTime_Scan(t *testing.T) {
tests := []struct {
name string
input interface{}
want string
wantErr bool
}{
{
name: "有效time.Time类型",
input: time.Date(2023, 10, 17, 14, 30, 0, 0, time.UTC),
want: "2023-10-17 14:30:00",
wantErr: false,
},
{
name: "有效字节切片",
input: []byte("2023-10-17 14:30:00"),
want: "2023-10-17 14:30:00",
wantErr: false,
},
{
name: "有效字符串",
input: "2023-10-17 14:30:00",
want: "2023-10-17 14:30:00",
wantErr: false,
},
{
name: "空值处理",
input: nil,
want: "",
wantErr: false,
},
{
name: "空字符串",
input: "",
want: "",
wantErr: false,
},
{
name: "空字节切片",
input: []byte{},
want: "",
wantErr: false,
},
{
name: "无效类型",
input: 123,
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var dt DateTime
err := dt.Scan(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("Scan() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && dt.String() != tt.want {
t.Errorf("Scan() = %v, want %v", dt.String(), tt.want)
}
})
}
}
// TestDateTime_Value 测试数据库值生成
func TestDateTime_Value(t *testing.T) {
tests := []struct {
name string
input DateTime
want interface{}
wantErr bool
}{
{
name: "有效日期时间",
input: NewDateTime(time.Date(2023, 10, 17, 14, 30, 0, 0, time.UTC)),
want: time.Date(2023, 10, 17, 14, 30, 0, 0, time.UTC),
wantErr: false,
},
{
name: "零值日期时间",
input: DateTime{},
want: nil,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.input.Value()
if (err != nil) != tt.wantErr {
t.Errorf("Value() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr {
if tt.want == nil {
if got != nil {
t.Errorf("Value() = %v, want nil", got)
}
} else {
wantTime := tt.want.(time.Time)
gotTime := got.(time.Time)
if !gotTime.Equal(wantTime) {
t.Errorf("Value() = %v, want %v", got, tt.want)
}
}
}
})
}
}
// TestDateTime_JSON 测试JSON序列化和反序列化
func TestDateTime_JSON(t *testing.T) {
tests := []struct {
name string
input string
want string
wantErr bool
}{
{
name: "有效日期时间JSON",
input: `"2023-10-17 14:30:00"`,
want: "2023-10-17 14:30:00",
wantErr: false,
},
{
name: "空值JSON",
input: "null",
want: "",
wantErr: false,
},
{
name: "空字符串JSON",
input: `""`,
want: "",
wantErr: false,
},
{
name: "无效日期时间格式",
input: `"2023-13-45 25:70:00"`,
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 测试反序列化
var dt DateTime
err := json.Unmarshal([]byte(tt.input), &dt)
if (err != nil) != tt.wantErr {
t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && dt.String() != tt.want {
t.Errorf("UnmarshalJSON() = %v, want %v", dt.String(), tt.want)
return
}
// 测试序列化(仅对有效用例)
if !tt.wantErr && tt.want != "" {
bytes, err := json.Marshal(dt)
if err != nil {
t.Errorf("MarshalJSON() error = %v", err)
return
}
expectedJSON := `"` + tt.want + `"`
if string(bytes) != expectedJSON {
t.Errorf("MarshalJSON() = %s, want %s", string(bytes), expectedJSON)
}
}
})
}
}
// TestDateTime_YAML 测试YAML序列化和反序列化
func TestDateTime_YAML(t *testing.T) {
tests := []struct {
name string
input string
want string
wantErr bool
}{
{
name: "有效日期时间YAML",
input: "2023-10-17 14:30:00",
want: "2023-10-17 14:30:00",
wantErr: false,
},
{
name: "空字符串YAML",
input: "",
want: "",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 创建YAML节点进行测试
node := &yaml.Node{
Kind: yaml.ScalarNode,
Value: tt.input,
}
var dt DateTime
err := dt.UnmarshalYAML(node)
if (err != nil) != tt.wantErr {
t.Errorf("UnmarshalYAML() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && dt.String() != tt.want {
t.Errorf("UnmarshalYAML() = %v, want %v", dt.String(), tt.want)
}
// 测试序列化
if !tt.wantErr && tt.want != "" {
result, err := dt.MarshalYAML()
if err != nil {
t.Errorf("MarshalYAML() error = %v", err)
return
}
if result != tt.want {
t.Errorf("MarshalYAML() = %v, want %v", result, tt.want)
}
}
})
}
}
// TestDateTime_String 测试字符串表示
func TestDateTime_String(t *testing.T) {
tests := []struct {
name string
input DateTime
want string
}{
{
name: "有效日期时间",
input: NewDateTime(time.Date(2023, 10, 17, 14, 30, 0, 0, time.UTC)),
want: "2023-10-17 14:30:00",
},
{
name: "零值日期时间",
input: DateTime{},
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.input.String(); got != tt.want {
t.Errorf("String() = %v, want %v", got, tt.want)
}
})
}
}
// TestParseDateTime 测试日期时间解析函数
func TestParseDateTime(t *testing.T) {
tests := []struct {
name string
input string
want string
wantErr bool
}{
{
name: "标准格式解析",
input: "2023-10-17 14:30:00",
want: "2023-10-17 14:30:00",
wantErr: false,
},
{
name: "斜杠格式日期",
input: "2023/10/17 14:30:00",
want: "2023-10-17 14:30:00",
wantErr: false,
},
{
name: "RFC3339格式",
input: "2023-10-17T14:30:00Z",
want: "2023-10-17 14:30:00",
wantErr: false,
},
{
name: "紧凑格式",
input: "20231017143000",
want: "2023-10-17 14:30:00",
wantErr: false,
},
{
name: "无效日期时间格式",
input: "invalid-datetime",
want: "",
wantErr: true,
},
{
name: "空字符串",
input: "",
want: "",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseDateTime(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("ParseDateTime() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && got.String() != tt.want {
t.Errorf("ParseDateTime() = %v, want %v", got.String(), tt.want)
}
})
}
}
// TestDateTime_Comparison 测试日期时间比较方法
func TestDateTime_Comparison(t *testing.T) {
dt1 := MustParseDateTime("2023-10-17 14:30:00")
dt2 := MustParseDateTime("2023-10-17 15:45:00")
dt3 := MustParseDateTime("2023-10-17 14:30:00") // 与dt1相同
t.Run("After方法", func(t *testing.T) {
if !dt2.After(dt1) {
t.Error("dt2应该在dt1之后")
}
if dt1.After(dt2) {
t.Error("dt1不应该在dt2之后")
}
})
t.Run("Before方法", func(t *testing.T) {
if !dt1.Before(dt2) {
t.Error("dt1应该在dt2之前")
}
if dt2.Before(dt1) {
t.Error("dt2不应该在dt1之前")
}
})
t.Run("Equal方法", func(t *testing.T) {
if !dt1.Equal(dt3) {
t.Error("dt1应该等于dt3")
}
if dt1.Equal(dt2) {
t.Error("dt1不应该等于dt2")
}
})
}
// TestDateTime_Arithmetic 测试日期时间算术运算
func TestDateTime_Arithmetic(t *testing.T) {
baseDateTime := MustParseDateTime("2023-10-17 14:30:00")
t.Run("Add方法", func(t *testing.T) {
result := baseDateTime.Add(2 * time.Hour)
expected := MustParseDateTime("2023-10-17 16:30:00")
if !result.Equal(expected) {
t.Errorf("Add(2小时) = %v, want %v", result, expected)
}
result = baseDateTime.Add(-30 * time.Minute)
expected = MustParseDateTime("2023-10-17 14:00:00")
if !result.Equal(expected) {
t.Errorf("Add(-30分钟) = %v, want %v", result, expected)
}
})
t.Run("AddDate方法", func(t *testing.T) {
result := baseDateTime.AddDate(0, 1, 0) // 加1个月
expected := MustParseDateTime("2023-11-17 14:30:00")
if !result.Equal(expected) {
t.Errorf("AddDate(0,1,0) = %v, want %v", result, expected)
}
result = baseDateTime.AddDate(1, 0, 0) // 加1年
expected = MustParseDateTime("2024-10-17 14:30:00")
if !result.Equal(expected) {
t.Errorf("AddDate(1,0,0) = %v, want %v", result, expected)
}
})
}
// TestDateTime_Components 测试日期时间组件获取
func TestDateTime_Components(t *testing.T) {
dt := MustParseDateTime("2023-10-17 14:30:45")
t.Run("Year方法", func(t *testing.T) {
if year := dt.Year(); year != 2023 {
t.Errorf("Year() = %v, want 2023", year)
}
})
t.Run("Month方法", func(t *testing.T) {
if month := dt.Month(); month != 10 {
t.Errorf("Month() = %v, want October", month)
}
})
t.Run("Day方法", func(t *testing.T) {
if day := dt.Day(); day != 17 {
t.Errorf("Day() = %v, want 17", day)
}
})
t.Run("Hour方法", func(t *testing.T) {
if hour := dt.Hour(); hour != 14 {
t.Errorf("Hour() = %v, want 14", hour)
}
})
t.Run("Minute方法", func(t *testing.T) {
if minute := dt.Minute(); minute != 30 {
t.Errorf("Minute() = %v, want 30", minute)
}
})
t.Run("Second方法", func(t *testing.T) {
if second := dt.Second(); second != 45 {
t.Errorf("Second() = %v, want 45", second)
}
})
}
// TestDateTime_ZeroValue 测试零值处理
func TestDateTime_ZeroValue(t *testing.T) {
var zeroDateTime DateTime
t.Run("IsZero方法", func(t *testing.T) {
if !zeroDateTime.IsZero() {
t.Error("IsZero()应该返回true对于零值")
}
validDateTime := MustParseDateTime("2023-10-17 14:30:00")
if validDateTime.IsZero() {
t.Error("IsZero()应该返回false对于有效日期时间")
}
})
t.Run("IsNotZero方法", func(t *testing.T) {
if zeroDateTime.IsNotZero() {
t.Error("IsNotZero()应该返回false对于零值")
}
validDateTime := MustParseDateTime("2023-10-17 14:30:00")
if !validDateTime.IsNotZero() {
t.Error("IsNotZero()应该返回true对于有效日期时间")
}
})
t.Run("零值的字符串表示", func(t *testing.T) {
if zeroDateTime.String() != "" {
t.Errorf("零值的String()应该返回空字符串, 得到: %s", zeroDateTime.String())
}
})
t.Run("零值的JSON序列化", func(t *testing.T) {
bytes, err := json.Marshal(zeroDateTime)
if err != nil {
t.Errorf("零值JSON序列化错误: %v", err)
}
if string(bytes) != "null" {
t.Errorf("零值JSON应该序列化为null, 得到: %s", string(bytes))
}
})
}
// TestNow 测试Now函数
func TestNow(t *testing.T) {
now := Now()
current := time.Now()
// 检查返回的日期时间组件是否匹配当前时间(允许微小误差)
if now.Year() != current.Year() || now.Month() != int(current.Month()) || now.Day() != current.Day() {
t.Error("Now()返回的日期应该与当前日期匹配")
}
}
// TestMustParseDateTime 测试MustParseDateTime函数
func TestMustParseDateTime(t *testing.T) {
t.Run("有效日期时间", func(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Error("有效日期时间不应该引起panic")
}
}()
dt := MustParseDateTime("2023-10-17 14:30:00")
if dt.String() != "2023-10-17 14:30:00" {
t.Errorf("MustParseDateTime() = %v, want 2023-10-17 14:30:00", dt.String())
}
})
t.Run("无效日期时间应该panic", func(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Error("无效日期时间应该引起panic")
}
}()
_ = MustParseDateTime("invalid-datetime")
})
}
// TestDateTime_Format 测试自定义格式格式化
func TestDateTime_Format(t *testing.T) {
dt := MustParseDateTime("2023-10-17 14:30:00")
t.Run("自定义格式", func(t *testing.T) {
formatted := dt.Format("2006/01/02 15:04")
expected := "2023/10/17 14:30"
if formatted != expected {
t.Errorf("Format() = %v, want %v", formatted, expected)
}
})
t.Run("日期格式", func(t *testing.T) {
formatted := dt.Format("2006-01-02")
expected := "2023-10-17"
if formatted != expected {
t.Errorf("Format() = %v, want %v", formatted, expected)
}
})
}
// TestDateTime_Time 测试Time方法
func TestDateTime_Time(t *testing.T) {
dt := MustParseDateTime("2023-10-17 14:30:00")
timeVal := dt.Time()
if timeVal.Year() != 2023 || timeVal.Month() != 10 || timeVal.Day() != 17 ||
timeVal.Hour() != 14 || timeVal.Minute() != 30 || timeVal.Second() != 0 {
t.Error("Time()返回的time.Time值与原始值不匹配")
}
}
// TestDateTime_Unix 测试时间戳方法
func TestDateTime_Unix(t *testing.T) {
dt := MustParseDateTime("2023-10-17 14:30:00")
unixTime := dt.Unix()
// 验证时间戳是否正确(使用已知值进行验证)
expected := time.Date(2023, 10, 17, 14, 30, 0, 0, time.UTC).Unix()
if unixTime != expected {
t.Errorf("Unix() = %v, want %v", unixTime, expected)
}
}
// TestDateTime_EdgeCases 测试边界情况
func TestDateTime_EdgeCases(t *testing.T) {
t.Run("闰年测试", func(t *testing.T) {
leapDateTime := MustParseDateTime("2020-02-29 14:30:00")
if leapDateTime.String() != "2020-02-29 14:30:00" {
t.Error("闰年日期时间处理错误")
}
})
t.Run("时区处理", func(t *testing.T) {
utcTime := time.Date(2023, 10, 17, 22, 0, 0, 0, time.UTC)
cstTime := time.Date(2023, 10, 18, 6, 0, 0, 0, time.FixedZone("CST", 8*3600))
utcDateTime := DateTime(utcTime)
cstDateTime := DateTime(cstTime)
// 不同时区的相同时间点应该相等
if !utcDateTime.Equal(cstDateTime) {
t.Error("不同时区的相同时间点应该相等")
}
})
t.Run("月份边界", func(t *testing.T) {
lastDay := MustParseDateTime("2023-01-31 23:59:59")
nextMonth := lastDay.AddDate(0, 1, 0) // 加1个月
if nextMonth.String() != "2023-02-28 23:59:59" {
t.Errorf("月份边界处理错误: %v", nextMonth)
}
})
}
// TestDateTime_Integration 集成测试
func TestDateTime_Integration(t *testing.T) {
t.Run("完整序列化循环", func(t *testing.T) {
original := MustParseDateTime("2023-10-17 14:30:00")
// JSON序列化 -> 反序列化
jsonData, err := json.Marshal(original)
if err != nil {
t.Fatalf("JSON序列化失败: %v", err)
}
var fromJSON DateTime
err = json.Unmarshal(jsonData, &fromJSON)
if err != nil {
t.Fatalf("JSON反序列化失败: %v", err)
}
if !original.Equal(fromJSON) {
t.Error("JSON序列化循环失败")
}
// 数据库值循环
dbValue, err := original.Value()
if err != nil {
t.Fatalf("Value()失败: %v", err)
}
var fromDB DateTime
err = fromDB.Scan(dbValue)
if err != nil {
t.Fatalf("Scan()失败: %v", err)
}
if !original.Equal(fromDB) {
t.Error("数据库循环失败")
}
})
}
// BenchmarkDateTime_Parse 性能测试:日期时间解析
func BenchmarkDateTime_Parse(b *testing.B) {
testCases := []string{
"2023-10-17 14:30:00",
"2023/10/17 14:30:00",
"2023-10-17T14:30:00Z",
"",
}
for _, tc := range testCases {
b.Run(tc, func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = ParseDateTime(tc)
}
})
}
}
// BenchmarkDateTime_JSONMarshal 性能测试:JSON序列化
func BenchmarkDateTime_JSONMarshal(b *testing.B) {
dt := MustParseDateTime("2023-10-17 14:30:00")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = json.Marshal(dt)
}
}
// BenchmarkDateTime_JSONUnmarshal 性能测试:JSON反序列化
func BenchmarkDateTime_JSONUnmarshal(b *testing.B) {
testCases := []struct {
name string
data []byte
}{
{"有效日期时间", []byte(`"2023-10-17 14:30:00"`)},
{"空值", []byte(`null`)},
}
for _, tc := range testCases {
b.Run(tc.name, func(b *testing.B) {
var dt DateTime
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = json.Unmarshal(tc.data, &dt)
}
})
}
}
package xtime
import (
"database/sql/driver"
"encoding/json"
"fmt"
"strings"
"time"
"gopkg.in/yaml.v3"
)
type Time time.Time
const timeFormat = "15:04:05"
// Scan 改进:增强空值处理和错误信息
func (t *Time) Scan(value interface{}) error {
if value == nil {
*t = Time(time.Time{}) // 明确处理数据库NULL值
return nil
}
switch v := value.(type) {
case []byte:
if len(v) == 0 {
*t = Time(time.Time{})
return nil
}
return t.parseString(string(v))
case string:
if v == "" {
*t = Time(time.Time{})
return nil
}
return t.parseString(v)
case time.Time:
// 提取时间部分,忽略日期
hour, min, sec := v.Clock()
*t = Time(time.Date(0, 1, 1, hour, min, sec, 0, v.Location()))
return nil
default:
return fmt.Errorf("Time.Scan: 不支持的扫描类型 %T, 期望: string, []byte 或 time.Time", value)
}
}
// Value 改进:更安全的零值处理
func (t Time) Value() (driver.Value, error) {
if t.IsZero() {
return nil, nil // 零值对应数据库NULL
}
return t.Time().Format(timeFormat), nil
}
// MarshalJSON 改进:使用标准JSON编码和更好的零值处理
func (t Time) MarshalJSON() ([]byte, error) {
if t.IsZero() {
return []byte("null"), nil
}
// 使用标准JSON编码器避免手动拼接问题
return json.Marshal(t.Time().Format(timeFormat))
}
// UnmarshalJSON 改进:增强格式兼容性
func (t *Time) UnmarshalJSON(data []byte) error {
str := strings.TrimSpace(string(data))
// 处理null和空值
if str == "null" || str == `""` || str == "" {
*t = Time(time.Time{})
return nil
}
// 安全去除JSON引号
var timeStr string
if len(str) >= 2 && str[0] == '"' && str[len(str)-1] == '"' {
timeStr = str[1 : len(str)-1]
} else {
timeStr = str
}
return t.parseString(timeStr)
}
// MarshalYAML 改进:符合yaml.v3接口
func (t Time) MarshalYAML() (interface{}, error) {
if t.IsZero() {
return nil, nil
}
return t.Time().Format(timeFormat), nil
}
// UnmarshalYAML 改进:使用正确的yaml.v3接口
func (t *Time) UnmarshalYAML(value *yaml.Node) error {
if value == nil || value.Kind != yaml.ScalarNode {
return fmt.Errorf("Time.UnmarshalYAML: 时间必须为标量值(字符串)")
}
str := strings.TrimSpace(value.Value)
if str == "" || str == "null" {
*t = Time(time.Time{})
return nil
}
return t.parseString(str)
}
// Time 返回time.Time类型(仅包含时间部分)
func (t Time) Time() time.Time {
tm := time.Time(t)
if tm.IsZero() {
return time.Time{}
}
// 确保只返回时间部分,日期设为基准值
hour, min, sec := tm.Clock()
return time.Date(0, 1, 1, hour, min, sec, 0, tm.Location())
}
// String 返回时间字符串表示
func (t Time) String() string {
if t.IsZero() {
return ""
}
return t.Time().Format(timeFormat)
}
// IsZero 检查是否为零值
func (t Time) IsZero() bool {
return time.Time(t).IsZero()
}
// 内部解析方法 - 统一时间解析逻辑
// func (t *Time) parseString(s string) error {
// s = strings.TrimSpace(s)
// if s == "" {
// *t = Time(time.Time{})
// return nil
// }
// // 支持多种时间格式
// formats := []string{
// "15:04:05", // 标准格式
// "15:04", // 省略秒
// "15:04:05.000", // 带毫秒
// }
// var firstErr error
// for _, format := range formats {
// parsed, err := time.Parse(format, s)
// if err == nil {
// // 成功解析,提取时间部分
// hour, min, sec := parsed.Clock()
// *t = Time(time.Date(0, 1, 1, hour, min, sec, 0, parsed.Location()))
// return nil
// }
// if firstErr == nil {
// firstErr = err
// }
// }
// return fmt.Errorf("Time.parseString: 无法解析时间 %q, 支持的格式示例: 15:04:05", s)
// }
// parseString 内部解析方法 - 统一时间解析逻辑
func (t *Time) parseString(s string) error {
s = strings.TrimSpace(s)
if s == "" {
*t = Time(time.Time{})
return nil
}
// 支持多种时间格式(按优先级排序)
formats := []string{
"15:04:05", // 标准格式
"15:04", // 省略秒
"15:04:05.000", // 带毫秒
"15:04:05.000000", // 带微秒
"15:04:05.000000000", // 带纳秒
"150405", // 紧凑格式(6位数字:小时分钟秒)
"15:04:05-0700", // 带时区偏移
"15:04:05Z07:00", // 带时区(RFC3339格式)
}
var firstErr error
for _, format := range formats {
parsed, err := time.Parse(format, s)
if err == nil {
// 成功解析,提取时间部分
hour, min, sec := parsed.Clock()
*t = Time(time.Date(0, 1, 1, hour, min, sec, 0, parsed.Location()))
return nil
}
if firstErr == nil {
firstErr = err
}
}
return fmt.Errorf("Time.parseString: 无法解析时间 %q, 支持的格式示例: 15:04:05, 15:04, 143000", s)
}
// 新增实用方法
// Hour 返回小时
func (t Time) Hour() int {
return t.Time().Hour()
}
// Minute 返回分钟
func (t Time) Minute() int {
return t.Time().Minute()
}
// Second 返回秒
func (t Time) Second() int {
return t.Time().Second()
}
// Before 检查当前时间是否在另一个时间之前
func (t Time) Before(other Time) bool {
return t.Time().Before(other.Time())
}
// After 检查当前时间是否在另一个时间之后
func (t Time) After(other Time) bool {
return t.Time().After(other.Time())
}
// Equal 检查两个时间是否相等
func (t Time) Equal(other Time) bool {
return t.Time().Equal(other.Time())
}
// AddHours 添加指定小时
func (t Time) AddHours(hours int) Time {
newTime := t.Time().Add(time.Duration(hours) * time.Hour)
return Time(newTime)
}
// AddMinutes 添加指定分钟
func (t Time) AddMinutes(minutes int) Time {
newTime := t.Time().Add(time.Duration(minutes) * time.Minute)
return Time(newTime)
}
// ParseTime 从字符串解析时间
func ParseTime(s string) (Time, error) {
var t Time
err := t.parseString(s)
return t, err
}
// MustParseTime 从字符串解析时间,解析失败时panic
func MustParseTime(s string) Time {
t, err := ParseTime(s)
if err != nil {
panic(fmt.Sprintf("MustParseTime 解析失败: %q, 错误: %v", s, err))
}
return t
}
// NowTime 返回当前时间(忽略日期部分)
func NowTime() Time {
now := time.Now()
return Time(time.Date(0, 1, 1, now.Hour(), now.Minute(), now.Second(), 0, now.Location()))
}
// MarshalCSV 实现CSV序列化方法
func (t Time) MarshalCSV() (string, error) {
tm := time.Time(t)
if tm.IsZero() {
return "", nil
}
return tm.Format("15:04:05"), nil
}
// UnmarshalCSV 实现CSV反序列化方法
func (t *Time) UnmarshalCSV(csv string) error {
if csv == "" {
*t = Time(time.Time{})
return nil
}
// 支持多种时间格式
formats := []string{
"15:04:05",
"15:04",
"3:04:05 PM",
"15:04:05.000",
}
var tm time.Time
var err error
for _, format := range formats {
tm, err = time.Parse(format, csv)
if err == nil {
// 使用固定的日期部分(如今天)但只保留时间
now := time.Now()
combined := time.Date(now.Year(), now.Month(), now.Day(),
tm.Hour(), tm.Minute(), tm.Second(), 0, time.Local)
*t = Time(combined)
return nil
}
}
return fmt.Errorf("无法解析时间格式: %s", csv)
}
package xtime
import (
"encoding/json"
"testing"
"time"
"gopkg.in/yaml.v3"
)
// TestTime_Scan 测试数据库扫描功能
func TestTime_Scan(t *testing.T) {
tests := []struct {
name string
input interface{}
want string
wantErr bool
}{
{
name: "有效字节切片",
input: []byte("14:30:00"),
want: "14:30:00",
wantErr: false,
},
{
name: "有效字符串",
input: "14:30:00",
want: "14:30:00",
wantErr: false,
},
{
name: "有效time.Time类型",
input: time.Date(0, 1, 1, 14, 30, 0, 0, time.UTC),
want: "14:30:00",
wantErr: false,
},
{
name: "空值处理",
input: nil,
want: "",
wantErr: false,
},
{
name: "空字符串",
input: "",
want: "",
wantErr: false,
},
{
name: "空字节切片",
input: []byte{},
want: "",
wantErr: false,
},
{
name: "无效类型",
input: 123,
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var tm Time
err := tm.Scan(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("Scan() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && tm.String() != tt.want {
t.Errorf("Scan() = %v, want %v", tm.String(), tt.want)
}
})
}
}
// TestTime_Value 测试数据库值生成
func TestTime_Value(t *testing.T) {
tests := []struct {
name string
input Time
want interface{}
wantErr bool
}{
{
name: "有效时间",
input: MustParseTime("14:30:00"),
want: "14:30:00",
wantErr: false,
},
{
name: "零值时间",
input: Time{},
want: nil,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.input.Value()
if (err != nil) != tt.wantErr {
t.Errorf("Value() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && got != tt.want {
t.Errorf("Value() = %v, want %v", got, tt.want)
}
})
}
}
// TestTime_JSON 测试JSON序列化和反序列化
func TestTime_JSON(t *testing.T) {
tests := []struct {
name string
input string
want string
wantErr bool
}{
{
name: "有效时间JSON",
input: `"14:30:00"`,
want: "14:30:00",
wantErr: false,
},
{
name: "空值JSON",
input: "null",
want: "",
wantErr: false,
},
{
name: "空字符串JSON",
input: `""`,
want: "",
wantErr: false,
},
{
name: "无效时间格式",
input: `"25:70:00"`, // 无效的时间
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 测试反序列化
var tm Time
err := json.Unmarshal([]byte(tt.input), &tm)
if (err != nil) != tt.wantErr {
t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && tm.String() != tt.want {
t.Errorf("UnmarshalJSON() = %v, want %v", tm.String(), tt.want)
return
}
// 测试序列化(仅对有效用例)
if !tt.wantErr && tt.want != "" {
bytes, err := json.Marshal(tm)
if err != nil {
t.Errorf("MarshalJSON() error = %v", err)
return
}
expectedJSON := `"` + tt.want + `"`
if string(bytes) != expectedJSON {
t.Errorf("MarshalJSON() = %s, want %s", string(bytes), expectedJSON)
}
}
})
}
}
// TestTime_YAML 测试YAML序列化和反序列化
func TestTime_YAML(t *testing.T) {
tests := []struct {
name string
input string
want string
wantErr bool
}{
{
name: "有效时间YAML",
input: "14:30:00",
want: "14:30:00",
wantErr: false,
},
{
name: "空字符串YAML",
input: "",
want: "",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 创建YAML节点进行测试
node := &yaml.Node{
Kind: yaml.ScalarNode,
Value: tt.input,
}
var tm Time
err := tm.UnmarshalYAML(node)
if (err != nil) != tt.wantErr {
t.Errorf("UnmarshalYAML() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && tm.String() != tt.want {
t.Errorf("UnmarshalYAML() = %v, want %v", tm.String(), tt.want)
}
// 测试序列化
if !tt.wantErr && tt.want != "" {
result, err := tm.MarshalYAML()
if err != nil {
t.Errorf("MarshalYAML() error = %v", err)
return
}
if result != tt.want {
t.Errorf("MarshalYAML() = %v, want %v", result, tt.want)
}
}
})
}
}
// TestTime_String 测试字符串表示
func TestTime_String(t *testing.T) {
tests := []struct {
name string
input Time
want string
}{
{
name: "有效时间",
input: MustParseTime("14:30:00"),
want: "14:30:00",
},
{
name: "零值时间",
input: Time{},
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.input.String(); got != tt.want {
t.Errorf("String() = %v, want %v", got, tt.want)
}
})
}
}
// TestParseTime 测试时间解析函数
func TestParseTime(t *testing.T) {
tests := []struct {
name string
input string
want string
wantErr bool
}{
{
name: "标准格式解析",
input: "14:30:00",
want: "14:30:00",
wantErr: false,
},
{
name: "省略秒格式",
input: "14:30",
want: "14:30:00",
wantErr: false,
},
{
name: "紧凑格式",
input: "143000",
want: "14:30:00",
wantErr: false,
},
{
name: "无效时间格式",
input: "25:70:00", // 无效的小时和分钟
want: "",
wantErr: true,
},
{
name: "空字符串",
input: "",
want: "",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseTime(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("ParseTime() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && got.String() != tt.want {
t.Errorf("ParseTime() = %v, want %v", got.String(), tt.want)
}
})
}
}
// TestTime_Comparison 测试时间比较方法
func TestTime_Comparison(t *testing.T) {
time1 := MustParseTime("14:30:00")
time2 := MustParseTime("15:45:00")
time3 := MustParseTime("14:30:00") // 与time1相同
t.Run("After方法", func(t *testing.T) {
if !time2.After(time1) {
t.Error("time2应该在time1之后")
}
if time1.After(time2) {
t.Error("time1不应该在time2之后")
}
})
t.Run("Before方法", func(t *testing.T) {
if !time1.Before(time2) {
t.Error("time1应该在time2之前")
}
if time2.Before(time1) {
t.Error("time2不应该在time1之前")
}
})
t.Run("Equal方法", func(t *testing.T) {
if !time1.Equal(time3) {
t.Error("time1应该等于time3")
}
if time1.Equal(time2) {
t.Error("time1不应该等于time2")
}
})
}
// TestTime_Arithmetic 测试时间算术运算
func TestTime_Arithmetic(t *testing.T) {
baseTime := MustParseTime("14:30:00")
t.Run("AddHours方法", func(t *testing.T) {
result := baseTime.AddHours(2)
expected := MustParseTime("16:30:00")
if !result.Equal(expected) {
t.Errorf("AddHours(2) = %v, want %v", result, expected)
}
result = baseTime.AddHours(-1)
expected = MustParseTime("13:30:00")
if !result.Equal(expected) {
t.Errorf("AddHours(-1) = %v, want %v", result, expected)
}
// 测试跨日边界
lateTime := MustParseTime("23:30:00")
result = lateTime.AddHours(2)
expected = MustParseTime("01:30:00") // 第二天凌晨
if !result.Equal(expected) {
t.Errorf("AddHours跨日边界 = %v, want %v", result, expected)
}
})
t.Run("AddMinutes方法", func(t *testing.T) {
result := baseTime.AddMinutes(30)
expected := MustParseTime("15:00:00")
if !result.Equal(expected) {
t.Errorf("AddMinutes(30) = %v, want %v", result, expected)
}
result = baseTime.AddMinutes(-15)
expected = MustParseTime("14:15:00")
if !result.Equal(expected) {
t.Errorf("AddMinutes(-15) = %v, want %v", result, expected)
}
// 测试跨小时边界
result = baseTime.AddMinutes(90) // 1小时30分钟
expected = MustParseTime("16:00:00")
if !result.Equal(expected) {
t.Errorf("AddMinutes跨小时边界 = %v, want %v", result, expected)
}
})
}
// TestTime_Components 测试时间组件获取
func TestTime_Components(t *testing.T) {
tm := MustParseTime("14:30:45")
t.Run("Hour方法", func(t *testing.T) {
if hour := tm.Hour(); hour != 14 {
t.Errorf("Hour() = %v, want 14", hour)
}
})
t.Run("Minute方法", func(t *testing.T) {
if minute := tm.Minute(); minute != 30 {
t.Errorf("Minute() = %v, want 30", minute)
}
})
t.Run("Second方法", func(t *testing.T) {
if second := tm.Second(); second != 45 {
t.Errorf("Second() = %v, want 45", second)
}
})
}
// TestTime_ZeroValue 测试零值处理
func TestTime_ZeroValue(t *testing.T) {
var zeroTime Time
t.Run("IsZero方法", func(t *testing.T) {
if !zeroTime.IsZero() {
t.Error("IsZero()应该返回true对于零值")
}
validTime := MustParseTime("14:30:00")
if validTime.IsZero() {
t.Error("IsZero()应该返回false对于有效时间")
}
})
t.Run("零值的字符串表示", func(t *testing.T) {
if zeroTime.String() != "" {
t.Errorf("零值的String()应该返回空字符串, 得到: %s", zeroTime.String())
}
})
t.Run("零值的JSON序列化", func(t *testing.T) {
bytes, err := json.Marshal(zeroTime)
if err != nil {
t.Errorf("零值JSON序列化错误: %v", err)
}
if string(bytes) != "null" {
t.Errorf("零值JSON应该序列化为null, 得到: %s", string(bytes))
}
})
}
// TestNowTime 测试NowTime函数
func TestNowTime(t *testing.T) {
now := NowTime()
current := time.Now()
// 检查返回的时间组件是否匹配当前时间
if now.Hour() != current.Hour() || now.Minute() != current.Minute() || now.Second() != current.Second() {
t.Error("NowTime()返回的时间应该与当前时间匹配")
}
}
// TestMustParseTime 测试MustParseTime函数
func TestMustParseTime(t *testing.T) {
t.Run("有效时间", func(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Error("有效时间不应该引起panic")
}
}()
tm := MustParseTime("14:30:00")
if tm.String() != "14:30:00" {
t.Errorf("MustParseTime() = %v, want 14:30:00", tm.String())
}
})
t.Run("无效时间应该panic", func(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Error("无效时间应该引起panic")
}
}()
_ = MustParseTime("25:70:00")
})
}
// TestTime_EdgeCases 测试边界情况
func TestTime_EdgeCases(t *testing.T) {
t.Run("午夜时间", func(t *testing.T) {
midnight := MustParseTime("00:00:00")
if midnight.String() != "00:00:00" {
t.Error("午夜时间处理错误")
}
})
t.Run("最大有效时间", func(t *testing.T) {
maxTime := MustParseTime("23:59:59")
if maxTime.String() != "23:59:59" {
t.Error("最大时间处理错误")
}
})
t.Run("时间溢出处理", func(t *testing.T) {
// 测试接近午夜的时间加法
almostMidnight := MustParseTime("23:59:30")
result := almostMidnight.AddMinutes(2) // 应该变成00:01:30
expected := MustParseTime("00:01:30")
if !result.Equal(expected) {
t.Errorf("时间溢出处理错误: %v, want %v", result, expected)
}
})
}
// TestTime_Integration 集成测试
func TestTime_Integration(t *testing.T) {
t.Run("完整序列化循环", func(t *testing.T) {
original := MustParseTime("14:30:00")
// JSON序列化 -> 反序列化
jsonData, err := json.Marshal(original)
if err != nil {
t.Fatalf("JSON序列化失败: %v", err)
}
var fromJSON Time
err = json.Unmarshal(jsonData, &fromJSON)
if err != nil {
t.Fatalf("JSON反序列化失败: %v", err)
}
if !original.Equal(fromJSON) {
t.Error("JSON序列化循环失败")
}
// 数据库值循环
dbValue, err := original.Value()
if err != nil {
t.Fatalf("Value()失败: %v", err)
}
var fromDB Time
err = fromDB.Scan(dbValue)
if err != nil {
t.Fatalf("Scan()失败: %v", err)
}
if !original.Equal(fromDB) {
t.Error("数据库循环失败")
}
})
}
// BenchmarkTime_Parse 性能测试:时间解析
func BenchmarkTime_Parse(b *testing.B) {
testCases := []string{
"14:30:00",
"14:30",
"143000",
"",
}
for _, tc := range testCases {
b.Run(tc, func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = ParseTime(tc)
}
})
}
}
// BenchmarkTime_JSONMarshal 性能测试:JSON序列化
func BenchmarkTime_JSONMarshal(b *testing.B) {
tm := MustParseTime("14:30:00")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = json.Marshal(tm)
}
}
// BenchmarkTime_JSONUnmarshal 性能测试:JSON反序列化
func BenchmarkTime_JSONUnmarshal(b *testing.B) {
testCases := []struct {
name string
data []byte
}{
{"有效时间", []byte(`"14:30:00"`)},
{"空值", []byte(`null`)},
}
for _, tc := range testCases {
b.Run(tc.name, func(b *testing.B) {
var tm Time
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = json.Unmarshal(tc.data, &tm)
}
})
}
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论