Compare commits
27 Commits
v0.0.47
...
v1.0.0.beta18
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a1cb90766 | |||
| ce536eb5bc | |||
| 0244e330fd | |||
| 035bac10cc | |||
| cb808f9c68 | |||
| 22735aeaae | |||
| dd6eeb5dfe | |||
| ac6409f3e2 | |||
| 36a6932056 | |||
| 72dab0e968 | |||
| b716434f62 | |||
| 8ed7dd77e4 | |||
| ae1c9c695e | |||
| 66995c0c78 | |||
| 120e690795 | |||
| bc2bb25578 | |||
| b37fc5cb24 | |||
| 614dd9917f | |||
| 00bc3432bd | |||
| ffec10d1cc | |||
| 3993fa9b32 | |||
| 20b92b7d1c | |||
| 05b9a94d41 | |||
| 49f4475991 | |||
| 76bfdd03dd | |||
| c18f2fdcf7 | |||
| 74c6154c59 |
+8
-7
@@ -1,16 +1,17 @@
|
||||
FROM golang:latest AS backend-builder
|
||||
RUN apt update
|
||||
RUN apt install -y build-essential
|
||||
FROM gocv/opencv:latest AS backend-builder
|
||||
RUN echo "开始安装依赖包..."
|
||||
RUN apt update && apt install -y upx-ucl
|
||||
|
||||
WORKDIR /build
|
||||
COPY . .
|
||||
ENV CGO_ENABLED=1
|
||||
RUN echo "开始打包二进制文件"
|
||||
RUN go build -ldflags="-s -w" -o bin/server
|
||||
|
||||
RUN apt install -y upx-ucl
|
||||
|
||||
RUN echo "开始压缩二进制文件"
|
||||
RUN upx --best --lzma bin/server
|
||||
|
||||
FROM golang:latest
|
||||
FROM gocv/opencv:latest
|
||||
|
||||
COPY --from=backend-builder /build/bin/server /app/server
|
||||
#COPY static /app/static
|
||||
#COPY static/*.so /app/
|
||||
|
||||
+14
-1
@@ -14,7 +14,20 @@ func Run() {
|
||||
IDType: &field.TypeInfo{
|
||||
Type: field.TypeInt64,
|
||||
},
|
||||
Features: gen.AllFeatures,
|
||||
Features: []gen.Feature{
|
||||
gen.FeaturePrivacy,
|
||||
gen.FeatureIntercept,
|
||||
gen.FeatureEntQL,
|
||||
gen.FeatureNamedEdges,
|
||||
gen.FeatureBidiEdgeRefs,
|
||||
gen.FeatureSnapshot,
|
||||
gen.FeatureSchemaConfig,
|
||||
gen.FeatureLock,
|
||||
gen.FeatureModifier,
|
||||
gen.FeatureExecQuery,
|
||||
gen.FeatureUpsert,
|
||||
gen.FeatureVersionedMigration,
|
||||
},
|
||||
})
|
||||
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
+8
-6
@@ -4,14 +4,16 @@ url = "https://devocr.likedream.xyz"
|
||||
[database]
|
||||
type = "mysql"
|
||||
dsn = "test_likedream_go:EpXckXmxawDEQGxJ@tcp(database.out:3306)/test_likedream_go?parseTime=true"
|
||||
|
||||
dsn2 = "host=database.out user=dev password=JPmKjmpcaG3EYAJZ dbname=dev port=5432 sslmode=disable TimeZone=Asia/Shanghai"
|
||||
# host=localhost user=gorm password=gorm dbname=gorm port=9920 sslmode=disable TimeZone=Asia/Shanghai
|
||||
[server]
|
||||
port = 8090
|
||||
url = "https://devocr.likedream.xyz"
|
||||
openapiPath= "/api.json"
|
||||
swaggerPath= "/swagger"
|
||||
emailConfirmBaseUrl = "https://dev.likedream.xyz/confirm/"
|
||||
|
||||
emailConfirmBaseUrl = "http://localhost:3200/confirm/"
|
||||
emailResetPasswordUrl = "http://localhost:3200/reset-password/"
|
||||
balanceMigrateUrl = "http://localhost:3200/balance-migrate/"
|
||||
[env]
|
||||
# 可选值: dev, prod
|
||||
mode = "dev"
|
||||
@@ -30,7 +32,7 @@ smtpUsername = "demo@likedream.xyz"
|
||||
smtpPassword = "baca122..."
|
||||
# 发件人邮箱和显示名称
|
||||
fromEmail = "demo@likedream.xyz"
|
||||
fromName = "今日校园系统"
|
||||
fromName = "若梦系统"
|
||||
|
||||
|
||||
[s3]
|
||||
@@ -40,6 +42,6 @@ baseUrl = "http://s3.out"
|
||||
bucket = "likedream"
|
||||
|
||||
[cpdaily]
|
||||
appVersion = "5.7.0"
|
||||
appVersion = "9.7.2"
|
||||
systemVersion = "iOS 17"
|
||||
calVersion = "3.0.0"
|
||||
calVersion = "firstv"
|
||||
@@ -1,6 +1,6 @@
|
||||
module likedream-goframe
|
||||
|
||||
go 1.24
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
entgo.io/ent v0.14.5
|
||||
@@ -33,7 +33,10 @@ require (
|
||||
github.com/yalue/onnxruntime_go v1.12.1
|
||||
go.uber.org/automaxprocs v1.6.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.39.0
|
||||
gocv.io/x/gocv v0.42.0
|
||||
golang.org/x/crypto v0.43.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
resty.dev/v3 v3.0.0-beta.3
|
||||
)
|
||||
|
||||
@@ -71,6 +74,12 @@ require (
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/grokify/html-strip-tags-go v0.1.0 // indirect
|
||||
github.com/hashicorp/hcl/v2 v2.18.1 // 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/klauspost/cpuid/v2 v2.0.9 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/magiconair/properties v1.8.9 // indirect
|
||||
@@ -100,13 +109,13 @@ require (
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
||||
golang.org/x/image v0.29.0 // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
golang.org/x/mod v0.28.0 // indirect
|
||||
golang.org/x/net v0.45.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
golang.org/x/tools v0.37.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -111,6 +111,18 @@ github.com/hashicorp/hcl/v2 v2.18.1 h1:6nxnOJFku1EuSawSD81fuviYUV8DxFr3fp2dUi3ZY
|
||||
github.com/hashicorp/hcl/v2 v2.18.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE=
|
||||
github.com/hibiken/asynq v0.25.1 h1:phj028N0nm15n8O2ims+IvJ2gz4k2auvermngh9JhTw=
|
||||
github.com/hibiken/asynq v0.25.1/go.mod h1:pazWNOLBu0FEynQRBvHA26qdIKRSmfdIfUm4HdsLmXg=
|
||||
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/joomcode/errorx v1.2.0 h1:7Y/fguon+9r6a/75Rv3nrUwS7nXNEcJjLShjCvz00Og=
|
||||
github.com/joomcode/errorx v1.2.0/go.mod h1:Mbz68VA9hsQLT50iCQQUZ2Z1XYAKYB4EoFkFCTFyiJM=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
||||
@@ -134,8 +146,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
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/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
@@ -179,6 +191,7 @@ github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqe
|
||||
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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
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=
|
||||
@@ -231,6 +244,8 @@ 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/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
gocv.io/x/gocv v0.42.0 h1:AAsrFJH2aIsQHukkCovWqj0MCGZleQpVyf5gNVRXjQI=
|
||||
gocv.io/x/gocv v0.42.0/go.mod h1:zYdWMj29WAEznM3Y8NsU3A0TRq/wR/cy75jeUypThqU=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
@@ -240,8 +255,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs=
|
||||
golang.org/x/image v0.29.0 h1:HcdsyR4Gsuys/Axh0rDEmlBmB68rW1U9BUdB3UVHsas=
|
||||
golang.org/x/image v0.29.0/go.mod h1:RVJROnf3SLK8d26OW91j4FrIHGbsJ8QnbEocVTOWQDA=
|
||||
@@ -250,8 +265,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
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/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
@@ -261,8 +276,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
|
||||
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -271,8 +286,8 @@ golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -290,8 +305,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -312,8 +327,8 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -322,8 +337,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
@@ -335,6 +350,10 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
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/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
resty.dev/v3 v3.0.0-beta.3 h1:3kEwzEgCnnS6Ob4Emlk94t+I/gClyoah7SnNi67lt+E=
|
||||
resty.dev/v3 v3.0.0-beta.3/go.mod h1:OgkqiPvTDtOuV4MGZuUDhwOpkY8enjOsjjMzeOHefy4=
|
||||
|
||||
@@ -12,22 +12,64 @@ type CaptchaReq struct {
|
||||
|
||||
type UserLoginReq struct {
|
||||
g.Meta `path:"/user/login" method:"POST" tags:"UserLogin" summary:"用户登录"`
|
||||
Username string `json:"username" v:"required"`
|
||||
Password string `json:"password" v:"required"`
|
||||
CaptchaKey string `json:"captchaKey" v:"required"`
|
||||
Dots []captcha.ConfirmData `json:"dots" v:"required"`
|
||||
Username string `json:"username" v:"required#用户名或密码不能为空"`
|
||||
Password string `json:"password" v:"required#用户名或密码不能为空"`
|
||||
CaptchaKey string `json:"captchaKey" v:"required#验证码不能不能为空"`
|
||||
Dots []captcha.ConfirmData `json:"dots" v:"required#验证码不能为空"`
|
||||
}
|
||||
|
||||
type UserRegisterReq struct {
|
||||
g.Meta `path:"/user/register" method:"POST" tags:"UserLogin" summary:"用户注册"`
|
||||
Email string `json:"email" v:"required|email|regex:^.*@qq\\.com$#邮箱不能为空|邮箱格式不正确|必须是QQ邮箱"`
|
||||
Username string `json:"username" v:"required|length:2,20#用户名不能为空|用户名长度应在2-20之间" `
|
||||
Password string `json:"password" v:"required|length:6,20#密码不能为空|密码长度应在6-20之间" `
|
||||
ConfirmPassword string `json:"confirmPassword" v:"required|same:password#确认密码不能为空|两次输入的密码不一致"`
|
||||
InviteCode string `json:"inviteCode"`
|
||||
Email string `json:"email" v:"required|email|regex:^.*@qq\\.com$#邮箱不能为空|邮箱格式不正确|必须是QQ邮箱"`
|
||||
Username string `json:"username" v:"required|length:2,20#用户名不能为空|用户名长度应在2-20之间" `
|
||||
Password string `json:"password" v:"required|length:6,20#密码不能为空|密码长度应在6-20之间" `
|
||||
ConfirmPassword string `json:"confirmPassword" v:"required|same:password#确认密码不能为空|两次输入的密码不一致"`
|
||||
InviteCode string `json:"inviteCode"`
|
||||
CaptchaKey string `json:"captchaKey" v:"required#验证码不能不能为空"`
|
||||
Dots []captcha.ConfirmData `json:"dots" v:"required#验证码不能为空"`
|
||||
}
|
||||
|
||||
type UserConfirmReq struct {
|
||||
g.Meta `path:"/user/confirm" method:"POST" tags:"UserLogin" summary:"邮箱二次验证"`
|
||||
Token string `json:"token" v:"required"`
|
||||
}
|
||||
|
||||
type ResendVerificationReq struct {
|
||||
g.Meta `path:"/user/resendVerification" method:"POST" tags:"UserLogin" summary:"重新发送验证邮件"`
|
||||
Email string `json:"email" v:"required|email|regex:^.*@qq\\.com$#邮箱不能为空|邮箱格式不正确|必须是QQ邮箱"`
|
||||
CaptchaKey string `json:"captchaKey" v:"required#验证码不能不能为空"`
|
||||
Dots []captcha.ConfirmData `json:"dots" v:"required#验证码不能为空"`
|
||||
}
|
||||
|
||||
type ForgotPasswordReq struct {
|
||||
g.Meta `path:"/user/forgotPassword" method:"POST" tags:"UserLogin" summary:"忘记密码-发送重置密码邮件"`
|
||||
Email string `json:"email" v:"required|email|regex:^.*@qq\\.com$#邮箱不能为空|邮箱格式不正确|必须是QQ邮箱"`
|
||||
CaptchaKey string `json:"captchaKey" v:"required#验证码不能不能为空"`
|
||||
Dots []captcha.ConfirmData `json:"dots" v:"required#验证码不能为空"`
|
||||
}
|
||||
|
||||
type ForgetResetPasswordReq struct {
|
||||
g.Meta `path:"/user/forgetResetPassword" method:"POST" tags:"UserLogin" summary:"忘记密码-重置密码"`
|
||||
Token string `json:"token" v:"required#token不能为空"`
|
||||
NewPassword string `json:"newPassword" v:"required|length:6,20#密码不能为空|密码长度应在6-20之间"`
|
||||
ConfirmPassword string `json:"confirmPassword" v:"required|same:newPassword#确认密码不能为空|两次输入的密码不一致"`
|
||||
}
|
||||
|
||||
type ResetPasswordInfoReq struct {
|
||||
g.Meta `path:"/user/resetPassword/info" method:"POST" tags:"UserLogin" summary:"获取重置密码用户信息"`
|
||||
Token string `json:"token" v:"required#token不能为空"`
|
||||
}
|
||||
|
||||
type RequestBalanceMigrateReq struct {
|
||||
g.Meta `path:"/user/requestBalanceMigrate" method:"POST" tags:"UserLogin" summary:"请求积分转移链接"`
|
||||
Email string `json:"email" v:"required|email|regex:^.*@qq\\.com$#邮箱不能为空|邮箱格式不正确|必须是QQ邮箱"`
|
||||
CaptchaKey string `json:"captchaKey" v:"required#验证码不能不能为空"`
|
||||
Dots []captcha.ConfirmData `json:"dots" v:"required#验证码不能为空"`
|
||||
}
|
||||
|
||||
type BalanceMigrateReq struct {
|
||||
g.Meta `path:"/user/balanceMigrate" method:"POST" tags:"UserLogin" summary:"执行积分转移" auth:"false"`
|
||||
Token string `json:"token" v:"required#token不能为空"`
|
||||
CaptchaKey string `json:"captchaKey" v:"required#验证码不能不能为空"`
|
||||
Dots []captcha.ConfirmData `json:"dots" v:"required#验证码不能为空"`
|
||||
}
|
||||
|
||||
@@ -1,225 +0,0 @@
|
||||
package classifier
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
ort "github.com/yalue/onnxruntime_go"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type ImageClassifier struct {
|
||||
session *ort.AdvancedSession
|
||||
inputShape []int64
|
||||
classes []string
|
||||
inputTensor *ort.Tensor[float32]
|
||||
outputTensor *ort.Tensor[float32]
|
||||
}
|
||||
|
||||
// NewImageClassifier 创建新的图片分类器
|
||||
func NewImageClassifier(modelPath, classesPath string) (*ImageClassifier, error) {
|
||||
// 初始化ONNX Runtime
|
||||
exePath := strings.ToLower(os.Args[0])
|
||||
// 获取绝对路径
|
||||
absPath, err := filepath.Abs(exePath)
|
||||
dir := filepath.Dir(absPath)
|
||||
systemOS := runtime.GOOS
|
||||
switch systemOS {
|
||||
case "windows":
|
||||
dir = filepath.Join(dir, "onnxruntime.dll")
|
||||
break
|
||||
case "darwin":
|
||||
dir = filepath.Join(dir, "onnxruntime.dylib")
|
||||
break
|
||||
case "linux":
|
||||
dir = filepath.Join(dir, "onnxruntime.so")
|
||||
break
|
||||
}
|
||||
ort.SetSharedLibraryPath(dir)
|
||||
err = ort.InitializeEnvironment()
|
||||
if err != nil {
|
||||
logger.Logger.Error("初始化onnx:", zap.String("绝对路径", absPath), zap.String("父文件夹", dir))
|
||||
return nil, fmt.Errorf("初始化ONNX Runtime失败: %v", err)
|
||||
}
|
||||
|
||||
// 加载类别名称
|
||||
classes, err := loadClasses(classesPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("加载类别失败: %v", err)
|
||||
}
|
||||
|
||||
// 创建session选项
|
||||
options, err := ort.NewSessionOptions()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建session选项失败: %v", err)
|
||||
}
|
||||
defer options.Destroy()
|
||||
|
||||
// 创建输入输出tensor
|
||||
inputShape := ort.NewShape(1, 3, 224, 224)
|
||||
outputShape := ort.NewShape(1, int64(len(classes)))
|
||||
|
||||
inputTensor, err := ort.NewEmptyTensor[float32](inputShape)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建输入tensor失败: %v", err)
|
||||
}
|
||||
|
||||
outputTensor, err := ort.NewEmptyTensor[float32](outputShape)
|
||||
if err != nil {
|
||||
inputTensor.Destroy()
|
||||
return nil, fmt.Errorf("创建输出tensor失败: %v", err)
|
||||
}
|
||||
|
||||
// 加载模型
|
||||
session, err := ort.NewAdvancedSession(modelPath,
|
||||
[]string{"input"}, []string{"output"},
|
||||
[]ort.ArbitraryTensor{inputTensor}, []ort.ArbitraryTensor{outputTensor},
|
||||
options)
|
||||
if err != nil {
|
||||
inputTensor.Destroy()
|
||||
outputTensor.Destroy()
|
||||
return nil, fmt.Errorf("加载模型失败: %v", err)
|
||||
}
|
||||
|
||||
return &ImageClassifier{
|
||||
session: session,
|
||||
inputShape: []int64{1, 3, 224, 224},
|
||||
classes: classes,
|
||||
inputTensor: inputTensor,
|
||||
outputTensor: outputTensor,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// loadClasses 从文件加载类别名称
|
||||
func loadClasses(path string) ([]string, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var classes []string
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
classes = append(classes, scanner.Text())
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return classes, nil
|
||||
}
|
||||
|
||||
// Predict 预测图片类别
|
||||
func (ic *ImageClassifier) Predict(imgPath string) (string, float32, error) {
|
||||
// 加载并预处理图片
|
||||
inputData, err := ic.preprocessImage(imgPath)
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("预处理图片失败: %v", err)
|
||||
}
|
||||
|
||||
// 设置输入数据
|
||||
copy(ic.inputTensor.GetData(), inputData)
|
||||
|
||||
// 运行推理
|
||||
err = ic.session.Run()
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("推理失败: %v", err)
|
||||
}
|
||||
|
||||
// 获取输出数据
|
||||
outputData := ic.outputTensor.GetData()
|
||||
|
||||
// 找到最大概率的类别
|
||||
maxIdx := 0
|
||||
maxProb := outputData[0]
|
||||
for i, prob := range outputData {
|
||||
if prob > maxProb {
|
||||
maxProb = prob
|
||||
maxIdx = i
|
||||
}
|
||||
}
|
||||
|
||||
// 应用softmax获取概率
|
||||
expSum := float32(0)
|
||||
for _, v := range outputData {
|
||||
expSum += exp(v)
|
||||
}
|
||||
probability := exp(outputData[maxIdx]) / expSum
|
||||
|
||||
return ic.classes[maxIdx], probability, nil
|
||||
}
|
||||
|
||||
// PredictFromBytes 从字节数据预测图片类别
|
||||
func (ic *ImageClassifier) PredictFromBytes(imgData []byte) (string, float32, error) {
|
||||
// 预处理图片
|
||||
inputData, err := ic.preprocessImageBytes(imgData)
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("预处理图片失败: %v", err)
|
||||
}
|
||||
|
||||
// 设置输入数据
|
||||
copy(ic.inputTensor.GetData(), inputData)
|
||||
|
||||
// 运行推理
|
||||
err = ic.session.Run()
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("推理失败: %v", err)
|
||||
}
|
||||
|
||||
// 获取输出数据
|
||||
outputData := ic.outputTensor.GetData()
|
||||
|
||||
// 找到最大概率的类别
|
||||
maxIdx := 0
|
||||
maxProb := outputData[0]
|
||||
for i, prob := range outputData {
|
||||
if prob > maxProb {
|
||||
maxProb = prob
|
||||
maxIdx = i
|
||||
}
|
||||
}
|
||||
|
||||
// 应用softmax获取概率
|
||||
expSum := float32(0)
|
||||
for _, v := range outputData {
|
||||
expSum += exp(v)
|
||||
}
|
||||
probability := exp(outputData[maxIdx]) / expSum
|
||||
|
||||
return ic.classes[maxIdx], probability, nil
|
||||
}
|
||||
|
||||
// Close 关闭分类器
|
||||
func (ic *ImageClassifier) Close() error {
|
||||
if ic.inputTensor != nil {
|
||||
ic.inputTensor.Destroy()
|
||||
}
|
||||
if ic.outputTensor != nil {
|
||||
ic.outputTensor.Destroy()
|
||||
}
|
||||
if ic.session != nil {
|
||||
ic.session.Destroy()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// exp 计算e的x次方(简单实现)
|
||||
func exp(x float32) float32 {
|
||||
// 使用泰勒级数近似
|
||||
result := float32(1.0)
|
||||
term := float32(1.0)
|
||||
for i := 1; i < 20; i++ {
|
||||
term *= x / float32(i)
|
||||
result += term
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
package classifier
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"os"
|
||||
)
|
||||
|
||||
// preprocessImage 预处理图片文件
|
||||
func (ic *ImageClassifier) preprocessImage(imgPath string) ([]float32, error) {
|
||||
file, err := os.Open(imgPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
img, _, err := image.Decode(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ic.processImage(img)
|
||||
}
|
||||
|
||||
// preprocessImageBytes 预处理图片字节数据
|
||||
func (ic *ImageClassifier) preprocessImageBytes(imgData []byte) ([]float32, error) {
|
||||
img, _, err := image.Decode(bytes.NewReader(imgData))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ic.processImage(img)
|
||||
}
|
||||
|
||||
// processImage 处理图片为模型输入格式
|
||||
func (ic *ImageClassifier) processImage(img image.Image) ([]float32, error) {
|
||||
// 调整图片大小到224x224
|
||||
resized := resizeImage(img, 224, 224)
|
||||
|
||||
// 转换为float32数组,并进行归一化
|
||||
// ImageNet的均值和标准差
|
||||
mean := []float32{0.485, 0.456, 0.406}
|
||||
std := []float32{0.229, 0.224, 0.225}
|
||||
|
||||
data := make([]float32, 3*224*224)
|
||||
|
||||
for y := 0; y < 224; y++ {
|
||||
for x := 0; x < 224; x++ {
|
||||
r, g, b, _ := resized.At(x, y).RGBA()
|
||||
|
||||
// 转换为0-1范围
|
||||
rNorm := float32(r) / 65535.0
|
||||
gNorm := float32(g) / 65535.0
|
||||
bNorm := float32(b) / 65535.0
|
||||
|
||||
// 应用归一化
|
||||
idx := y*224 + x
|
||||
data[idx] = (rNorm - mean[0]) / std[0] // R通道
|
||||
data[224*224+idx] = (gNorm - mean[1]) / std[1] // G通道
|
||||
data[2*224*224+idx] = (bNorm - mean[2]) / std[2] // B通道
|
||||
}
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// resizeImage 调整图片大小
|
||||
func resizeImage(img image.Image, width, height int) image.Image {
|
||||
bounds := img.Bounds()
|
||||
srcW := bounds.Dx()
|
||||
srcH := bounds.Dy()
|
||||
|
||||
dst := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
|
||||
for y := 0; y < height; y++ {
|
||||
for x := 0; x < width; x++ {
|
||||
// 简单的最近邻插值
|
||||
srcX := x * srcW / width
|
||||
srcY := y * srcH / height
|
||||
|
||||
if srcX >= srcW {
|
||||
srcX = srcW - 1
|
||||
}
|
||||
if srcY >= srcH {
|
||||
srcY = srcH - 1
|
||||
}
|
||||
|
||||
dst.Set(x, y, img.At(srcX, srcY))
|
||||
}
|
||||
}
|
||||
|
||||
return dst
|
||||
}
|
||||
|
||||
// centerCrop 中心裁剪图片
|
||||
func centerCrop(img image.Image, size int) image.Image {
|
||||
bounds := img.Bounds()
|
||||
width := bounds.Dx()
|
||||
height := bounds.Dy()
|
||||
|
||||
var x0, y0 int
|
||||
if width > height {
|
||||
x0 = (width - height) / 2
|
||||
y0 = 0
|
||||
width = height
|
||||
} else {
|
||||
x0 = 0
|
||||
y0 = (height - width) / 2
|
||||
height = width
|
||||
}
|
||||
|
||||
cropped := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
for y := 0; y < height; y++ {
|
||||
for x := 0; x < width; x++ {
|
||||
cropped.Set(x, y, img.At(x0+x, y0+y))
|
||||
}
|
||||
}
|
||||
|
||||
return resizeImage(cropped, size, size)
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package classifier
|
||||
|
||||
import "likedream-goframe/internal/utils"
|
||||
|
||||
var imageClassifier, _ = NewImageClassifier("static/model.onnx", "static/classes.txt")
|
||||
|
||||
// 图片数据 & 类别名称
|
||||
// return 是否匹配
|
||||
func ClassifyImage(imgData []byte, className string) bool {
|
||||
if imageClassifier == nil {
|
||||
c, err := NewImageClassifier("static/model.onnx", "static/classes.txt")
|
||||
utils.OkOrPanic(err)
|
||||
imageClassifier = c
|
||||
}
|
||||
name, _, _ := imageClassifier.PredictFromBytes(imgData)
|
||||
return className == name
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package cryptoutil
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/container/gvar"
|
||||
)
|
||||
|
||||
func TestDesDecrypt(t *testing.T) {
|
||||
decodeString, _ := base64.StdEncoding.DecodeString("64JITpWPkKuGMhi1m7Ikxu1ekL6VQOdjmJvaQPI07MWbLETtJsxzkk2bCUVNaWgUULa3wS9LH+ANO5CUBeOXi02CcS/8lN/tYDFYCyuK7dLN+q0fD0gQlUsuHwy/78EkwZa9VrLh4e519vM9AxR2GnK4WGpumeH7TGJjDkLH0AitKfUYQnil9AKZEVgSSkHkg9ko64qj0QJvzmo76zOkMpIb1+1Pn2cO+5soTSl8BCsP/8hOP5HdOMVBCYH++iSK3O5sYz/cwZq/G8aJrqmEFcDHbgTouXyD5kxa4lb72Vw3Ku+eH+3qDwYMYxWrIcAm")
|
||||
decrypt := DesDecrypt(decodeString, Cat)
|
||||
str := "{\"lon\":\"113.045246\",\"lat\":\"28.077659\",\"model\":\"847b5b9c-2d97-41ec-8a5d-e9ac79da090d\",\"appVersion\":\"9.7.2\",\"userId\":\"430181200504150038\",\"systemName\":\"android\",\"deviceId\":\"09E46EC9-57C1-4724-BE53-5879CA80A2A0\",\"cpdailyVersion\":\"9.7.2\",\"systemVersion\":\"iOS 17\"}"
|
||||
encrypt := DesEncrypt([]byte(str), Cat)
|
||||
toString := base64.StdEncoding.EncodeToString(encrypt)
|
||||
t.Log(toString)
|
||||
t.Log(string(decrypt))
|
||||
d2, _ := base64.StdEncoding.DecodeString("86JzX4MBsorI5KasKBHUj6orO1qcPWbo758Ux6G2NUoRuGCoZnE+cJW5slZvsflBrKITPeitF7GTisMNRJxRqlA8cLXAqyo9ZlBPPx0MM3KjILHiA/EvrRrmamo7vi1d/F0ChQc8jLfMgkkcOg4FBmM1H8Yg6KyYdMNN7anIbIdJ+6RPEsakjAIw0r4eawofSh5E0QclDry0v36GK3UA3GDBBfqSub8V5PRL7fQty/R0lx7+mfc2AIXXjUVsv85WQKoyyUwg3siWpQL5S1cbtQNUXClSQP1IAGROnBftvdrib01PR1h7Cp+kvE3HK0+/8l0FoGJ1aijff1t9T9MaVXM/jSUFwkA7tmZIjodrUDhZcCMNm5YLm1ZO4UVBQO118FyOBjSLet0nPc0Wg0/dTO0XZoo9e2bCQwjIQC2G8TdzHDxvDjGEc6hX++nZrN23n19bAgqdvzZt58KftznE9s5t4CEjiHx3ykyidGEGr/VEi/05uOtm8/EsFUVp8+MyVgaTxoaq/4LBu28L59U6+6mZjOFUt49STR5p3UPI3ZJQLe2lmnx9BdqrIHbdXgYNu5oNzrDM8m3z3ns9M2HEGg==")
|
||||
d := AesDecryptForCat(d2)
|
||||
t.Log(d)
|
||||
|
||||
form := map[string]*gvar.Var{
|
||||
"appVersion": gvar.New("9.7.2"),
|
||||
"bodyString": gvar.New("0u7hBd+6O11ZaGsT51FUAVfB72waqtTx5oQQLVEEAuCM29x8lQc41i9/dF6+9wjQNC05jdJTBmHlwFoLD09hfsnQWSyjl/ys124RUQL3r/Um5AwNdKiJE24YQsyQlP3ZamUIFlrNSxmn/DfZ/jR9oZhiVKZSlygA2LNLxtNOyk4rETfqKpZKFOnThbXy5Hf779jUDmPNwn1ZrdWN8UHm6wO+EahrrX60vC02VUHs7r3rMrDWZjBJ57eSgD0QwZabcIb/u9kx1AD8OAxINar3X+N+uAhfrxjHd6v6OVvnc2I9iPLEGNFRKZ94jq64hnATOSH6S/IR/QbDoxebWpGUXm3ALYZeoni09uyhG7IE5NtRw/OmqYtdknrs+c0qyOOBkvxPlqKo+SV4qq7xtrn9RWeZIigiyM8mVtdovN1T/x3DnHxd/AYoSS3/cGkg+2QHzWey/Xf5c4g0EvhaGlE0C7ejKIn5wp0XeHyvAjm4ZVyUhgTN2dZVCQ6ChxXOpLnVDWPNeeZ9vysqVl1ZSkQdCz20gy3p9RfYuArluQqjwIoWQe632Cc/Ep++99V5Tc9iIlLr93PfxFaJJfov3avLKw=="),
|
||||
"calVersion": gvar.New("firstv"),
|
||||
"deviceId": gvar.New("09E46EC9-57C1-4724-BE53-5879CA80A2A0"),
|
||||
"lat": gvar.New("28.077659"),
|
||||
"lon": gvar.New("113.045246"),
|
||||
"model": gvar.New("847b5b9c-2d97-41ec-8a5d-e9ac79da090d"),
|
||||
"systemName": gvar.New("android"),
|
||||
"systemVersion": gvar.New("iOS 17"),
|
||||
"userId": gvar.New("430181200504150038"),
|
||||
"version": gvar.New("first_v4"),
|
||||
}
|
||||
sign := GetSubmitSign(form)
|
||||
t.Log(sign)
|
||||
}
|
||||
|
||||
const submitSignSalt = "hru89rwr87Nthifa" // 请根据实际情况替换为真实盐值
|
||||
|
||||
func GetSubmitSign(form map[string]*gvar.Var) string {
|
||||
// 按照指定顺序构建哈希数据
|
||||
keys := []string{
|
||||
"appVersion",
|
||||
"bodyString",
|
||||
"deviceId",
|
||||
"lat",
|
||||
"lon",
|
||||
"model",
|
||||
"systemName",
|
||||
"systemVersion",
|
||||
"userId",
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
for _, key := range keys {
|
||||
val := ""
|
||||
if v, ok := form[key]; ok && v != nil {
|
||||
val = v.String()
|
||||
}
|
||||
// 这里是 key=UrlEncode(key)&,注意是对 key 进行 url encode
|
||||
sb.WriteString(key)
|
||||
sb.WriteString("=")
|
||||
sb.WriteString(url.QueryEscape(val))
|
||||
sb.WriteString("&")
|
||||
}
|
||||
sb.WriteString(submitSignSalt)
|
||||
|
||||
// 计算MD5
|
||||
hash := md5.Sum([]byte(sb.String()))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package cryptoutil
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// RSAKeyPair RSA密钥对结构体
|
||||
type RSAKeyPair struct {
|
||||
n *big.Int // 模数
|
||||
e *big.Int // 公钥指数
|
||||
}
|
||||
|
||||
// NewRSAKeyPair 创建RSA密钥对
|
||||
// exponent: 公钥指数的十六进制字符串,通常是 "10001" (65537)
|
||||
// privateExponent: 私钥指数的十六进制字符串,加密时可以为空
|
||||
// modulus: 模数的十六进制字符串
|
||||
func NewRSAKeyPair(exponent, privateExponent, modulus string) (*RSAKeyPair, error) {
|
||||
// 解析公钥指数
|
||||
e := new(big.Int)
|
||||
if _, ok := e.SetString(exponent, 16); !ok {
|
||||
return nil, fmt.Errorf("invalid exponent: %s", exponent)
|
||||
}
|
||||
|
||||
// 解析模数
|
||||
n := new(big.Int)
|
||||
if _, ok := n.SetString(modulus, 16); !ok {
|
||||
return nil, fmt.Errorf("invalid modulus: %s", modulus)
|
||||
}
|
||||
|
||||
return &RSAKeyPair{
|
||||
n: n,
|
||||
e: e,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Encrypt 使用公钥加密数据,返回十六进制字符串
|
||||
func (k *RSAKeyPair) Encrypt(plaintext string) (string, error) {
|
||||
ciphertext, err := k.EncryptBytes([]byte(plaintext))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// 返回十六进制编码的密文
|
||||
return hex.EncodeToString(ciphertext), nil
|
||||
}
|
||||
|
||||
// EncryptBytes 使用公钥加密字节数据,返回加密后的字节
|
||||
// 使用 PKCS1v15 填充方式,兼容 JavaScript RSA 实现
|
||||
func (k *RSAKeyPair) EncryptBytes(plaintext []byte) ([]byte, error) {
|
||||
// 计算密钥长度(字节)
|
||||
keySize := (k.n.BitLen() + 7) / 8
|
||||
|
||||
// PKCS1v15 填充
|
||||
padded, err := pkcs1v15Pad(plaintext, keySize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 将填充后的数据转换为大整数
|
||||
m := new(big.Int).SetBytes(padded)
|
||||
|
||||
// 执行 RSA 加密: c = m^e mod n
|
||||
c := new(big.Int).Exp(m, k.e, k.n)
|
||||
|
||||
// 将结果转换为字节数组,确保长度与密钥长度一致
|
||||
ciphertext := c.Bytes()
|
||||
if len(ciphertext) < keySize {
|
||||
// 如果结果长度不足,在前面填充零
|
||||
result := make([]byte, keySize)
|
||||
copy(result[keySize-len(ciphertext):], ciphertext)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
return ciphertext, nil
|
||||
}
|
||||
|
||||
// pkcs1v15Pad 实现 PKCS#1 v1.5 填充
|
||||
func pkcs1v15Pad(data []byte, keySize int) ([]byte, error) {
|
||||
if len(data) > keySize-11 {
|
||||
return nil, fmt.Errorf("data too long: %d bytes (max %d)", len(data), keySize-11)
|
||||
}
|
||||
|
||||
// PKCS1v15 格式: 0x00 || 0x02 || PS || 0x00 || data
|
||||
// 其中 PS 是随机非零字节,长度至少为 8 字节
|
||||
padLen := keySize - len(data) - 3
|
||||
|
||||
padded := make([]byte, keySize)
|
||||
padded[0] = 0x00
|
||||
padded[1] = 0x02
|
||||
|
||||
// 生成随机非零字节作为填充
|
||||
ps := make([]byte, padLen)
|
||||
_, err := rand.Read(ps)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate random padding: %w", err)
|
||||
}
|
||||
|
||||
// 确保所有填充字节都是非零
|
||||
for i := 0; i < padLen; i++ {
|
||||
for ps[i] == 0 {
|
||||
rand.Read(ps[i : i+1])
|
||||
}
|
||||
padded[2+i] = ps[i]
|
||||
}
|
||||
|
||||
padded[2+padLen] = 0x00
|
||||
copy(padded[3+padLen:], data)
|
||||
|
||||
return padded, nil
|
||||
}
|
||||
|
||||
// GetModulus 获取模数
|
||||
func (k *RSAKeyPair) GetModulus() *big.Int {
|
||||
return k.n
|
||||
}
|
||||
|
||||
// GetExponent 获取公钥指数
|
||||
func (k *RSAKeyPair) GetExponent() *big.Int {
|
||||
return k.e
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package cryptoutil
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRSAKeyPair(t *testing.T) {
|
||||
// 使用你提供的参数创建 RSA 密钥对
|
||||
exponent := "10001"
|
||||
modulus := "a1155dc73de04614604cbed62cc3b3198a639eb7b7d19cc2dfe3e787fe435f1464beee42a9a3cab6f1d3bfe66cd00317220ee770fd779b24c12c7698e251e875"
|
||||
|
||||
keyPair, err := NewRSAKeyPair(exponent, "", modulus)
|
||||
if err != nil {
|
||||
t.Fatalf("创建 RSA 密钥对失败: %v", err)
|
||||
}
|
||||
|
||||
// 测试加密
|
||||
plaintext := "241703070163"
|
||||
ciphertext, err := keyPair.Encrypt(plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("加密失败: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("明文: %s", plaintext)
|
||||
t.Logf("密文(十六进制): %s", ciphertext)
|
||||
|
||||
// 验证密文不为空
|
||||
if len(ciphertext) == 0 {
|
||||
t.Error("密文为空")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRSAKeyPair_EncryptBytes(t *testing.T) {
|
||||
exponent := "10001"
|
||||
modulus := "a1155dc73de04614604cbed62cc3b3198a639eb7b7d19cc2dfe3e787fe435f1464beee42a9a3cab6f1d3bfe66cd00317220ee770fd779b24c12c7698e251e875"
|
||||
|
||||
keyPair, err := NewRSAKeyPair(exponent, "", modulus)
|
||||
if err != nil {
|
||||
t.Fatalf("创建 RSA 密钥对失败: %v", err)
|
||||
}
|
||||
|
||||
// 测试字节加密
|
||||
plaintext := []byte("Test data 测试数据")
|
||||
ciphertext, err := keyPair.EncryptBytes(plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("字节加密失败: %v", err)
|
||||
}
|
||||
|
||||
if len(ciphertext) == 0 {
|
||||
t.Error("密文字节为空")
|
||||
}
|
||||
|
||||
t.Logf("密文长度: %d bytes", len(ciphertext))
|
||||
}
|
||||
@@ -7,6 +7,9 @@ import (
|
||||
|
||||
func GetCpdailyLoginBySchoolInfo(ctx context.Context, info *school.SchoolDetailInfo) CpdailyLogin {
|
||||
switch info.Id {
|
||||
// 南昌理工学院
|
||||
case "d6e8f2ca-10b2-4599-bdb8-4b33d419658d":
|
||||
return NewNut(ctx)
|
||||
default:
|
||||
if info.IsIap() {
|
||||
return NewIap(ctx)
|
||||
|
||||
@@ -16,10 +16,12 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/avast/retry-go/v4"
|
||||
"github.com/deatil/go-cryptobin/cryptobin/crypto"
|
||||
"github.com/samber/oops"
|
||||
"github.com/tidwall/gjson"
|
||||
@@ -56,7 +58,7 @@ func (c *CpdailyCasLogin) GetClient() *resty.Client {
|
||||
func (c *CpdailyCasLogin) Login(ctx context.Context, schoolDetail *school.SchoolDetailInfo, username, password string) {
|
||||
jar := cookiejar.NewPersistCookieJar()
|
||||
|
||||
c.client = c.client.SetCookieJar(jar).SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
|
||||
c.client.SetCookieJar(jar).SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
|
||||
|
||||
// 获取合法的登录URL
|
||||
loginUrl := getCasValidLoginUrl(schoolDetail)
|
||||
@@ -76,7 +78,7 @@ func (c *CpdailyCasLogin) Login(ctx context.Context, schoolDetail *school.School
|
||||
loginResponseUrl := loginPageResponse.RawResponse.Request.URL
|
||||
|
||||
// 关闭client的重定向
|
||||
c.client = c.client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||
c.client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||
|
||||
// 解析登录页面
|
||||
loginPage, err := goquery.NewDocumentFromReader(bytes.NewReader(loginPageBytes))
|
||||
@@ -188,20 +190,35 @@ func (c *CpdailyCasLogin) Login(ctx context.Context, schoolDetail *school.School
|
||||
} else {
|
||||
allInputs["password"] = encryptCasPassword(password, salt)
|
||||
}
|
||||
|
||||
var SliderCaptchaIds = []string{"1018764144934965"}
|
||||
// 判断是否需要验证码
|
||||
if isCasNeedCaptcha(ctx, loginResponseUrl, loginType, username, c.client) {
|
||||
// 解决验证码
|
||||
captchaUrl := getCasCaptchaUrl(loginResponseUrl, loginType)
|
||||
// 如果是滑块验证码
|
||||
if slices.Contains(SliderCaptchaIds, schoolDetail.Id) {
|
||||
e := retry.Do(func() error {
|
||||
return utils.Catch(func() {
|
||||
captchaUrl := getCasSliderCaptchaUrl(loginResponseUrl)
|
||||
verifyUrl := getCasSliderVerifyUrl(loginResponseUrl)
|
||||
ResolveCasSliderCaptcha(ctx, captchaUrl, verifyUrl, c.client)
|
||||
})
|
||||
}, retry.MaxDelay(15*time.Second), retry.Attempts(5), retry.Context(ctx))
|
||||
if e != nil {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "滑块验证码解析失败").WithInnerError(e), oops.WithContext(ctx).Wrap)
|
||||
}
|
||||
} else {
|
||||
// 解决常规图片验证码
|
||||
captchaUrl := getCasCaptchaUrl(loginResponseUrl, loginType)
|
||||
|
||||
captchaResult := ResolveOcrCaptcha(ctx, captchaUrl, c.client)
|
||||
captchaResult := ResolveOcrCaptcha(ctx, captchaUrl, c.client)
|
||||
|
||||
switch loginType {
|
||||
case 0:
|
||||
allInputs["captchaResponse"] = captchaResult
|
||||
default:
|
||||
allInputs["captcha"] = captchaResult
|
||||
switch loginType {
|
||||
case 0:
|
||||
allInputs["captchaResponse"] = captchaResult
|
||||
default:
|
||||
allInputs["captcha"] = captchaResult
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
loginPostResponse, err := c.client.R().
|
||||
@@ -334,7 +351,7 @@ func handleCasLoginSuccess(ctx context.Context, response *resty.Response, school
|
||||
jumpUrl, err := response.RawResponse.Location()
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
client = client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
|
||||
client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
|
||||
|
||||
verifyResponse, err := client.R().
|
||||
WithContext(ctx).
|
||||
@@ -399,7 +416,7 @@ func submitCasKickOut(ctx context.Context, requestUrl string, doc *goquery.Docum
|
||||
submitForm[name] = value
|
||||
}
|
||||
|
||||
client = client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||
client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||
|
||||
response, err := client.R().
|
||||
SetFormData(submitForm).
|
||||
@@ -529,3 +546,12 @@ func getCasCaptchaUrl(url *url.URL, loginType int) string {
|
||||
return fmt.Sprintf("%s://%s/authserver/getCaptcha.htl", url.Scheme, url.Host)
|
||||
}
|
||||
}
|
||||
|
||||
func getCasSliderCaptchaUrl(url *url.URL) string {
|
||||
// /authserver/common/openSliderCaptcha.htl?_=1762873725346
|
||||
return fmt.Sprintf("%s://%s/authserver/common/openSliderCaptcha.htl", url.Scheme, url.Host)
|
||||
}
|
||||
|
||||
func getCasSliderVerifyUrl(url *url.URL) string {
|
||||
return fmt.Sprintf("%s://%s/authserver/common/verifySliderCaptcha.htl", url.Scheme, url.Host)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
)
|
||||
|
||||
func ActivateSession(ctx context.Context, host string, client *resty.Client) {
|
||||
client = client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
|
||||
client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
|
||||
|
||||
host = strings.TrimSuffix(host, "/")
|
||||
|
||||
@@ -78,19 +78,19 @@ func ActivateSession(ctx context.Context, host string, client *resty.Client) {
|
||||
activateStep1()
|
||||
activateStep2()
|
||||
|
||||
client = client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||
client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||
}
|
||||
|
||||
func CookieEnabled(ctx context.Context, host string, client *resty.Client) bool {
|
||||
client = client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
|
||||
client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
|
||||
defer func() {
|
||||
client = client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||
client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||
}()
|
||||
host = strings.TrimSuffix(host, "/")
|
||||
sessionResponse, err := client.R().
|
||||
WithContext(ctx).
|
||||
SetBody(map[string]any{}).
|
||||
Post(fmt.Sprintf("%s/wec-counselor-attendance-apps/student/attendance/getStuSignInfosByWeekMonth", host))
|
||||
Post(fmt.Sprintf("%s/wec-counselor-attendance-apps/student/attendance/getStuAttendacesInOneDay", host))
|
||||
|
||||
utils.OkOrPanic(err)
|
||||
defer sessionResponse.Body.Close()
|
||||
|
||||
@@ -34,7 +34,7 @@ func (c *CpdailyIapLogin) Login(ctx context.Context, schoolDetail *school.School
|
||||
loginUrl := getIapValidLoginUrl(schoolDetail)
|
||||
|
||||
jar := cookiejar.NewPersistCookieJar()
|
||||
c.client = c.client.SetCookieJar(jar).
|
||||
c.client.SetCookieJar(jar).
|
||||
SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||
|
||||
ltInfoResponse, err := c.client.R().
|
||||
@@ -103,7 +103,7 @@ func (c *CpdailyIapLogin) Login(ctx context.Context, schoolDetail *school.School
|
||||
Wrap(err)
|
||||
})
|
||||
|
||||
c.client = c.client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
|
||||
c.client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
|
||||
|
||||
finalLoginResponse, err := c.client.R().
|
||||
WithContext(ctx).
|
||||
@@ -123,7 +123,7 @@ func (c *CpdailyIapLogin) Login(ctx context.Context, schoolDetail *school.School
|
||||
|
||||
ActivateSession(ctx, schoolDetail.WecloudUrl, c.client)
|
||||
|
||||
c.client = c.client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||
c.client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||
default:
|
||||
loginResponseBytes, err := io.ReadAll(loginResponse.Body)
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"likedream-goframe/internal/application/cookiejar"
|
||||
"likedream-goframe/internal/application/cpdaily/cryptoutil"
|
||||
"likedream-goframe/internal/application/cpdaily/school"
|
||||
"likedream-goframe/internal/application/httpclient"
|
||||
"likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/utils"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/samber/oops"
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
// 南阳理工学院登录实现 RSA加密 & 验证码必出
|
||||
type CpdailyNutLogin struct {
|
||||
client *resty.Client
|
||||
}
|
||||
|
||||
func (c CpdailyNutLogin) Login(ctx context.Context, schoolDetail *school.SchoolDetailInfo, username, password string) {
|
||||
jar := cookiejar.NewPersistCookieJar()
|
||||
|
||||
c.client.SetCookieJar(jar).SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
|
||||
|
||||
// 获取合法的登录URL
|
||||
loginUrl := getCasValidLoginUrl(schoolDetail)
|
||||
|
||||
// 请求登录页面并获取真正的登录URL
|
||||
loginPageResponse, err := c.client.R().
|
||||
WithContext(ctx).
|
||||
Get(loginUrl)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
defer loginPageResponse.Body.Close()
|
||||
loginPageBytes, err := io.ReadAll(loginPageResponse.Body)
|
||||
utils.OkOrPanic(err)
|
||||
loginResponseUrl := loginPageResponse.RawResponse.Request.URL
|
||||
// 关闭client的重定向
|
||||
c.client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||
|
||||
// 解析登录页面
|
||||
loginPage, err := goquery.NewDocumentFromReader(bytes.NewReader(loginPageBytes))
|
||||
utils.OkOrPanic(err)
|
||||
// 判断登录类型
|
||||
var (
|
||||
allInputs = map[string]string{}
|
||||
fm1Node = loginPage.Find("#fm1").First()
|
||||
)
|
||||
inputs := fm1Node.Find("input")
|
||||
|
||||
for _, input := range inputs.EachIter() {
|
||||
name, exists := input.Attr("name")
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
value, _ := input.Attr("value")
|
||||
allInputs[name] = value
|
||||
}
|
||||
|
||||
allInputs["username"] = username
|
||||
|
||||
// 处理密码
|
||||
|
||||
type NutPubKeyResponse struct {
|
||||
Modulus string `json:"modulus"`
|
||||
Exponent string `json:"exponent"`
|
||||
}
|
||||
var pubKey NutPubKeyResponse
|
||||
pubKeyResponse, err := c.client.R().Get(getNutPubKey(loginResponseUrl))
|
||||
utils.OkOrPanic(err)
|
||||
defer pubKeyResponse.Body.Close()
|
||||
pubKeyBytes, err := io.ReadAll(pubKeyResponse.Body)
|
||||
utils.OkOrPanic(err)
|
||||
sonic.Unmarshal(pubKeyBytes, &pubKey)
|
||||
pair, err := cryptoutil.NewRSAKeyPair(pubKey.Exponent, "", pubKey.Modulus)
|
||||
utils.OkOrPanic(err)
|
||||
password, err = pair.Encrypt(reverseStringASCII(password))
|
||||
utils.OkOrPanic(err)
|
||||
allInputs["password"] = password
|
||||
if isNutNeedCaptcha(ctx, loginResponseUrl, c.client) {
|
||||
// 解决常规图片验证码
|
||||
captchaUrl := getNutCaptchaUrl(loginResponseUrl)
|
||||
|
||||
captchaResult := ResolveOcrCaptcha(ctx, captchaUrl, c.client)
|
||||
|
||||
allInputs["authcode"] = captchaResult
|
||||
} else {
|
||||
allInputs["authcode"] = ""
|
||||
}
|
||||
|
||||
// 登录提交
|
||||
loginPostResponse, err := c.client.R().
|
||||
SetFormData(allInputs).
|
||||
WithContext(ctx).
|
||||
Post(loginResponseUrl.String())
|
||||
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.
|
||||
WithContext(ctx).
|
||||
With("loginResponseUrl", loginResponseUrl).
|
||||
With("allInputs", allInputs).
|
||||
Wrap(err)
|
||||
})
|
||||
|
||||
defer loginPostResponse.Body.Close()
|
||||
|
||||
switch loginPostResponse.StatusCode() {
|
||||
case 302:
|
||||
handleCasLoginSuccess(ctx, loginPostResponse, schoolDetail, username, password, c.client)
|
||||
case 200:
|
||||
fallthrough
|
||||
case 401:
|
||||
doc, err := goquery.NewDocumentFromReader(loginPostResponse.Body)
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.
|
||||
WithContext(ctx).
|
||||
With("loginResponseUrl", loginResponseUrl).
|
||||
With("allInputs", allInputs).
|
||||
Wrap(err)
|
||||
})
|
||||
|
||||
if isCasNeedKickOut(doc) {
|
||||
kickoutResponse := submitCasKickOut(ctx, loginResponseUrl.String(), doc, c.client)
|
||||
handleCasLoginSuccess(ctx, kickoutResponse, schoolDetail, username, password, c.client)
|
||||
return
|
||||
}
|
||||
|
||||
message := getCasErrorMessage(doc)
|
||||
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, fmt.Sprintf("今日校园登录失败: %s", message)), func(err error) error {
|
||||
return oops.
|
||||
WithContext(ctx).
|
||||
With("loginResponseUrl", loginResponseUrl).
|
||||
With("allInputs", allInputs).
|
||||
With("errorMessage", message).
|
||||
Wrap(errors.NewBusinessError(503, message))
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
func getNutCaptchaUrl(url *url.URL) string {
|
||||
return fmt.Sprintf("%s://%s/cas/kaptcha", url.Scheme, url.Host)
|
||||
}
|
||||
func isNutNeedCaptcha(ctx context.Context, url *url.URL, client *resty.Client) bool {
|
||||
|
||||
requestURL := getNutNeedCaptchaUrl(url)
|
||||
|
||||
captchaResponse, err := client.R().
|
||||
WithContext(ctx).
|
||||
Get(requestURL)
|
||||
|
||||
utils.OkOrPanic(oops.With().
|
||||
With("requestURL", requestURL).
|
||||
WithContext(ctx).
|
||||
Wrap(err))
|
||||
|
||||
defer captchaResponse.Body.Close()
|
||||
|
||||
if captchaResponse.IsError() {
|
||||
utils.OkOrPanic(oops.With().
|
||||
With("requestURL", requestURL).
|
||||
WithContext(ctx).
|
||||
Wrap(errors.NewBusinessError(503, "请求验证码失败")))
|
||||
}
|
||||
|
||||
responseBytes, err := io.ReadAll(captchaResponse.Body)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
return strings.ToLower(strings.TrimSpace(string(responseBytes))) != "false"
|
||||
}
|
||||
|
||||
func getNutPubKey(url *url.URL) string {
|
||||
return fmt.Sprintf("%s://%s/cas/v2/getPubKey", url.Scheme, url.Host)
|
||||
}
|
||||
func getNutNeedCaptchaUrl(url *url.URL) string {
|
||||
return fmt.Sprintf("%s://%s/cas/v2/getKaptchaStatus", url.Scheme, url.Host)
|
||||
}
|
||||
|
||||
func (c CpdailyNutLogin) GetCookieJar() http.CookieJar {
|
||||
return c.client.CookieJar()
|
||||
}
|
||||
|
||||
func (c CpdailyNutLogin) GetCookieString() string {
|
||||
cookieJar := c.GetCookieJar()
|
||||
serializableJar := cookieJar.(*cookiejar.PersistCookieJar)
|
||||
return serializableJar.Serialize()
|
||||
}
|
||||
|
||||
func (c CpdailyNutLogin) GetClient() *resty.Client {
|
||||
return c.client
|
||||
}
|
||||
|
||||
func NewNut(ctx context.Context) *CpdailyNutLogin {
|
||||
return &CpdailyNutLogin{
|
||||
client: httpclient.Cpdaily.Clone(ctx),
|
||||
}
|
||||
}
|
||||
func reverseStringASCII(s string) string {
|
||||
bytes := []byte(s)
|
||||
for i, j := 0, len(bytes)-1; i < j; i, j = i+1, j-1 {
|
||||
bytes[i], bytes[j] = bytes[j], bytes[i]
|
||||
}
|
||||
return string(bytes)
|
||||
}
|
||||
@@ -1,72 +1,56 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"image"
|
||||
"io"
|
||||
"likedream-goframe/internal/application/httpclient"
|
||||
"likedream-goframe/internal/common/cst"
|
||||
"likedream-goframe/internal/application/recognizer/ddddocr"
|
||||
"likedream-goframe/internal/utils"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/samber/oops"
|
||||
"github.com/tidwall/gjson"
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
func ResolveOcrCaptcha(ctx context.Context, url string, client *resty.Client) string {
|
||||
captchaResponse, err := client.R().
|
||||
WithContext(ctx).
|
||||
Get(url)
|
||||
|
||||
byteData := DownCode2Byte(ctx, url, client)
|
||||
img, err := BytesToImage(byteData)
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.
|
||||
With("captchaUrl", url).
|
||||
With("BytesToImage", url).
|
||||
WithContext(ctx).
|
||||
Wrap(err)
|
||||
})
|
||||
|
||||
defer func() {
|
||||
_ = captchaResponse.Body.Close()
|
||||
}()
|
||||
|
||||
ocrUrl := g.Cfg().MustGetWithEnv(ctx, cst.ConfigOcrServerUrl).String()
|
||||
|
||||
ocrResponse, err := httpclient.Common.R().
|
||||
SetMultipartFields(&resty.MultipartField{
|
||||
Name: "file",
|
||||
FileName: "captcha.jpg",
|
||||
ContentType: captchaResponse.Header().Get("Content-Type"),
|
||||
Reader: captchaResponse.Body,
|
||||
}).
|
||||
WithContext(ctx).
|
||||
Post(ocrUrl + "/ocr")
|
||||
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.
|
||||
With("captchaUrl", url).
|
||||
WithContext(ctx).
|
||||
Wrap(err)
|
||||
})
|
||||
|
||||
defer func() {
|
||||
_ = ocrResponse.Body.Close()
|
||||
}()
|
||||
|
||||
responseBytes, err := io.ReadAll(ocrResponse.Body)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
responseJSON := gjson.ParseBytes(responseBytes)
|
||||
|
||||
data := responseJSON.Get("data").String()
|
||||
|
||||
if data == "" {
|
||||
utils.OkOrPanic(oops.
|
||||
In("ResolveOcrCaptcha").
|
||||
With("captchaUrl", url).
|
||||
With("message", responseJSON.Get("message").String()).
|
||||
WithContext(ctx).
|
||||
New("OCR服务返回数据为空"))
|
||||
}
|
||||
|
||||
data := ddddocr.AutoOCRVerification(img)
|
||||
return data
|
||||
}
|
||||
|
||||
func DownCode2Byte(ctx context.Context, url string, client *resty.Client) []byte {
|
||||
res, err := client.R().
|
||||
WithContext(ctx).
|
||||
Get(url)
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.
|
||||
With("captchaUrl", url).
|
||||
WithContext(ctx).
|
||||
Wrap(err)
|
||||
})
|
||||
defer res.Body.Close()
|
||||
all, err := io.ReadAll(res.Body)
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.
|
||||
With("captchaUrl", url).
|
||||
WithContext(ctx).
|
||||
Wrap(err)
|
||||
})
|
||||
return all
|
||||
}
|
||||
|
||||
// 通用解码函数,自动检测格式
|
||||
func BytesToImage(imgBytes []byte) (image.Image, error) {
|
||||
img, _, err := image.Decode(bytes.NewReader(imgBytes))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return img, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"likedream-goframe/internal/application/recognizer/slider"
|
||||
"likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/utils"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/samber/oops"
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
type CasSliderCaptchaReponse struct {
|
||||
BigImage string `json:"bigImage"`
|
||||
SmallImage string `json:"smallImage"`
|
||||
TagWidth int `json:"tagWidth"`
|
||||
YHeight int `json:"yHeight"`
|
||||
}
|
||||
|
||||
type CasSliderResult struct {
|
||||
ErrorCode int `json:"errorCode"`
|
||||
ErrorMsg string `json:"errorMsg"`
|
||||
}
|
||||
|
||||
func ResolveCasSliderCaptcha(ctx context.Context, captchaUrl string, verifyUrl string, client *resty.Client) {
|
||||
res, err := client.R().
|
||||
WithContext(ctx).
|
||||
Get(captchaUrl)
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.
|
||||
With("captchaUrl", captchaUrl).
|
||||
WithContext(ctx).
|
||||
Wrap(err)
|
||||
})
|
||||
defer res.Body.Close()
|
||||
bytes, _ := io.ReadAll(res.Body)
|
||||
var captcha CasSliderCaptchaReponse
|
||||
sonic.Unmarshal(bytes, &captcha)
|
||||
width := slider.VerifySliderWidthByBase64(captcha.BigImage, captcha.SmallImage)
|
||||
res2, err2 := client.R().
|
||||
WithContext(ctx).
|
||||
SetFormData(map[string]string{
|
||||
"canvasLength": "280",
|
||||
"moveLength": fmt.Sprintf("%d", width),
|
||||
}).
|
||||
Post(verifyUrl)
|
||||
utils.OkOrPanic(err2, func(err error) error {
|
||||
return oops.
|
||||
With("verifyUrl", verifyUrl).
|
||||
WithContext(ctx).
|
||||
Wrap(err)
|
||||
})
|
||||
defer res2.Body.Close()
|
||||
bytes, _ = io.ReadAll(res2.Body)
|
||||
str := string(bytes)
|
||||
if len(str) > 0 {
|
||||
var result CasSliderResult
|
||||
sonic.Unmarshal(bytes, &result)
|
||||
if result.ErrorCode != 1 {
|
||||
utils.OkOrPanic(oops.WithContext(ctx).Wrap(errors.NewBusinessError(500, "滑块解析错误")))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,27 +2,22 @@ package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"likedream-goframe/internal/application/cookiejar"
|
||||
"likedream-goframe/internal/application/cpdaily/school"
|
||||
"likedream-goframe/internal/application/httpclient"
|
||||
errors2 "likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/ent/cptask"
|
||||
"likedream-goframe/internal/utils"
|
||||
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
func GetCpdailyTaskService(ctx context.Context, Type cptask.TaskType, cookie string, schoolId string) CpdailyTask {
|
||||
detail := school.GetSchoolDetail(ctx, schoolId)
|
||||
cookieJar := cookiejar.NewPersistCookieJar()
|
||||
cookieJar.Deserialize(cookie)
|
||||
client := httpclient.Cpdaily.Clone(ctx)
|
||||
client.SetCookieJar(cookieJar)
|
||||
func GetCpdailyTaskService(ctx context.Context, Type cptask.TaskType, client *resty.Client, schoolDetail school.SchoolDetailInfo) CpdailyTask {
|
||||
switch Type {
|
||||
case cptask.TaskTypeCollection:
|
||||
return NewCollectionTask(client, detail.WecloudUrl)
|
||||
return NewCollectionTask(client, schoolDetail.WecloudUrl)
|
||||
case cptask.TaskTypeSign:
|
||||
return NewAutoSignTask(client, detail.WecloudUrl)
|
||||
return NewAutoSignTask(client, schoolDetail.WecloudUrl)
|
||||
case cptask.TaskTypeSleepCheck:
|
||||
return NewSleepCheckTask(client, detail.WecloudUrl)
|
||||
return NewSleepCheckTask(client, schoolDetail.WecloudUrl)
|
||||
default:
|
||||
utils.OkOrPanic(errors2.NewBusinessError(400, "不支持的任务类型"))
|
||||
}
|
||||
|
||||
@@ -354,7 +354,7 @@ func (c *CpdailyCollectionTask) ParseFormJson(ctx context.Context, rawJson map[s
|
||||
return list
|
||||
}
|
||||
|
||||
func (c *CpdailyCollectionTask) FillCustomForm(ctx context.Context, form map[string]*gvar.Var, paramList []*formtype.CpTaskFormItem, deviceId, username string) map[string]*gvar.Var {
|
||||
func (c *CpdailyCollectionTask) FillCustomForm(ctx context.Context, form map[string]*gvar.Var, paramList []*formtype.CpTaskFormItem, deviceId, imgId string) map[string]*gvar.Var {
|
||||
taskInfo := form["taskInfo"].MapStrVar()
|
||||
rows := form["rows"].Array()
|
||||
filledForm := make([]map[string]*gvar.Var, 0)
|
||||
|
||||
@@ -12,6 +12,6 @@ type CpdailyTask interface {
|
||||
QueryFormByTitle(ctx context.Context, titleGlob string) map[string]*gvar.Var
|
||||
QueryFormByYearMonth(ctx context.Context, titleGlob string, year, month string) map[string]*gvar.Var
|
||||
ParseFormJson(ctx context.Context, rawJson map[string]*gvar.Var) []*formtype.CpTaskFormItem
|
||||
FillCustomForm(ctx context.Context, form map[string]*gvar.Var, paramList []*formtype.CpTaskFormItem, deviceId, username string) map[string]*gvar.Var
|
||||
FillCustomForm(ctx context.Context, form map[string]*gvar.Var, paramList []*formtype.CpTaskFormItem, deviceId, imgId string) map[string]*gvar.Var
|
||||
Submit(ctx context.Context, form map[string]*gvar.Var, username, deviceId string)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"likedream-goframe/internal/application/cpdaily/types"
|
||||
"likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/utils"
|
||||
"math/rand/v2"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -384,18 +383,14 @@ func (c *CpdailyAutoSignTask) ParseFormJson(ctx context.Context, rawJson map[str
|
||||
}
|
||||
list = append(list, dto)
|
||||
|
||||
var current uint = 1
|
||||
|
||||
if isPhoto := rows["isPhoto"]; isPhoto != nil && isPhoto.Int() == 1 {
|
||||
dto = &formtype.CpTaskFormItem{
|
||||
Title: "签到照片",
|
||||
ColName: "signImg",
|
||||
Type: formtype.ItemTypeImage,
|
||||
Required: true,
|
||||
Number: current,
|
||||
}
|
||||
list = append(list, dto)
|
||||
current++
|
||||
}
|
||||
|
||||
// 是否范围外签到
|
||||
@@ -404,7 +399,6 @@ func (c *CpdailyAutoSignTask) ParseFormJson(ctx context.Context, rawJson map[str
|
||||
ColName: "isMalposition",
|
||||
Type: formtype.ItemTypeSingle,
|
||||
Required: true,
|
||||
Number: current,
|
||||
Options: []formtype.CpTaskFormItemOption{
|
||||
{
|
||||
Content: "是",
|
||||
@@ -418,7 +412,6 @@ func (c *CpdailyAutoSignTask) ParseFormJson(ctx context.Context, rawJson map[str
|
||||
},
|
||||
}
|
||||
list = append(list, dto)
|
||||
current++
|
||||
|
||||
// 范围外签到原因
|
||||
dto = &formtype.CpTaskFormItem{
|
||||
@@ -427,16 +420,15 @@ func (c *CpdailyAutoSignTask) ParseFormJson(ctx context.Context, rawJson map[str
|
||||
Type: formtype.ItemTypeInput,
|
||||
Required: true,
|
||||
LogicWid: "abnormalReason",
|
||||
Number: current,
|
||||
}
|
||||
list = append(list, dto)
|
||||
current++
|
||||
|
||||
current := uint(1)
|
||||
// 处理额外字段
|
||||
if isNeedExtra := rows["isNeedExtra"]; isNeedExtra != nil && isNeedExtra.Int() == 1 {
|
||||
extraField := rows["extraField"].Array()
|
||||
for i, field := range extraField {
|
||||
fieldObj := field.(map[string]*gvar.Var)
|
||||
for _, field := range extraField {
|
||||
fieldObj := gvar.New(field).MapStrVar()
|
||||
title := fieldObj["title"].String()
|
||||
isRequired := fieldObj["isRequired"].Bool()
|
||||
hasOtherItems := fieldObj["hasOtherItems"].Bool()
|
||||
@@ -445,16 +437,17 @@ func (c *CpdailyAutoSignTask) ParseFormJson(ctx context.Context, rawJson map[str
|
||||
Title: title,
|
||||
ColName: title,
|
||||
Type: formtype.ItemTypeSingle,
|
||||
Number: uint(i),
|
||||
Number: current,
|
||||
Required: isRequired,
|
||||
IsOther: hasOtherItems,
|
||||
}
|
||||
current++
|
||||
|
||||
// 处理选项
|
||||
extraFieldItems := fieldObj["extraFieldItems"].Array()
|
||||
var options []formtype.CpTaskFormItemOption
|
||||
for _, item := range extraFieldItems {
|
||||
itemObj := item.(map[string]*gvar.Var)
|
||||
itemObj := gvar.New(item).MapStrVar()
|
||||
option := formtype.CpTaskFormItemOption{
|
||||
Content: itemObj["content"].String(),
|
||||
IsOtherItems: itemObj["isOtherItems"].Bool(),
|
||||
@@ -473,7 +466,7 @@ func (c *CpdailyAutoSignTask) ParseFormJson(ctx context.Context, rawJson map[str
|
||||
return list
|
||||
}
|
||||
|
||||
func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[string]*gvar.Var, paramList []*formtype.CpTaskFormItem, deviceId, username string) map[string]*gvar.Var {
|
||||
func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[string]*gvar.Var, paramList []*formtype.CpTaskFormItem, deviceId, imgId string) map[string]*gvar.Var {
|
||||
var resultForm map[string]*gvar.Var
|
||||
|
||||
// 填充必要变量
|
||||
@@ -514,27 +507,11 @@ func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[strin
|
||||
utils.OkOrPanic(errors.NewBusinessError(400, "签到需要图片,但是当前图片为空"), oops.WithContext(ctx).Wrap)
|
||||
}
|
||||
|
||||
imageUuidSeq := strings.SplitSeq(targetParam.Content, ",")
|
||||
|
||||
var imageUuids []string
|
||||
|
||||
for imageUuid := range imageUuidSeq {
|
||||
imageUuid = strings.TrimSpace(imageUuid)
|
||||
if imageUuid == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
imageUuids = append(imageUuids, imageUuid)
|
||||
}
|
||||
|
||||
if len(imageUuids) == 0 {
|
||||
if imgId == "" {
|
||||
utils.OkOrPanic(errors.NewBusinessError(400, "签到照片未上传"), oops.WithContext(ctx).Wrap)
|
||||
}
|
||||
|
||||
// 随机选择一张照片
|
||||
imageUuid := imageUuids[rand.IntN(len(imageUuids))]
|
||||
|
||||
imageId, e := uuid.Parse(imageUuid)
|
||||
imageId, e := uuid.Parse(imgId)
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||
|
||||
previewUrl := imageuploader.UploadImageFromStorage(ctx, c.host, imageId, types.TaskTypeSign, c.client)
|
||||
@@ -554,7 +531,9 @@ func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[strin
|
||||
ticket = submitCaptchaResolver.GetCaptchaTicket(ctx, captchaInfo)
|
||||
})
|
||||
}, retry.MaxDelay(15*time.Second), retry.Attempts(5), retry.Context(ctx))
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "提交验证码识别失败").WithInnerError(e), oops.WithContext(ctx).Wrap)
|
||||
if e != nil {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "提交验证码识别失败").WithInnerError(e), oops.WithContext(ctx).Wrap)
|
||||
}
|
||||
|
||||
resultForm["ticket"] = gvar.New(ticket)
|
||||
} else {
|
||||
@@ -577,8 +556,9 @@ func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[strin
|
||||
var extraFieldItemValues []map[string]*gvar.Var
|
||||
|
||||
for i, field := range extraField {
|
||||
current := uint(i + 1)
|
||||
// 检查数字是否在范围内
|
||||
found := slices.Contains(numbers, uint(i))
|
||||
found := slices.Contains(numbers, current)
|
||||
if !found {
|
||||
utils.OkOrPanic(errors.NewBusinessError(500, "非法的额外信息数据"), oops.WithContext(ctx).Wrap)
|
||||
}
|
||||
@@ -586,7 +566,7 @@ func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[strin
|
||||
// 找到对应的参数
|
||||
var targetParam *formtype.CpTaskFormItem
|
||||
for _, v := range paramList {
|
||||
if v.Number == uint(i) {
|
||||
if v.Number == current {
|
||||
targetParam = v
|
||||
break
|
||||
}
|
||||
@@ -596,7 +576,7 @@ func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[strin
|
||||
continue
|
||||
}
|
||||
|
||||
fieldObj := field.(map[string]*gvar.Var)
|
||||
fieldObj := gvar.New(field).MapStrVar()
|
||||
fieldTitle := fieldObj["title"].String()
|
||||
|
||||
if targetParam.Title != fieldTitle {
|
||||
@@ -607,7 +587,7 @@ func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[strin
|
||||
var extraFieldItemValue map[string]*gvar.Var
|
||||
|
||||
for _, item := range extraFieldItems {
|
||||
itemObj := item.(map[string]*gvar.Var)
|
||||
itemObj := gvar.New(item).MapStrVar()
|
||||
content := itemObj["content"].String()
|
||||
|
||||
if content == targetParam.Content {
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"likedream-goframe/internal/common/cst"
|
||||
"likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/utils"
|
||||
"math/rand/v2"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -190,11 +189,7 @@ func (c *CpdailySleepCheckTask) QueryFormByTitle(ctx context.Context, titleGlob
|
||||
SetBody(map[string]any{}).
|
||||
WithContext(ctx).
|
||||
Post(c.getReqUrl("getStuAttendacesInOneDay"))
|
||||
//
|
||||
response, err = c.client.R().
|
||||
SetBody(map[string]any{}).
|
||||
WithContext(ctx).
|
||||
Post(c.getReqUrl("getStuAttendacesInOneDay"))
|
||||
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.With("host", c.host).
|
||||
In("CpdailySleepCheckTask.QueryFormByTitle").
|
||||
@@ -463,18 +458,15 @@ func (c *CpdailySleepCheckTask) ParseFormJson(ctx context.Context, rawJson map[s
|
||||
}
|
||||
list = append(list, dto)
|
||||
|
||||
var current uint = 1
|
||||
|
||||
if isPhoto := rows["isPhoto"]; isPhoto != nil && isPhoto.Int() == 1 {
|
||||
dto = &formtype.CpTaskFormItem{
|
||||
Title: "查寝照片",
|
||||
ColName: "signImg",
|
||||
Type: formtype.ItemTypeImage,
|
||||
Required: true,
|
||||
Number: current,
|
||||
}
|
||||
list = append(list, dto)
|
||||
current++
|
||||
|
||||
}
|
||||
|
||||
// 是否范围外签到
|
||||
@@ -483,7 +475,6 @@ func (c *CpdailySleepCheckTask) ParseFormJson(ctx context.Context, rawJson map[s
|
||||
ColName: "isMalposition",
|
||||
Type: formtype.ItemTypeSingle,
|
||||
Required: true,
|
||||
Number: current,
|
||||
Options: []formtype.CpTaskFormItemOption{
|
||||
{
|
||||
Content: "是",
|
||||
@@ -497,7 +488,6 @@ func (c *CpdailySleepCheckTask) ParseFormJson(ctx context.Context, rawJson map[s
|
||||
},
|
||||
}
|
||||
list = append(list, dto)
|
||||
current++
|
||||
|
||||
for key, item := range list {
|
||||
list[key].Show = item.LogicWid == ""
|
||||
@@ -506,7 +496,7 @@ func (c *CpdailySleepCheckTask) ParseFormJson(ctx context.Context, rawJson map[s
|
||||
return list
|
||||
}
|
||||
|
||||
func (c *CpdailySleepCheckTask) FillCustomForm(ctx context.Context, form map[string]*gvar.Var, paramList []*formtype.CpTaskFormItem, deviceId, username string) map[string]*gvar.Var {
|
||||
func (c *CpdailySleepCheckTask) FillCustomForm(ctx context.Context, form map[string]*gvar.Var, paramList []*formtype.CpTaskFormItem, deviceId, imgId string) map[string]*gvar.Var {
|
||||
var resultForm map[string]*gvar.Var
|
||||
|
||||
// 填充必要变量
|
||||
@@ -544,9 +534,10 @@ func (c *CpdailySleepCheckTask) FillCustomForm(ctx context.Context, form map[str
|
||||
return utils.Catch(func() {
|
||||
ticket = submitCaptchaResolver.GetCaptchaTicket(ctx, captchaInfo)
|
||||
})
|
||||
}, retry.MaxDelay(15*time.Second), retry.Attempts(5), retry.Context(ctx))
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "提交验证码识别失败").WithInnerError(e), oops.WithContext(ctx).Wrap)
|
||||
|
||||
}, retry.Delay(15*time.Second), retry.Attempts(5), retry.Context(ctx))
|
||||
if e != nil {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "提交验证码识别失败").WithInnerError(e), oops.WithContext(ctx).Wrap)
|
||||
}
|
||||
resultForm["ticket"] = gvar.New(ticket)
|
||||
} else {
|
||||
resultForm["ticket"] = gvar.New("")
|
||||
@@ -565,27 +556,11 @@ func (c *CpdailySleepCheckTask) FillCustomForm(ctx context.Context, form map[str
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "未找到查寝照片参数,请上传照片"), oops.WithContext(ctx).Wrap)
|
||||
}
|
||||
|
||||
imageUuidSeq := strings.SplitSeq(targetParam.Content, ",")
|
||||
|
||||
var imageUuids []string
|
||||
|
||||
for imageUuid := range imageUuidSeq {
|
||||
imageUuid = strings.TrimSpace(imageUuid)
|
||||
if imageUuid == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
imageUuids = append(imageUuids, imageUuid)
|
||||
}
|
||||
|
||||
if len(imageUuids) == 0 {
|
||||
if imgId == "" {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "未找到查寝照片参数,请上传照片"), oops.WithContext(ctx).Wrap)
|
||||
}
|
||||
|
||||
// 随机选择一张照片
|
||||
imageUuid := imageUuids[rand.IntN(len(imageUuids))]
|
||||
|
||||
imageId, e := uuid.Parse(imageUuid)
|
||||
imageId, e := uuid.Parse(imgId)
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||
|
||||
previewUrl := imageuploader.UploadImageFromStorage(ctx, c.host, imageId, types.TaskTypeSleepCheck, c.client)
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/gogf/gf/v2/container/gvar"
|
||||
)
|
||||
|
||||
const submitSignSalt = "HashSalt" // 请根据实际情况替换为真实盐值
|
||||
const submitSignSalt = "hru89rwr87Nthifa" // 请根据实际情况替换为真实盐值
|
||||
|
||||
func GetSubmitSign(form map[string]*gvar.Var) string {
|
||||
// 按照指定顺序构建哈希数据
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package classifier
|
||||
|
||||
import (
|
||||
"embed"
|
||||
_ "embed"
|
||||
"likedream-goframe/internal/application/recognizer"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
"likedream-goframe/internal/utils"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -11,11 +13,8 @@ import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
//go:embed static/classes.txt
|
||||
var classesBytes []byte
|
||||
|
||||
//go:embed static/model.onnx
|
||||
var modelBytes []byte
|
||||
//go:embed static/*
|
||||
var fs embed.FS
|
||||
var imageClassifier *ImageClassifier
|
||||
|
||||
func init() {
|
||||
@@ -39,10 +38,14 @@ func initModel() (modelPath, classesPath string) {
|
||||
os.Mkdir(staticPath, 0755)
|
||||
}
|
||||
modelPath = filepath.Join(staticPath, "model.onnx")
|
||||
modelBytes, err := fs.ReadFile("static/model.onnx")
|
||||
utils.OkOrPanic(err)
|
||||
if _, err := os.Stat(modelPath); os.IsNotExist(err) {
|
||||
os.WriteFile(modelPath, modelBytes, 0644)
|
||||
}
|
||||
classesPath = filepath.Join(staticPath, "classes.txt")
|
||||
classesBytes, err := fs.ReadFile("static/classes.txt")
|
||||
utils.OkOrPanic(err)
|
||||
if _, err := os.Stat(classesPath); os.IsNotExist(err) {
|
||||
os.WriteFile(classesPath, classesBytes, 0644)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package ddddocr
|
||||
|
||||
import (
|
||||
"embed"
|
||||
_ "embed"
|
||||
"likedream-goframe/internal/application/recognizer"
|
||||
"os"
|
||||
@@ -8,8 +9,8 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:embed static/common_old.onnx
|
||||
var commonOldBytes []byte
|
||||
//go:embed static/*
|
||||
var fs embed.FS
|
||||
|
||||
func initModel() string {
|
||||
if !recognizer.OnnxIsInit {
|
||||
@@ -24,8 +25,9 @@ func initModel() string {
|
||||
os.Mkdir(staticPath, 0755)
|
||||
}
|
||||
modelPath := filepath.Join(staticPath, "common_old.onnx")
|
||||
modelBytes, _ := fs.ReadFile("static/common_old.onnx")
|
||||
if _, err := os.Stat(modelPath); os.IsNotExist(err) {
|
||||
os.WriteFile(modelPath, commonOldBytes, 0644)
|
||||
os.WriteFile(modelPath, modelBytes, 0644)
|
||||
}
|
||||
return modelPath
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package recognizer
|
||||
|
||||
import (
|
||||
"embed"
|
||||
_ "embed"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
"os"
|
||||
@@ -12,14 +13,8 @@ import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
//go:embed assets/onnxruntime.dll
|
||||
var onnxRuntimeFileWindows []byte
|
||||
|
||||
//go:embed assets/libonnxruntime.1.23.2.dylib
|
||||
var onnxRuntimeFileDarwin []byte
|
||||
|
||||
//go:embed assets/libonnxruntime.so.1.23.2
|
||||
var onnxRuntimeFileLinux []byte
|
||||
//go:embed static/*
|
||||
var fs embed.FS
|
||||
|
||||
// OnnxIsInit 初始化onnx配置
|
||||
var OnnxIsInit = false
|
||||
@@ -48,15 +43,15 @@ func getOnnxRuntimeName() (onnxRuntimeFileName string, onnxRuntimeFileBytes []by
|
||||
switch systemOS {
|
||||
case "windows":
|
||||
onnxRuntimeFileName = "onnxruntime.dll"
|
||||
onnxRuntimeFileBytes = onnxRuntimeFileWindows
|
||||
onnxRuntimeFileBytes, _ = fs.ReadFile("static/onnxruntime.dll")
|
||||
return
|
||||
case "darwin":
|
||||
onnxRuntimeFileName = "onnxruntime.dylib"
|
||||
onnxRuntimeFileBytes = onnxRuntimeFileDarwin
|
||||
onnxRuntimeFileBytes, _ = fs.ReadFile("static/libonnxruntime.1.23.2.dylib")
|
||||
return
|
||||
case "linux":
|
||||
onnxRuntimeFileName = "onnxruntime.so"
|
||||
onnxRuntimeFileBytes = onnxRuntimeFileLinux
|
||||
onnxRuntimeFileBytes, _ = fs.ReadFile("static/libonnxruntime.so.1.23.2")
|
||||
return
|
||||
}
|
||||
logger.Logger.Panic("暂不支持的操作系统:", zap.String("系统名称", systemOS))
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,192 @@
|
||||
package slider
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"os"
|
||||
|
||||
"gocv.io/x/gocv"
|
||||
)
|
||||
|
||||
func VerifySliderWidth(bgImgPath, sliderImgPath string) int {
|
||||
// 检查背景图片文件是否存在
|
||||
if _, err := os.Stat(bgImgPath); os.IsNotExist(err) {
|
||||
return -1
|
||||
}
|
||||
|
||||
// 检查滑块图片文件是否存在
|
||||
if _, err := os.Stat(sliderImgPath); os.IsNotExist(err) {
|
||||
return -1
|
||||
}
|
||||
|
||||
// 读取背景图片
|
||||
bgImg := gocv.IMRead(bgImgPath, gocv.IMReadColor)
|
||||
if bgImg.Empty() {
|
||||
return -1
|
||||
}
|
||||
defer bgImg.Close()
|
||||
|
||||
// 读取滑块图片(包含 alpha 通道)
|
||||
sliderImgWithAlpha := gocv.IMRead(sliderImgPath, gocv.IMReadUnchanged)
|
||||
if sliderImgWithAlpha.Empty() {
|
||||
return -1
|
||||
}
|
||||
defer sliderImgWithAlpha.Close()
|
||||
|
||||
// 分离滑块图片的通道
|
||||
channels := gocv.Split(sliderImgWithAlpha)
|
||||
defer func() {
|
||||
for i := range channels {
|
||||
channels[i].Close()
|
||||
}
|
||||
}()
|
||||
|
||||
var sliderImg gocv.Mat
|
||||
var mask gocv.Mat
|
||||
|
||||
// 如果有 alpha 通道(4通道),提取 mask
|
||||
if len(channels) == 4 {
|
||||
// 前3个通道是 BGR
|
||||
sliderImg = gocv.NewMat()
|
||||
defer sliderImg.Close()
|
||||
gocv.Merge(channels[:3], &sliderImg)
|
||||
|
||||
// 使用 alpha 通道作为 mask
|
||||
mask = channels[3].Clone()
|
||||
defer mask.Close()
|
||||
} else if len(channels) == 3 {
|
||||
// 没有透明通道,直接使用原图
|
||||
sliderImg = sliderImgWithAlpha.Clone()
|
||||
defer sliderImg.Close()
|
||||
mask = gocv.NewMat() // 空 mask
|
||||
defer mask.Close()
|
||||
} else {
|
||||
return -1
|
||||
}
|
||||
|
||||
// 创建结果矩阵用于存储匹配结果
|
||||
result := gocv.NewMat()
|
||||
defer result.Close()
|
||||
|
||||
// 直接在原图上使用模板匹配找到滑块位置
|
||||
if mask.Empty() {
|
||||
gocv.MatchTemplate(bgImg, sliderImg, &result, gocv.TmCcoeffNormed, gocv.NewMat())
|
||||
} else {
|
||||
// 带 mask 的模板匹配
|
||||
gocv.MatchTemplate(bgImg, sliderImg, &result, gocv.TmCcoeffNormed, mask)
|
||||
}
|
||||
|
||||
// 找到最佳匹配位置
|
||||
_, maxVal, _, maxLoc := gocv.MinMaxLoc(result)
|
||||
|
||||
// 如果匹配度太低,返回 -1 表示失败
|
||||
if maxVal < 0.6 {
|
||||
return -1
|
||||
}
|
||||
|
||||
// 获取原图上的 x 坐标,然后按比例缩放到目标尺寸
|
||||
originalWidth := bgImg.Cols()
|
||||
targetWidth := 280
|
||||
scale := float64(targetWidth) / float64(originalWidth)
|
||||
scaledX := int(float64(maxLoc.X) * scale)
|
||||
|
||||
// 返回缩放后的 x 坐标(滑块需要移动的距离)
|
||||
return scaledX
|
||||
}
|
||||
|
||||
// VerifySliderWidthByBase64 通过 base64 编码的图片计算滑块验证码需要移动的距离
|
||||
// bgImgBase64: 背景图片的 base64 编码
|
||||
// sliderImgBase64: 滑块图片的 base64 编码
|
||||
// 返回: 滑块需要移动的 x 距离(已缩放到 280 宽度),失败返回 -1
|
||||
func VerifySliderWidthByBase64(bgImgBase64, sliderImgBase64 string) int {
|
||||
// 解码背景图片的 base64
|
||||
bgImgData, err := base64.StdEncoding.DecodeString(bgImgBase64)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
// 解码滑块图片的 base64
|
||||
sliderImgData, err := base64.StdEncoding.DecodeString(sliderImgBase64)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
// 从内存中读取背景图片
|
||||
bgImg, err := gocv.IMDecode(bgImgData, gocv.IMReadColor)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
if bgImg.Empty() {
|
||||
return -1
|
||||
}
|
||||
defer bgImg.Close()
|
||||
|
||||
// 从内存中读取滑块图片(包含 alpha 通道)
|
||||
sliderImgWithAlpha, err := gocv.IMDecode(sliderImgData, gocv.IMReadUnchanged)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
if sliderImgWithAlpha.Empty() {
|
||||
return -1
|
||||
}
|
||||
defer sliderImgWithAlpha.Close()
|
||||
|
||||
// 分离滑块图片的通道
|
||||
channels := gocv.Split(sliderImgWithAlpha)
|
||||
defer func() {
|
||||
for i := range channels {
|
||||
channels[i].Close()
|
||||
}
|
||||
}()
|
||||
|
||||
var sliderImg gocv.Mat
|
||||
var mask gocv.Mat
|
||||
|
||||
// 如果有 alpha 通道(4通道),提取 mask
|
||||
if len(channels) == 4 {
|
||||
// 前3个通道是 BGR
|
||||
sliderImg = gocv.NewMat()
|
||||
defer sliderImg.Close()
|
||||
gocv.Merge(channels[:3], &sliderImg)
|
||||
|
||||
// 使用 alpha 通道作为 mask
|
||||
mask = channels[3].Clone()
|
||||
defer mask.Close()
|
||||
} else if len(channels) == 3 {
|
||||
// 没有透明通道,直接使用原图
|
||||
sliderImg = sliderImgWithAlpha.Clone()
|
||||
defer sliderImg.Close()
|
||||
mask = gocv.NewMat() // 空 mask
|
||||
defer mask.Close()
|
||||
} else {
|
||||
return -1
|
||||
}
|
||||
|
||||
// 创建结果矩阵用于存储匹配结果
|
||||
result := gocv.NewMat()
|
||||
defer result.Close()
|
||||
|
||||
// 直接在原图上使用模板匹配找到滑块位置
|
||||
if mask.Empty() {
|
||||
gocv.MatchTemplate(bgImg, sliderImg, &result, gocv.TmCcoeffNormed, gocv.NewMat())
|
||||
} else {
|
||||
// 带 mask 的模板匹配
|
||||
gocv.MatchTemplate(bgImg, sliderImg, &result, gocv.TmCcoeffNormed, mask)
|
||||
}
|
||||
|
||||
// 找到最佳匹配位置
|
||||
_, maxVal, _, maxLoc := gocv.MinMaxLoc(result)
|
||||
|
||||
// 如果匹配度太低,返回 -1 表示失败
|
||||
if maxVal < 0.6 {
|
||||
return -1
|
||||
}
|
||||
|
||||
// 获取原图上的 x 坐标,然后按比例缩放到目标尺寸
|
||||
originalWidth := bgImg.Cols()
|
||||
targetWidth := 280
|
||||
scale := float64(targetWidth) / float64(originalWidth)
|
||||
scaledX := int(float64(maxLoc.X) * scale)
|
||||
|
||||
// 返回缩放后的 x 坐标(滑块需要移动的距离)
|
||||
return scaledX
|
||||
}
|
||||
@@ -3,22 +3,25 @@ package cst
|
||||
import "time"
|
||||
|
||||
const (
|
||||
ConfigOcrServerUrl = "ocr.url"
|
||||
ConfigEnvMode = "env.mode"
|
||||
ConfigDatabaseType = "database.type"
|
||||
ConfigDatabaseDsn = "database.dsn"
|
||||
ConfigServerPort = "server.port"
|
||||
ConfigEmailConfirmBaseUrl = "server.emailConfirmBaseUrl"
|
||||
ConfigAuthorizationKey = "server.authorization"
|
||||
ConfigJwtKey = "server.jwt"
|
||||
ConfigRuoliCaptchaBaseUrl = "ruoliCaptcha.baseUrl"
|
||||
ConfigS3AccessKey = "s3.accessKey"
|
||||
ConfigS3SecretKey = "s3.secretKey"
|
||||
ConfigS3BaseUrl = "s3.baseUrl"
|
||||
ConfigS3Bucket = "s3.bucket"
|
||||
ConfigCpdailyAppVersion = "cpdaily.appVersion"
|
||||
ConfigCpdailySystemVersion = "cpdaily.systemVersion"
|
||||
ConfigCpdailyCalVersion = "cpdaily.calVersion"
|
||||
ConfigOcrServerUrl = "ocr.url"
|
||||
ConfigEnvMode = "env.mode"
|
||||
ConfigDatabaseType = "database.type"
|
||||
ConfigDatabaseDsn = "database.dsn"
|
||||
ConfigDatabaseDsn2 = "database.dsn2"
|
||||
ConfigServerPort = "server.port"
|
||||
ConfigEmailConfirmBaseUrl = "server.emailConfirmBaseUrl"
|
||||
ConfigEmailResetPasswordUrl = "server.emailResetPasswordUrl"
|
||||
ConfigBalanceMigrateUrl = "server.balanceMigrateUrl"
|
||||
ConfigAuthorizationKey = "server.authorization"
|
||||
ConfigJwtKey = "server.jwt"
|
||||
ConfigRuoliCaptchaBaseUrl = "ruoliCaptcha.baseUrl"
|
||||
ConfigS3AccessKey = "s3.accessKey"
|
||||
ConfigS3SecretKey = "s3.secretKey"
|
||||
ConfigS3BaseUrl = "s3.baseUrl"
|
||||
ConfigS3Bucket = "s3.bucket"
|
||||
ConfigCpdailyAppVersion = "cpdaily.appVersion"
|
||||
ConfigCpdailySystemVersion = "cpdaily.systemVersion"
|
||||
ConfigCpdailyCalVersion = "cpdaily.calVersion"
|
||||
|
||||
ConfigRedisUrl = "redis.url"
|
||||
|
||||
@@ -35,8 +38,8 @@ const (
|
||||
|
||||
const (
|
||||
// 应用程序常量
|
||||
AppName = "今日校园系统"
|
||||
AppCopyright = "© 2024 今日校园系统. 保留所有权利."
|
||||
AppName = "若梦系统"
|
||||
AppCopyright = "© 2025 若梦系统. 保留所有权利."
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -3,6 +3,8 @@ package cst
|
||||
const (
|
||||
RedisUserInfo = "userinfo:"
|
||||
RedisVerifyCode = "verifycode:"
|
||||
RedisResetPassword = "resetpassword:"
|
||||
RedisBalanceMigrate = "balancemigrate:" // 积分转移token
|
||||
RedisCpUserLogin = "cpuserlogin:"
|
||||
RedisCpUserAuthorize = "cpuser:authorize:"
|
||||
)
|
||||
@@ -10,6 +12,8 @@ const (
|
||||
var (
|
||||
RedisUserInfoExpire = int64(86400)
|
||||
RedisVerifyCodeExpire = int64(86400)
|
||||
RedisResetPasswordExpire = int64(3600) // 重置密码链接1小时有效
|
||||
RedisBalanceMigrateExpire = int64(3600) // 积分转移链接1小时有效
|
||||
RedisCpUserLoginExpire = int64(180)
|
||||
RedisCpUserAuthorizeExpire = int64(1800)
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
func CheckHasData() bool {
|
||||
count, err := Global.SysUser.Query().Count(context.Background())
|
||||
count, err := Global.Debug().SysUser.Query().Count(context.Background())
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return err
|
||||
})
|
||||
@@ -36,7 +36,7 @@ func GenerateData() {
|
||||
// 生成用户
|
||||
func generateUser() {
|
||||
ctx := gctx.New()
|
||||
Global.SysUser.Create().
|
||||
Global.Debug().SysUser.Create().
|
||||
SetUsername("admin").
|
||||
SetPassword(utils.PasswordBcryptHash("123456")).
|
||||
SetEmail("admin@qq.com").
|
||||
@@ -50,15 +50,15 @@ func generateUser() {
|
||||
// 生成角色
|
||||
func generateRoles() {
|
||||
ctx := gctx.New()
|
||||
Global.SysRole.Create().SetName("管理员").SetLabel(cst.KeyRoleAdmin).SetDescription("超级管理员角色").SetOrder(1).ExecX(ctx)
|
||||
Global.SysRole.Create().SetName("代理").SetLabel(cst.KeyRoleAgent).SetDescription("代理角色").SetOrder(2).ExecX(ctx)
|
||||
Global.SysRole.Create().SetName("普通用户").SetLabel(cst.KeyRoleUser).SetDescription("普通用户角色").SetOrder(3).ExecX(ctx)
|
||||
Global.Debug().SysRole.Create().SetName("管理员").SetLabel(cst.KeyRoleAdmin).SetDescription("超级管理员角色").SetOrder(1).ExecX(ctx)
|
||||
Global.Debug().SysRole.Create().SetName("代理").SetLabel(cst.KeyRoleAgent).SetDescription("代理角色").SetOrder(2).ExecX(ctx)
|
||||
Global.Debug().SysRole.Create().SetName("普通用户").SetLabel(cst.KeyRoleUser).SetDescription("普通用户角色").SetOrder(3).ExecX(ctx)
|
||||
}
|
||||
|
||||
// 生成菜单
|
||||
func generateMenus() {
|
||||
ctx := gctx.New()
|
||||
sysM := Global.SysMenu.Create().
|
||||
sysM := Global.Debug().SysMenu.Create().
|
||||
SetName("系统管理").
|
||||
SetLabel("system").
|
||||
SetType(sysmenu.TypeGroup).
|
||||
@@ -66,7 +66,7 @@ func generateMenus() {
|
||||
SetRoute("/system").
|
||||
SetOrder(1).
|
||||
SaveX(ctx)
|
||||
userM := Global.SysMenu.Create().
|
||||
userM := Global.Debug().SysMenu.Create().
|
||||
SetName("用户管理").
|
||||
SetLabel("user").
|
||||
SetType(sysmenu.TypeMenu).
|
||||
@@ -77,7 +77,7 @@ func generateMenus() {
|
||||
SetParent(sysM).
|
||||
SaveX(ctx)
|
||||
// 新增 编辑 删除按钮
|
||||
Global.SysMenu.Create().
|
||||
Global.Debug().SysMenu.Create().
|
||||
SetName("新增").
|
||||
SetLabel("add").
|
||||
SetType(sysmenu.TypeButton).
|
||||
@@ -85,7 +85,7 @@ func generateMenus() {
|
||||
SetOrder(1).
|
||||
SetParent(userM).
|
||||
SaveX(ctx)
|
||||
Global.SysMenu.Create().
|
||||
Global.Debug().SysMenu.Create().
|
||||
SetName("编辑").
|
||||
SetLabel("edit").
|
||||
SetType(sysmenu.TypeButton).
|
||||
@@ -94,7 +94,7 @@ func generateMenus() {
|
||||
SetParent(userM).
|
||||
SaveX(ctx)
|
||||
// 删除按钮
|
||||
Global.SysMenu.Create().
|
||||
Global.Debug().SysMenu.Create().
|
||||
SetName("删除").
|
||||
SetLabel("delete").
|
||||
SetType(sysmenu.TypeButton).
|
||||
@@ -103,7 +103,7 @@ func generateMenus() {
|
||||
SetParent(userM).
|
||||
SaveX(ctx)
|
||||
// 列表
|
||||
Global.SysMenu.Create().
|
||||
Global.Debug().SysMenu.Create().
|
||||
SetName("列表").
|
||||
SetLabel("list").
|
||||
SetType(sysmenu.TypeButton).
|
||||
@@ -111,7 +111,7 @@ func generateMenus() {
|
||||
SetOrder(4).
|
||||
SetParent(userM).
|
||||
SaveX(ctx)
|
||||
roleM := Global.SysMenu.Create().
|
||||
roleM := Global.Debug().SysMenu.Create().
|
||||
SetName("角色管理").
|
||||
SetLabel("role").
|
||||
SetType(sysmenu.TypeMenu).
|
||||
@@ -122,7 +122,7 @@ func generateMenus() {
|
||||
SetParent(sysM).
|
||||
SaveX(ctx)
|
||||
// 新增 编辑 删除按钮
|
||||
Global.SysMenu.Create().
|
||||
Global.Debug().SysMenu.Create().
|
||||
SetName("新增").
|
||||
SetLabel("add").
|
||||
SetType(sysmenu.TypeButton).
|
||||
@@ -130,7 +130,7 @@ func generateMenus() {
|
||||
SetOrder(1).
|
||||
SetParent(roleM).
|
||||
SaveX(ctx)
|
||||
Global.SysMenu.Create().
|
||||
Global.Debug().SysMenu.Create().
|
||||
SetName("编辑").
|
||||
SetLabel("edit").
|
||||
SetType(sysmenu.TypeButton).
|
||||
@@ -139,7 +139,7 @@ func generateMenus() {
|
||||
SetParent(roleM).
|
||||
SaveX(ctx)
|
||||
// 删除按钮
|
||||
Global.SysMenu.Create().
|
||||
Global.Debug().SysMenu.Create().
|
||||
SetName("删除").
|
||||
SetLabel("delete").
|
||||
SetType(sysmenu.TypeButton).
|
||||
@@ -147,7 +147,7 @@ func generateMenus() {
|
||||
SetOrder(3).
|
||||
SetParent(roleM).
|
||||
SaveX(ctx)
|
||||
Global.SysMenu.Create().
|
||||
Global.Debug().SysMenu.Create().
|
||||
SetName("列表").
|
||||
SetLabel("list").
|
||||
SetType(sysmenu.TypeButton).
|
||||
@@ -155,7 +155,7 @@ func generateMenus() {
|
||||
SetOrder(4).
|
||||
SetParent(roleM).
|
||||
SaveX(ctx)
|
||||
menuM := Global.SysMenu.Create().
|
||||
menuM := Global.Debug().SysMenu.Create().
|
||||
SetName("菜单管理").
|
||||
SetLabel("menu").
|
||||
SetType(sysmenu.TypeMenu).
|
||||
@@ -166,7 +166,7 @@ func generateMenus() {
|
||||
SetParent(sysM).
|
||||
SaveX(ctx)
|
||||
// 新增 编辑 删除按钮
|
||||
Global.SysMenu.Create().
|
||||
Global.Debug().SysMenu.Create().
|
||||
SetName("新增").
|
||||
SetLabel("add").
|
||||
SetType(sysmenu.TypeButton).
|
||||
@@ -174,7 +174,7 @@ func generateMenus() {
|
||||
SetOrder(1).
|
||||
SetParent(menuM).
|
||||
SaveX(ctx)
|
||||
Global.SysMenu.Create().
|
||||
Global.Debug().SysMenu.Create().
|
||||
SetName("编辑").
|
||||
SetLabel("edit").
|
||||
SetType(sysmenu.TypeButton).
|
||||
@@ -183,7 +183,7 @@ func generateMenus() {
|
||||
SetParent(menuM).
|
||||
SaveX(ctx)
|
||||
// 删除按钮
|
||||
Global.SysMenu.Create().
|
||||
Global.Debug().SysMenu.Create().
|
||||
SetName("删除").
|
||||
SetLabel("delete").
|
||||
SetType(sysmenu.TypeButton).
|
||||
@@ -191,7 +191,7 @@ func generateMenus() {
|
||||
SetOrder(3).
|
||||
SetParent(menuM).
|
||||
SaveX(ctx)
|
||||
Global.SysMenu.Create().
|
||||
Global.Debug().SysMenu.Create().
|
||||
SetName("列表").
|
||||
SetLabel("list").
|
||||
SetType(sysmenu.TypeButton).
|
||||
@@ -204,14 +204,14 @@ func generateMenus() {
|
||||
// 关联角色
|
||||
func relationRoles() {
|
||||
ctx := gctx.New()
|
||||
admin := Global.SysUser.Query().Where(sysuser.UsernameEQ("admin")).OnlyX(ctx)
|
||||
role := Global.SysRole.Query().Where(sysrole.LabelEQ(cst.KeyRoleAdmin)).AllX(ctx)
|
||||
Global.SysUser.UpdateOne(admin).AddRoles(role...).ExecX(ctx)
|
||||
admin := Global.Debug().SysUser.Query().Where(sysuser.UsernameEQ("admin")).OnlyX(ctx)
|
||||
role := Global.Debug().SysRole.Query().Where(sysrole.LabelEQ(cst.KeyRoleAdmin)).AllX(ctx)
|
||||
Global.Debug().SysUser.UpdateOne(admin).AddRoles(role...).ExecX(ctx)
|
||||
}
|
||||
|
||||
// 关联菜单
|
||||
func relationMenus() {
|
||||
ctx := gctx.New()
|
||||
adminRole := Global.SysRole.Query().Where(sysrole.LabelEQ(cst.KeyRoleAdmin)).OnlyX(ctx)
|
||||
Global.SysRole.UpdateOne(adminRole).AddMenus(Global.SysMenu.Query().AllX(ctx)...).ExecX(ctx)
|
||||
adminRole := Global.Debug().SysRole.Query().Where(sysrole.LabelEQ(cst.KeyRoleAdmin)).OnlyX(ctx)
|
||||
Global.Debug().SysRole.UpdateOne(adminRole).AddMenus(Global.Debug().SysMenu.Query().AllX(ctx)...).ExecX(ctx)
|
||||
}
|
||||
|
||||
@@ -11,10 +11,14 @@ import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var Global *ent.Client
|
||||
|
||||
var GlobalGorm *gorm.DB
|
||||
|
||||
func InitDB() {
|
||||
dbType, err := g.Cfg().Get(gctx.New(), cst.ConfigDatabaseType)
|
||||
utils.OkOrPanic(err)
|
||||
@@ -23,8 +27,24 @@ func InitDB() {
|
||||
client, err := ent.Open(dbType.String(), dbDsn.String())
|
||||
utils.OkOrPanic(err)
|
||||
// 自动迁移
|
||||
if err := client.Schema.Create(context.Background()); err != nil {
|
||||
if err := client.Debug().Schema.Create(context.Background()); err != nil {
|
||||
logger.Logger.Fatal("failed creating schema resources: %v", zap.Error(err))
|
||||
}
|
||||
Global = client
|
||||
dbDsn2, err := g.Cfg().Get(gctx.New(), cst.ConfigDatabaseDsn2)
|
||||
utils.OkOrPanic(err)
|
||||
GlobalGorm, err = gorm.Open(postgres.Open(dbDsn2.String()), &gorm.Config{})
|
||||
if err != nil {
|
||||
logger.Logger.Fatal("连接pg数据库失败: %v", zap.Error(err))
|
||||
}
|
||||
// 可选:配置连接池
|
||||
sqlDB, err := GlobalGorm.DB()
|
||||
if err != nil {
|
||||
logger.Logger.Fatal("获取数据库实例失败: %v", zap.Error(err))
|
||||
}
|
||||
|
||||
// 设置连接池参数
|
||||
sqlDB.SetMaxIdleConns(10) // 设置空闲连接池中连接的最大数量
|
||||
sqlDB.SetMaxOpenConns(100) // 设置打开数据库连接的最大数量
|
||||
sqlDB.SetConnMaxLifetime(0) // 设置连接可复用的最大时间(0表示永久)
|
||||
}
|
||||
|
||||
@@ -25,4 +25,7 @@ type IUserInfoV1 interface {
|
||||
InviteUserList(ctx context.Context, req *v1.CommonPageReq) (*utils.Response[[]*v1.UserInviteRes], error)
|
||||
// 生成邀请码
|
||||
GenerateInviteCode(ctx context.Context, req *v1.CommonPostReq) (*utils.Response[any], error)
|
||||
|
||||
RequestBalanceMigrate(ctx context.Context, req *v1.RequestBalanceMigrateReq) (*utils.Response[any], error)
|
||||
BalanceMigrate(ctx context.Context, req *v1.BalanceMigrateReq) (*utils.Response[any], error)
|
||||
}
|
||||
|
||||
@@ -13,4 +13,8 @@ type IUserLoginV1 interface {
|
||||
Login(ctx context.Context, req *v1.UserLoginReq) (*utils.Response[any], error)
|
||||
Register(ctx context.Context, req *v1.UserRegisterReq) (*utils.Response[any], error)
|
||||
Confirm(ctx context.Context, req *v1.UserConfirmReq) (*utils.Response[any], error)
|
||||
ResendVerification(ctx context.Context, req *v1.ResendVerificationReq) (*utils.Response[any], error)
|
||||
ForgotPassword(ctx context.Context, req *v1.ForgotPasswordReq) (*utils.Response[any], error)
|
||||
ResetPassword(ctx context.Context, req *v1.ForgetResetPasswordReq) (*utils.Response[any], error)
|
||||
GetResetPasswordInfo(ctx context.Context, req *v1.ResetPasswordInfoReq) (*utils.Response[any], error)
|
||||
}
|
||||
|
||||
@@ -4,8 +4,12 @@ import (
|
||||
"context"
|
||||
v1 "likedream-goframe/internal/api/v1"
|
||||
"likedream-goframe/internal/ent"
|
||||
"likedream-goframe/internal/framework/captcha"
|
||||
"likedream-goframe/internal/framework/redis"
|
||||
"likedream-goframe/internal/service"
|
||||
"likedream-goframe/internal/utils"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
)
|
||||
|
||||
type UserInfoV1 struct {
|
||||
@@ -60,3 +64,34 @@ func (c *UserInfoV1) GenerateInviteCode(ctx context.Context, req *v1.CommonPostR
|
||||
defer utils.Pass(&err)
|
||||
return service.Global.UserInfo.GenerateInviteCode(ctx, req), nil
|
||||
}
|
||||
func (c *UserInfoV1) RequestBalanceMigrate(ctx context.Context, req *v1.RequestBalanceMigrateReq) (res *utils.Response[any], err error) {
|
||||
defer utils.Pass(&err)
|
||||
client := redis.GetClient()
|
||||
dotsStr := client.GetDel(ctx, req.CaptchaKey).Val()
|
||||
if dotsStr == "" {
|
||||
return utils.Fail(10003, "验证码错误"), nil
|
||||
}
|
||||
var dots captcha.ConfirmData
|
||||
err = sonic.Unmarshal([]byte(dotsStr), &dots)
|
||||
utils.OkOrPanic(err)
|
||||
if !captcha.VerifyCaptcha(dots, req.Dots) {
|
||||
return utils.Fail(10003, "验证码错误"), nil
|
||||
}
|
||||
return service.Global.UserInfo.RequestBalanceMigrate(ctx, req), nil
|
||||
}
|
||||
|
||||
func (c *UserInfoV1) BalanceMigrate(ctx context.Context, req *v1.BalanceMigrateReq) (res *utils.Response[any], err error) {
|
||||
defer utils.Pass(&err)
|
||||
client := redis.GetClient()
|
||||
dotsStr := client.GetDel(ctx, req.CaptchaKey).Val()
|
||||
if dotsStr == "" {
|
||||
return utils.Fail(10003, "验证码错误"), nil
|
||||
}
|
||||
var dots captcha.ConfirmData
|
||||
err = sonic.Unmarshal([]byte(dotsStr), &dots)
|
||||
utils.OkOrPanic(err)
|
||||
if !captcha.VerifyCaptcha(dots, req.Dots) {
|
||||
return utils.Fail(10003, "验证码错误"), nil
|
||||
}
|
||||
return service.Global.UserInfo.BalanceMigrate(ctx, req), nil
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"likedream-goframe/internal/service"
|
||||
"likedream-goframe/internal/utils"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
)
|
||||
|
||||
type UserLoginV1 struct {
|
||||
@@ -39,7 +41,7 @@ func (c *UserLoginV1) Login(ctx context.Context, req *v1.UserLoginReq) (res *uti
|
||||
return utils.Fail(10003, "验证码错误"), nil
|
||||
}
|
||||
var dots captcha.ConfirmData
|
||||
err = json.Unmarshal([]byte(dotsStr), &dots)
|
||||
err = sonic.Unmarshal([]byte(dotsStr), &dots)
|
||||
utils.OkOrPanic(err)
|
||||
if !captcha.VerifyCaptcha(dots, req.Dots) {
|
||||
return utils.Fail(10003, "验证码错误"), nil
|
||||
@@ -49,9 +51,60 @@ func (c *UserLoginV1) Login(ctx context.Context, req *v1.UserLoginReq) (res *uti
|
||||
|
||||
func (c *UserLoginV1) Register(ctx context.Context, req *v1.UserRegisterReq) (res *utils.Response[any], err error) {
|
||||
defer utils.Pass(&err)
|
||||
client := redis.GetClient()
|
||||
dotsStr := client.GetDel(ctx, req.CaptchaKey).Val()
|
||||
if dotsStr == "" {
|
||||
return utils.Fail(10003, "验证码错误"), nil
|
||||
}
|
||||
var dots captcha.ConfirmData
|
||||
err = sonic.Unmarshal([]byte(dotsStr), &dots)
|
||||
utils.OkOrPanic(err)
|
||||
if !captcha.VerifyCaptcha(dots, req.Dots) {
|
||||
return utils.Fail(10003, "验证码错误"), nil
|
||||
}
|
||||
return service.Global.UserLogin.Register(ctx, req), nil
|
||||
}
|
||||
|
||||
func (c *UserLoginV1) Confirm(ctx context.Context, req *v1.UserConfirmReq) (*utils.Response[any], error) {
|
||||
return service.Global.UserLogin.Confirm(ctx, req), nil
|
||||
}
|
||||
|
||||
func (c *UserLoginV1) ResendVerification(ctx context.Context, req *v1.ResendVerificationReq) (res *utils.Response[any], err error) {
|
||||
defer utils.Pass(&err)
|
||||
client := redis.GetClient()
|
||||
dotsStr := client.GetDel(ctx, req.CaptchaKey).Val()
|
||||
if dotsStr == "" {
|
||||
return utils.Fail(10003, "验证码错误"), nil
|
||||
}
|
||||
var dots captcha.ConfirmData
|
||||
err = sonic.Unmarshal([]byte(dotsStr), &dots)
|
||||
utils.OkOrPanic(err)
|
||||
if !captcha.VerifyCaptcha(dots, req.Dots) {
|
||||
return utils.Fail(10003, "验证码错误"), nil
|
||||
}
|
||||
return service.Global.UserLogin.ResendVerification(ctx, req), nil
|
||||
}
|
||||
|
||||
func (c *UserLoginV1) ForgotPassword(ctx context.Context, req *v1.ForgotPasswordReq) (res *utils.Response[any], err error) {
|
||||
defer utils.Pass(&err)
|
||||
client := redis.GetClient()
|
||||
dotsStr := client.GetDel(ctx, req.CaptchaKey).Val()
|
||||
if dotsStr == "" {
|
||||
return utils.Fail(10003, "验证码错误"), nil
|
||||
}
|
||||
var dots captcha.ConfirmData
|
||||
err = sonic.Unmarshal([]byte(dotsStr), &dots)
|
||||
utils.OkOrPanic(err)
|
||||
if !captcha.VerifyCaptcha(dots, req.Dots) {
|
||||
return utils.Fail(10003, "验证码错误"), nil
|
||||
}
|
||||
return service.Global.UserLogin.ForgotPassword(ctx, req), nil
|
||||
}
|
||||
|
||||
func (c *UserLoginV1) ResetPassword(ctx context.Context, req *v1.ForgetResetPasswordReq) (*utils.Response[any], error) {
|
||||
return service.Global.UserLogin.ResetPassword(ctx, req), nil
|
||||
}
|
||||
|
||||
func (c *UserLoginV1) GetResetPasswordInfo(ctx context.Context, req *v1.ResetPasswordInfoReq) (*utils.Response[any], error) {
|
||||
return service.Global.UserLogin.GetResetPasswordInfo(ctx, req), nil
|
||||
}
|
||||
|
||||
+12
-1
@@ -39,6 +39,8 @@ type CpTask struct {
|
||||
SuccessCount int `json:"success_count,omitempty"`
|
||||
// FormData holds the value of the "form_data" field.
|
||||
FormData string `json:"form_data,omitempty"`
|
||||
// UsedImg holds the value of the "used_img" field.
|
||||
UsedImg string `json:"used_img,omitempty"`
|
||||
// Step holds the value of the "step" field.
|
||||
Step cptask.Step `json:"step,omitempty"`
|
||||
// NotificationType holds the value of the "notification_type" field.
|
||||
@@ -109,7 +111,7 @@ func (*CpTask) scanValues(columns []string) ([]any, error) {
|
||||
values[i] = new(ent2.CustomTime)
|
||||
case cptask.FieldID, cptask.FieldCpUserID, cptask.FieldUserID, cptask.FieldFrequency, cptask.FieldSuccessCount, cptask.FieldExecuteHour, cptask.FieldExecuteMinute, cptask.FieldNextExecuteTime:
|
||||
values[i] = new(sql.NullInt64)
|
||||
case cptask.FieldTaskType, cptask.FieldTitle, cptask.FieldTaskStatus, cptask.FieldFormData, cptask.FieldStep, cptask.FieldNotificationType:
|
||||
case cptask.FieldTaskType, cptask.FieldTitle, cptask.FieldTaskStatus, cptask.FieldFormData, cptask.FieldUsedImg, cptask.FieldStep, cptask.FieldNotificationType:
|
||||
values[i] = new(sql.NullString)
|
||||
default:
|
||||
values[i] = new(sql.UnknownType)
|
||||
@@ -192,6 +194,12 @@ func (_m *CpTask) assignValues(columns []string, values []any) error {
|
||||
} else if value.Valid {
|
||||
_m.FormData = value.String
|
||||
}
|
||||
case cptask.FieldUsedImg:
|
||||
if value, ok := values[i].(*sql.NullString); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field used_img", values[i])
|
||||
} else if value.Valid {
|
||||
_m.UsedImg = value.String
|
||||
}
|
||||
case cptask.FieldStep:
|
||||
if value, ok := values[i].(*sql.NullString); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field step", values[i])
|
||||
@@ -306,6 +314,9 @@ func (_m *CpTask) String() string {
|
||||
builder.WriteString("form_data=")
|
||||
builder.WriteString(_m.FormData)
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("used_img=")
|
||||
builder.WriteString(_m.UsedImg)
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("step=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.Step))
|
||||
builder.WriteString(", ")
|
||||
|
||||
@@ -36,6 +36,8 @@ const (
|
||||
FieldSuccessCount = "success_count"
|
||||
// FieldFormData holds the string denoting the form_data field in the database.
|
||||
FieldFormData = "form_data"
|
||||
// FieldUsedImg holds the string denoting the used_img field in the database.
|
||||
FieldUsedImg = "used_img"
|
||||
// FieldStep holds the string denoting the step field in the database.
|
||||
FieldStep = "step"
|
||||
// FieldNotificationType holds the string denoting the notification_type field in the database.
|
||||
@@ -90,6 +92,7 @@ var Columns = []string{
|
||||
FieldTaskStatus,
|
||||
FieldSuccessCount,
|
||||
FieldFormData,
|
||||
FieldUsedImg,
|
||||
FieldStep,
|
||||
FieldNotificationType,
|
||||
FieldExecuteHour,
|
||||
@@ -284,6 +287,11 @@ func ByFormData(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldFormData, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByUsedImg orders the results by the used_img field.
|
||||
func ByUsedImg(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldUsedImg, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByStep orders the results by the step field.
|
||||
func ByStep(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldStep, opts...).ToFunc()
|
||||
|
||||
@@ -96,6 +96,11 @@ func FormData(v string) predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldEQ(FieldFormData, v))
|
||||
}
|
||||
|
||||
// UsedImg applies equality check predicate on the "used_img" field. It's identical to UsedImgEQ.
|
||||
func UsedImg(v string) predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldEQ(FieldUsedImg, v))
|
||||
}
|
||||
|
||||
// ExecuteHour applies equality check predicate on the "execute_hour" field. It's identical to ExecuteHourEQ.
|
||||
func ExecuteHour(v int) predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldEQ(FieldExecuteHour, v))
|
||||
@@ -551,6 +556,81 @@ func FormDataContainsFold(v string) predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldContainsFold(FieldFormData, v))
|
||||
}
|
||||
|
||||
// UsedImgEQ applies the EQ predicate on the "used_img" field.
|
||||
func UsedImgEQ(v string) predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldEQ(FieldUsedImg, v))
|
||||
}
|
||||
|
||||
// UsedImgNEQ applies the NEQ predicate on the "used_img" field.
|
||||
func UsedImgNEQ(v string) predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldNEQ(FieldUsedImg, v))
|
||||
}
|
||||
|
||||
// UsedImgIn applies the In predicate on the "used_img" field.
|
||||
func UsedImgIn(vs ...string) predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldIn(FieldUsedImg, vs...))
|
||||
}
|
||||
|
||||
// UsedImgNotIn applies the NotIn predicate on the "used_img" field.
|
||||
func UsedImgNotIn(vs ...string) predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldNotIn(FieldUsedImg, vs...))
|
||||
}
|
||||
|
||||
// UsedImgGT applies the GT predicate on the "used_img" field.
|
||||
func UsedImgGT(v string) predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldGT(FieldUsedImg, v))
|
||||
}
|
||||
|
||||
// UsedImgGTE applies the GTE predicate on the "used_img" field.
|
||||
func UsedImgGTE(v string) predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldGTE(FieldUsedImg, v))
|
||||
}
|
||||
|
||||
// UsedImgLT applies the LT predicate on the "used_img" field.
|
||||
func UsedImgLT(v string) predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldLT(FieldUsedImg, v))
|
||||
}
|
||||
|
||||
// UsedImgLTE applies the LTE predicate on the "used_img" field.
|
||||
func UsedImgLTE(v string) predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldLTE(FieldUsedImg, v))
|
||||
}
|
||||
|
||||
// UsedImgContains applies the Contains predicate on the "used_img" field.
|
||||
func UsedImgContains(v string) predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldContains(FieldUsedImg, v))
|
||||
}
|
||||
|
||||
// UsedImgHasPrefix applies the HasPrefix predicate on the "used_img" field.
|
||||
func UsedImgHasPrefix(v string) predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldHasPrefix(FieldUsedImg, v))
|
||||
}
|
||||
|
||||
// UsedImgHasSuffix applies the HasSuffix predicate on the "used_img" field.
|
||||
func UsedImgHasSuffix(v string) predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldHasSuffix(FieldUsedImg, v))
|
||||
}
|
||||
|
||||
// UsedImgIsNil applies the IsNil predicate on the "used_img" field.
|
||||
func UsedImgIsNil() predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldIsNull(FieldUsedImg))
|
||||
}
|
||||
|
||||
// UsedImgNotNil applies the NotNil predicate on the "used_img" field.
|
||||
func UsedImgNotNil() predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldNotNull(FieldUsedImg))
|
||||
}
|
||||
|
||||
// UsedImgEqualFold applies the EqualFold predicate on the "used_img" field.
|
||||
func UsedImgEqualFold(v string) predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldEqualFold(FieldUsedImg, v))
|
||||
}
|
||||
|
||||
// UsedImgContainsFold applies the ContainsFold predicate on the "used_img" field.
|
||||
func UsedImgContainsFold(v string) predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldContainsFold(FieldUsedImg, v))
|
||||
}
|
||||
|
||||
// StepEQ applies the EQ predicate on the "step" field.
|
||||
func StepEQ(v Step) predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldEQ(FieldStep, v))
|
||||
|
||||
@@ -109,6 +109,20 @@ func (_c *CpTaskCreate) SetNillableFormData(v *string) *CpTaskCreate {
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetUsedImg sets the "used_img" field.
|
||||
func (_c *CpTaskCreate) SetUsedImg(v string) *CpTaskCreate {
|
||||
_c.mutation.SetUsedImg(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetNillableUsedImg sets the "used_img" field if the given value is not nil.
|
||||
func (_c *CpTaskCreate) SetNillableUsedImg(v *string) *CpTaskCreate {
|
||||
if v != nil {
|
||||
_c.SetUsedImg(*v)
|
||||
}
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetStep sets the "step" field.
|
||||
func (_c *CpTaskCreate) SetStep(v cptask.Step) *CpTaskCreate {
|
||||
_c.mutation.SetStep(v)
|
||||
@@ -382,6 +396,10 @@ func (_c *CpTaskCreate) createSpec() (*CpTask, *sqlgraph.CreateSpec) {
|
||||
_spec.SetField(cptask.FieldFormData, field.TypeString, value)
|
||||
_node.FormData = value
|
||||
}
|
||||
if value, ok := _c.mutation.UsedImg(); ok {
|
||||
_spec.SetField(cptask.FieldUsedImg, field.TypeString, value)
|
||||
_node.UsedImg = value
|
||||
}
|
||||
if value, ok := _c.mutation.Step(); ok {
|
||||
_spec.SetField(cptask.FieldStep, field.TypeEnum, value)
|
||||
_node.Step = value
|
||||
@@ -633,6 +651,24 @@ func (u *CpTaskUpsert) ClearFormData() *CpTaskUpsert {
|
||||
return u
|
||||
}
|
||||
|
||||
// SetUsedImg sets the "used_img" field.
|
||||
func (u *CpTaskUpsert) SetUsedImg(v string) *CpTaskUpsert {
|
||||
u.Set(cptask.FieldUsedImg, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateUsedImg sets the "used_img" field to the value that was provided on create.
|
||||
func (u *CpTaskUpsert) UpdateUsedImg() *CpTaskUpsert {
|
||||
u.SetExcluded(cptask.FieldUsedImg)
|
||||
return u
|
||||
}
|
||||
|
||||
// ClearUsedImg clears the value of the "used_img" field.
|
||||
func (u *CpTaskUpsert) ClearUsedImg() *CpTaskUpsert {
|
||||
u.SetNull(cptask.FieldUsedImg)
|
||||
return u
|
||||
}
|
||||
|
||||
// SetStep sets the "step" field.
|
||||
func (u *CpTaskUpsert) SetStep(v cptask.Step) *CpTaskUpsert {
|
||||
u.Set(cptask.FieldStep, v)
|
||||
@@ -921,6 +957,27 @@ func (u *CpTaskUpsertOne) ClearFormData() *CpTaskUpsertOne {
|
||||
})
|
||||
}
|
||||
|
||||
// SetUsedImg sets the "used_img" field.
|
||||
func (u *CpTaskUpsertOne) SetUsedImg(v string) *CpTaskUpsertOne {
|
||||
return u.Update(func(s *CpTaskUpsert) {
|
||||
s.SetUsedImg(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateUsedImg sets the "used_img" field to the value that was provided on create.
|
||||
func (u *CpTaskUpsertOne) UpdateUsedImg() *CpTaskUpsertOne {
|
||||
return u.Update(func(s *CpTaskUpsert) {
|
||||
s.UpdateUsedImg()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearUsedImg clears the value of the "used_img" field.
|
||||
func (u *CpTaskUpsertOne) ClearUsedImg() *CpTaskUpsertOne {
|
||||
return u.Update(func(s *CpTaskUpsert) {
|
||||
s.ClearUsedImg()
|
||||
})
|
||||
}
|
||||
|
||||
// SetStep sets the "step" field.
|
||||
func (u *CpTaskUpsertOne) SetStep(v cptask.Step) *CpTaskUpsertOne {
|
||||
return u.Update(func(s *CpTaskUpsert) {
|
||||
@@ -1391,6 +1448,27 @@ func (u *CpTaskUpsertBulk) ClearFormData() *CpTaskUpsertBulk {
|
||||
})
|
||||
}
|
||||
|
||||
// SetUsedImg sets the "used_img" field.
|
||||
func (u *CpTaskUpsertBulk) SetUsedImg(v string) *CpTaskUpsertBulk {
|
||||
return u.Update(func(s *CpTaskUpsert) {
|
||||
s.SetUsedImg(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateUsedImg sets the "used_img" field to the value that was provided on create.
|
||||
func (u *CpTaskUpsertBulk) UpdateUsedImg() *CpTaskUpsertBulk {
|
||||
return u.Update(func(s *CpTaskUpsert) {
|
||||
s.UpdateUsedImg()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearUsedImg clears the value of the "used_img" field.
|
||||
func (u *CpTaskUpsertBulk) ClearUsedImg() *CpTaskUpsertBulk {
|
||||
return u.Update(func(s *CpTaskUpsert) {
|
||||
s.ClearUsedImg()
|
||||
})
|
||||
}
|
||||
|
||||
// SetStep sets the "step" field.
|
||||
func (u *CpTaskUpsertBulk) SetStep(v cptask.Step) *CpTaskUpsertBulk {
|
||||
return u.Update(func(s *CpTaskUpsert) {
|
||||
|
||||
@@ -171,6 +171,26 @@ func (_u *CpTaskUpdate) ClearFormData() *CpTaskUpdate {
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetUsedImg sets the "used_img" field.
|
||||
func (_u *CpTaskUpdate) SetUsedImg(v string) *CpTaskUpdate {
|
||||
_u.mutation.SetUsedImg(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableUsedImg sets the "used_img" field if the given value is not nil.
|
||||
func (_u *CpTaskUpdate) SetNillableUsedImg(v *string) *CpTaskUpdate {
|
||||
if v != nil {
|
||||
_u.SetUsedImg(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearUsedImg clears the value of the "used_img" field.
|
||||
func (_u *CpTaskUpdate) ClearUsedImg() *CpTaskUpdate {
|
||||
_u.mutation.ClearUsedImg()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetStep sets the "step" field.
|
||||
func (_u *CpTaskUpdate) SetStep(v cptask.Step) *CpTaskUpdate {
|
||||
_u.mutation.SetStep(v)
|
||||
@@ -460,6 +480,12 @@ func (_u *CpTaskUpdate) sqlSave(ctx context.Context) (_node int, err error) {
|
||||
if _u.mutation.FormDataCleared() {
|
||||
_spec.ClearField(cptask.FieldFormData, field.TypeString)
|
||||
}
|
||||
if value, ok := _u.mutation.UsedImg(); ok {
|
||||
_spec.SetField(cptask.FieldUsedImg, field.TypeString, value)
|
||||
}
|
||||
if _u.mutation.UsedImgCleared() {
|
||||
_spec.ClearField(cptask.FieldUsedImg, field.TypeString)
|
||||
}
|
||||
if value, ok := _u.mutation.Step(); ok {
|
||||
_spec.SetField(cptask.FieldStep, field.TypeEnum, value)
|
||||
}
|
||||
@@ -765,6 +791,26 @@ func (_u *CpTaskUpdateOne) ClearFormData() *CpTaskUpdateOne {
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetUsedImg sets the "used_img" field.
|
||||
func (_u *CpTaskUpdateOne) SetUsedImg(v string) *CpTaskUpdateOne {
|
||||
_u.mutation.SetUsedImg(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableUsedImg sets the "used_img" field if the given value is not nil.
|
||||
func (_u *CpTaskUpdateOne) SetNillableUsedImg(v *string) *CpTaskUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetUsedImg(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearUsedImg clears the value of the "used_img" field.
|
||||
func (_u *CpTaskUpdateOne) ClearUsedImg() *CpTaskUpdateOne {
|
||||
_u.mutation.ClearUsedImg()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetStep sets the "step" field.
|
||||
func (_u *CpTaskUpdateOne) SetStep(v cptask.Step) *CpTaskUpdateOne {
|
||||
_u.mutation.SetStep(v)
|
||||
@@ -1084,6 +1130,12 @@ func (_u *CpTaskUpdateOne) sqlSave(ctx context.Context) (_node *CpTask, err erro
|
||||
if _u.mutation.FormDataCleared() {
|
||||
_spec.ClearField(cptask.FieldFormData, field.TypeString)
|
||||
}
|
||||
if value, ok := _u.mutation.UsedImg(); ok {
|
||||
_spec.SetField(cptask.FieldUsedImg, field.TypeString, value)
|
||||
}
|
||||
if _u.mutation.UsedImgCleared() {
|
||||
_spec.ClearField(cptask.FieldUsedImg, field.TypeString)
|
||||
}
|
||||
if value, ok := _u.mutation.Step(); ok {
|
||||
_spec.SetField(cptask.FieldStep, field.TypeEnum, value)
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ var schemaGraph = func() *sqlgraph.Schema {
|
||||
cptask.FieldTaskStatus: {Type: field.TypeEnum, Column: cptask.FieldTaskStatus},
|
||||
cptask.FieldSuccessCount: {Type: field.TypeInt, Column: cptask.FieldSuccessCount},
|
||||
cptask.FieldFormData: {Type: field.TypeString, Column: cptask.FieldFormData},
|
||||
cptask.FieldUsedImg: {Type: field.TypeString, Column: cptask.FieldUsedImg},
|
||||
cptask.FieldStep: {Type: field.TypeEnum, Column: cptask.FieldStep},
|
||||
cptask.FieldNotificationType: {Type: field.TypeEnum, Column: cptask.FieldNotificationType},
|
||||
cptask.FieldExecuteHour: {Type: field.TypeInt, Column: cptask.FieldExecuteHour},
|
||||
@@ -905,6 +906,11 @@ func (f *CpTaskFilter) WhereFormData(p entql.StringP) {
|
||||
f.Where(p.Field(cptask.FieldFormData))
|
||||
}
|
||||
|
||||
// WhereUsedImg applies the entql string predicate on the used_img field.
|
||||
func (f *CpTaskFilter) WhereUsedImg(p entql.StringP) {
|
||||
f.Where(p.Field(cptask.FieldUsedImg))
|
||||
}
|
||||
|
||||
// WhereStep applies the entql string predicate on the step field.
|
||||
func (f *CpTaskFilter) WhereStep(p entql.StringP) {
|
||||
f.Where(p.Field(cptask.FieldStep))
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
// ENT数据库文件,请勿编辑。
|
||||
|
||||
package internal
|
||||
|
||||
const IncrementStarts = "{\"cp_image\":0,\"cp_schools\":4294967296,\"cp_task_logs\":12884901888,\"cp_tasks\":8589934592,\"cp_users\":17179869184,\"sys_agent\":21474836480,\"sys_card\":25769803776,\"sys_menu\":30064771072,\"sys_role\":34359738368,\"sys_user\":38654705664,\"sys_user_logs\":42949672960}"
|
||||
File diff suppressed because one or more lines are too long
@@ -60,6 +60,7 @@ var (
|
||||
{Name: "task_status", Type: field.TypeEnum, Enums: []string{"not_start", "doing", "done", "fail", "pause", "queue", "unknown"}},
|
||||
{Name: "success_count", Type: field.TypeInt, Default: 0},
|
||||
{Name: "form_data", Type: field.TypeString, Nullable: true, Size: 2147483647},
|
||||
{Name: "used_img", Type: field.TypeString, Nullable: true},
|
||||
{Name: "step", Type: field.TypeEnum, Enums: []string{"form", "execute", "complete"}},
|
||||
{Name: "notification_type", Type: field.TypeEnum, Enums: []string{"none", "cp_user_email", "system_user_email"}},
|
||||
{Name: "execute_hour", Type: field.TypeInt, Nullable: true},
|
||||
@@ -76,13 +77,13 @@ var (
|
||||
ForeignKeys: []*schema.ForeignKey{
|
||||
{
|
||||
Symbol: "cp_tasks_cp_users_cptasks",
|
||||
Columns: []*schema.Column{CpTasksColumns[14]},
|
||||
Columns: []*schema.Column{CpTasksColumns[15]},
|
||||
RefColumns: []*schema.Column{CpUsersColumns[0]},
|
||||
OnDelete: schema.NoAction,
|
||||
},
|
||||
{
|
||||
Symbol: "cp_tasks_sys_user_cp_task",
|
||||
Columns: []*schema.Column{CpTasksColumns[15]},
|
||||
Columns: []*schema.Column{CpTasksColumns[16]},
|
||||
RefColumns: []*schema.Column{SysUserColumns[0]},
|
||||
OnDelete: schema.Cascade,
|
||||
},
|
||||
@@ -380,56 +381,35 @@ var (
|
||||
func init() {
|
||||
CpImageTable.ForeignKeys[0].RefTable = SysUserTable
|
||||
CpImageTable.Annotation = &entsql.Annotation{
|
||||
Table: "cp_image",
|
||||
IncrementStart: func(i int) *int { return &i }(0),
|
||||
}
|
||||
CpSchoolsTable.Annotation = &entsql.Annotation{
|
||||
IncrementStart: func(i int) *int { return &i }(4294967296),
|
||||
Table: "cp_image",
|
||||
}
|
||||
CpTasksTable.ForeignKeys[0].RefTable = CpUsersTable
|
||||
CpTasksTable.ForeignKeys[1].RefTable = SysUserTable
|
||||
CpTasksTable.Annotation = &entsql.Annotation{
|
||||
IncrementStart: func(i int) *int { return &i }(8589934592),
|
||||
}
|
||||
CpTaskLogsTable.ForeignKeys[0].RefTable = CpTasksTable
|
||||
CpTaskLogsTable.ForeignKeys[1].RefTable = CpUsersTable
|
||||
CpTaskLogsTable.ForeignKeys[2].RefTable = SysUserTable
|
||||
CpTaskLogsTable.Annotation = &entsql.Annotation{
|
||||
IncrementStart: func(i int) *int { return &i }(12884901888),
|
||||
}
|
||||
CpUsersTable.ForeignKeys[0].RefTable = CpSchoolsTable
|
||||
CpUsersTable.ForeignKeys[1].RefTable = SysUserTable
|
||||
CpUsersTable.Annotation = &entsql.Annotation{
|
||||
IncrementStart: func(i int) *int { return &i }(17179869184),
|
||||
}
|
||||
SysAgentTable.ForeignKeys[0].RefTable = SysUserTable
|
||||
SysAgentTable.Annotation = &entsql.Annotation{
|
||||
Table: "sys_agent",
|
||||
IncrementStart: func(i int) *int { return &i }(21474836480),
|
||||
Table: "sys_agent",
|
||||
}
|
||||
SysCardTable.ForeignKeys[0].RefTable = SysUserTable
|
||||
SysCardTable.ForeignKeys[1].RefTable = SysUserTable
|
||||
SysCardTable.ForeignKeys[2].RefTable = SysUserTable
|
||||
SysCardTable.Annotation = &entsql.Annotation{
|
||||
Table: "sys_card",
|
||||
IncrementStart: func(i int) *int { return &i }(25769803776),
|
||||
Table: "sys_card",
|
||||
}
|
||||
SysMenuTable.ForeignKeys[0].RefTable = SysMenuTable
|
||||
SysMenuTable.Annotation = &entsql.Annotation{
|
||||
Table: "sys_menu",
|
||||
IncrementStart: func(i int) *int { return &i }(30064771072),
|
||||
Table: "sys_menu",
|
||||
}
|
||||
SysRoleTable.Annotation = &entsql.Annotation{
|
||||
Table: "sys_role",
|
||||
IncrementStart: func(i int) *int { return &i }(34359738368),
|
||||
Table: "sys_role",
|
||||
}
|
||||
SysUserTable.ForeignKeys[0].RefTable = SysUserTable
|
||||
SysUserTable.Annotation = &entsql.Annotation{
|
||||
Table: "sys_user",
|
||||
IncrementStart: func(i int) *int { return &i }(38654705664),
|
||||
}
|
||||
SysUserLogsTable.Annotation = &entsql.Annotation{
|
||||
IncrementStart: func(i int) *int { return &i }(42949672960),
|
||||
Table: "sys_user",
|
||||
}
|
||||
SysRoleMenusTable.ForeignKeys[0].RefTable = SysRoleTable
|
||||
SysRoleMenusTable.ForeignKeys[1].RefTable = SysMenuTable
|
||||
|
||||
@@ -1467,6 +1467,7 @@ type CpTaskMutation struct {
|
||||
success_count *int
|
||||
addsuccess_count *int
|
||||
form_data *string
|
||||
used_img *string
|
||||
step *cptask.Step
|
||||
notification_type *cptask.NotificationType
|
||||
execute_hour *int
|
||||
@@ -1999,6 +2000,55 @@ func (m *CpTaskMutation) ResetFormData() {
|
||||
delete(m.clearedFields, cptask.FieldFormData)
|
||||
}
|
||||
|
||||
// SetUsedImg sets the "used_img" field.
|
||||
func (m *CpTaskMutation) SetUsedImg(s string) {
|
||||
m.used_img = &s
|
||||
}
|
||||
|
||||
// UsedImg returns the value of the "used_img" field in the mutation.
|
||||
func (m *CpTaskMutation) UsedImg() (r string, exists bool) {
|
||||
v := m.used_img
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// OldUsedImg returns the old "used_img" field's value of the CpTask entity.
|
||||
// If the CpTask object wasn't provided to the builder, the object is fetched from the database.
|
||||
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
|
||||
func (m *CpTaskMutation) OldUsedImg(ctx context.Context) (v string, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldUsedImg is only allowed on UpdateOne operations")
|
||||
}
|
||||
if m.id == nil || m.oldValue == nil {
|
||||
return v, errors.New("OldUsedImg requires an ID field in the mutation")
|
||||
}
|
||||
oldValue, err := m.oldValue(ctx)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("querying old value for OldUsedImg: %w", err)
|
||||
}
|
||||
return oldValue.UsedImg, nil
|
||||
}
|
||||
|
||||
// ClearUsedImg clears the value of the "used_img" field.
|
||||
func (m *CpTaskMutation) ClearUsedImg() {
|
||||
m.used_img = nil
|
||||
m.clearedFields[cptask.FieldUsedImg] = struct{}{}
|
||||
}
|
||||
|
||||
// UsedImgCleared returns if the "used_img" field was cleared in this mutation.
|
||||
func (m *CpTaskMutation) UsedImgCleared() bool {
|
||||
_, ok := m.clearedFields[cptask.FieldUsedImg]
|
||||
return ok
|
||||
}
|
||||
|
||||
// ResetUsedImg resets all changes to the "used_img" field.
|
||||
func (m *CpTaskMutation) ResetUsedImg() {
|
||||
m.used_img = nil
|
||||
delete(m.clearedFields, cptask.FieldUsedImg)
|
||||
}
|
||||
|
||||
// SetStep sets the "step" field.
|
||||
func (m *CpTaskMutation) SetStep(c cptask.Step) {
|
||||
m.step = &c
|
||||
@@ -2436,7 +2486,7 @@ func (m *CpTaskMutation) Type() string {
|
||||
// order to get all numeric fields that were incremented/decremented, call
|
||||
// AddedFields().
|
||||
func (m *CpTaskMutation) Fields() []string {
|
||||
fields := make([]string, 0, 15)
|
||||
fields := make([]string, 0, 16)
|
||||
if m.create_time != nil {
|
||||
fields = append(fields, cptask.FieldCreateTime)
|
||||
}
|
||||
@@ -2467,6 +2517,9 @@ func (m *CpTaskMutation) Fields() []string {
|
||||
if m.form_data != nil {
|
||||
fields = append(fields, cptask.FieldFormData)
|
||||
}
|
||||
if m.used_img != nil {
|
||||
fields = append(fields, cptask.FieldUsedImg)
|
||||
}
|
||||
if m.step != nil {
|
||||
fields = append(fields, cptask.FieldStep)
|
||||
}
|
||||
@@ -2510,6 +2563,8 @@ func (m *CpTaskMutation) Field(name string) (ent.Value, bool) {
|
||||
return m.SuccessCount()
|
||||
case cptask.FieldFormData:
|
||||
return m.FormData()
|
||||
case cptask.FieldUsedImg:
|
||||
return m.UsedImg()
|
||||
case cptask.FieldStep:
|
||||
return m.Step()
|
||||
case cptask.FieldNotificationType:
|
||||
@@ -2549,6 +2604,8 @@ func (m *CpTaskMutation) OldField(ctx context.Context, name string) (ent.Value,
|
||||
return m.OldSuccessCount(ctx)
|
||||
case cptask.FieldFormData:
|
||||
return m.OldFormData(ctx)
|
||||
case cptask.FieldUsedImg:
|
||||
return m.OldUsedImg(ctx)
|
||||
case cptask.FieldStep:
|
||||
return m.OldStep(ctx)
|
||||
case cptask.FieldNotificationType:
|
||||
@@ -2638,6 +2695,13 @@ func (m *CpTaskMutation) SetField(name string, value ent.Value) error {
|
||||
}
|
||||
m.SetFormData(v)
|
||||
return nil
|
||||
case cptask.FieldUsedImg:
|
||||
v, ok := value.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.SetUsedImg(v)
|
||||
return nil
|
||||
case cptask.FieldStep:
|
||||
v, ok := value.(cptask.Step)
|
||||
if !ok {
|
||||
@@ -2769,6 +2833,9 @@ func (m *CpTaskMutation) ClearedFields() []string {
|
||||
if m.FieldCleared(cptask.FieldFormData) {
|
||||
fields = append(fields, cptask.FieldFormData)
|
||||
}
|
||||
if m.FieldCleared(cptask.FieldUsedImg) {
|
||||
fields = append(fields, cptask.FieldUsedImg)
|
||||
}
|
||||
if m.FieldCleared(cptask.FieldExecuteHour) {
|
||||
fields = append(fields, cptask.FieldExecuteHour)
|
||||
}
|
||||
@@ -2795,6 +2862,9 @@ func (m *CpTaskMutation) ClearField(name string) error {
|
||||
case cptask.FieldFormData:
|
||||
m.ClearFormData()
|
||||
return nil
|
||||
case cptask.FieldUsedImg:
|
||||
m.ClearUsedImg()
|
||||
return nil
|
||||
case cptask.FieldExecuteHour:
|
||||
m.ClearExecuteHour()
|
||||
return nil
|
||||
@@ -2842,6 +2912,9 @@ func (m *CpTaskMutation) ResetField(name string) error {
|
||||
case cptask.FieldFormData:
|
||||
m.ResetFormData()
|
||||
return nil
|
||||
case cptask.FieldUsedImg:
|
||||
m.ResetUsedImg()
|
||||
return nil
|
||||
case cptask.FieldStep:
|
||||
m.ResetStep()
|
||||
return nil
|
||||
|
||||
@@ -25,6 +25,7 @@ func (CpTask) Fields() []ent.Field {
|
||||
field.Enum("task_status").Values("not_start", "doing", "done", "fail", "pause", "queue", "unknown"),
|
||||
field.Int("success_count").Default(0),
|
||||
field.Text("form_data").Optional(),
|
||||
field.String("used_img").Optional(),
|
||||
field.Enum("step").Values("form", "execute", "complete"),
|
||||
field.Enum("notification_type").Values("none", "cp_user_email", "system_user_email"),
|
||||
field.Int("execute_hour").Optional().Nillable(),
|
||||
|
||||
@@ -18,6 +18,8 @@ import (
|
||||
"likedream-goframe/internal/framework/model/email"
|
||||
email2 "likedream-goframe/internal/service/email"
|
||||
"likedream-goframe/internal/utils"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
@@ -90,16 +92,17 @@ func (c *CpdailyTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task)
|
||||
|
||||
// 查询学校详情
|
||||
schoolDetail := school.GetSchoolDetail(ctx, targetTask.Edges.CpUser.SchoolID)
|
||||
restyClient := login.GetClient(ctx, targetTask.Edges.CpUser.Cookie)
|
||||
cookie := targetTask.Edges.CpUser.Cookie
|
||||
restyClient := login.GetClient(ctx, cookie)
|
||||
if !login.CookieEnabled(ctx, schoolDetail.WecloudUrl, restyClient) {
|
||||
loginExecutor := login.GetCpdailyLoginBySchoolInfo(ctx, &schoolDetail)
|
||||
loginExecutor.Login(ctx, &schoolDetail, targetTask.Edges.CpUser.Username, targetTask.Edges.CpUser.Password)
|
||||
cookie := loginExecutor.GetCookieString()
|
||||
db.Global.CpUser.UpdateOne(targetTask.Edges.CpUser).SetCookie(cookie).ExecX(ctx)
|
||||
cookie = loginExecutor.GetCookieString()
|
||||
db.Global.CpUser.UpdateOneID(targetTask.Edges.CpUser.ID).SetCookie(cookie).ExecX(ctx)
|
||||
restyClient = login.GetClient(ctx, cookie)
|
||||
}
|
||||
// 获取任务执行handler
|
||||
taskExecutor := task2.GetCpdailyTaskService(ctx, targetTask.TaskType, targetTask.Edges.CpUser.Cookie, targetTask.Edges.CpUser.SchoolID)
|
||||
taskExecutor := task2.GetCpdailyTaskService(ctx, targetTask.TaskType, restyClient, schoolDetail)
|
||||
|
||||
cpdailyRealTask := taskExecutor.QueryFormByTitle(ctx, targetTask.Title)
|
||||
|
||||
@@ -130,13 +133,14 @@ func (c *CpdailyTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task)
|
||||
default:
|
||||
utils.OkOrPanic(errors.NewBusinessErrorWithDetail(503, "不支持的设备类型", targetTask.Edges.CpUser.MobileType))
|
||||
}
|
||||
|
||||
submitForm := taskExecutor.FillCustomForm(ctx, cpdailyRealTask, customFormItems, deviceID, targetTask.Edges.CpUser.Username)
|
||||
// 固定获取图片id
|
||||
imgId := GetNextImgId(customFormItems, targetTask.UsedImg)
|
||||
submitForm := taskExecutor.FillCustomForm(ctx, cpdailyRealTask, customFormItems, deviceID, imgId)
|
||||
taskExecutor.Submit(ctx, submitForm, targetTask.Edges.CpUser.Username, deviceID)
|
||||
|
||||
// 任务完成后重置状态为 TaskStatusDone,更新下次执行时间 以便下次继续执行
|
||||
executeTime := utils.GetCpdailyNextExecuteTime(targetTask)
|
||||
db.Global.CpTask.UpdateOneID(taskPayload.CpTaskID).SetTaskStatus(cptask.TaskStatusDone).SetNextExecuteTime(executeTime).AddSuccessCount(1).ExecX(ctx)
|
||||
db.Global.CpTask.UpdateOneID(taskPayload.CpTaskID).SetTaskStatus(cptask.TaskStatusDone).SetUsedImg(imgId).SetNextExecuteTime(executeTime).AddSuccessCount(1).ExecX(ctx)
|
||||
|
||||
// 创建日志
|
||||
db.Global.CpTaskLog.Create().
|
||||
@@ -156,6 +160,26 @@ func (c *CpdailyTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task)
|
||||
|
||||
return
|
||||
}
|
||||
func GetNextImgId(forms []*formtype.CpTaskFormItem, imgId string) string {
|
||||
var allImgIds []string
|
||||
for _, item := range forms {
|
||||
if item.ColName == "signImg" {
|
||||
allImgIds = strings.Split(item.Content, ",")
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(allImgIds) == 0 {
|
||||
return ""
|
||||
}
|
||||
if imgId == "" {
|
||||
return allImgIds[0]
|
||||
}
|
||||
index := slices.Index(allImgIds, imgId)
|
||||
if index == -1 || index == len(allImgIds)-1 {
|
||||
return allImgIds[0]
|
||||
}
|
||||
return allImgIds[index+1]
|
||||
}
|
||||
|
||||
// sendTaskNotificationEmail 发送任务通知邮件
|
||||
func (c *CpdailyTaskHandler) sendTaskNotificationEmail(ctx context.Context, task *ent.CpTask, status email.TaskStatus, details map[string]string) {
|
||||
|
||||
@@ -4,12 +4,14 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
"likedream-goframe/internal/framework/model/email"
|
||||
"likedream-goframe/internal/utils"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/samber/oops"
|
||||
"github.com/wneessen/go-mail"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type EmailTaskHandler struct {
|
||||
@@ -43,6 +45,6 @@ func (h *EmailTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task) (e
|
||||
|
||||
e = client.DialAndSendWithContext(ctx, message)
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||
|
||||
logger.Logger.Info("邮件发送完成", zap.String("邮箱", payload.To))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/hibiken/asynq"
|
||||
@@ -79,7 +80,6 @@ func (ts *TaskScanner) StopScan() {
|
||||
case <-ts.stopSign:
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *TaskScanner) scanCpdailyTask() {
|
||||
|
||||
currentTime := time.Now().In(cst2.TimeZoneShangHai)
|
||||
@@ -88,7 +88,15 @@ func (ts *TaskScanner) scanCpdailyTask() {
|
||||
minExecuteTime := currentTime.Add(-30 * time.Minute).Unix()
|
||||
|
||||
allTasks := db.Global.CpTask.Query().
|
||||
Where(cptask.NextExecuteTimeLTE(currentTime.Unix()), cptask.NextExecuteTimeNotNil(), cptask.NextExecuteTimeGT(0), cptask.StepEQ(cptask.StepComplete), cptask.TaskStatusEQ(cptask.TaskStatusNotStart)).
|
||||
Where(
|
||||
cptask.NextExecuteTimeLTE(currentTime.Unix()),
|
||||
cptask.NextExecuteTimeNotNil(),
|
||||
cptask.NextExecuteTimeGT(0),
|
||||
cptask.StepEQ(cptask.StepComplete),
|
||||
cptask.TaskStatusEQ(cptask.TaskStatusNotStart),
|
||||
func(selector *sql.Selector) {
|
||||
selector.Where(sql.ExprP("success_count < frequency"))
|
||||
}).
|
||||
Select(cptask.FieldID, cptask.FieldExecuteHour, cptask.FieldExecuteMinute, cptask.FieldNextExecuteTime).
|
||||
AllX(ts.ctx)
|
||||
|
||||
@@ -117,7 +125,7 @@ func (ts *TaskScanner) scanCpdailyTask() {
|
||||
e = utils.Catch(func() {
|
||||
queue.AddTask(ts.ctx, asynq.NewTask(cst2.TaskCpdaily, payloadWriter.Bytes()),
|
||||
asynq.TaskID(fmt.Sprintf(cst2.TaskCpdailyId, task.ID)),
|
||||
asynq.MaxRetry(3),
|
||||
asynq.MaxRetry(4),
|
||||
asynq.Unique(10*time.Second),
|
||||
asynq.ProcessAt(execTime),
|
||||
asynq.Deadline(execTime.Add(12*time.Minute)),
|
||||
|
||||
@@ -2,9 +2,9 @@ package schedule
|
||||
|
||||
import (
|
||||
"context"
|
||||
"likedream-goframe/internal/utils"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
|
||||
"github.com/samber/oops"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// StopTask 让任务队列停止执行某任务
|
||||
@@ -17,5 +17,8 @@ func StopTask(ctx context.Context, queue, taskID string) {
|
||||
queue = "default"
|
||||
}
|
||||
e := globalInspector.DeleteTask(queue, taskID)
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).With("queue", queue, "taskID", taskID).Wrap)
|
||||
if e != nil {
|
||||
logger.Logger.Error("删除定时任务失败", zap.Error(e))
|
||||
}
|
||||
//utils.OkOrPanic(e, oops.WithContext(ctx).With("queue", queue, "taskID", taskID).Wrap)
|
||||
}
|
||||
|
||||
@@ -18,17 +18,17 @@ func ExceptionHandler(r *ghttp.Request) {
|
||||
if r.Response.Status == 404 {
|
||||
return
|
||||
}
|
||||
if r.Response.BufferLength() > 0 {
|
||||
return
|
||||
}
|
||||
if r.Response.Header().Get("Content-Type") == "image/png" {
|
||||
return
|
||||
}
|
||||
var (
|
||||
err = r.GetError()
|
||||
res = r.GetHandlerResponse()
|
||||
code = gerror.Code(err)
|
||||
)
|
||||
if err == nil && r.Response.BufferLength() > 0 {
|
||||
return
|
||||
}
|
||||
if r.Response.Header().Get("Content-Type") == "image/png" {
|
||||
return
|
||||
}
|
||||
|
||||
// 如果没有异常,则直接返回
|
||||
if err == nil && res != nil {
|
||||
@@ -49,6 +49,10 @@ func ExceptionHandler(r *ghttp.Request) {
|
||||
tmpErr := errors.CodeParam
|
||||
tmpErr.SetMessage((err.(*ent.ConstraintError)).Error())
|
||||
code = tmpErr.Code()
|
||||
} else if ent.IsNotFound(err) {
|
||||
tmpErr := errors.CodeNotFound
|
||||
tmpErr.SetMessage("数据未找到")
|
||||
code = tmpErr.Code()
|
||||
} else if code == gcode.CodeNil {
|
||||
code = errors.CodeInternal.Code()
|
||||
} else if code == gcode.CodeValidationFailed {
|
||||
|
||||
@@ -16,8 +16,8 @@ var BalanceService = &BalanceServiceV1{}
|
||||
type BalanceServiceV1 struct {
|
||||
}
|
||||
|
||||
func (v *BalanceServiceV1) ChangeUserBalance(ctx context.Context, userId int64, balance decimal.Decimal) error {
|
||||
return db.WithTx(ctx, func(c context.Context, tx *ent.Tx) {
|
||||
func (v *BalanceServiceV1) ChangeUserBalance(ctx context.Context, userId int64, balance decimal.Decimal) {
|
||||
err := db.WithTx(ctx, func(c context.Context, tx *ent.Tx) {
|
||||
// 使用 SELECT ... FOR UPDATE 锁定用户记录
|
||||
user, err := tx.SysUser.Query().
|
||||
Where(sysuser.ID(userId)).
|
||||
@@ -39,5 +39,6 @@ func (v *BalanceServiceV1) ChangeUserBalance(ctx context.Context, userId int64,
|
||||
ExecX(ctx)
|
||||
tx.Commit()
|
||||
}, nil)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"time"
|
||||
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/samber/oops"
|
||||
@@ -50,7 +51,7 @@ func (v CpTaskServiceV1) GetRecentTasks(ctx context.Context, req *v1.GetRecentTa
|
||||
db2.Global.CpUser.UpdateOne(cpUser).SetCookie(cookie).ExecX(ctx)
|
||||
client = login.GetClient(ctx, cookie)
|
||||
}
|
||||
taskService := task.GetCpdailyTaskService(ctx, req.Type, cpUser.Cookie, cpUser.SchoolID)
|
||||
taskService := task.GetCpdailyTaskService(ctx, req.Type, client, schoolDetail)
|
||||
titleList := taskService.GetRecentTaskTitle(ctx)
|
||||
return utils.SuccessWithData[[]string](titleList)
|
||||
}
|
||||
@@ -111,20 +112,21 @@ func (v CpTaskServiceV1) TaskFormData(ctx context.Context, req *v1.GetCpTaskForm
|
||||
}
|
||||
if cpTask.FormData != "" {
|
||||
var formData []*formtype.CpTaskFormItem
|
||||
err := json.Unmarshal([]byte(cpTask.FormData), &formData)
|
||||
err := sonic.Unmarshal([]byte(cpTask.FormData), &formData)
|
||||
utils.OkOrPanic(err)
|
||||
return utils.SuccessWithData[any](formData)
|
||||
}
|
||||
schoolDetail := school.GetSchoolDetail(ctx, cpTask.Edges.CpUser.SchoolID)
|
||||
client := login.GetClient(ctx, cpUser.Cookie)
|
||||
cookie := cpUser.Cookie
|
||||
client := login.GetClient(ctx, cookie)
|
||||
if !login.CookieEnabled(ctx, schoolDetail.WecloudUrl, client) {
|
||||
loginExecutor := login.GetCpdailyLoginBySchoolInfo(ctx, &schoolDetail)
|
||||
loginExecutor.Login(ctx, &schoolDetail, cpUser.Username, cpUser.Password)
|
||||
cpUser.Cookie = loginExecutor.GetCookieString()
|
||||
db2.Global.CpUser.UpdateOne(cpUser).SetCookie(cpUser.Cookie).ExecX(ctx)
|
||||
client = login.GetClient(ctx, cpUser.Cookie)
|
||||
cookie = loginExecutor.GetCookieString()
|
||||
db2.Global.CpUser.UpdateOneID(cpUser.ID).SetCookie(cookie).ExecX(ctx)
|
||||
client = login.GetClient(ctx, cookie)
|
||||
}
|
||||
taskService := task.GetCpdailyTaskService(ctx, cpTask.TaskType, cpUser.Cookie, cpUser.SchoolID)
|
||||
taskService := task.GetCpdailyTaskService(ctx, cpTask.TaskType, client, schoolDetail)
|
||||
formMap := taskService.QueryFormByTitle(ctx, cpTask.Title)
|
||||
formDetail := taskService.ParseFormJson(ctx, formMap)
|
||||
return utils.SuccessWithData[any](formDetail)
|
||||
@@ -178,9 +180,6 @@ func (v CpTaskServiceV1) PostExecute(ctx context.Context, req *v1.PostCpTaskExec
|
||||
utils.OkOrPanic(errors2.NewBusinessError(400, "执行次数应不低于原有次数"))
|
||||
}
|
||||
|
||||
// 记录原始任务状态,用于判断是否需要立即扫描
|
||||
originalStatus := cpTask.TaskStatus
|
||||
|
||||
//扣积分
|
||||
basePoint := decimal.NewFromInt(cst2.BasePointCpdaily)
|
||||
if user.Edges.Agent != nil {
|
||||
@@ -190,35 +189,20 @@ func (v CpTaskServiceV1) PostExecute(ctx context.Context, req *v1.PostCpTaskExec
|
||||
balance.BalanceService.ChangeUserBalance(ctx, user.ID, needPoint.Neg())
|
||||
// 更新任务
|
||||
e := db2.WithTx(ctx, func(ctx context.Context, tx *ent.Tx) {
|
||||
cpTask := db2.Global.CpTask.UpdateOne(cpTask).
|
||||
update := db2.Global.CpTask.UpdateOne(cpTask).
|
||||
SetStep(cptask.StepComplete).
|
||||
SetFrequency(req.Frequency).
|
||||
SetTaskStatus(cptask.TaskStatusNotStart).
|
||||
SetNotificationType(req.NotificationType).
|
||||
SetExecuteHour(req.ExecuteHour).
|
||||
SetExecuteMinute(req.ExecuteMinute).
|
||||
SaveX(ctx)
|
||||
|
||||
// 计算下次执行时间
|
||||
var nextExecuteAt int64
|
||||
|
||||
// 只有在原始状态为未开始时,才考虑5分钟窗口立即扫描
|
||||
if originalStatus == cptask.TaskStatusNotStart {
|
||||
currentTime := time.Now().In(cst2.TimeZoneShangHai)
|
||||
executeTime := time.Date(currentTime.Year(), currentTime.Month(), currentTime.Day(), req.ExecuteHour, req.ExecuteMinute, 0, 0, cst2.TimeZoneShangHai)
|
||||
timeDiff := executeTime.Sub(currentTime)
|
||||
|
||||
if timeDiff >= -5*time.Minute && timeDiff <= 5*time.Minute {
|
||||
// 执行时间在当前时间前后5分钟内,设置为当前时间,让扫描器立即扫描到
|
||||
nextExecuteAt = currentTime.Unix()
|
||||
} else {
|
||||
// 否则使用正常的计算逻辑
|
||||
nextExecuteAt = utils.GetCpdailyNextExecuteTime(cpTask)
|
||||
}
|
||||
} else {
|
||||
// 其他状态(暂停、失败等),使用正常的计算逻辑
|
||||
nextExecuteAt = utils.GetCpdailyNextExecuteTime(cpTask)
|
||||
SetExecuteMinute(req.ExecuteMinute)
|
||||
if cpTask.Step == cptask.StepExecute {
|
||||
update.SetTaskStatus(cptask.TaskStatusNotStart)
|
||||
}
|
||||
cpTask := update.SaveX(ctx)
|
||||
|
||||
// 其他状态(暂停、失败等),使用正常的计算逻辑
|
||||
nextExecuteAt := utils.GetCpdailyNextExecuteTime(cpTask)
|
||||
|
||||
db2.Global.CpTask.UpdateOne(cpTask).SetNextExecuteTime(nextExecuteAt).ExecX(ctx)
|
||||
}, &sql.TxOptions{})
|
||||
@@ -283,6 +267,7 @@ func (v CpTaskServiceV1) ExecuteNow(ctx context.Context, req *v1.ExecuteNowReq)
|
||||
asynq.Unique(5*time.Second),
|
||||
asynq.MaxRetry(0),
|
||||
asynq.Timeout(10*time.Minute),
|
||||
asynq.Deadline(time.Now().Add(10*time.Minute)),
|
||||
asynq.Queue(cst2.TaskQueueCpdaily),
|
||||
)
|
||||
// 更新未开始的任务状态为队列中
|
||||
|
||||
@@ -2,7 +2,6 @@ package cpdaily
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
v1 "likedream-goframe/internal/api/v1"
|
||||
"likedream-goframe/internal/application/cpdaily/login"
|
||||
"likedream-goframe/internal/application/cpdaily/school"
|
||||
@@ -17,6 +16,7 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/samber/oops"
|
||||
)
|
||||
|
||||
@@ -53,7 +53,7 @@ func (v CpUserServiceV1) ConfirmAdd(ctx context.Context, req *v1.CpUserConfirmAd
|
||||
Wrap(err)
|
||||
})
|
||||
var sc school.SchoolDetailInfo
|
||||
json.Unmarshal([]byte(schoolInfo), &sc)
|
||||
sonic.Unmarshal([]byte(schoolInfo), &sc)
|
||||
loginExecutor := login.GetCpdailyLoginBySchoolInfo(ctx, &sc)
|
||||
loginExecutor.(*login.CpdailyCasLogin).ConfirmAdd(ctx, &sc, req.Username, req.Password, req.Key, req.DynamicCode)
|
||||
db.Global.CpUser.Create().
|
||||
|
||||
@@ -101,6 +101,29 @@ func (s *EmailServiceV1) SendTaskNotificationEmail(ctx context.Context, toEmail,
|
||||
return utils.SuccessWithMsg("通知邮件发送成功")
|
||||
}
|
||||
|
||||
// SendResetPasswordEmail 发送重置密码邮件
|
||||
func (s *EmailServiceV1) SendResetPasswordEmail(ctx context.Context, toEmail, username string, token string) *utils.Response[any] {
|
||||
if s.config.SMTPUsername == "" || s.config.SMTPPassword == "" {
|
||||
logger.Logger.Error("邮件服务未配置")
|
||||
return utils.FailBusiness("邮件服务未配置")
|
||||
}
|
||||
|
||||
subject := fmt.Sprintf("重置%s账户密码", cst2.AppName)
|
||||
body, err := s.renderResetPasswordEmailTemplate(username, token)
|
||||
if err != nil {
|
||||
logger.Logger.Error("邮件模板渲染失败")
|
||||
return utils.FailBusiness(fmt.Sprintf("邮件模板渲染失败: %v", err))
|
||||
}
|
||||
|
||||
err = s.sendEmail(ctx, toEmail, subject, body)
|
||||
if err != nil {
|
||||
logger.Logger.Error("邮件发送失败")
|
||||
return utils.FailBusiness(fmt.Sprintf("邮件发送失败: %v", err))
|
||||
}
|
||||
|
||||
return utils.SuccessWithMsg("重置密码邮件发送成功")
|
||||
}
|
||||
|
||||
// sendEmail 发送邮件的底层方法
|
||||
func (s *EmailServiceV1) sendEmail(ctx context.Context, to, subject, body string) error {
|
||||
|
||||
@@ -123,7 +146,7 @@ func (s *EmailServiceV1) sendEmail(ctx context.Context, to, subject, body string
|
||||
asynq.MaxRetry(3),
|
||||
asynq.Timeout(10*time.Minute),
|
||||
)
|
||||
logger.Logger.Info("添加任务: ", zap.String("task_id", task.ID))
|
||||
logger.Logger.Info("添加任务: ", zap.String("task_id", task.ID), zap.String("邮箱", to))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -190,3 +213,72 @@ func (s *EmailServiceV1) renderTaskNotificationTemplate(taskName string, status
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// renderResetPasswordEmailTemplate 渲染重置密码邮件模板
|
||||
func (s *EmailServiceV1) renderResetPasswordEmailTemplate(username string, token string) (string, error) {
|
||||
// 单独解析重置密码模板
|
||||
tmpl, err := template.ParseFS(templateFiles, "templates/layout.gohtml", "templates/reset_password.gohtml")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("解析重置密码邮件模板失败: %v", err)
|
||||
}
|
||||
|
||||
get, err := g.Cfg().Get(gctx.New(), cst2.ConfigEmailResetPasswordUrl)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
data := NewResetPasswordTemplateData(username, cst2.AppName, cst2.AppCopyright, get.String()+token)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = tmpl.ExecuteTemplate(&buf, "reset_password.gohtml", data)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("渲染重置密码邮件模板失败: %v", err)
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// SendBalanceMigrateEmail 发送积分转移邮件
|
||||
func (s *EmailServiceV1) SendBalanceMigrateEmail(ctx context.Context, toEmail, username string, balance int, token string) *utils.Response[any] {
|
||||
if s.config.SMTPUsername == "" || s.config.SMTPPassword == "" {
|
||||
logger.Logger.Error("邮件服务未配置")
|
||||
return utils.FailBusiness("邮件服务未配置")
|
||||
}
|
||||
|
||||
subject := fmt.Sprintf("%s积分转移通知", cst2.AppName)
|
||||
body, err := s.renderBalanceMigrateEmailTemplate(username, balance, token)
|
||||
if err != nil {
|
||||
logger.Logger.Error("邮件模板渲染失败")
|
||||
return utils.FailBusiness(fmt.Sprintf("邮件模板渲染失败: %v", err))
|
||||
}
|
||||
|
||||
err = s.sendEmail(ctx, toEmail, subject, body)
|
||||
if err != nil {
|
||||
logger.Logger.Error("邮件发送失败")
|
||||
return utils.FailBusiness(fmt.Sprintf("邮件发送失败: %v", err))
|
||||
}
|
||||
|
||||
return utils.SuccessWithMsg("积分转移邮件发送成功")
|
||||
}
|
||||
|
||||
// renderBalanceMigrateEmailTemplate 渲染积分转移邮件模板
|
||||
func (s *EmailServiceV1) renderBalanceMigrateEmailTemplate(username string, balance int, token string) (string, error) {
|
||||
// 单独解析积分转移模板
|
||||
tmpl, err := template.ParseFS(templateFiles, "templates/layout.gohtml", "templates/balance_migrate.gohtml")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("解析积分转移邮件模板失败: %v", err)
|
||||
}
|
||||
|
||||
get, err := g.Cfg().Get(gctx.New(), cst2.ConfigBalanceMigrateUrl)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
data := NewBalanceMigrateTemplateData(username, cst2.AppName, cst2.AppCopyright, balance, get.String()+token)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = tmpl.ExecuteTemplate(&buf, "balance_migrate.gohtml", data)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("渲染积分转移邮件模板失败: %v", err)
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
@@ -64,12 +64,62 @@ func NewTaskNotificationTemplateData(taskName string, status email.TaskStatus, d
|
||||
}
|
||||
}
|
||||
|
||||
// ResetPasswordTemplateData 重置密码邮件模板数据
|
||||
type ResetPasswordTemplateData struct {
|
||||
BaseTemplateData
|
||||
Username string `json:"username"` // 用户名
|
||||
ResetURL string `json:"reset_url"` // 重置密码链接
|
||||
}
|
||||
|
||||
// NewResetPasswordTemplateData 创建重置密码邮件模板数据
|
||||
func NewResetPasswordTemplateData(username, appName, copyright string, resetURL string) *ResetPasswordTemplateData {
|
||||
return &ResetPasswordTemplateData{
|
||||
BaseTemplateData: BaseTemplateData{
|
||||
Title: "重置密码 - " + appName,
|
||||
HeaderIcon: "🔒",
|
||||
HeaderTitle: "重置您的密码",
|
||||
HeaderColor: "#f56565",
|
||||
AppName: appName,
|
||||
AppCopyright: copyright,
|
||||
},
|
||||
Username: username,
|
||||
ResetURL: resetURL,
|
||||
}
|
||||
}
|
||||
|
||||
// BalanceMigrateTemplateData 积分转移邮件模板数据
|
||||
type BalanceMigrateTemplateData struct {
|
||||
BaseTemplateData
|
||||
Username string `json:"username"` // 用户名
|
||||
Balance int `json:"balance"` // 积分余额
|
||||
MigrateURL string `json:"migrate_url"` // 转移链接
|
||||
}
|
||||
|
||||
// NewBalanceMigrateTemplateData 创建积分转移邮件模板数据
|
||||
func NewBalanceMigrateTemplateData(username, appName, copyright string, balance int, migrateURL string) *BalanceMigrateTemplateData {
|
||||
return &BalanceMigrateTemplateData{
|
||||
BaseTemplateData: BaseTemplateData{
|
||||
Title: "积分转移 - " + appName,
|
||||
HeaderIcon: "💰",
|
||||
HeaderTitle: "积分转移通知",
|
||||
HeaderColor: "#ff9800",
|
||||
AppName: appName,
|
||||
AppCopyright: copyright,
|
||||
},
|
||||
Username: username,
|
||||
Balance: balance,
|
||||
MigrateURL: migrateURL,
|
||||
}
|
||||
}
|
||||
|
||||
// TemplateType 模板类型枚举
|
||||
type TemplateType string
|
||||
|
||||
const (
|
||||
TemplateTypeSignup TemplateType = "signup" // 注册模板
|
||||
TemplateTypeTaskNotification TemplateType = "task_notification" // 任务通知模板
|
||||
TemplateTypeResetPassword TemplateType = "reset_password" // 重置密码模板
|
||||
TemplateTypeBalanceMigrate TemplateType = "balance_migrate" // 积分转移模板
|
||||
)
|
||||
|
||||
// GetTemplatePath 获取模板文件路径
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
{{- /*gotype: likedream-goframe/internal/service/email.BalanceMigrateTemplateData*/ -}}
|
||||
|
||||
{{template "layout" .}}
|
||||
|
||||
{{define "extra-styles"}}
|
||||
<style>
|
||||
.balance-info {
|
||||
background: linear-gradient(90deg, #fff9f0 0%, #ffe5cc 100%);
|
||||
border-radius: 8px;
|
||||
padding: 20px 24px;
|
||||
border-left: 4px solid #ff9800;
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.balance-amount {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #ff9800;
|
||||
text-align: center;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.warning-box {
|
||||
background: linear-gradient(90deg, #fff5f5 0%, #ffe5e5 100%);
|
||||
border-radius: 8px;
|
||||
padding: 20px 24px;
|
||||
border-left: 4px solid #f56565;
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.warning-box p {
|
||||
margin: 8px 0;
|
||||
font-size: 14px;
|
||||
color: #c53030;
|
||||
}
|
||||
</style>
|
||||
{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<h2>亲爱的 <span class="highlight-text">{{.Username}}</span>,您好!</h2>
|
||||
<p>我们检测到您在旧系统中有积分余额,现在可以将积分转移到新系统。</p>
|
||||
|
||||
<div class="balance-info">
|
||||
<p style="margin: 0; text-align: center; font-size: 16px;">您的旧系统积分余额</p>
|
||||
<div class="balance-amount">{{.Balance}} 积分</div>
|
||||
</div>
|
||||
|
||||
<p>点击下面的按钮完成积分转移:</p>
|
||||
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="{{.MigrateURL}}"
|
||||
style="background-color: #ff9800; color: white; padding: 12px 24px;
|
||||
text-decoration: none; border-radius: 4px; display: inline-block;">
|
||||
立即转移积分
|
||||
</a>
|
||||
</div>
|
||||
<p>如果按钮无法点击,请复制以下链接到浏览器地址栏:</p>
|
||||
<p style="word-break: break-all; color: #666;">{{.MigrateURL}}</p>
|
||||
|
||||
<p><strong>链接有效期:24小时</strong></p>
|
||||
|
||||
<div class="warning-box">
|
||||
<p style="margin: 0;"><strong>⚠️ 重要提示</strong></p>
|
||||
<p>• 积分转移后将从旧系统清空,请确认后再操作</p>
|
||||
<p>• 每个账户只能转移一次</p>
|
||||
<p>• 请勿将此链接分享给其他人</p>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<p>如果您在转移过程中遇到任何问题,请随时联系我们的技术支持团队。</p>
|
||||
<p><strong>祝您使用愉快!</strong> 💰</p>
|
||||
{{end}}
|
||||
@@ -0,0 +1,59 @@
|
||||
{{- /*gotype: likedream-goframe/internal/service/email.ResetPasswordTemplateData*/ -}}
|
||||
|
||||
{{template "layout" .}}
|
||||
|
||||
{{define "extra-styles"}}
|
||||
<style>
|
||||
.warning-box {
|
||||
background: linear-gradient(90deg, #fff5f5 0%, #ffe5e5 100%);
|
||||
border-radius: 8px;
|
||||
padding: 20px 24px;
|
||||
border-left: 4px solid #f56565;
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.warning-box p {
|
||||
margin: 8px 0;
|
||||
font-size: 14px;
|
||||
color: #c53030;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: linear-gradient(90deg, #f0f9ff 0%, #e0f2fe 100%);
|
||||
border-radius: 8px;
|
||||
padding: 20px 24px;
|
||||
border-left: 4px solid #3b82f6;
|
||||
margin: 24px 0;
|
||||
}
|
||||
</style>
|
||||
{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<h2>亲爱的 <span class="highlight-text">{{.Username}}</span>,您好!</h2>
|
||||
<p>我们收到了您重置<strong>{{.AppName}}</strong>账户密码的请求。请点击下面的链接重置您的密码:</p>
|
||||
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="{{.ResetURL}}"
|
||||
style="background-color: #f56565; color: white; padding: 12px 24px;
|
||||
text-decoration: none; border-radius: 4px; display: inline-block;">
|
||||
重置密码
|
||||
</a>
|
||||
</div>
|
||||
<p>如果按钮无法点击,请复制以下链接到浏览器地址栏:</p>
|
||||
<p style="word-break: break-all; color: #666;">{{.ResetURL}}</p>
|
||||
|
||||
<div class="info-box">
|
||||
<p style="margin: 0; color: #1e40af;"><strong>⏰ 重要提示:链接有效期为 1 小时</strong></p>
|
||||
</div>
|
||||
|
||||
<div class="warning-box">
|
||||
<p style="margin: 0;"><strong>⚠️ 安全提醒</strong></p>
|
||||
<p>如果您没有请求重置密码,请忽略此邮件。您的账户仍然是安全的。</p>
|
||||
<p>请勿将此链接分享给任何人!</p>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<p>如果您在重置密码过程中遇到任何问题,请随时联系我们的技术支持团队。</p>
|
||||
<p><strong>祝您使用愉快!</strong> 🔒</p>
|
||||
{{end}}
|
||||
@@ -2,6 +2,7 @@ package user_login
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
v1 "likedream-goframe/internal/api/v1"
|
||||
"likedream-goframe/internal/common/cst"
|
||||
@@ -12,8 +13,11 @@ import (
|
||||
"likedream-goframe/internal/ent/sysrole"
|
||||
"likedream-goframe/internal/ent/sysuser"
|
||||
"likedream-goframe/internal/framework/ent2"
|
||||
"likedream-goframe/internal/framework/redis"
|
||||
"likedream-goframe/internal/service/balance"
|
||||
"likedream-goframe/internal/service/email"
|
||||
"likedream-goframe/internal/utils"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -249,3 +253,128 @@ func buildPermissionList(list []*ent.SysMenu) []string {
|
||||
}
|
||||
return permissionList
|
||||
}
|
||||
|
||||
func (s *UserInfoServiceV1) RequestBalanceMigrate(ctx context.Context, req *v1.RequestBalanceMigrateReq) *utils.Response[any] {
|
||||
// 获取当前登录用户(新系统)
|
||||
currentUser := utils.GetUserInfoFromCtx(ctx)
|
||||
newSystemEmail := currentUser.Email
|
||||
|
||||
// 旧系统的邮箱从请求参数获取
|
||||
oldSystemEmail := req.Email
|
||||
|
||||
// 定义旧系统用户结构
|
||||
type AspNetUser struct {
|
||||
Email string `gorm:"column:email"`
|
||||
UserName string `gorm:"column:user_name"`
|
||||
Balance int `gorm:"column:balance"`
|
||||
}
|
||||
|
||||
// 查询旧系统数据库中是否存在该邮箱的用户
|
||||
var oldUser AspNetUser
|
||||
err := db.GlobalGorm.Table("AspNetUsers").Where("email = ?", oldSystemEmail).First(&oldUser).Error
|
||||
if err != nil {
|
||||
// 用户不存在或查询失败,为了安全仍返回成功
|
||||
return utils.SuccessWithMsg("如果该邮箱在旧系统中存在积分,转移链接已发送")
|
||||
}
|
||||
|
||||
// 检查积分是否大于0
|
||||
if oldUser.Balance <= 0 {
|
||||
return utils.SuccessWithMsg("如果该邮箱在旧系统中存在积分,转移链接已发送")
|
||||
}
|
||||
|
||||
// 发送积分转移邮件
|
||||
go func() {
|
||||
ctx2 := context.Background()
|
||||
client := redis.GetClient()
|
||||
|
||||
// 获取并删除旧的 token(如果存在)
|
||||
// 使用新系统邮箱作为key,因为token是关联到当前登录用户的
|
||||
oldTokenKey := cst.RedisBalanceMigrate + "email:" + newSystemEmail
|
||||
oldToken := client.GetDel(ctx2, oldTokenKey).Val()
|
||||
if oldToken != "" {
|
||||
// 删除旧 token 对应的转移记录
|
||||
client.Del(ctx2, cst.RedisBalanceMigrate+oldToken)
|
||||
}
|
||||
|
||||
// 生成新的 token
|
||||
token := make([]byte, 32)
|
||||
_, _ = rand.Read(token)
|
||||
tokenStr := base64.URLEncoding.EncodeToString(token)
|
||||
|
||||
// Token存储格式:token -> "新系统邮箱|旧系统邮箱"
|
||||
tokenValue := newSystemEmail + "|" + oldSystemEmail
|
||||
err2 := client.SetEx(ctx2, cst.RedisBalanceMigrate+tokenStr, tokenValue, time.Duration(cst.RedisBalanceMigrateExpire)*time.Second).Err()
|
||||
utils.OkOrPanic(err2, oops.WithContext(ctx2).Wrap)
|
||||
|
||||
// 保存新系统邮箱到 token 的映射,方便下次删除
|
||||
client.SetEx(ctx2, oldTokenKey, tokenStr, time.Duration(cst.RedisBalanceMigrateExpire)*time.Second)
|
||||
|
||||
// 发送积分转移邮件到新系统邮箱
|
||||
emailService := email.NewV1()
|
||||
emailService.SendBalanceMigrateEmail(ctx2, oldSystemEmail, currentUser.Username, oldUser.Balance, tokenStr)
|
||||
}()
|
||||
// 无论是否满足条件,都返回成功,避免泄露用户信息
|
||||
return utils.SuccessWithMsg("如果该邮箱在旧系统中存在积分,转移链接已发送")
|
||||
}
|
||||
|
||||
func (s *UserInfoServiceV1) BalanceMigrate(ctx context.Context, req *v1.BalanceMigrateReq) *utils.Response[any] {
|
||||
client := redis.GetClient()
|
||||
tokenValue := client.GetDel(ctx, cst.RedisBalanceMigrate+req.Token).Val()
|
||||
if tokenValue == "" {
|
||||
return utils.FailParam("无效或已过期的积分转移链接")
|
||||
}
|
||||
|
||||
// 解析 token 值:格式为 "新系统邮箱|旧系统邮箱"
|
||||
emails := strings.Split(tokenValue, "|")
|
||||
if len(emails) != 2 {
|
||||
return utils.FailParam("无效的积分转移链接")
|
||||
}
|
||||
newSystemEmail := emails[0]
|
||||
oldSystemEmail := emails[1]
|
||||
|
||||
// 删除新系统邮箱对应的 token 映射
|
||||
client.Del(ctx, cst.RedisBalanceMigrate+"email:"+newSystemEmail)
|
||||
|
||||
// 定义旧系统用户结构
|
||||
type AspNetUser struct {
|
||||
Email string `gorm:"column:email"`
|
||||
UserName string `gorm:"column:user_name"`
|
||||
Balance int `gorm:"column:balance"`
|
||||
}
|
||||
|
||||
// 查询旧系统用户
|
||||
var oldUser AspNetUser
|
||||
err := db.GlobalGorm.Table("AspNetUsers").Where("email = ?", oldSystemEmail).First(&oldUser).Error
|
||||
if err != nil {
|
||||
return utils.FailParam("旧系统中未找到该用户或积分已转移")
|
||||
}
|
||||
|
||||
// 检查积分是否大于0
|
||||
if oldUser.Balance <= 0 {
|
||||
return utils.FailParam("您的旧系统积分余额为0,无需转移")
|
||||
}
|
||||
|
||||
// 查询新系统用户
|
||||
newUser, err := db.Global.SysUser.Query().WithAgent().Where(sysuser.EmailEQ(newSystemEmail)).Only(ctx)
|
||||
if ent.IsNotFound(err) {
|
||||
return utils.FailParam("新系统中未找到该用户")
|
||||
}
|
||||
|
||||
b := decimal.NewFromInt32(int32(oldUser.Balance))
|
||||
if newUser.Edges.Agent != nil {
|
||||
b = utils.TransformBalance(b, newUser.Edges.Agent.Rate)
|
||||
}
|
||||
balance.BalanceService.ChangeUserBalance(ctx, newUser.ID, b)
|
||||
|
||||
// 清空旧系统积分
|
||||
err = db.GlobalGorm.Table("AspNetUsers").Where("email = ?", oldSystemEmail).Update("balance", 0).Error
|
||||
if err != nil {
|
||||
// 旧系统更新失败,记录日志但不影响返回结果(因为新系统已经转移成功)
|
||||
// 回滚
|
||||
balance.BalanceService.ChangeUserBalance(ctx, newUser.ID, b.Neg())
|
||||
// 这里可以考虑回滚新系统的操作,但为了简化流程暂时保留新系统的更新
|
||||
return utils.FailBusiness("旧系统积分清空失败,请联系管理员")
|
||||
}
|
||||
|
||||
return utils.SuccessWithMsg("积分转移成功!您的积分已转移到新系统")
|
||||
}
|
||||
|
||||
@@ -84,11 +84,14 @@ func (s *UserLoginServiceV1) Register(ctx context.Context, req *v1.UserRegisterR
|
||||
// 注册成功后发送欢迎邮件(异步发送,不影响注册结果)
|
||||
go func() {
|
||||
ctx2 := context.Background()
|
||||
client := redis.GetClient()
|
||||
token := make([]byte, 32)
|
||||
_, _ = rand.Read(token)
|
||||
tokenStr := base64.URLEncoding.EncodeToString(token)
|
||||
err2 := redis.GetClient().SetEx(ctx2, cst2.RedisVerifyCode+tokenStr, req.Email, time.Duration(cst2.RedisVerifyCodeExpire)*time.Second).Err()
|
||||
err2 := client.SetEx(ctx2, cst2.RedisVerifyCode+tokenStr, req.Email, time.Duration(cst2.RedisVerifyCodeExpire)*time.Second).Err()
|
||||
utils.OkOrPanic(err2, oops.WithContext(ctx2).Wrap)
|
||||
// 保存邮箱到 token 的映射,方便重新发送时删除旧 token
|
||||
client.SetEx(ctx2, cst2.RedisVerifyCode+"email:"+req.Email, tokenStr, time.Duration(cst2.RedisVerifyCodeExpire)*time.Second)
|
||||
// 创建新的context避免原context被取消
|
||||
emailService := email.NewV1()
|
||||
emailService.SendSignupEmail(ctx2, req.Email, req.Username, tokenStr)
|
||||
@@ -110,6 +113,8 @@ func (s *UserLoginServiceV1) Confirm(ctx context.Context, req *v1.UserConfirmReq
|
||||
if emailStr == "" {
|
||||
return utils.FailParam("非法的验证")
|
||||
}
|
||||
// 删除邮箱对应的 token 映射
|
||||
client.Del(ctx, cst2.RedisVerifyCode+"email:"+emailStr)
|
||||
// 查询用户
|
||||
user := db.Global.SysUser.Query().Where(sysuser.EmailEQ(emailStr)).OnlyX(ctx)
|
||||
db.Global.SysUser.UpdateOne(user).SetVerify(true).SaveX(ctx)
|
||||
@@ -118,3 +123,135 @@ func (s *UserLoginServiceV1) Confirm(ctx context.Context, req *v1.UserConfirmReq
|
||||
r.Response.Header().Set(cst2.AuthorizationKey, utils.GenerateToken(user.ID))
|
||||
return utils.SuccessWithMsg("验证成功")
|
||||
}
|
||||
|
||||
func (s *UserLoginServiceV1) ResendVerification(ctx context.Context, req *v1.ResendVerificationReq) *utils.Response[any] {
|
||||
// 查询数据库中是否存在该邮箱的未激活用户
|
||||
user, err := db.Global.SysUser.Query().Where(
|
||||
sysuser.EmailEQ(req.Email),
|
||||
sysuser.VerifyEQ(false),
|
||||
).Only(ctx)
|
||||
|
||||
// 如果用户存在且未激活,则重新发送激活邮件
|
||||
if err == nil && user != nil {
|
||||
go func() {
|
||||
ctx2 := context.Background()
|
||||
client := redis.GetClient()
|
||||
|
||||
// 获取并删除旧的 token(如果存在)
|
||||
oldTokenKey := cst2.RedisVerifyCode + "email:" + req.Email
|
||||
oldToken := client.GetDel(ctx2, oldTokenKey).Val()
|
||||
if oldToken != "" {
|
||||
// 删除旧 token 对应的验证码记录
|
||||
client.Del(ctx2, cst2.RedisVerifyCode+oldToken)
|
||||
}
|
||||
|
||||
// 生成新的 token
|
||||
token := make([]byte, 32)
|
||||
_, _ = rand.Read(token)
|
||||
tokenStr := base64.URLEncoding.EncodeToString(token)
|
||||
|
||||
// 设置新的 token 到 Redis
|
||||
err2 := client.SetEx(ctx2, cst2.RedisVerifyCode+tokenStr, req.Email, time.Duration(cst2.RedisVerifyCodeExpire)*time.Second).Err()
|
||||
utils.OkOrPanic(err2, oops.WithContext(ctx2).Wrap)
|
||||
|
||||
// 保存邮箱到 token 的映射,方便下次删除
|
||||
client.SetEx(ctx2, oldTokenKey, tokenStr, time.Duration(cst2.RedisVerifyCodeExpire)*time.Second)
|
||||
|
||||
// 发送激活邮件
|
||||
emailService := email.NewV1()
|
||||
emailService.SendSignupEmail(ctx2, req.Email, user.Username, tokenStr)
|
||||
}()
|
||||
}
|
||||
|
||||
// 无论用户是否存在,都返回成功,避免泄露用户信息
|
||||
return utils.SuccessWithMsg("如果该邮箱存在未激活账户,验证邮件已重新发送")
|
||||
}
|
||||
|
||||
func (s *UserLoginServiceV1) ForgotPassword(ctx context.Context, req *v1.ForgotPasswordReq) *utils.Response[any] {
|
||||
// 查询数据库中是否存在该邮箱的已激活用户
|
||||
user, err := db.Global.SysUser.Query().Where(
|
||||
sysuser.EmailEQ(req.Email),
|
||||
sysuser.VerifyEQ(true),
|
||||
).Only(ctx)
|
||||
|
||||
// 如果用户存在且已激活,则发送重置密码邮件
|
||||
if err == nil && user != nil {
|
||||
go func() {
|
||||
ctx2 := context.Background()
|
||||
client := redis.GetClient()
|
||||
|
||||
// 获取并删除旧的 token(如果存在)
|
||||
oldTokenKey := cst2.RedisResetPassword + "email:" + req.Email
|
||||
oldToken := client.GetDel(ctx2, oldTokenKey).Val()
|
||||
if oldToken != "" {
|
||||
// 删除旧 token 对应的重置密码记录
|
||||
client.Del(ctx2, cst2.RedisResetPassword+oldToken)
|
||||
}
|
||||
|
||||
// 生成新的 token
|
||||
token := make([]byte, 32)
|
||||
_, _ = rand.Read(token)
|
||||
tokenStr := base64.URLEncoding.EncodeToString(token)
|
||||
|
||||
// 设置新的 token 到 Redis
|
||||
err2 := client.SetEx(ctx2, cst2.RedisResetPassword+tokenStr, req.Email, time.Duration(cst2.RedisResetPasswordExpire)*time.Second).Err()
|
||||
utils.OkOrPanic(err2, oops.WithContext(ctx2).Wrap)
|
||||
|
||||
// 保存邮箱到 token 的映射,方便下次删除
|
||||
client.SetEx(ctx2, oldTokenKey, tokenStr, time.Duration(cst2.RedisResetPasswordExpire)*time.Second)
|
||||
|
||||
// 发送重置密码邮件
|
||||
emailService := email.NewV1()
|
||||
emailService.SendResetPasswordEmail(ctx2, req.Email, user.Username, tokenStr)
|
||||
}()
|
||||
}
|
||||
|
||||
// 无论用户是否存在,都返回成功,避免泄露用户信息
|
||||
return utils.SuccessWithMsg("如果该邮箱已注册,重置密码邮件已发送")
|
||||
}
|
||||
|
||||
func (s *UserLoginServiceV1) ResetPassword(ctx context.Context, req *v1.ForgetResetPasswordReq) *utils.Response[any] {
|
||||
client := redis.GetClient()
|
||||
emailStr := client.GetDel(ctx, cst2.RedisResetPassword+req.Token).Val()
|
||||
if emailStr == "" {
|
||||
return utils.FailParam("无效或已过期的重置密码链接")
|
||||
}
|
||||
// 删除邮箱对应的 token 映射
|
||||
client.Del(ctx, cst2.RedisResetPassword+"email:"+emailStr)
|
||||
|
||||
// 查询用户
|
||||
user, err := db.Global.SysUser.Query().Where(sysuser.EmailEQ(emailStr)).Only(ctx)
|
||||
if ent.IsNotFound(err) {
|
||||
return utils.FailParam("用户不存在")
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
db.Global.SysUser.UpdateOne(user).SetPassword(utils.PasswordBcryptHash(req.NewPassword)).SaveX(ctx)
|
||||
|
||||
return utils.SuccessWithMsg("密码重置成功,请使用新密码登录")
|
||||
}
|
||||
|
||||
func (s *UserLoginServiceV1) GetResetPasswordInfo(ctx context.Context, req *v1.ResetPasswordInfoReq) *utils.Response[any] {
|
||||
client := redis.GetClient()
|
||||
// 使用 Get 而不是 GetDel,不删除 token
|
||||
emailStr := client.Get(ctx, cst2.RedisResetPassword+req.Token).Val()
|
||||
if emailStr == "" {
|
||||
return utils.FailParam("无效或已过期的重置密码链接")
|
||||
}
|
||||
|
||||
// 查询用户
|
||||
user, err := db.Global.SysUser.Query().Where(sysuser.EmailEQ(emailStr)).Only(ctx)
|
||||
if ent.IsNotFound(err) {
|
||||
return utils.FailParam("无效或已过期的重置密码链接")
|
||||
}
|
||||
type ResetPasswordInfoRes struct {
|
||||
Username string `json:"username"` // 用户名
|
||||
Email string `json:"email"` // 邮箱
|
||||
}
|
||||
|
||||
// 返回用户信息
|
||||
return utils.SuccessWithData[any](&ResetPasswordInfoRes{
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/png"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
"os"
|
||||
)
|
||||
|
||||
@@ -13,10 +14,11 @@ func ReadImg(imgFile string) (image.Image, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
img, err := png.Decode(f)
|
||||
img, s, err := image.Decode(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logger.Logger.Info(s)
|
||||
f.Close()
|
||||
return img, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user