diff --git a/go.mod b/go.mod index f62d5b5..e618f76 100644 --- a/go.mod +++ b/go.mod @@ -3,15 +3,22 @@ module planA go 1.25.0 require ( + github.com/boombuler/barcode v1.1.0 + github.com/disintegration/imaging v1.6.2 + github.com/fogleman/gg v1.3.0 github.com/go-playground/validator/v10 v10.30.1 github.com/go-redis/redis/v8 v8.11.5 + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.1 + github.com/makiuchi-d/gozxing v0.1.1 github.com/minio/minio-go/v7 v7.1.0 + github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 github.com/panjf2000/ants/v2 v2.11.4 github.com/robfig/cron/v3 v3.0.1 + github.com/valyala/fasthttp v1.72.0 + golang.org/x/image v0.43.0 golang.org/x/time v0.14.0 - gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/mysql v1.6.0 gorm.io/gorm v1.31.1 modernc.org/sqlite v1.46.1 @@ -19,56 +26,43 @@ require ( require ( filippo.io/edwards25519 v1.2.0 // indirect - github.com/bytedance/gopkg v0.1.3 // indirect - github.com/bytedance/sonic v1.15.0 // indirect - github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/andybalholm/brotli v1.2.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cloudwego/base64x v0.1.6 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect - github.com/gin-contrib/sse v1.1.0 // indirect - github.com/gin-gonic/gin v1.12.0 // indirect github.com/go-ini/ini v1.67.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-sql-driver/mysql v1.9.3 // indirect - github.com/goccy/go-json v0.10.5 // indirect - github.com/goccy/go-yaml v1.19.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/compress v1.18.2 // indirect + github.com/klauspost/compress v1.18.6 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/crc32 v1.3.0 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/minio/crc64nvme v1.1.1 // indirect github.com/minio/md5-simd v1.1.2 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect - github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/philhofer/fwd v1.2.0 // indirect - github.com/quic-go/qpack v0.6.0 // indirect - github.com/quic-go/quic-go v0.59.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/rs/xid v1.6.0 // indirect + github.com/stretchr/testify v1.11.1 // indirect github.com/tinylib/msgp v1.6.1 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.3.1 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/zeebo/xxh3 v1.1.0 // indirect - go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/arch v0.22.0 // indirect - golang.org/x/crypto v0.48.0 // indirect + golang.org/x/crypto v0.53.0 // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect - golang.org/x/net v0.51.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.41.0 // indirect - golang.org/x/text v0.34.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect + golang.org/x/net v0.56.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/text v0.38.0 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect modernc.org/libc v1.67.6 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect diff --git a/go.sum b/go.sum index 2f48b09..311afea 100644 --- a/go.sum +++ b/go.sum @@ -1,32 +1,26 @@ filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= -github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= -github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= -github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= -github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= -github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= -github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= -github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= +github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo= +github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= +github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= -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.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= -github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= @@ -41,11 +35,8 @@ github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= -github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= -github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= -github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -58,34 +49,32 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD 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/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= -github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU= -github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 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/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM= github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +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/makiuchi-d/gozxing v0.1.1 h1:xxqijhoedi+/lZlhINteGbywIrewVdVv2wl9r5O9S1I= +github.com/makiuchi-d/gozxing v0.1.1/go.mod h1:eRIHbOjX7QWxLIDJoQuMLhuXg9LAuw6znsUtRkNw9DU= 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/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI= github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= -github.com/minio/minio-go/v7 v7.0.99 h1:2vH/byrwUkIpFQFOilvTfaUpvAX3fEFhEzO+DR3DlCE= -github.com/minio/minio-go/v7 v7.0.99/go.mod h1:EtGNKtlX20iL2yaYnxEigaIvj0G0GwSDnifnG8ClIdw= github.com/minio/minio-go/v7 v7.1.0 h1:QEt5IStDpxgGjEdtOgpiZ5QhmSl3ax7qy61vi2SwHO8= github.com/minio/minio-go/v7 v7.1.0/go.mod h1:Dm7WS1AgLmBa0NcQD6SeJnJf+K/EUW3GR7Ks6olB3OA= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/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/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= @@ -98,81 +87,68 @@ github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= github.com/panjf2000/ants/v2 v2.11.4 h1:UJQbtN1jIcI5CYNocTj0fuAUYvsLjPoYi0YuhqV/Y48= github.com/panjf2000/ants/v2 v2.11.4/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek= -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/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= -github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= -github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= -github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= -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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY= github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= -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.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= -github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.72.0 h1:R7kYdoWhn1ye1fVpP+cDHDJwYm3NkwLliwgzJ/Abg7M= +github.com/valyala/fasthttp v1.72.0/go.mod h1:zsbLTYqcpIktdQytlVBwIjY9La5d6bs990nBxWg8efk= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= -go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= -go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= -golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= -golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= -golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= -golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= -golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY= +golang.org/x/image v0.43.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= -golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= -golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 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.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= diff --git a/modules/image/image.go b/modules/image/image.go index bf88774..28a5a08 100644 --- a/modules/image/image.go +++ b/modules/image/image.go @@ -1,46 +1,9 @@ package image import ( - "encoding/json" - "fmt" - "os" - "path/filepath" - "planA/planB/config" - "syscall" - "unsafe" + planBImage "planA/planB/modules/image" ) -var ( - gImageDll *ImageDLL -) - -// ImageDLL 图片工具DLL结构 -type ImageDLL struct { - Dll *syscall.DLL - AddWatermarkFromURLEx *syscall.Proc // 打水印 -} - -// InitImageDll 初始化 imageDLL -func InitImageDll() (*ImageDLL, error) { - fileConfig, getDllFileConfigErr := config.GetFileUrlConfig() - if getDllFileConfigErr != nil { - return nil, getDllFileConfigErr - } - dllPath := filepath.Join(fileConfig.ImageDll, "image.dll") - if _, err := os.Stat(dllPath); os.IsNotExist(err) { - return nil, fmt.Errorf("Image DLL 不存在: %s", dllPath) - } - dll, err := syscall.LoadDLL(dllPath) - if err != nil { - return nil, fmt.Errorf("加载Image DLL 失败: %s", err) - } - gImageDll = &ImageDLL{ - Dll: dll, - AddWatermarkFromURLEx: dll.MustFindProc("AddWatermarkFromURLEx"), - } - return gImageDll, nil -} - // WatermarkConfig 添加水印 type WatermarkConfig struct { SourceImageURL string // 源图片URL地址 @@ -58,50 +21,6 @@ type WatermarkConfig struct { } // AddWatermarkFromURLExs 添加水印 -func (m *ImageDLL) AddWatermarkFromURLExs(sourceImageUrl, watermarkUrl string) (string, error) { - - watermarkConfig := WatermarkConfig{ - SourceImageURL: sourceImageUrl, - WatermarkURL: watermarkUrl, - Position: "center", - Opacity: 1.0, - Scale: 1.0, - TileSpacing: 50, - Timeout: 30, - OutputFormat: "jpeg", - JPEGQuality: 95, - } - watermarkConfigJson, err := json.Marshal(watermarkConfig) - if err != nil { - return "", fmt.Errorf("JSON序列化失败: %v", err) - } - - proc, err := m.Dll.FindProc("AddWatermarkFromURLEx") - if err != nil { - return "", fmt.Errorf("找不到函数 AddWatermarkFromURLEx: %v", err) - } - watermarkConfigJsonPtr, _ := syscall.BytePtrFromString(string(watermarkConfigJson)) - - resultPtr, _, _ := proc.Call( - uintptr(unsafe.Pointer(watermarkConfigJsonPtr)), - ) - result := cStr(resultPtr) - return result, nil -} - -// cStr 将 C 字符串指针转换为 Go 字符串 -func cStr(ptr uintptr) string { - if ptr == 0 { - return "" - } - var b []byte - for { - c := *(*byte)(unsafe.Pointer(ptr)) - if c == 0 { - break - } - b = append(b, c) - ptr++ - } - return string(b) +func AddWatermarkFromURLExs(sourceImageUrl, watermarkUrl string) (string, error) { + return planBImage.AddWatermarkFromURLExs(sourceImageUrl, watermarkUrl) } diff --git a/planB/dispatcher/kongfuzi/kongfizi.go b/planB/dispatcher/kongfuzi/kongfizi.go index 8ee33c9..9d8e740 100644 --- a/planB/dispatcher/kongfuzi/kongfizi.go +++ b/planB/dispatcher/kongfuzi/kongfizi.go @@ -457,7 +457,7 @@ func UploadImageToKfz(imgUrl string) (string, error) { // 修复:先判断 ErrorResponse 是否为 nil if uploadData.ErrorResponse != nil && uploadData.ErrorResponse.Code != 0 { fmt.Println("错误码:", uploadData.ErrorResponse.Code) - return "", fmt.Errorf("上传图片失败2 错误码 %v 错误描述 %v", uploadData.ErrorResponse.Code, uploadData.ErrorResponse.SubMsg) + return "", fmt.Errorf("上传图片失败2 错误码 %v 错误描述 %v 原始json %v", uploadData.ErrorResponse.Code, uploadData.ErrorResponse.SubMsg, upload) } // 修复:判断 SuccessResponse 是否为 nil @@ -1296,6 +1296,8 @@ func template17(taskMsg planAType.TaskBody) (planBTypeKfz.GoodsAdd17, error) { //分类编号 if value, exists := golabl.KfzGetCommonCategory[string(taskMsg.BookInfo.CatIdObject.KongFuZiCatId)]; exists { goodsAdd.CatId = value + } else if value, exists = golabl.KfzGetCommonCategory[string("图书/普通图书/"+taskMsg.BookInfo.CatIdObject.KongFuZiCatId)]; exists { + goodsAdd.CatId = value } else { goodsAdd.CatId = "43000000000000000" } diff --git a/planB/initialization/dll/dll.go b/planB/initialization/dll/dll.go index fe635dd..09e0392 100644 --- a/planB/initialization/dll/dll.go +++ b/planB/initialization/dll/dll.go @@ -1,7 +1,6 @@ package dll import ( - "planA/planB/initialization/dll/image" "planA/planB/initialization/dll/kfz" "planA/planB/initialization/dll/logs" "planA/planB/initialization/dll/pdd" @@ -15,11 +14,6 @@ func GetDllSetToG() error { if getPddDllSetToGErr != nil { return getPddDllSetToGErr } - // 初始化 ImageDll - getImageDllSetToGErr := image.GetImageDllSetToG() - if getImageDllSetToGErr != nil { - return getImageDllSetToGErr - } // 初始化 XianYuDll getXianYuDllSetToGErr := xianYu.GetXianYuDllSetToG() if getXianYuDllSetToGErr != nil { diff --git a/planB/initialization/dll/image/image.go b/planB/initialization/dll/image/image.go deleted file mode 100644 index 7544975..0000000 --- a/planB/initialization/dll/image/image.go +++ /dev/null @@ -1,16 +0,0 @@ -package image - -import ( - "planA/planB/initialization/golabl" - "planA/planB/modules/image" -) - -// GetImageDllSetToG 获取图片DLL -func GetImageDllSetToG() error { - imageDll, imageDllErr := image.InitImageDll(golabl.Config.FileUrl.ImageDll) - if imageDllErr != nil { - return imageDllErr - } - golabl.ImageDll = imageDll - return nil -} diff --git a/planB/initialization/golabl/golabl.go b/planB/initialization/golabl/golabl.go index 8385a7f..be2223d 100644 --- a/planB/initialization/golabl/golabl.go +++ b/planB/initialization/golabl/golabl.go @@ -3,7 +3,6 @@ package golabl import ( "context" "planA/planB/interfaces" - "planA/planB/modules/image" "planA/planB/modules/kfz" "planA/planB/modules/logs" "planA/planB/modules/pdd" @@ -28,7 +27,6 @@ var ( TaskType string // 全局任务类型 MinIo *planBType.MinIOClient // 全局 MinIO PddDll *pdd.PddDLL // 全局拼多多 DLL - ImageDll *image.ImageDLL // 全局 ImageDll XianYuDll *xianYuDll.XianYuDLL // 全局 闲鱼 DLL LogDll *logs.LoggerDLL // 全局日志 DLL KfzDll *kfz.KfzDLL // 全局孔夫子 DLL diff --git a/planB/modules/image/image.dll b/planB/modules/image/image.dll deleted file mode 100644 index 1975e04..0000000 Binary files a/planB/modules/image/image.dll and /dev/null differ diff --git a/planB/modules/image/image.go b/planB/modules/image/image.go index 1e4bbee..f6ff918 100644 --- a/planB/modules/image/image.go +++ b/planB/modules/image/image.go @@ -1,68 +1,15 @@ package image import ( + "encoding/base64" "encoding/json" "fmt" - "os" - "path/filepath" - "syscall" - "unsafe" + + planBTypeModules "planA/planB/type/modules" ) -var ( - gImageDll *ImageDLL - - // Windows API - 使用 C 运行时库 - libc = syscall.NewLazyDLL("msvcrt.dll") - procFree = libc.NewProc("free") - procMalloc = libc.NewProc("malloc") -) - -// ImageDLL 图片工具DLL结构 -type ImageDLL struct { - Dll *syscall.DLL - AddWatermarkFromURLEx *syscall.Proc // 打水印 -} - -// InitImageDll 初始化 imageDLL -func InitImageDll(url string) (*ImageDLL, error) { - dllPath := filepath.Join(url, "image.dll") - if _, err := os.Stat(dllPath); os.IsNotExist(err) { - return nil, fmt.Errorf("Image DLL 不存在: %s", dllPath) - } - dll, err := syscall.LoadDLL(dllPath) - if err != nil { - return nil, fmt.Errorf("加载Image DLL 失败: %s", err) - } - gImageDll = &ImageDLL{ - Dll: dll, - AddWatermarkFromURLEx: dll.MustFindProc("AddWatermarkFromURLEx"), - } - return gImageDll, nil -} - -// WatermarkConfig 添加水印 -type WatermarkConfig struct { - SourceImageURL string // 源图片URL地址 - WatermarkURL string // 水印图片URL地址 - WatermarkBase64 string // 水印图片base64编码字符串(新增,优先使用) - Opacity float64 // 不透明度 (0.0-1.0) - Position string // 位置: center, top-left, top-right, bottom-left, bottom-right, tile - TileSpacing int // 平铺时的间距 - Scale float64 // 水印缩放比例 (0.0-1.0) - Rotation float64 // 旋转角度 (度数) - XOffset int // X轴偏移量 - YOffset int // Y轴偏移量 - Timeout int // 下载超时时间(秒),默认30秒 - OutputFormat string // 输出格式: "jpeg", "png", "auto"(默认auto,根据源图片格式)auto - JPEGQuality int // JPEG质量 (1-100),默认95 - TargetWidth int // 目标宽度(0表示不缩放) - TargetHeight int // 目标高度(0表示不缩放) - ResizeMode string // 缩放模式: "fit"(适应,保持比例,可能有黑边), "fill"(填充,裁剪), "stretch"(拉伸) -} - // AddWatermarkFromURLExs 添加水印 -func (m *ImageDLL) AddWatermarkFromURLExs(sourceImageUrl, watermarkUrl string) (string, error) { +func AddWatermarkFromURLExs(sourceImageUrl, watermarkUrl string) (string, error) { watermarkConfig := WatermarkConfig{ SourceImageURL: sourceImageUrl, WatermarkBase64: watermarkUrl, @@ -77,65 +24,35 @@ func (m *ImageDLL) AddWatermarkFromURLExs(sourceImageUrl, watermarkUrl string) ( TargetHeight: 800, ResizeMode: "fit", } - watermarkConfigJson, err := json.Marshal(watermarkConfig) + + imgBytes, imgFormat, err := AddWatermarkFromURL(watermarkConfig) + if err != nil { + return "", fmt.Errorf("打水印失败: %v", err) + } + + // 构建带MIME前缀的Base64数据(匹配原DLL返回格式) + var base64Data string + switch imgFormat { + case "jpeg", "jpg": + base64Data = "data:image/jpeg;base64," + base64.StdEncoding.EncodeToString(imgBytes) + case "png": + base64Data = "data:image/png;base64," + base64.StdEncoding.EncodeToString(imgBytes) + case "gif": + base64Data = "data:image/gif;base64," + base64.StdEncoding.EncodeToString(imgBytes) + default: + base64Data = "data:image/jpeg;base64," + base64.StdEncoding.EncodeToString(imgBytes) + } + + result := planBTypeModules.ImageResult{ + Success: true, + Format: imgFormat, + Data: base64Data, + } + + resultJson, err := json.Marshal(result) if err != nil { return "", fmt.Errorf("JSON序列化失败: %v", err) } - proc, err := m.Dll.FindProc("AddWatermarkFromURLEx") - if err != nil { - return "", fmt.Errorf("找不到函数 AddWatermarkFromURLEx: %v", err) - } - - // 分配内存并确保释放 - jsonStr := string(watermarkConfigJson) - jsonPtr := cString(jsonStr) - defer freeCString(jsonPtr) - - // 调用 DLL 函数 - resultPtr, _, _ := proc.Call( - uintptr(unsafe.Pointer(jsonPtr)), - ) - result := cStr(resultPtr) - return result, nil -} - -// cString 分配 C 字符串(使用 malloc) -func cString(str string) unsafe.Pointer { - // 计算需要的内存大小 - size := len(str) + 1 - ptr, _, _ := procMalloc.Call(uintptr(size)) - if ptr == 0 { - return nil - } - // 复制字符串内容 - for i := 0; i < len(str); i++ { - *(*byte)(unsafe.Pointer(ptr + uintptr(i))) = str[i] - } - *(*byte)(unsafe.Pointer(ptr + uintptr(len(str)))) = 0 // 结尾加 \0 - return unsafe.Pointer(ptr) -} - -// freeCString 释放 C 字符串 -func freeCString(ptr unsafe.Pointer) { - if ptr != nil { - procFree.Call(uintptr(ptr)) - } -} - -// cStr 将 C 字符串指针转换为 Go 字符串 -func cStr(ptr uintptr) string { - if ptr == 0 { - return "" - } - var b []byte - for { - c := *(*byte)(unsafe.Pointer(ptr)) - if c == 0 { - break - } - b = append(b, c) - ptr++ - } - return string(b) + return string(resultJson), nil } diff --git a/planB/modules/image/imageUtil.go b/planB/modules/image/imageUtil.go new file mode 100644 index 0000000..af2134c --- /dev/null +++ b/planB/modules/image/imageUtil.go @@ -0,0 +1,2154 @@ +package image + +import ( + "bytes" + "encoding/base64" + "fmt" + "github.com/boombuler/barcode" + "github.com/boombuler/barcode/code128" + "github.com/boombuler/barcode/code39" + "github.com/boombuler/barcode/ean" + "github.com/disintegration/imaging" + "github.com/fogleman/gg" + "github.com/golang/freetype" + "github.com/golang/freetype/truetype" + "github.com/makiuchi-d/gozxing" + "github.com/makiuchi-d/gozxing/qrcode" + "github.com/nfnt/resize" + "golang.org/x/image/draw" + "golang.org/x/image/math/fixed" + "image" + "image/color" + "image/jpeg" + "image/png" + "math" + "os" + "path/filepath" + "runtime" + "strings" + "sync" + "time" + + "github.com/valyala/fasthttp" +) + +// Config 配置结构体 +type Config struct { + OutputDir string // 输出目录路径 + FileName string // 文件名 + MatchDir string // 满足条件的图片目录名 + UnmatchDir string // 不满足条件的图片目录名 + EqualHeightDir string // 等高的图片目录名 + WhiteDir string // 白色底图的图片目录名 + WhiteBorderPngDir string // 去白边转PNG的图片目录名 + WhiteHeightZoomDir string // 缩放的图片目录 + CropDir string // 裁切的图片目录 + MinWhitePct float64 // 纯白占比下限(0-1) + MaxWhitePct float64 // 纯白占比上限(0-1) + Extensions []string // 支持的图片扩展名 +} + +// 检查图片 +func validateConfig(config *Config) error { + // 检查百分比范围 + if config.MinWhitePct < 0 || config.MinWhitePct > 1 { + return fmt.Errorf("纯白占比下限必须在0-1之间") + } + + if config.MaxWhitePct < 0 || config.MaxWhitePct > 1 { + return fmt.Errorf("纯白占比上限必须在0-1之间") + } + + if config.MinWhitePct > config.MaxWhitePct { + return fmt.Errorf("下限不能大于上限") + } + + return nil +} + +// 创建目录功能 +func createDirs(config *Config) error { + // 创建输出根目录 + if err := os.MkdirAll(config.OutputDir, 0755); err != nil { + return err + } + + // 创建匹配目录 + matchPath := filepath.Join(config.OutputDir, config.MatchDir) + if err := os.MkdirAll(matchPath, 0755); err != nil { + return err + } + + // 创建不匹配目录 + unmatchPath := filepath.Join(config.OutputDir, config.UnmatchDir) + if err := os.MkdirAll(unmatchPath, 0755); err != nil { + return err + } + + equalHeightPath := filepath.Join(config.OutputDir, config.EqualHeightDir) + if err := os.MkdirAll(equalHeightPath, 0755); err != nil { + return err + } + + whitePath := filepath.Join(config.OutputDir, config.WhiteDir) + if err := os.MkdirAll(whitePath, 0755); err != nil { + return err + } + + whiteBorderPngPath := filepath.Join(config.OutputDir, config.WhiteBorderPngDir) + if err := os.MkdirAll(whiteBorderPngPath, 0755); err != nil { + return err + } + + whiteHeightZoomPath := filepath.Join(config.OutputDir, config.WhiteHeightZoomDir) + if err := os.MkdirAll(whiteHeightZoomPath, 0755); err != nil { + return err + } + + cropPath := filepath.Join(config.OutputDir, config.CropDir) + if err := os.MkdirAll(cropPath, 0755); err != nil { + return err + } + return nil +} + +// 计算纯白占比 +func calculateWhitePercentage(imagePath string) (float64, error) { + // 打开图片文件 + file, err := os.Open(imagePath) + if err != nil { + return 0, err + } + defer file.Close() + + // 解码图片 + img, _, err := image.Decode(file) + if err != nil { + return 0, err + } + + bounds := img.Bounds() + totalPixels := bounds.Dx() * bounds.Dy() + whitePixels := 0 + + // 遍历每个像素,判断是否为纯白色 + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + for x := bounds.Min.X; x < bounds.Max.X; x++ { + pixel := color.RGBAModel.Convert(img.At(x, y)).(color.RGBA) + + // 判断是否为纯白色 (R=255, G=255, B=255) + if pixel.R == 255 && pixel.G == 255 && pixel.B == 255 { + whitePixels++ + } + } + } + + return float64(whitePixels) / float64(totalPixels), nil +} + +// 复制文件到相应目录 +func copyToDestination(srcPath string, config *Config, isMatch bool) error { + filename := filepath.Base(srcPath) + + // 确定目标目录 + var destDir string + if isMatch { + destDir = filepath.Join(config.OutputDir, config.MatchDir) + } else { + destDir = filepath.Join(config.OutputDir, config.UnmatchDir) + } + + destPath := filepath.Join(destDir, filename) + + // 读取源文件 + data, err := os.ReadFile(srcPath) + if err != nil { + return err + } + + // 写入目标文件 + return os.WriteFile(destPath, data, 0644) +} + +// 保存文件 +func saveImage(outputPath string, img image.Image, format string) error { + // 创建输出文件 + outFile, err := os.Create(outputPath) + if err != nil { + return err + } + defer outFile.Close() + + // 根据原始格式保存图片 + switch format { + case "jpeg", "jpg": + // JPEG 格式可以设置质量参数 + return jpeg.Encode(outFile, img, &jpeg.Options{Quality: 95}) + case "png": + // PNG 格式通常不需要质量参数 + return png.Encode(outFile, img) + default: + // 默认使用 JPEG 格式 + return jpeg.Encode(outFile, img, &jpeg.Options{Quality: 95}) + } +} + +// 检测图片纯白占比 +func processImage(config *Config) error { + // 创建输出目录 + if err := createDirs(config); err != nil { + return fmt.Errorf("创建目录失败: %v\n", err) + } + + if err := validateConfig(config); err != nil { + return err + } + + // 计算纯白占比 + whitePct, err := calculateWhitePercentage(config.FileName) + if err != nil { + return fmt.Errorf("错误: %v\n", err) + } + // 判断是否在范围内 + isMatch := whitePct >= config.MinWhitePct && whitePct <= config.MaxWhitePct + status := "❌ 不满足" + if isMatch { + status = "✅ 满足" + } + fmt.Printf("纯白占比: %.2f%% %s\n", whitePct*100, status) + + // 复制文件到相应目录 + if err = copyToDestination(config.FileName, config, isMatch); err != nil { + return fmt.Errorf("复制失败: %v\n", err) + } + return nil +} + +// 根据原始图片生成新的白底图片 +func createWhiteBottomCenteredImage(config *Config, width, height int) (string, error) { + // 创建输出目录 + if err := createDirs(config); err != nil { + return "", fmt.Errorf("创建目录失败: %v", err) + } + // 打开图片 + file, err := os.Open(config.FileName) + if err != nil { + return "", fmt.Errorf("打开图片失败: %v", err) + } + defer file.Close() + // 解码原始图片 + img, format, err := image.Decode(file) + if err != nil { + return "", fmt.Errorf("图片解码失败: %v", err) + } + + // 创建透明背景 + dst := image.NewRGBA(image.Rect(0, 0, width, height)) + + // 设置背景颜色 + var bgColor color.Color + bgColor = color.RGBA{R: 0, G: 0, B: 0, A: 0} // 白色 + + // 填充透明背景 + draw.Draw(dst, dst.Bounds(), &image.Uniform{C: bgColor}, image.Point{}, draw.Src) + + // 计算居中位置 + srcBounds := img.Bounds() + srcWidth := srcBounds.Dx() + srcHeight := srcBounds.Dy() + + x := (width - srcWidth) / 2 + y := (height - srcHeight) / 2 + + // 将原图绘制到中央 + draw.Draw(dst, image.Rect(x, y, x+srcWidth, y+srcHeight), img, image.Point{}, draw.Over) + + filename := filepath.Base(config.FileName) + destPath := filepath.Join(config.OutputDir, config.WhiteDir, filename) + saveImage(destPath, dst, format) + + return destPath, nil +} + +// 根据高度生成等比例图片 +func resizeToHeightQuality(config *Config, targetHeight int) (string, error) { + // 创建输出目录 + if err := createDirs(config); err != nil { + return "", fmt.Errorf("创建目录失败: %v", err) + } + // 打开图片 + file, err := os.Open(config.FileName) + if err != nil { + return "", fmt.Errorf("打开图片失败: %v", err) + } + defer file.Close() + // 解码原始图片 + img, format, err := image.Decode(file) + if err != nil { + return "", fmt.Errorf("图片解码失败: %v", err) + } + + bounds := img.Bounds() + srcWidth := bounds.Dx() + srcHeight := bounds.Dy() + + // 计算等比例缩放后的宽度 + targetWidth := uint(float64(srcWidth) * float64(targetHeight) / float64(srcHeight)) + // 使用 Lanczos3 插值算法进行高质量缩放 + imageNew := resize.Resize(targetWidth, uint(targetHeight), img, resize.Lanczos3) + + // 保存图片到指定目录下 + filename := filepath.Base(config.FileName) + destPath := filepath.Join(config.OutputDir, config.EqualHeightDir, filename) + err = saveImage(destPath, imageNew, format) + if err != nil { + return "", fmt.Errorf("保存图片失败: %v", err) + } + + return destPath, nil +} + +// ImageToPNGConverter 图片去白边并转为PNG +type ImageToPNGConverter struct { + Threshold int + Margin int + BgColor color.RGBA + DetectColor *color.RGBA + KeepTransparent bool + PNGCompressLevel png.CompressionLevel + Quality int +} + +// 去掉白边并转PNG图片工具 +func removeWhiteBorderAndPNG(config *Config) (string, error) { + // 创建输出目录 + if err := createDirs(config); err != nil { + return "", fmt.Errorf("创建目录失败: %v", err) + } + // 打开图片 + file, err := os.Open(config.FileName) + if err != nil { + return "", fmt.Errorf("打开图片失败: %v", err) + } + defer file.Close() + // 解码原始图片 + img, _, err := image.Decode(file) + if err != nil { + return "", fmt.Errorf("图片解码失败: %v", err) + } + + compressLevel := 6 + + // 创建转换器 + compressionLevel := png.DefaultCompression + switch { + case compressLevel <= 0: + compressionLevel = png.NoCompression + case compressLevel >= 9: + compressionLevel = png.BestCompression + default: + // 使用默认压缩级别 + } + + converter := newImageToPNGConverter( + 240, + 0, + &color.RGBA{R: 255, G: 255, B: 255, A: 255}, + nil, + false, + compressionLevel, + 95, + ) + + toPNG := converter.convertToPNG(img, true) + + // 保存图片到指定目录下 + filename := filepath.Base(config.FileName) + // 去除扩展名 + nameWithoutExt := strings.TrimSuffix(filename, filepath.Ext(filename)) + destPath := filepath.Join(config.OutputDir, config.WhiteBorderPngDir, nameWithoutExt+".png") + saveImage(destPath, toPNG, "png") + return destPath, nil +} + +// newImageToPNGConverter 创建新的转换器 +func newImageToPNGConverter(threshold, margin int, bgColor, detectColor *color.RGBA, + keepTransparent bool, compressLevel png.CompressionLevel, quality int) *ImageToPNGConverter { + + // 默认背景色为白色 + bg := color.RGBA{R: 255, G: 255, B: 255, A: 255} + if bgColor != nil { + bg = *bgColor + } + + return &ImageToPNGConverter{ + Threshold: threshold, + Margin: margin, + BgColor: bg, + DetectColor: detectColor, + KeepTransparent: keepTransparent, + PNGCompressLevel: compressLevel, + Quality: quality, + } +} + +// ConvertToPNG 转换图片为PNG格式 +func (c *ImageToPNGConverter) convertToPNG(img image.Image, addBackground bool) image.Image { + // 先裁剪白边 + trimmed := c.trimImage(img) + + // 检查是否有alpha通道 + _, hasAlpha := trimmed.(*image.NRGBA) + if !hasAlpha { + _, hasAlpha = trimmed.(*image.RGBA) + } + + if hasAlpha { + if c.KeepTransparent { + // 保持透明 + return trimmed + } else if addBackground { + // 添加背景色 + bg := image.NewRGBA(trimmed.Bounds()) + draw.Draw(bg, bg.Bounds(), &image.Uniform{C: c.BgColor}, image.Point{}, draw.Src) + draw.Draw(bg, bg.Bounds(), trimmed, trimmed.Bounds().Min, draw.Over) + return bg + } + } else { + // 非透明图像 + if c.KeepTransparent { + // 转换为RGBA + rgba := image.NewRGBA(trimmed.Bounds()) + draw.Draw(rgba, rgba.Bounds(), trimmed, trimmed.Bounds().Min, draw.Src) + return rgba + } + return trimmed + } + + return trimmed +} + +// TrimImage 裁剪图片白边 +func (c *ImageToPNGConverter) trimImage(img image.Image) image.Image { + borders := c.findBorders(img) + + // 创建一个新的图像并裁剪 + trimmed := imaging.Crop(img, borders) + return trimmed +} + +// FindBorders 查找图片的有效边界 +func (c *ImageToPNGConverter) findBorders(img image.Image) image.Rectangle { + bounds := img.Bounds() + width := bounds.Dx() + height := bounds.Dy() + + // 检查图像是否有alpha通道 + _, hasAlpha := img.(*image.NRGBA) + if !hasAlpha { + _, hasAlpha = img.(*image.RGBA) + } + + // 初始化边界 + left := width + top := height + right := 0 + bottom := 0 + + // 查找非背景区域 + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + for x := bounds.Min.X; x < bounds.Max.X; x++ { + pixel := img.At(x, y) + if !c.isBackgroundColor(pixel, hasAlpha) { + if x < left { + left = x + } + if x > right { + right = x + } + if y < top { + top = y + } + if y > bottom { + bottom = y + } + } + } + } + + // 如果没有找到非背景区域,返回整个图像 + if left > right || top > bottom { + return bounds + } + + // 添加边距 + left = max(bounds.Min.X, left-c.Margin) + top = max(bounds.Min.Y, top-c.Margin) + right = min(bounds.Max.X, right+c.Margin+1) + bottom = min(bounds.Max.Y, bottom+c.Margin+1) + + return image.Rect(left, top, right, bottom) +} + +// IsBackgroundColor 判断像素是否为背景色 +func (c *ImageToPNGConverter) isBackgroundColor(pixel color.Color, hasAlpha bool) bool { + r, g, b, a := pixel.RGBA() + + // 转换为8位值 + r8 := uint8(r >> 8) + g8 := uint8(g >> 8) + b8 := uint8(b >> 8) + a8 := uint8(a >> 8) + + // 检查透明度 + if hasAlpha && a8 < 25 { // 透明度 > 90% + return true + } + + // 如果指定了检测颜色 + if c.DetectColor != nil { + dr, dg, db, _ := c.DetectColor.RGBA() + dr8 := uint8(dr >> 8) + dg8 := uint8(dg >> 8) + db8 := uint8(db >> 8) + + // threshold 是 int 类型,需要转换为 uint8 比较 + threshold8 := uint8(255 - c.Threshold) + return absDiff(r8, dr8) <= threshold8 && + absDiff(g8, dg8) <= threshold8 && + absDiff(b8, db8) <= threshold8 + } + + // 自动检测白色/浅色背景 + // 注意:这里的 c.Threshold 是 int,需要转换为 uint8 + threshold8 := uint8(c.Threshold) + return r8 >= threshold8 && + g8 >= threshold8 && + b8 >= threshold8 +} + +// 图片缩放 +func resizeWTToHeightQuality(config *Config, dsWidth, dsHeight int) (string, error) { + // 创建输出目录 + if err := createDirs(config); err != nil { + return "", fmt.Errorf("创建目录失败: %v", err) + } + // 打开图片 + file, err := os.Open(config.FileName) + if err != nil { + return "", fmt.Errorf("打开图片失败: %v", err) + } + defer file.Close() + // 解码原始图片 + img, format, err := image.Decode(file) + if err != nil { + return "", fmt.Errorf("图片解码失败: %v", err) + } + + // 使用Lanczos3算法缩放图片 + optimized := resizeImageOptimized(img, dsWidth, dsHeight) + + // 保存图片到指定目录下 + filename := filepath.Base(config.FileName) + destPath := filepath.Join(config.OutputDir, config.WhiteHeightZoomDir, filename) + err = saveImage(destPath, optimized, format) + if err != nil { + return "", fmt.Errorf("保存图片失败: %v", err) + } + + return destPath, nil +} + +// 使用Lanczos3算法缩放图片 +func resizeImageOptimized(src image.Image, dstWidth, dstHeight int) image.Image { + srcBounds := src.Bounds() + srcWidth := srcBounds.Dx() + srcHeight := srcBounds.Dy() + + // 创建目标图片 + dst := image.NewRGBA(image.Rect(0, 0, dstWidth, dstHeight)) + + // 计算缩放比例 + xScale := float64(srcWidth) / float64(dstWidth) + yScale := float64(srcHeight) / float64(dstHeight) + + // Lanczos3算法半径 + radius := 3.0 + + // 预计算x方向的权重 + xWeights := make([][]float64, dstWidth) + xIndices := make([][]int, dstWidth) + + for x := 0; x < dstWidth; x++ { + srcX := float64(x) * xScale + startX := int(math.Max(0, math.Floor(srcX-radius+0.5))) + endX := int(math.Min(float64(srcWidth-1), math.Floor(srcX+radius))) + + weights := make([]float64, 0, endX-startX+1) + indices := make([]int, 0, endX-startX+1) + + for sx := startX; sx <= endX; sx++ { + xDist := float64(sx) + 0.5 - srcX + weight := lanczos3(xDist) + if weight != 0 { + weights = append(weights, weight) + indices = append(indices, sx) + } + } + + xWeights[x] = weights + xIndices[x] = indices + } + + // 处理每一行 + for y := 0; y < dstHeight; y++ { + srcY := float64(y) * yScale + startY := int(math.Max(0, math.Floor(srcY-radius+0.5))) + endY := int(math.Min(float64(srcHeight-1), math.Floor(srcY+radius))) + + // 预计算y方向的权重 + yWeights := make([]float64, 0, endY-startY+1) + yIndices := make([]int, 0, endY-startY+1) + + for sy := startY; sy <= endY; sy++ { + yDist := float64(sy) + 0.5 - srcY + weight := lanczos3(yDist) + if weight != 0 { + yWeights = append(yWeights, weight) + yIndices = append(yIndices, sy) + } + } + + // 处理每一列 + for x := 0; x < dstWidth; x++ { + var rSum, gSum, bSum, aSum, weightSum float64 + + // 应用预计算的权重 + for i, sy := range yIndices { + yWeight := yWeights[i] + + for j, sx := range xIndices[x] { + xWeight := xWeights[x][j] + weight := xWeight * yWeight + + // 获取源像素颜色 + srcColor := src.At(sx+srcBounds.Min.X, sy+srcBounds.Min.Y) + r, g, b, a := srcColor.RGBA() + + // 累加加权颜色值 + rSum += float64(r>>8) * weight + gSum += float64(g>>8) * weight + bSum += float64(b>>8) * weight + aSum += float64(a>>8) * weight + weightSum += weight + } + } + + // 防止除以零 + if weightSum == 0 { + weightSum = 1 + } + + // 计算最终颜色值 + r := clamp(rSum / weightSum) + g := clamp(gSum / weightSum) + b := clamp(bSum / weightSum) + a := clamp(aSum / weightSum) + + // 设置目标像素 + dst.SetRGBA(x, y, color.RGBA{r, g, b, a}) + } + } + + return dst +} + +// 函数计算Lanczos3核函数值 +func lanczos3(x float64) float64 { + if x == 0 { + return 1.0 + } + if x < -3 || x > 3 { + return 0.0 + } + return (3 * math.Sin(math.Pi*x) * math.Sin(math.Pi*x/3)) / (math.Pi * math.Pi * x * x) +} + +// 将值限制在0-255范围内 +func clamp(v float64) uint8 { + if v < 0 { + return 0 + } + if v > 255 { + return 255 + } + return uint8(v + 0.5) +} + +// 图片裁切 +func cropImage(config *Config, x, y, width, height int) (string, error) { + // 创建输出目录 + if err := createDirs(config); err != nil { + return "", fmt.Errorf("创建目录失败: %v", err) + } + + // 打开图片 + file, err := os.Open(config.FileName) + if err != nil { + return "", fmt.Errorf("打开图片失败: %v", err) + } + defer file.Close() + // 解码原始图片 + img, format, err := image.Decode(file) + if err != nil { + return "", fmt.Errorf("图片解码失败: %v", err) + } + + fmt.Printf("输入图片: %s (%dx%d, 格式: %s)\n", + config.FileName, img.Bounds().Dx(), img.Bounds().Dy(), format) + + imgFile, err := basicCrop(img, x, y, width, height) + if err != nil { + return "", err + } + + // 保存图片到指定目录下 + filename := filepath.Base(config.FileName) + destPath := filepath.Join(config.OutputDir, config.CropDir, filename) + err = saveImage(destPath, imgFile, format) + if err != nil { + return "", fmt.Errorf("保存图片失败: %v", err) + } + return destPath, nil +} + +// basicCrop 基础裁切功能 +func basicCrop(src image.Image, x, y, width, height int) (image.Image, error) { + srcBounds := src.Bounds() + srcWidth := srcBounds.Dx() + srcHeight := srcBounds.Dy() + + // 验证裁切参数 + if x < 0 || y < 0 || width <= 0 || height <= 0 { + return nil, fmt.Errorf("invalid crop parameters: x=%d, y=%d, width=%d, height=%d", + x, y, width, height) + } + + if x >= srcWidth || y >= srcHeight { + return nil, fmt.Errorf("crop start point (%d, %d) is outside image bounds (%dx%d)", + x, y, srcWidth, srcHeight) + } + + // 调整裁切尺寸以避免超出边界 + if x+width > srcWidth { + width = srcWidth - x + } + if y+height > srcHeight { + height = srcHeight - y + } + + // 从源图像中裁切 + cropped := image.NewRGBA(image.Rect(0, 0, width, height)) + for cy := 0; cy < height; cy++ { + for cx := 0; cx < width; cx++ { + cropped.Set(cx, cy, src.At(x+cx, y+cy)) + } + } + + return cropped, nil +} + +// 识别二维码 +func scanQRCode(fileName string) (bool, string, error) { + file, err := os.Open(fileName) + if err != nil { + return false, "", fmt.Errorf("打开图片失败: %v", err) + } + defer file.Close() + + // 解码图像 + img, _, err := image.Decode(file) + if err != nil { + return false, "", fmt.Errorf("解码图片失败: %v", err) + } + + // 创建二维码读取器 + bmp, err := gozxing.NewBinaryBitmapFromImage(img) + if err != nil { + return false, "", fmt.Errorf("创建位图失败: %v", err) + } + + // 解码二维码 + reader := qrcode.NewQRCodeReader() + + result, err := reader.Decode(bmp, nil) + if err != nil { + return false, "", analyzeQRCodeError(err) + } + + // 6. 打印二维码内容 + fmt.Printf("二维码内容: %s\n", result.GetText()) + + // 7. 获取二维码位置点 + points := result.GetResultPoints() + if len(points) < 3 { + fmt.Println("未找到足够的定位点") + } + + return true, result.GetText(), nil +} + +// 识别二维码 +func scanQRCodeNew(fileName string) (bool, string, error) { + file, err := os.Open(fileName) + if err != nil { + return false, "", fmt.Errorf("打开图片失败: %v", err) + } + defer file.Close() + + // 解码图像 + img, _, err := image.Decode(file) + if err != nil { + return false, "", fmt.Errorf("解码图片失败: %v", err) + } + + // 创建二维码读取器 + bmp, err := gozxing.NewBinaryBitmapFromImage(img) + if err != nil { + return false, "", fmt.Errorf("创建位图失败: %v", err) + } + + // 解码二维码 + reader := qrcode.NewQRCodeReader() + + result, err := reader.Decode(bmp, nil) + if err != nil { + return false, "", analyzeQRCodeError(err) + } + + // 6. 打印二维码内容 + fmt.Printf("二维码内容: %s\n", result.GetText()) + + // 7. 获取二维码位置点 + points := result.GetResultPoints() + if len(points) < 3 { + fmt.Println("未找到足够的定位点") + } + + // 8. 计算二维码边界框 + minX, minY := int(points[0].GetX()), int(points[0].GetY()) + maxX, maxY := minX, minY + + for _, point := range points { + x, y := int(point.GetX()), int(point.GetY()) + if x < minX { + minX = x + } + if x > maxX { + maxX = x + } + if y < minY { + minY = y + } + if y > maxY { + maxY = y + } + } + + // 9. 添加一些边距(可选) + margin := 10 + minX -= margin + minY -= margin + maxX += margin + maxY += margin + + // 确保坐标不超出图片范围 + bounds := img.Bounds() + if minX < bounds.Min.X { + minX = bounds.Min.X + } + if minY < bounds.Min.Y { + minY = bounds.Min.Y + } + if maxX > bounds.Max.X { + maxX = bounds.Max.X + } + if maxY > bounds.Max.Y { + maxY = bounds.Max.Y + } + + // 10. 创建裁剪区域 + qrRect := image.Rect(minX, minY, maxX, maxY) + fmt.Printf("二维码位置: %v\n", qrRect) + + // 11. 创建新图片并复制二维码区域 + qrImg := image.NewRGBA(qrRect) + draw.Draw(qrImg, qrImg.Bounds(), img, qrRect.Min, draw.Src) + + return true, result.GetText(), nil +} + +// 图像预处理 +func preprocessImage(img image.Image) image.Image { + // 转为灰度图 + gray := imaging.Grayscale(img) + + // 增强对比度 + enhanced := imaging.AdjustContrast(gray, 20) + + // 高斯模糊去噪 + blurred := imaging.Blur(enhanced, 1.0) + + // 自适应二值化 + binary := adaptiveThreshold(blurred) + + return binary +} + +// 二值化处理 +func adaptiveThreshold(img image.Image) image.Image { + bounds := img.Bounds() + dst := image.NewGray(bounds) + + // 局部自适应阈值 + blockSize := 15 + c := 2.0 + + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + for x := bounds.Min.X; x < bounds.Max.X; x++ { + // 计算局部均值 + sum := 0.0 + count := 0 + + for dy := -blockSize / 2; dy <= blockSize/2; dy++ { + for dx := -blockSize / 2; dx <= blockSize/2; dx++ { + nx, ny := x+dx, y+dy + if nx >= bounds.Min.X && nx < bounds.Max.X && + ny >= bounds.Min.Y && ny < bounds.Max.Y { + r, _, _, _ := img.At(nx, ny).RGBA() + sum += float64(r >> 8) + count++ + } + } + } + + localMean := sum / float64(count) + r, _, _, _ := img.At(x, y).RGBA() + gray := float64(r >> 8) + + if gray > localMean-c { + dst.SetGray(x, y, color.Gray{Y: 255}) + } else { + dst.SetGray(x, y, color.Gray{Y: 0}) + } + } + } + return dst +} + +// 分析二维码错误类型 +func analyzeQRCodeError(err error) error { + if err == nil { + return nil + } + // 检查不同类型的错误 + switch e := err.(type) { + case gozxing.ChecksumException: + return fmt.Errorf("二维码校验失败(可能部分损坏或被遮挡): %v", e) + case gozxing.FormatException: + return fmt.Errorf("二维码格式错误(可能不是有效的二维码): %v", e) + case gozxing.NotFoundException: + return fmt.Errorf("二维码格式错误: %v", e) + default: + return fmt.Errorf("解码失败: %v", err) + } +} + +// 生成二维码 +func generateQRCode(content string, width int, height int, fileName string) (string, error) { + // 创建 QR 码写入器 + writer := qrcode.NewQRCodeWriter() + + // 设置编码参数 + hints := make(map[gozxing.EncodeHintType]interface{}) + + // 纠错级别设置 + hints[gozxing.EncodeHintType_ERROR_CORRECTION] = "H" + + // 字符集 + hints[gozxing.EncodeHintType_CHARACTER_SET] = "UTF-8" + + // 边距 + hints[gozxing.EncodeHintType_MARGIN] = 4 + + // 生成二维码 + bitMatrix, err := writer.Encode(content, gozxing.BarcodeFormat_QR_CODE, width, height, hints) + if err != nil { + return "", fmt.Errorf("生成二维码失败: %v", err) + } + + // 创建图像 + img := image.NewRGBA(image.Rect(0, 0, width, height)) + + // 白色背景 + 黑色二维码点(合并为单次遍历) + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + if bitMatrix.Get(x, y) { + img.Set(x, y, color.Black) + } else { + img.Set(x, y, color.White) + } + } + } + + // 保存文件 + file, err := os.Create(fileName) + if err != nil { + return "", fmt.Errorf("保存二维码失败: %v", err) + } + defer file.Close() + + png.Encode(file, img) + return fmt.Sprintf("生成二维码成功: %v", fileName), nil +} + +// 简单的自动换行实现(按字符数,适合中英文混合) +// 返回处理后的行列表和是否还有更多内容未显示 +func simpleAutoWrap(text string, maxCharsPerLine int) ([]string, bool) { + var lines []string + var currentLine strings.Builder + charCount := 0 + + for _, r := range text { + // 换行符处理 + if r == '\n' { + if currentLine.Len() > 0 { + lines = append(lines, currentLine.String()) + currentLine.Reset() + charCount = 0 + } + continue + } + + // 计算字符宽度(中文算2个字符,英文算1个) + charWidth := 1 + if r >= 0x4E00 && r <= 0x9FFF { // 中文范围 + charWidth = 2 + } else if r >= 0x3000 && r <= 0x303F { // 中文标点 + charWidth = 2 + } + + // 检查是否需要换行 + if charCount+charWidth > maxCharsPerLine && currentLine.Len() > 0 { + lines = append(lines, currentLine.String()) + currentLine.Reset() + charCount = 0 + } + + currentLine.WriteRune(r) + charCount += charWidth + } + + if currentLine.Len() > 0 { + lines = append(lines, currentLine.String()) + } + + return lines, false // 返回false表示所有内容都已处理 +} + +// 创建带中文字体的文本图片,支持超出部分显示... +// text 文本, width, height 宽度高度, fontSize 文字大小, outputPath 输入路径 +func createChineseTextImage(text string, width, height int, fontSize float64, outputPath string) (string, error) { + // 获取字体(使用缓存避免重复加载) + f, err := getCachedFont() + if err != nil { + return "", err + } + + // 创建图片 + img := image.NewRGBA(image.Rect(0, 0, width, height)) + + // 白色背景 + white := color.RGBA{255, 255, 255, 255} + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + img.Set(x, y, white) + } + } + + // 创建freetype上下文 + c := freetype.NewContext() + c.SetDPI(72) + c.SetFont(f) + c.SetFontSize(fontSize) + c.SetClip(img.Bounds()) + c.SetDst(img) + c.SetSrc(image.NewUniform(color.Black)) + + // 计算可用的文本宽度(左右各留50像素边距) + availableWidth := width - 100 + + // 根据字体大小计算每行大约可以显示多少个字符 + charsPerLine := int(float64(availableWidth) / fontSize * 1.7) + if charsPerLine < 10 { + charsPerLine = 10 + } + + // 计算最大可显示行数 + // X坐标:从左侧20像素开始 + // Y坐标:从顶部20像素 + 字体高度 + lineSpacing := int(c.PointToFixed(fontSize*1.5) >> 6) + topMargin := 50 + int(c.PointToFixed(fontSize)>>6) + bottomMargin := 50 + maxLines := (height - topMargin - bottomMargin) / lineSpacing + if maxLines <= 0 { + maxLines = 1 + } + + // 使用简单的自动换行,获取所有行 + allLines, _ := simpleAutoWrap(text, charsPerLine) + + // 检查是否有超出图片的内容 + hasMore := len(allLines) > maxLines + displayLines := allLines + if hasMore { + // 只显示前 maxLines-1 行,最后一行添加 "..." + displayLines = allLines[:maxLines-1] + + // 获取最后一行文本,并确保能显示 "..." + lastLine := allLines[maxLines-1] + lastLineChars := 0 + var truncatedLine strings.Builder + + for _, r := range lastLine { + // 计算字符宽度 + charWidth := 1 + if r >= 0x4E00 && r <= 0x9FFF { + charWidth = 2 + } else if r >= 0x3000 && r <= 0x303F { + charWidth = 2 + } + + // 检查是否还能添加字符(留出3个字符给"...") + if lastLineChars+charWidth > charsPerLine-3 { + break + } + + truncatedLine.WriteRune(r) + lastLineChars += charWidth + } + + // 添加 "..." + truncatedText := truncatedLine.String() + " ..." + displayLines = append(displayLines, truncatedText) + } else { + // 如果内容不多,直接显示所有行 + displayLines = allLines + if len(displayLines) > maxLines { + displayLines = displayLines[:maxLines] + } + } + + // 设置起始位置 + startX := 50 + startY := topMargin + + // 绘制每行文字 + pt := freetype.Pt(startX, startY) + for i, line := range displayLines { + // 确保不会超出图片底部 + if i*lineSpacing > height-bottomMargin { + break + } + + // 绘制文字 + _, err = c.DrawString(line, pt) + if err != nil { + return "", fmt.Errorf("绘制文字失败: %v", err) + } + + // 移动到下一行 + pt.Y += c.PointToFixed(fontSize * 1.5) + } + + // 保存图片 + file, err := os.Create(outputPath) + if err != nil { + return "", fmt.Errorf("创建文件失败: %v", err) + } + defer file.Close() + err = png.Encode(file, img) + if err != nil { + return "", fmt.Errorf("编码并写入失败: %v", err) + } + + return outputPath, nil +} + +// 获取系统字体路径 +func getDefaultFontPath() string { + switch runtime.GOOS { + case "windows": + paths := []string{ + "C:/Windows/Fonts/simhei.ttf", // 黑体 + "C:/Windows/Fonts/simsun.ttc", // 宋体 + "C:/Windows/Fonts/msyh.ttc", // 微软雅黑 + } + for _, path := range paths { + if _, err := os.Stat(path); err == nil { + return path + } + } + case "darwin": + return "/System/Library/Fonts/PingFang.ttc" + case "linux": + paths := []string{ + "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc", + "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc", + } + for _, path := range paths { + if _, err := os.Stat(path); err == nil { + return path + } + } + } + return "" +} + +// 绘制中文文本(支持自动换行) +func drawChineseText(img *image.RGBA, title, author, publisher string) error { + fontObj, err := getCachedFont() + if err != nil { + return err + } + + // 创建freetype上下文 + c := freetype.NewContext() + c.SetDPI(72) + c.SetFont(fontObj) + c.SetSrc(image.NewUniform(color.Black)) + c.SetClip(img.Bounds()) + c.SetDst(img) + + // 定义绘制区域的宽度 + maxWidth := 400 // 可根据需要调整 + + // 1. 绘制书名(使用较大字体,支持多行) + c.SetFontSize(45) + titleLines := wrapText(title, fontObj, 45, maxWidth) + // 限制书名最多显示3行 + if len(titleLines) > 3 { + titleLines = titleLines[:3] + } + titleY := 250 // 起始Y坐标 + titleLineHeight := 60 // 行高 + + for i, line := range titleLines { + lineWidth := calculateStringWidth(line, fontObj, 45) + pt := freetype.Pt((800-int(lineWidth))/2+30, titleY+i*titleLineHeight) + if _, err := c.DrawString(line, pt); err != nil { + return fmt.Errorf("绘制书名失败: %v", err) + } + } + + // 2. 绘制作者(使用中等字体,支持多行) + c.SetFontSize(28) + authorLines := wrapText(author, fontObj, 28, maxWidth) + // 限制作者最多显示2行 + if len(authorLines) > 2 { + authorLines = authorLines[:2] + } + authorY := 420 // 起始Y坐标 + authorLineHeight := 40 // 行高 + + for i, line := range authorLines { + lineWidth := calculateStringWidth(line, fontObj, 28) + pt := freetype.Pt((800-int(lineWidth))/2+30, authorY+i*authorLineHeight) + if _, err := c.DrawString(line, pt); err != nil { + return fmt.Errorf("绘制作者失败: %v", err) + } + } + + // 3. 绘制出版社(使用中等字体,支持多行) + c.SetFontSize(18) + publisherLines := wrapText(publisher, fontObj, 18, maxWidth) + // 限制出版社最多显示2行 + if len(publisherLines) > 1 { + publisherLines = publisherLines[:1] + } + publisherY := 700 // 起始Y坐标 + publisherLineHeight := 30 // 行高 + + for i, line := range publisherLines { + lineWidth := calculateStringWidth(line, fontObj, 18) + pt := freetype.Pt((800-int(lineWidth))/2+30, publisherY+i*publisherLineHeight) + if _, err := c.DrawString(line, pt); err != nil { + return fmt.Errorf("绘制出版社失败: %v", err) + } + } + + // 4. 绘制右下角文字 + err = drawBottomRightText(img, fontObj) + if err != nil { + return fmt.Errorf("绘制右下角文字失败: %v", err) + } + + return nil +} + +// 绘制右下角文字 +func drawBottomRightText(img *image.RGBA, fontObj *truetype.Font) error { + // 设置文字内容 + line1 := "此为实例图片" + line2 := "联系客服获取实图" + + // 设置字体大小 + fontSize := 8.0 + + // 计算右边界距 + rightMargin := 10 // 距离右边界的像素 + bottomMargin := 10 // 距离底部的像素 + + // 计算行高 + lineHeight := int(fontSize * 1.5) + + // 创建freetype上下文 + c := freetype.NewContext() + c.SetDPI(72) + c.SetFont(fontObj) + c.SetClip(img.Bounds()) + c.SetDst(img) + + // 计算第二行(最后一行)的位置 + line2Width := calculateStringWidth(line2, fontObj, fontSize) + x2 := img.Bounds().Dx() - line2Width - rightMargin + y2 := img.Bounds().Dy() - bottomMargin + + // 计算第一行的位置 + line1Width := calculateStringWidth(line1, fontObj, fontSize) + x1 := img.Bounds().Dx() - line1Width - rightMargin + y1 := y2 - lineHeight + + // 设置字体大小 + c.SetFontSize(fontSize) + + // 方法1:多层绘制实现描边效果 + strokeRadius := 1.0 // 描边半径 + + // 绘制描边(灰色,8个方向) + strokeColor := color.RGBA{128, 128, 128, 150} // 灰色 + c.SetSrc(image.NewUniform(strokeColor)) + + // 为第二行绘制描边 + offsets := []struct{ dx, dy float64 }{ + {-strokeRadius, -strokeRadius}, {0, -strokeRadius}, {strokeRadius, -strokeRadius}, + {-strokeRadius, 0}, {strokeRadius, 0}, + {-strokeRadius, strokeRadius}, {0, strokeRadius}, {strokeRadius, strokeRadius}, + } + + for _, offset := range offsets { + pt2 := freetype.Pt(int(float64(x2)+offset.dx), int(float64(y2)+offset.dy)) + if _, err := c.DrawString(line2, pt2); err != nil { + return fmt.Errorf("绘制第二行描边失败: %v", err) + } + + pt1 := freetype.Pt(int(float64(x1)+offset.dx), int(float64(y1)+offset.dy)) + if _, err := c.DrawString(line1, pt1); err != nil { + return fmt.Errorf("绘制第一行描边失败: %v", err) + } + } + + // 绘制白色文字(覆盖在中间) + textColor := color.RGBA{255, 255, 255, 255} // 白色 + c.SetSrc(image.NewUniform(textColor)) + + // 绘制第二行文字 + pt2 := freetype.Pt(x2, y2) + if _, err := c.DrawString(line2, pt2); err != nil { + return fmt.Errorf("绘制第二行文字失败: %v", err) + } + + // 绘制第一行文字 + pt1 := freetype.Pt(x1, y1) + if _, err := c.DrawString(line1, pt1); err != nil { + return fmt.Errorf("绘制第一行文字失败: %v", err) + } + + return nil +} + +// 文本换行函数 +func wrapText(text string, fontObj *truetype.Font, fontSize float64, maxWidth int) []string { + var lines []string + var currentLine strings.Builder + currentLine.Grow(len(text)) + var currentWidth int + + for _, ch := range text { + charWidth := calculateStringWidth(string(ch), fontObj, fontSize) + + // 如果当前字符是换行符,直接换行 + if ch == '\n' { + if currentLine.Len() > 0 { + lines = append(lines, currentLine.String()) + currentLine.Reset() + } + currentWidth = 0 + continue + } + + // 如果加上当前字符会超出宽度,开始新行 + if currentWidth+charWidth > maxWidth && currentLine.Len() > 0 { + lines = append(lines, currentLine.String()) + currentLine.Reset() + currentLine.WriteRune(ch) + currentWidth = charWidth + } else { + currentLine.WriteRune(ch) + currentWidth += charWidth + } + } + + // 添加最后一行 + if currentLine.Len() > 0 { + lines = append(lines, currentLine.String()) + } + + return lines +} + +// 计算字符串宽度 +func calculateStringWidth(text string, fontObj *truetype.Font, fontSize float64) int { + width := 0 + for _, ch := range text { + idx := fontObj.Index(ch) + horizAdvance := fontObj.HMetric(fixed.Int26_6(fontSize), idx).AdvanceWidth + width += int(horizAdvance) + } + return width +} + +// 加载PNG图片 +func loadPNG(filename string) (*image.RGBA, error) { + file, err := os.Open(filename) + if err != nil { + return nil, err + } + defer file.Close() + + img, err := png.Decode(file) + if err != nil { + return nil, err + } + + rgba := image.NewRGBA(img.Bounds()) + draw.Draw(rgba, rgba.Bounds(), img, img.Bounds().Min, draw.Src) + + return rgba, nil +} + +// 保存为PNG文件 +func savePNG(img image.Image, filename string) error { + file, err := os.Create(filename) + if err != nil { + return err + } + defer file.Close() + + return png.Encode(file, img) +} + +const ( + CODE128 = "code128" + EAN13 = "ean13" + CODE39 = "code39" +) + +// 根据类型生成条形码 +func generateBarcode(barcodeType, content, filename string) (string, error) { + switch barcodeType { + case CODE128: + return generateCode128(content, filename) + case EAN13: + return generateEAN13(content, filename) + case CODE39: + return generateCode39(content, filename) + } + return "", fmt.Errorf("条形码类型不存在: %s", barcodeType) +} + +// 生成Code128条形码 +func generateCode128(content, filename string) (string, error) { + // 创建条形码 + code, err := code128.Encode(content) + if err != nil { + return "", fmt.Errorf("创建条形码失败: %v", err) + } + + // 缩放条形码尺寸 + scaledCode, err := barcode.Scale(code, 250, 85) + if err != nil { + return "", fmt.Errorf("缩放条形码尺寸失败: %v", err) + } + + // 创建带文字的图像 + imgWidth := 250 + imgHeight := 110 // 条形码高度 + 文字区域高度 + dc := gg.NewContext(imgWidth, imgHeight) + + // 设置白色背景 + dc.SetColor(color.White) + dc.Clear() + + dc.DrawImage(scaledCode, 0, 10) + + fontPath := getDefaultFontPath() + if fontPath == "" { + return "", fmt.Errorf("未找到系统字体文件,请手动指定字体路径") + } + + // 设置文字属性 + if err := dc.LoadFontFace("/System/Library/Fonts/Helvetica.ttc", 14); err != nil { + // 如果字体文件不存在,使用默认字体 + dc.LoadFontFace("Arial.ttf", 14) + } + + dc.SetColor(color.Black) + + // 在条形码下方居中绘制文字0 + textY := float64(85 + 20) // 条形码高度100 + 20像素间距 + textWidth, _ := dc.MeasureString(content) + textX := (float64(imgWidth) - textWidth) / 2 + dc.DrawString(content, textX, textY) + + // 保存图像 + err = dc.SavePNG(filename) + if err != nil { + return "", fmt.Errorf("保存图像失败: %v", err) + } + return filename, nil +} + +// 生成EAN13条形码 +func generateEAN13(content, filename string) (string, error) { + eanCode, err := ean.Encode(content) + if err != nil { + return "", fmt.Errorf("创建条形码失败: %v", err) + } + + scaledEanCode, err := barcode.Scale(eanCode, 250, 85) + if err != nil { + return "", fmt.Errorf("缩放条形码尺寸失败: %v", err) + } + + // 创建带文字的图像 + imgWidth := 250 + imgHeight := 110 // 条形码高度 + 文字区域高度 + dc := gg.NewContext(imgWidth, imgHeight) + + // 设置白色背景 + dc.SetColor(color.White) + dc.Clear() + + dc.DrawImage(scaledEanCode, 0, 10) + + fontPath := getDefaultFontPath() + if fontPath == "" { + return "", fmt.Errorf("未找到系统字体文件,请手动指定字体路径") + } + + // 设置文字属性 + if err := dc.LoadFontFace("/System/Library/Fonts/Helvetica.ttc", 14); err != nil { + // 如果字体文件不存在,使用默认字体 + dc.LoadFontFace("Arial.ttf", 14) + } + + dc.SetColor(color.Black) + + // 在条形码下方居中绘制文字0 + textY := float64(85 + 20) // 条形码高度100 + 20像素间距 + textWidth, _ := dc.MeasureString(content) + textX := (float64(imgWidth) - textWidth) / 2 + dc.DrawString(content, textX, textY) + + // 保存图像 + err = dc.SavePNG(filename) + if err != nil { + return "", fmt.Errorf("保存图像失败: %v", err) + } + return filename, nil +} + +// 生成Code39 +func generateCode39(content, filename string) (string, error) { + code, err := code39.Encode(content, true, true) + if err != nil { + return "", fmt.Errorf("创建条形码失败: %v", err) + } + + scaled, err := barcode.Scale(code, 250, 85) + if err != nil { + return "", fmt.Errorf("缩放条形码尺寸失败: %v", err) + } + + // 创建带文字的图像 + imgWidth := 250 + imgHeight := 110 // 条形码高度 + 文字区域高度 + dc := gg.NewContext(imgWidth, imgHeight) + + // 设置白色背景 + dc.SetColor(color.White) + dc.Clear() + + dc.DrawImage(scaled, 0, 10) + + fontPath := getDefaultFontPath() + if fontPath == "" { + return "", fmt.Errorf("未找到系统字体文件,请手动指定字体路径") + } + + // 设置文字属性 + if err := dc.LoadFontFace("/System/Library/Fonts/Helvetica.ttc", 14); err != nil { + // 如果字体文件不存在,使用默认字体 + dc.LoadFontFace("Arial.ttf", 14) + } + + dc.SetColor(color.Black) + + // 在条形码下方居中绘制文字0 + textY := float64(85 + 20) // 条形码高度100 + 20像素间距 + textWidth, _ := dc.MeasureString(content) + textX := (float64(imgWidth) - textWidth) / 2 + dc.DrawString(content, textX, textY) + + // 保存图像 + err = dc.SavePNG(filename) + if err != nil { + return "", fmt.Errorf("保存图像失败: %v", err) + } + return filename, nil +} + +// =================== 辅助函数 ======================= + +// 辅助函数 +func absDiff(a, b uint8) uint8 { + if a > b { + return a - b + } + return b - a +} + +// =========================== 添加水印 =============================== + +// WatermarkConfig 水印配置结构体 +type WatermarkConfig struct { + SourceImageURL string // 源图片URL地址 + WatermarkURL string // 水印图片URL地址 + WatermarkBase64 string // 水印图片base64编码字符串(新增,优先使用) + Opacity float64 // 不透明度 (0.0-1.0) + Position string // 位置: center, top-left, top-right, bottom-left, bottom-right, tile + TileSpacing int // 平铺时的间距 + Scale float64 // 水印缩放比例 (0.0-1.0) + Rotation float64 // 旋转角度 (度数) + XOffset int // X轴偏移量AddWatermarkFromURLEx + YOffset int // Y轴偏移量 + Timeout int // 下载超时时间(秒),默认30秒 + OutputFormat string // 输出格式: "jpeg", "png", "auto"(默认auto,根据源图片格式) + JPEGQuality int // JPEG质量 (1-100),默认95 + TargetWidth int // 目标宽度(0表示不缩放) + TargetHeight int // 目标高度(0表示不缩放) + ResizeMode string // 缩放模式: "fit"(适应,保持比例,可能有黑边), "fill"(填充,裁剪), "stretch"(拉伸) +} + +// 字体缓存,避免重复读取/解析字体文件(5-15MB) +var ( + fontCacheOnce sync.Once + cachedFont *truetype.Font + cachedFontErr error +) + +func getCachedFont() (*truetype.Font, error) { + fontCacheOnce.Do(func() { + fontPath := getDefaultFontPath() + if fontPath == "" { + cachedFontErr = fmt.Errorf("未找到系统字体文件,请手动指定字体路径") + return + } + fontBytes, err := os.ReadFile(fontPath) + if err != nil { + cachedFontErr = fmt.Errorf("读取字体文件失败: %v", err) + return + } + cachedFont, cachedFontErr = truetype.Parse(fontBytes) + }) + return cachedFont, cachedFontErr +} + +// 将客户端声明为全局变量或缓存 +var httpClient = &fasthttp.Client{ + MaxConnsPerHost: 100, + ReadTimeout: 30 * time.Second, + WriteTimeout: 30 * time.Second, +} + +// loadImageFromURL 直接从URL加载图片 +func loadImageFromURL(url string, timeout int) (image.Image, string, error) { + // 设置默认超时 + if timeout <= 0 { + timeout = 120 + } + + // 创建请求和响应对象 + req := fasthttp.AcquireRequest() + resp := fasthttp.AcquireResponse() + defer fasthttp.ReleaseRequest(req) + defer fasthttp.ReleaseResponse(resp) + + // 设置请求URL和方法 + req.SetRequestURI(url) + req.Header.SetMethod("GET") + + // 设置请求头 + req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7") + req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6") + req.Header.Set("Cache-Control", "max-age=0") + req.Header.Set("If-Modified-Since", "Mon, 09 Mar 2026 07:43:59 GMT") + req.Header.Set("Priority", "u=0, i") + req.Header.Set("Sec-Ch-Ua", `"Not:A-Brand";v="99", "Microsoft Edge";v="145", "Chromium";v="145"`) + req.Header.Set("Sec-Ch-Ua-Mobile", "?0") + req.Header.Set("Sec-Ch-Ua-Platform", "Windows") + req.Header.Set("Sec-Fetch-Dest", "document") + req.Header.Set("Sec-Fetch-Mode", "navigate") + req.Header.Set("Sec-Fetch-Site", "none") + req.Header.Set("Sec-Fetch-User", "?1") + req.Header.Set("Upgrade-Insecure-Requests", "1") + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 Edg/145.0.0.0") + + // 发送请求 + err := httpClient.Do(req, resp) + if err != nil { + return nil, "", fmt.Errorf("请求失败: %v\n", err) + } + + // 复制body数据,避免引用问题 + body := make([]byte, len(resp.Body())) + copy(body, resp.Body()) + + // 直接从响应体解码图片 + img, format, err := image.Decode(bytes.NewReader(body)) + if err != nil { + return nil, "", fmt.Errorf("解码图片失败: %v", err) + } + + return img, format, nil +} + +// 添加新的辅助函数:从base64字符串加载图片 +func loadImageFromBase64(base64Str string) (image.Image, string, error) { + // 移除可能的 data:image/xxx;base64, 前缀 + base64Str = strings.TrimPrefix(base64Str, "data:image/jpeg;base64,") + base64Str = strings.TrimPrefix(base64Str, "data:image/jpg;base64,") + base64Str = strings.TrimPrefix(base64Str, "data:image/png;base64,") + base64Str = strings.TrimPrefix(base64Str, "data:image/gif;base64,") + base64Str = strings.TrimPrefix(base64Str, "data:image/webp;base64,") + + // 解码base64 + imgData, err := base64.StdEncoding.DecodeString(base64Str) + if err != nil { + return nil, "", fmt.Errorf("解码base64失败: %v", err) + } + + // 检测图片格式 + format := detectImageFormat(imgData) + + // 解码图片 + img, _, err := image.Decode(bytes.NewReader(imgData)) + if err != nil { + return nil, "", fmt.Errorf("解码base64图片失败: %v", err) + } + + return img, format, nil +} + +// 检测图片格式的辅助函数 +func detectImageFormat(data []byte) string { + if len(data) < 12 { + return "unknown" + } + + // PNG: 137 80 78 71 13 10 26 10 + if len(data) >= 8 && data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47 { + return "png" + } + + // JPEG: 255 216 255 + if len(data) >= 3 && data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF { + return "jpg" + } + + // GIF: 71 73 70 + if len(data) >= 3 && data[0] == 0x47 && data[1] == 0x49 && data[2] == 0x46 { + return "gif" + } + + // WEBP: 82 73 70 70 + if len(data) >= 12 && data[0] == 0x52 && data[1] == 0x49 && data[2] == 0x46 && data[3] == 0x46 { + return "webp" + } + + return "unknown" +} + +// 判断HTTP状态码是否值得重试 +func isRetryableStatus(statusCode int) bool { + // 5xx服务器错误或429限流可以重试 + return statusCode >= 500 || statusCode == 429 +} + +// AddWatermarkFromURL 从URL加载图片添加水印并返回字节集 +func AddWatermarkFromURL(config WatermarkConfig) ([]byte, string, error) { + // 加载源图片 + srcImg, srcFormat, err := loadImageFromURL(config.SourceImageURL, config.Timeout) + if err != nil { + return nil, "", fmt.Errorf("加载源图片失败: %v", err) + } + + // 等比缩放原图到 TargetWidth x TargetHeight 范围内 + if config.TargetWidth > 0 || config.TargetHeight > 0 { + srcW := srcImg.Bounds().Dx() + srcH := srcImg.Bounds().Dy() + var newW, newH uint + if config.TargetWidth > 0 && config.TargetHeight > 0 { + scaleX := float64(config.TargetWidth) / float64(srcW) + scaleY := float64(config.TargetHeight) / float64(srcH) + scale := math.Min(scaleX, scaleY) + newW = uint(float64(srcW) * scale) + newH = uint(float64(srcH) * scale) + } else if config.TargetWidth > 0 { + newW = uint(config.TargetWidth) + newH = uint(float64(srcH) * float64(config.TargetWidth) / float64(srcW)) + } else { + newH = uint(config.TargetHeight) + newW = uint(float64(srcW) * float64(config.TargetHeight) / float64(srcH)) + } + srcImg = resize.Resize(newW, newH, srcImg, resize.Lanczos3) + } + + // 加载水印图片(支持URL或base64) + var watermarkImg image.Image + //var watermarkFormat string // 没用了 + + // 优先使用 base64 + if config.WatermarkBase64 != "" { + watermarkImg, _, err = loadImageFromBase64(config.WatermarkBase64) + if err != nil { + return nil, "", fmt.Errorf("从base64加载水印图片失败: %v", err) + } + } else if config.WatermarkURL != "" { + // 检查是否为base64格式的URL(以data:image开头) + if strings.HasPrefix(config.WatermarkURL, "data:image/") { + watermarkImg, _, err = loadImageFromBase64(config.WatermarkURL) + if err != nil { + return nil, "", fmt.Errorf("从base64 URL加载水印图片失败: %v", err) + } + } else { + // 普通URL + watermarkImg, _, err = loadImageFromURL(config.WatermarkURL, config.Timeout) + if err != nil { + return nil, "", fmt.Errorf("从URL加载水印图片失败: %v", err) + } + } + } else { + return nil, "", fmt.Errorf("必须提供水印图片(WatermarkURL或WatermarkBase64)") + } + + // 创建目标图片(RGBA格式以支持透明) + // 画布尺寸 = 水印尺寸 + watermarkBounds := watermarkImg.Bounds() + dst := image.NewRGBA(watermarkBounds) + + // 先绘制源图片(居中) + srcBounds := srcImg.Bounds() + x := (watermarkBounds.Dx() - srcBounds.Dx()) / 2 + y := (watermarkBounds.Dy() - srcBounds.Dy()) / 2 + draw.Draw(dst, image.Rect(x, y, x+srcBounds.Dx(), y+srcBounds.Dy()), srcImg, image.Point{}, draw.Src) + + // 处理水印 + err = applyWatermark(dst, watermarkImg, config) + if err != nil { + return nil, "", fmt.Errorf("应用水印失败: %v", err) + } + + // 按目标尺寸缩放 + finalImg := resizeOutputImage(dst, config) + + // 确定输出格式 + outputFormat := config.OutputFormat + if outputFormat == "" || outputFormat == "auto" { + outputFormat = srcFormat + } + + // 将图片编码为字节集 + imgBytes, err := encodeImageToBytes(finalImg, outputFormat, config.JPEGQuality) + if err != nil { + return nil, "", fmt.Errorf("编码图片失败: %v", err) + } + + return imgBytes, outputFormat, nil +} + +// encodeImageToBytes 将图片编码为字节集 +func encodeImageToBytes(img image.Image, format string, quality int) ([]byte, error) { + var buf bytes.Buffer + + switch strings.ToLower(format) { + case "jpeg", "jpg": + // 设置默认质量 + if quality <= 0 || quality > 100 { + quality = 95 + } + err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: quality}) + if err != nil { + return nil, fmt.Errorf("JPEG编码失败: %v", err) + } + case "png": + err := png.Encode(&buf, img) + if err != nil { + return nil, fmt.Errorf("PNG编码失败: %v", err) + } + default: + // 默认使用PNG + err := png.Encode(&buf, img) + if err != nil { + return nil, fmt.Errorf("PNG编码失败: %v", err) + } + } + + return buf.Bytes(), nil +} + +// applyWatermark 应用水印到图片 +func applyWatermark(dst *image.RGBA, watermark image.Image, config WatermarkConfig) error { + // 缩放水印 + watermark = scaleWatermark(watermark, config.Scale) + + // 根据位置绘制水印 + switch config.Position { + case "tile": + drawTileWatermark(dst, watermark, config) + default: + drawSingleWatermark(dst, watermark, config) + } + + return nil +} + +// scaleWatermark 缩放水印 +func scaleWatermark(watermark image.Image, scale float64) image.Image { + if scale <= 0 || scale >= 1 { + return watermark + } + + bounds := watermark.Bounds() + newWidth := uint(float64(bounds.Dx()) * scale) + newHeight := uint(float64(bounds.Dy()) * scale) + + if newWidth > 0 && newHeight > 0 { + return resize.Resize(newWidth, newHeight, watermark, resize.Lanczos3) + } + return watermark +} + +// drawSingleWatermark 绘制单个水印 +func drawSingleWatermark(dst *image.RGBA, watermark image.Image, config WatermarkConfig) { + bounds := dst.Bounds() + watermarkBounds := watermark.Bounds() + watermarkWidth := watermarkBounds.Dx() + watermarkHeight := watermarkBounds.Dy() + + // 计算位置 + var x, y int + + switch config.Position { + case "center": + x = (bounds.Dx()-watermarkWidth)/2 + config.XOffset + y = (bounds.Dy()-watermarkHeight)/2 + config.YOffset + case "top-left": + x = config.XOffset + y = config.YOffset + case "top-right": + x = bounds.Dx() - watermarkWidth - config.XOffset + y = config.YOffset + case "bottom-left": + x = config.XOffset + y = bounds.Dy() - watermarkHeight - config.YOffset + case "bottom-right": + x = bounds.Dx() - watermarkWidth - config.XOffset + y = bounds.Dy() - watermarkHeight - config.YOffset + default: // 默认居中 + x = (bounds.Dx()-watermarkWidth)/2 + config.XOffset + y = (bounds.Dy()-watermarkHeight)/2 + config.YOffset + } + + // 确保不超出边界 + x = max(0, min(x, bounds.Dx()-watermarkWidth)) + y = max(0, min(y, bounds.Dy()-watermarkHeight)) + + // 绘制水印 + drawWatermarkWithOpacity(dst, watermark, x, y, config.Opacity) +} + +// drawTileWatermark 平铺水印 +func drawTileWatermark(dst *image.RGBA, watermark image.Image, config WatermarkConfig) { + bounds := dst.Bounds() + watermarkBounds := watermark.Bounds() + watermarkWidth := watermarkBounds.Dx() + watermarkHeight := watermarkBounds.Dy() + + spacing := config.TileSpacing + if spacing < 0 { + spacing = 0 + } + + stepX := watermarkWidth + spacing + stepY := watermarkHeight + spacing + + // 计算起始偏移,使水印均匀分布 + startX := (bounds.Dx() % stepX) / 2 + startY := (bounds.Dy() % stepY) / 2 + + for y := startY; y < bounds.Dy(); y += stepY { + for x := startX; x < bounds.Dx(); x += stepX { + drawWatermarkWithOpacity(dst, watermark, x, y, config.Opacity) + } + } +} + +// drawWatermarkWithOpacity 绘制带透明度的水印 +func drawWatermarkWithOpacity(dst *image.RGBA, watermark image.Image, x, y int, opacity float64) { + if opacity < 0 { + opacity = 0 + } + if opacity > 1 { + opacity = 1 + } + + watermarkBounds := watermark.Bounds() + for wy := 0; wy < watermarkBounds.Dy(); wy++ { + for wx := 0; wx < watermarkBounds.Dx(); wx++ { + // 计算目标位置 + dx := x + wx + dy := y + wy + + // 确保在目标图片范围内 + if dx < 0 || dx >= dst.Bounds().Dx() || dy < 0 || dy >= dst.Bounds().Dy() { + continue + } + + // 获取水印像素 + wColor := watermark.At(wx, wy) + wr, wg, wb, wa := wColor.RGBA() + + // 如果有透明度,考虑水印本身的透明度 + if wa > 0 { + // 转换为8位 + wr8 := uint8(wr >> 8) + wg8 := uint8(wg >> 8) + wb8 := uint8(wb >> 8) + wa8 := uint8(wa >> 8) + + // 获取目标像素 + dstColor := dst.At(dx, dy) + dr, dg, db, _ := dstColor.RGBA() + dr8 := uint8(dr >> 8) + dg8 := uint8(dg >> 8) + db8 := uint8(db >> 8) + + // 混合颜色(考虑水印透明度和设置的不透明度) + alpha := float64(wa8) / 255.0 * opacity + + r := uint8(float64(dr8)*(1-alpha) + float64(wr8)*alpha) + g := uint8(float64(dg8)*(1-alpha) + float64(wg8)*alpha) + b := uint8(float64(db8)*(1-alpha) + float64(wb8)*alpha) + + dst.Set(dx, dy, color.RGBA{r, g, b, 255}) + } + } + } +} + +// resizeOutputImage 根据配置缩放输出图片 +func resizeOutputImage(img image.Image, config WatermarkConfig) image.Image { + if config.TargetWidth <= 0 && config.TargetHeight <= 0 { + return img + } + + srcBounds := img.Bounds() + srcW := srcBounds.Dx() + srcH := srcBounds.Dy() + + // 如果只设了一个维度,另一个按比例算 + targetW := config.TargetWidth + targetH := config.TargetHeight + if targetW <= 0 { + targetW = int(float64(targetH) * float64(srcW) / float64(srcH)) + } + if targetH <= 0 { + targetH = int(float64(targetW) * float64(srcH) / float64(srcW)) + } + + switch config.ResizeMode { + case "fit": + return resizeFit(img, srcW, srcH, targetW, targetH) + case "fill": + return resizeFill(img, srcW, srcH, targetW, targetH) + case "stretch": + fallthrough + default: + return resize.Resize(uint(targetW), uint(targetH), img, resize.Lanczos3) + } +} + +// resizeFit 等比缩放适应,多出部分填白 +func resizeFit(img image.Image, srcW, srcH, targetW, targetH int) image.Image { + scaleX := float64(targetW) / float64(srcW) + scaleY := float64(targetH) / float64(srcH) + scale := math.Min(scaleX, scaleY) + + newW := int(float64(srcW) * scale) + newH := int(float64(srcH) * scale) + + scaled := resize.Resize(uint(newW), uint(newH), img, resize.Lanczos3) + + // 居中放到白色画布上 + dst := image.NewRGBA(image.Rect(0, 0, targetW, targetH)) + draw.Draw(dst, dst.Bounds(), &image.Uniform{C: color.White}, image.Point{}, draw.Src) + + x := (targetW - newW) / 2 + y := (targetH - newH) / 2 + draw.Draw(dst, image.Rect(x, y, x+newW, y+newH), scaled, image.Point{}, draw.Over) + + return dst +} + +// resizeFill 等比缩放覆盖,溢出部分居中裁剪 +func resizeFill(img image.Image, srcW, srcH, targetW, targetH int) image.Image { + scaleX := float64(targetW) / float64(srcW) + scaleY := float64(targetH) / float64(srcH) + scale := math.Max(scaleX, scaleY) + + newW := int(float64(srcW) * scale) + newH := int(float64(srcH) * scale) + + scaled := resize.Resize(uint(newW), uint(newH), img, resize.Lanczos3) + + // 居中裁剪 + cropX := (newW - targetW) / 2 + cropY := (newH - targetH) / 2 + + dst := image.NewRGBA(image.Rect(0, 0, targetW, targetH)) + draw.Draw(dst, dst.Bounds(), scaled, image.Point{X: cropX, Y: cropY}, draw.Src) + + return dst +} diff --git a/planB/tool/tool.go b/planB/tool/tool.go index 6e867f3..f2da01e 100644 --- a/planB/tool/tool.go +++ b/planB/tool/tool.go @@ -15,8 +15,10 @@ import ( "path" "path/filepath" "planA/planB/initialization/golabl" + modulesImage "planA/planB/modules/image" "planA/planB/service" planBType "planA/planB/type" + planBTypeKfz "planA/planB/type/kfz" planBTypeModules "planA/planB/type/modules" planAType "planA/type" "strconv" @@ -278,7 +280,7 @@ func AddWatermarkFromURLExs(imgUrl []string, watermarkImgUrl string, watermarkPo maxRetries := 3 for retryCount := 0; retryCount <= maxRetries; retryCount++ { - newImgJson, addWatermarkFromURLExsErr = golabl.ImageDll.AddWatermarkFromURLExs(imgUrl[i], watermarkImgUrl) + newImgJson, addWatermarkFromURLExsErr = modulesImage.AddWatermarkFromURLExs(imgUrl[i], watermarkImgUrl) // 判断是否包含超时错误 if addWatermarkFromURLExsErr != nil && strings.Contains(addWatermarkFromURLExsErr.Error(), "dialing to the given TCP address timed out") { @@ -597,10 +599,23 @@ func UploadImageToKfz(watermarkFromURLExsBase64Arr []planBTypeModules.ImageResul return nil, saveBase64ImageByDateErr } //将图片上传到孔夫子 - _, kfzGoodsImageUploadErr := golabl.KfzDll.KfzGoodsImageUpload(golabl.Config.KfzConfig.AppId, golabl.Config.KfzConfig.AppSecret, golabl.Task.Header.ShopMsg.Token, imgTempUrl) + upload, kfzGoodsImageUploadErr := golabl.KfzDll.KfzGoodsImageUpload(golabl.Config.KfzConfig.AppId, golabl.Config.KfzConfig.AppSecret, golabl.Task.Header.ShopMsg.Token, imgTempUrl) if kfzGoodsImageUploadErr != nil { return nil, kfzGoodsImageUploadErr } + // 解析数据 + var uploadData planBTypeKfz.UploadImgRet + unmarshalErr := json.Unmarshal([]byte(upload), &uploadData) + if unmarshalErr != nil { + return nil, fmt.Errorf("解析上传图片数据失败 %v", unmarshalErr) + } + if uploadData.ErrorResponse != nil && uploadData.ErrorResponse.Code != 0 { + return nil, fmt.Errorf("上传图片失败 错误码 %v 错误描述 %v", uploadData.ErrorResponse.Code, uploadData.ErrorResponse.SubMsg) + } + if uploadData.SuccessResponse == nil { + return nil, fmt.Errorf("上传图片成功但返回数据为空") + } + imageUrlArr = append(imageUrlArr, uploadData.SuccessResponse.Image.Url) } return imageUrlArr, nil }