Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b5b2103372 | |||
| b234c87048 | |||
| f982aa5e0c | |||
| 22823328c0 | |||
| 323e49e375 | |||
| 526e4ac198 | |||
| 7469813e37 | |||
| 8eb4e40b57 | |||
| 311cf18708 | |||
| 1bde0af01d | |||
| 765ec6fb32 | |||
| 5cd8329519 | |||
| 1ea9170d89 | |||
| 29fc69d022 | |||
| 09e7a82882 | |||
| 1fff983d37 | |||
| 836e61b8b0 | |||
| 96044b8a88 | |||
| acb865596e | |||
| 59559f4201 | |||
| 4d516adea8 | |||
| a37368ddcf | |||
| 26dcfaf82d | |||
| e55a8866b1 | |||
| 0702135db2 | |||
| c668a76a99 | |||
| 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 |
+5
-8
@@ -1,19 +1,16 @@
|
||||
FROM golang:latest AS backend-builder
|
||||
RUN apt update
|
||||
RUN apt install -y build-essential
|
||||
FROM golang:alpine AS backend-builder
|
||||
WORKDIR /build
|
||||
COPY . .
|
||||
ENV CGO_ENABLED=1
|
||||
|
||||
RUN go build -ldflags="-s -w" -o bin/server
|
||||
|
||||
RUN apt install -y upx-ucl
|
||||
RUN apk add --no-cache upx
|
||||
|
||||
RUN upx --best --lzma bin/server
|
||||
|
||||
FROM golang:latest
|
||||
FROM alpine:latest
|
||||
COPY --from=backend-builder /build/bin/server /app/server
|
||||
#COPY static /app/static
|
||||
#COPY static/*.so /app/
|
||||
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
RUN ls -a
|
||||
|
||||
+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)
|
||||
|
||||
+15
-8
@@ -4,20 +4,23 @@ 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"
|
||||
|
||||
[ruoliCaptcha]
|
||||
baseUrl = ""
|
||||
[captcha]
|
||||
baseUrl = "http://captcha-resolver.out"
|
||||
#baseUrl = "http://localhost:8890"
|
||||
[redis]
|
||||
url = "redis://:jhkdjhkjdhsIUTYURTU_pEGmFP@database.out:56379"
|
||||
|
||||
@@ -30,7 +33,7 @@ smtpUsername = "demo@likedream.xyz"
|
||||
smtpPassword = "baca122..."
|
||||
# 发件人邮箱和显示名称
|
||||
fromEmail = "demo@likedream.xyz"
|
||||
fromName = "今日校园系统"
|
||||
fromName = "若梦系统"
|
||||
|
||||
|
||||
[s3]
|
||||
@@ -40,6 +43,10 @@ baseUrl = "http://s3.out"
|
||||
bucket = "likedream"
|
||||
|
||||
[cpdaily]
|
||||
appVersion = "5.7.0"
|
||||
appVersion = "9.7.2"
|
||||
systemVersion = "iOS 17"
|
||||
calVersion = "3.0.0"
|
||||
calVersion = "firstv"
|
||||
|
||||
|
||||
[task]
|
||||
concurrency = 20
|
||||
@@ -1,6 +1,6 @@
|
||||
module likedream-goframe
|
||||
|
||||
go 1.24
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
entgo.io/ent v0.14.5
|
||||
@@ -19,21 +19,20 @@ require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/hibiken/asynq v0.25.1
|
||||
github.com/joomcode/errorx v1.2.0
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
||||
github.com/redis/go-redis/v9 v9.11.0
|
||||
github.com/samber/oops v1.19.0
|
||||
github.com/shirou/gopsutil/v3 v3.24.5
|
||||
github.com/shopspring/decimal v1.4.0
|
||||
github.com/thedevsaddam/gojsonq v1.9.1
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
github.com/tidwall/match v1.1.1
|
||||
github.com/wenlng/go-captcha-assets v1.0.7
|
||||
github.com/wenlng/go-captcha/v2 v2.0.4
|
||||
github.com/wneessen/go-mail v0.6.2
|
||||
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
|
||||
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 +70,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 +105,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,12 +146,10 @@ 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=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
|
||||
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
@@ -179,6 +189,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=
|
||||
@@ -186,8 +197,6 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/thedevsaddam/gojsonq v1.9.1 h1:zQulEP43nwmq5EKrNWyIgJVbqDeMdC1qzXM/f5O15a0=
|
||||
github.com/thedevsaddam/gojsonq v1.9.1/go.mod h1:RBcQaITThgJAAYKH7FNp2onYodRz8URfsuEGpAch0NA=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -206,8 +215,6 @@ github.com/wenlng/go-captcha/v2 v2.0.4 h1:5cSUF36ZyA03qeDMjKmeXGpbYJMXEexZIYK3Vg
|
||||
github.com/wenlng/go-captcha/v2 v2.0.4/go.mod h1:5hac1em3uXoyC5ipZ0xFv9umNM/waQvYAQdr0cx/h34=
|
||||
github.com/wneessen/go-mail v0.6.2 h1:c6V7c8D2mz868z9WJ+8zDKtUyLfZ1++uAZmo2GRFji8=
|
||||
github.com/wneessen/go-mail v0.6.2/go.mod h1:L/PYjPK3/2ZlNb2/FjEBIn9n1rUWjW+Toy531oVmeb4=
|
||||
github.com/yalue/onnxruntime_go v1.12.1 h1:joCCmBnNjHy04jK9EMP/UV6oPPqySXlRgf3gcUcnI/g=
|
||||
github.com/yalue/onnxruntime_go v1.12.1/go.mod h1:b4X26A8pekNb1ACJ58wAXgNKeUCGEAQ9dmACut9Sm/4=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
@@ -240,8 +247,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 +257,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 +268,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 +278,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 +297,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 +319,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 +329,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 +342,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=
|
||||
|
||||
@@ -67,3 +67,8 @@ type PauseTaskReq struct {
|
||||
g.Meta `path:"/task/pause/{id}" method:"post" tags:"CpTask" summary:"暂停任务"`
|
||||
Id int64 `json:"id"`
|
||||
}
|
||||
|
||||
type FindLocationReq struct {
|
||||
g.Meta `path:"/task/location" method:"get" tags:"CpTask" summary:"查找地址"`
|
||||
Address string `json:"address"`
|
||||
}
|
||||
|
||||
@@ -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#验证码不能为空"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package captcha
|
||||
|
||||
func ApiSemiOCRVerification() {}
|
||||
@@ -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
|
||||
}
|
||||
@@ -67,7 +67,23 @@ func (a *AllSavePersistCookieJar) SetCookies(u *url.URL, cookies []*http.Cookie)
|
||||
if a.domainMap[mainDomain] == nil {
|
||||
a.domainMap[mainDomain] = make(cookieMap)
|
||||
}
|
||||
a.domainMap[mainDomain][cookie.Name] = cookie
|
||||
|
||||
// 查找是否存在相同 name, path 和 domain 的 cookie
|
||||
existingCookies := a.domainMap[mainDomain][cookie.Name]
|
||||
found := false
|
||||
for i, existing := range existingCookies {
|
||||
if existing.Path == cookie.Path && existing.Domain == cookie.Domain {
|
||||
// 找到完全匹配的 cookie,替换它
|
||||
existingCookies[i] = cookie
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
// 没有找到匹配的,添加新的 cookie
|
||||
a.domainMap[mainDomain][cookie.Name] = append(a.domainMap[mainDomain][cookie.Name], cookie)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,10 +94,43 @@ func (a *AllSavePersistCookieJar) Cookies(u *url.URL) []*http.Cookie {
|
||||
// 提取主域名
|
||||
mainDomain := extractMainDomain(u.Host)
|
||||
|
||||
if cookies, exists := a.domainMap[mainDomain]; exists {
|
||||
result := make([]*http.Cookie, 0, len(cookies))
|
||||
for _, cookie := range cookies {
|
||||
result = append(result, cookie)
|
||||
if cookiesList, exists := a.domainMap[mainDomain]; exists {
|
||||
result := make([]*http.Cookie, 0, len(cookiesList))
|
||||
requestPath := u.Path
|
||||
if requestPath == "" {
|
||||
requestPath = "/"
|
||||
}
|
||||
|
||||
for _, cookies := range cookiesList {
|
||||
// 如果只有一个 cookie,直接返回
|
||||
if len(cookies) == 1 {
|
||||
result = append(result, cookies[0])
|
||||
continue
|
||||
}
|
||||
|
||||
// 有多个同名 cookie,选择最匹配的(path 最长且匹配请求路径的)
|
||||
var bestMatch *http.Cookie
|
||||
bestMatchLen := -1
|
||||
|
||||
for _, cookie := range cookies {
|
||||
cookiePath := cookie.Path
|
||||
if cookiePath == "" {
|
||||
cookiePath = "/"
|
||||
}
|
||||
|
||||
// 检查 path 是否匹配
|
||||
if pathMatch(requestPath, cookiePath) {
|
||||
if len(cookiePath) > bestMatchLen {
|
||||
bestMatch = cookie
|
||||
bestMatchLen = len(cookiePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果找到匹配的,添加到结果中
|
||||
if bestMatch != nil {
|
||||
result = append(result, bestMatch)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -109,7 +158,30 @@ func (a *AllSavePersistCookieJar) Deserialize(content string) {
|
||||
|
||||
var domainMap domainMap
|
||||
err := sonic.Unmarshal([]byte(content), &domainMap)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
// 如果反序列化失败,尝试从旧格式转换
|
||||
if err != nil {
|
||||
// 旧格式: map[string]map[string]*http.Cookie
|
||||
type oldCookieMap map[string]*http.Cookie
|
||||
type oldDomainMap map[string]oldCookieMap
|
||||
|
||||
var oldMap oldDomainMap
|
||||
oldErr := sonic.Unmarshal([]byte(content), &oldMap)
|
||||
if oldErr != nil {
|
||||
// 两种格式都失败,抛出原始错误
|
||||
utils.OkOrPanic(err)
|
||||
return
|
||||
}
|
||||
|
||||
// 转换旧格式到新格式
|
||||
domainMap = make(map[string]cookieMap)
|
||||
for domain, cookies := range oldMap {
|
||||
domainMap[domain] = make(map[string][]*http.Cookie)
|
||||
for name, cookie := range cookies {
|
||||
domainMap[domain][name] = []*http.Cookie{cookie}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.domainMap = domainMap
|
||||
}
|
||||
@@ -142,7 +214,31 @@ func (a *AllSavePersistCookieJar) DeserializeBytes(content []byte) {
|
||||
var domainMap domainMap
|
||||
err := gob.NewDecoder(retReader).
|
||||
Decode(&domainMap)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
// 如果反序列化失败,尝试从旧格式转换
|
||||
if err != nil {
|
||||
// 旧格式: map[string]map[string]*http.Cookie
|
||||
type oldCookieMap map[string]*http.Cookie
|
||||
type oldDomainMap map[string]oldCookieMap
|
||||
|
||||
retReader = bytes.NewBuffer(content)
|
||||
var oldMap oldDomainMap
|
||||
oldErr := gob.NewDecoder(retReader).Decode(&oldMap)
|
||||
if oldErr != nil {
|
||||
// 两种格式都失败,抛出原始错误
|
||||
utils.OkOrPanic(err)
|
||||
return
|
||||
}
|
||||
|
||||
// 转换旧格式到新格式
|
||||
domainMap = make(map[string]cookieMap)
|
||||
for domain, cookies := range oldMap {
|
||||
domainMap[domain] = make(map[string][]*http.Cookie)
|
||||
for name, cookie := range cookies {
|
||||
domainMap[domain][name] = []*http.Cookie{cookie}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.domainMap = domainMap
|
||||
}
|
||||
@@ -151,10 +247,36 @@ func (a *AllSavePersistCookieJar) DeserializeReader(reader io.Reader) {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
|
||||
var domainMap domainMap
|
||||
err := gob.NewDecoder(reader).
|
||||
Decode(&domainMap)
|
||||
// 先读取所有数据,以便在失败时可以重试
|
||||
data, err := io.ReadAll(reader)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
var domainMap domainMap
|
||||
err = gob.NewDecoder(bytes.NewBuffer(data)).Decode(&domainMap)
|
||||
|
||||
// 如果反序列化失败,尝试从旧格式转换
|
||||
if err != nil {
|
||||
// 旧格式: map[string]map[string]*http.Cookie
|
||||
type oldCookieMap map[string]*http.Cookie
|
||||
type oldDomainMap map[string]oldCookieMap
|
||||
|
||||
var oldMap oldDomainMap
|
||||
oldErr := gob.NewDecoder(bytes.NewBuffer(data)).Decode(&oldMap)
|
||||
if oldErr != nil {
|
||||
// 两种格式都失败,抛出原始错误
|
||||
utils.OkOrPanic(err)
|
||||
return
|
||||
}
|
||||
|
||||
// 转换旧格式到新格式
|
||||
domainMap = make(map[string]cookieMap)
|
||||
for domain, cookies := range oldMap {
|
||||
domainMap[domain] = make(map[string][]*http.Cookie)
|
||||
for name, cookie := range cookies {
|
||||
domainMap[domain][name] = []*http.Cookie{cookie}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.domainMap = domainMap
|
||||
}
|
||||
|
||||
@@ -3,17 +3,19 @@ package cookiejar
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"github.com/bytedance/sonic"
|
||||
"io"
|
||||
"likedream-goframe/internal/utils"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
)
|
||||
|
||||
type hostMap map[string]cookieMap
|
||||
|
||||
type cookieMap map[string]*http.Cookie
|
||||
type cookieMap map[string][]*http.Cookie
|
||||
type PersistCookieJar struct {
|
||||
hostMap hostMap
|
||||
*sync.RWMutex
|
||||
@@ -33,14 +35,35 @@ func (p *PersistCookieJar) SetCookies(u *url.URL, cookies []*http.Cookie) {
|
||||
host := u.Host
|
||||
|
||||
if _, exists := p.hostMap[host]; !exists {
|
||||
p.hostMap = make(hostMap)
|
||||
p.hostMap[host] = make(cookieMap)
|
||||
}
|
||||
|
||||
for _, cookie := range cookies {
|
||||
if p.hostMap[host] == nil {
|
||||
p.hostMap[host] = make(cookieMap)
|
||||
}
|
||||
p.hostMap[host][cookie.Name] = cookie
|
||||
|
||||
// 检查并删除全局 cookie 中同名的 cookie
|
||||
if globalCookies, exists := p.hostMap[""]; exists {
|
||||
delete(globalCookies, cookie.Name)
|
||||
}
|
||||
|
||||
// 查找是否存在相同 name, path 和 domain 的 cookie
|
||||
existingCookies := p.hostMap[host][cookie.Name]
|
||||
found := false
|
||||
for i, existing := range existingCookies {
|
||||
if existing.Path == cookie.Path && existing.Domain == cookie.Domain {
|
||||
// 找到完全匹配的 cookie,替换它
|
||||
existingCookies[i] = cookie
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
// 没有找到匹配的,添加新的 cookie
|
||||
p.hostMap[host][cookie.Name] = append(p.hostMap[host][cookie.Name], cookie)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -50,14 +73,110 @@ func (p *PersistCookieJar) Cookies(u *url.URL) []*http.Cookie {
|
||||
defer p.RUnlock()
|
||||
|
||||
host := u.Host
|
||||
if cookies, exists := p.hostMap[host]; exists {
|
||||
result := make([]*http.Cookie, 0, len(cookies))
|
||||
for _, cookie := range cookies {
|
||||
result = append(result, cookie)
|
||||
}
|
||||
return result
|
||||
requestPath := u.Path
|
||||
if requestPath == "" {
|
||||
requestPath = "/"
|
||||
}
|
||||
return nil
|
||||
|
||||
result := make([]*http.Cookie, 0)
|
||||
addedNames := make(map[string]bool)
|
||||
|
||||
// 先添加匹配 host 的 cookie
|
||||
if cookiesList, exists := p.hostMap[host]; exists {
|
||||
for cookieName, cookies := range cookiesList {
|
||||
// 如果只有一个 cookie,直接返回
|
||||
if len(cookies) == 1 {
|
||||
result = append(result, cookies[0])
|
||||
addedNames[cookieName] = true
|
||||
continue
|
||||
}
|
||||
|
||||
// 有多个同名 cookie,选择最匹配的(path 最长且匹配请求路径的)
|
||||
var bestMatch *http.Cookie
|
||||
bestMatchLen := -1
|
||||
|
||||
for _, cookie := range cookies {
|
||||
cookiePath := cookie.Path
|
||||
if cookiePath == "" {
|
||||
cookiePath = "/"
|
||||
}
|
||||
|
||||
// 检查 path 是否匹配
|
||||
if pathMatch(requestPath, cookiePath) {
|
||||
if len(cookiePath) > bestMatchLen {
|
||||
bestMatch = cookie
|
||||
bestMatchLen = len(cookiePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果找到匹配的,添加到结果中
|
||||
if bestMatch != nil {
|
||||
result = append(result, bestMatch)
|
||||
addedNames[cookieName] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 再添加没有 host 的全局 cookie(避免重复)
|
||||
if globalCookies, exists := p.hostMap[""]; exists {
|
||||
for cookieName, cookies := range globalCookies {
|
||||
// 如果该名称的 cookie 已经从 host 中添加了,跳过
|
||||
if addedNames[cookieName] {
|
||||
continue
|
||||
}
|
||||
|
||||
// 如果只有一个 cookie,直接返回
|
||||
if len(cookies) == 1 {
|
||||
result = append(result, cookies[0])
|
||||
continue
|
||||
}
|
||||
|
||||
// 有多个同名 cookie,选择最匹配的
|
||||
var bestMatch *http.Cookie
|
||||
bestMatchLen := -1
|
||||
|
||||
for _, cookie := range cookies {
|
||||
cookiePath := cookie.Path
|
||||
if cookiePath == "" {
|
||||
cookiePath = "/"
|
||||
}
|
||||
|
||||
if pathMatch(requestPath, cookiePath) {
|
||||
if len(cookiePath) > bestMatchLen {
|
||||
bestMatch = cookie
|
||||
bestMatchLen = len(cookiePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if bestMatch != nil {
|
||||
result = append(result, bestMatch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// pathMatch 检查请求路径是否匹配 cookie 的 path
|
||||
// 根据 RFC 6265 的规则
|
||||
func pathMatch(requestPath, cookiePath string) bool {
|
||||
if requestPath == cookiePath {
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.HasPrefix(requestPath, cookiePath) {
|
||||
// cookiePath 以 / 结尾,或者 requestPath 的下一个字符是 /
|
||||
if strings.HasSuffix(cookiePath, "/") {
|
||||
return true
|
||||
}
|
||||
if len(requestPath) > len(cookiePath) && requestPath[len(cookiePath)] == '/' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *PersistCookieJar) Serialize() string {
|
||||
@@ -83,7 +202,29 @@ func (p *PersistCookieJar) Deserialize(content string) {
|
||||
var hostMap hostMap
|
||||
err := sonic.Unmarshal([]byte(content), &hostMap)
|
||||
|
||||
utils.OkOrPanic(err)
|
||||
// 如果反序列化失败,尝试从旧格式转换
|
||||
if err != nil {
|
||||
// 旧格式: map[string]map[string]*http.Cookie
|
||||
type oldCookieMap map[string]*http.Cookie
|
||||
type oldHostMap map[string]oldCookieMap
|
||||
|
||||
var oldMap oldHostMap
|
||||
oldErr := sonic.Unmarshal([]byte(content), &oldMap)
|
||||
if oldErr != nil {
|
||||
// 两种格式都失败,抛出原始错误
|
||||
utils.OkOrPanic(err)
|
||||
return
|
||||
}
|
||||
|
||||
// 转换旧格式到新格式
|
||||
hostMap = make(map[string]cookieMap)
|
||||
for host, cookies := range oldMap {
|
||||
hostMap[host] = make(map[string][]*http.Cookie)
|
||||
for name, cookie := range cookies {
|
||||
hostMap[host][name] = []*http.Cookie{cookie}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p.hostMap = hostMap
|
||||
}
|
||||
@@ -116,7 +257,31 @@ func (p *PersistCookieJar) DeserializeBytes(content []byte) {
|
||||
var hostMap hostMap
|
||||
err := gob.NewDecoder(retReader).
|
||||
Decode(&hostMap)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
// 如果反序列化失败,尝试从旧格式转换
|
||||
if err != nil {
|
||||
// 旧格式: map[string]map[string]*http.Cookie
|
||||
type oldCookieMap map[string]*http.Cookie
|
||||
type oldHostMap map[string]oldCookieMap
|
||||
|
||||
retReader = bytes.NewBuffer(content)
|
||||
var oldMap oldHostMap
|
||||
oldErr := gob.NewDecoder(retReader).Decode(&oldMap)
|
||||
if oldErr != nil {
|
||||
// 两种格式都失败,抛出原始错误
|
||||
utils.OkOrPanic(err)
|
||||
return
|
||||
}
|
||||
|
||||
// 转换旧格式到新格式
|
||||
hostMap = make(map[string]cookieMap)
|
||||
for host, cookies := range oldMap {
|
||||
hostMap[host] = make(map[string][]*http.Cookie)
|
||||
for name, cookie := range cookies {
|
||||
hostMap[host][name] = []*http.Cookie{cookie}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p.hostMap = hostMap
|
||||
}
|
||||
@@ -125,10 +290,63 @@ func (p *PersistCookieJar) DeserializeReader(reader io.Reader) {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
||||
var hostMap hostMap
|
||||
err := gob.NewDecoder(reader).
|
||||
Decode(&hostMap)
|
||||
// 先读取所有数据,以便在失败时可以重试
|
||||
data, err := io.ReadAll(reader)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
var hostMap hostMap
|
||||
err = gob.NewDecoder(bytes.NewBuffer(data)).Decode(&hostMap)
|
||||
|
||||
// 如果反序列化失败,尝试从旧格式转换
|
||||
if err != nil {
|
||||
// 旧格式: map[string]map[string]*http.Cookie
|
||||
type oldCookieMap map[string]*http.Cookie
|
||||
type oldHostMap map[string]oldCookieMap
|
||||
|
||||
var oldMap oldHostMap
|
||||
oldErr := gob.NewDecoder(bytes.NewBuffer(data)).Decode(&oldMap)
|
||||
if oldErr != nil {
|
||||
// 两种格式都失败,抛出原始错误
|
||||
utils.OkOrPanic(err)
|
||||
return
|
||||
}
|
||||
|
||||
// 转换旧格式到新格式
|
||||
hostMap = make(map[string]cookieMap)
|
||||
for host, cookies := range oldMap {
|
||||
hostMap[host] = make(map[string][]*http.Cookie)
|
||||
for name, cookie := range cookies {
|
||||
hostMap[host][name] = []*http.Cookie{cookie}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p.hostMap = hostMap
|
||||
}
|
||||
|
||||
// RemoveCookieInfo 将所有 host 的 cookie 移除 host 信息,存储为全局 cookie
|
||||
func (p *PersistCookieJar) RemoveCookieInfo() {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
||||
// 确保全局 cookie map 存在
|
||||
if _, exists := p.hostMap[""]; !exists {
|
||||
p.hostMap[""] = make(cookieMap)
|
||||
}
|
||||
|
||||
// 将所有 host 的 cookie 移动到全局
|
||||
for host, hostCookies := range p.hostMap {
|
||||
// 跳过全局 cookie 本身
|
||||
if host == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 将该 host 的所有 cookie 移动到全局
|
||||
for cookieName, cookies := range hostCookies {
|
||||
p.hostMap[""][cookieName] = cookies
|
||||
}
|
||||
|
||||
// 删除该 host 的 cookie
|
||||
delete(p.hostMap, host)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,3 +114,10 @@ func TestPersistCookieJar_DeserializeReader(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeseralizeCookie(t *testing.T) {
|
||||
var str = `{"cdpc.campusphere.net":{"MOD_AUTH_CAS":{"Name":"MOD_AUTH_CAS","Value":"ST-iap:1021988086918169:ST:8d7fe60b-dc7d-4fcd-b26b-5517e3ca1926:20251113162807","Quoted":false,"Path":"/","Domain":"","Expires":"0001-01-01T00:00:00Z","RawExpires":"","MaxAge":0,"Secure":false,"HttpOnly":true,"SameSite":0,"Partitioned":false,"Raw":"MOD_AUTH_CAS=ST-iap:1021988086918169:ST:8d7fe60b-dc7d-4fcd-b26b-5517e3ca1926:20251113162807; path=/; Httponly","Unparsed":null},"HWWAFSESID":{"Name":"HWWAFSESID","Value":"88d84731cf506df0bf","Quoted":false,"Path":"/","Domain":"","Expires":"0001-01-01T00:00:00Z","RawExpires":"","MaxAge":0,"Secure":false,"HttpOnly":false,"SameSite":0,"Partitioned":false,"Raw":"HWWAFSESID=88d84731cf506df0bf; path=/","Unparsed":null},"HWWAFSESTIME":{"Name":"HWWAFSESTIME","Value":"1763022486728","Quoted":false,"Path":"/","Domain":"","Expires":"0001-01-01T00:00:00Z","RawExpires":"","MaxAge":0,"Secure":false,"HttpOnly":false,"SameSite":0,"Partitioned":false,"Raw":"HWWAFSESTIME=1763022486728; path=/","Unparsed":null},"CONVERSATION":{"Name":"CONVERSATION","Value":"iap-1021988086918169-CONV-83bee566-7010-4ef6-ad17-4b9eebcccdf8","Quoted":false,"Path":"/iap","Domain":"","Expires":"0001-01-01T00:00:00Z","RawExpires":"","MaxAge":0,"Secure":false,"HttpOnly":true,"SameSite":0,"Partitioned":false,"Raw":"CONVERSATION=iap-1021988086918169-CONV-83bee566-7010-4ef6-ad17-4b9eebcccdf8; Path=/iap; HttpOnly","Unparsed":null},"CASTGC":{"Name":"CASTGC","Value":"iap-1021988086918169-TGT-717aa5db-92cb-46ac-a178-64b630946f43","Quoted":false,"Path":"/iap","Domain":"","Expires":"0001-01-01T00:00:00Z","RawExpires":"","MaxAge":0,"Secure":true,"HttpOnly":true,"SameSite":4,"Partitioned":false,"Raw":"CASTGC=iap-1021988086918169-TGT-717aa5db-92cb-46ac-a178-64b630946f43; Path=/iap; HttpOnly; Secure; SameSite=None","Unparsed":null}}}`
|
||||
jar := NewPersistCookieJar()
|
||||
jar.Deserialize(str)
|
||||
t.Log(jar)
|
||||
}
|
||||
|
||||
@@ -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,12 @@
|
||||
package cryptoutil
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
// MD5 对输入字符串进行 MD5 加密,返回 32 位小写十六进制字符串
|
||||
func MD5(input string) string {
|
||||
hash := md5.Sum([]byte(input))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package cryptoutil
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"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
|
||||
}
|
||||
|
||||
// NewRSAKeyPairFromPublicKey 从 X.509 格式的 base64 公钥字符串创建 RSA 密钥对
|
||||
// publicKeyBase64: X.509 DER 格式的 base64 编码公钥字符串
|
||||
func NewRSAKeyPairFromPublicKey(publicKeyBase64 string) (*RSAKeyPair, error) {
|
||||
// 解码 base64
|
||||
derBytes, err := base64.StdEncoding.DecodeString(publicKeyBase64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode base64: %w", err)
|
||||
}
|
||||
|
||||
// 解析 X.509 格式的公钥
|
||||
pubKey, err := x509.ParsePKIXPublicKey(derBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse public key: %w", err)
|
||||
}
|
||||
|
||||
// 转换为 RSA 公钥
|
||||
rsaPubKey, ok := pubKey.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("not an RSA public key")
|
||||
}
|
||||
|
||||
// 创建密钥对
|
||||
return &RSAKeyPair{
|
||||
n: rsaPubKey.N,
|
||||
e: big.NewInt(int64(rsaPubKey.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,87 @@
|
||||
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))
|
||||
}
|
||||
|
||||
func TestNewRSAKeyPairFromPublicKey(t *testing.T) {
|
||||
// 使用你提供的 X.509 格式的 base64 公钥
|
||||
publicKey := "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCpbRy8ZoyQvRPpUDIXycglTwVDcYrLcv7P9HI4E7/TAPZiI3GN0ckTUVVWRvoo/re3uCOOJK+F+Ufh19XiRIdoPYCULsSyEjcLLiyAS09mVOzhAQco84E/lM24T7rRTVID7LIgWPewyN8Nd5vzHET4Q3qTthoL8n82BwC1iXuVQIDAQAB"
|
||||
|
||||
keyPair, err := NewRSAKeyPairFromPublicKey(publicKey)
|
||||
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("密文为空")
|
||||
}
|
||||
|
||||
// 验证密文长度(1024位密钥应该产生128字节 = 256个十六进制字符)
|
||||
expectedHexLen := (keyPair.GetModulus().BitLen() + 7) / 8 * 2
|
||||
if len(ciphertext) != expectedHexLen {
|
||||
t.Errorf("密文长度不正确: 期望 %d, 实际 %d", expectedHexLen, len(ciphertext))
|
||||
}
|
||||
|
||||
t.Logf("密钥长度: %d bits", keyPair.GetModulus().BitLen())
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
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"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/samber/oops"
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
// 南阳理工学院登录实现 RSA加密 & 验证码必出
|
||||
type CpdailyAhjzuLogin struct {
|
||||
client *resty.Client
|
||||
}
|
||||
|
||||
func (c CpdailyAhjzuLogin) Login(ctx context.Context, schoolDetail *school.SchoolDetailInfo, username, password string) {
|
||||
jar := cookiejar.NewPersistCookieJar()
|
||||
|
||||
c.client.SetCookieJar(jar).SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||
|
||||
// 获取合法的登录URL
|
||||
loginUrl := getCasValidLoginUrl(schoolDetail)
|
||||
|
||||
// 请求登录页面并获取真正的登录URL
|
||||
loginPageResponse := getAhjzuLoginPageResponse(ctx, c.client, loginUrl)
|
||||
|
||||
defer loginPageResponse.Body.Close()
|
||||
loginPageBytes, err := io.ReadAll(loginPageResponse.Body)
|
||||
utils.OkOrPanic(err)
|
||||
loginResponseUrl := loginPageResponse.RawResponse.Request.URL
|
||||
|
||||
// 解析登录页面
|
||||
loginPage, err := goquery.NewDocumentFromReader(bytes.NewReader(loginPageBytes))
|
||||
utils.OkOrPanic(err)
|
||||
// 判断登录类型
|
||||
var (
|
||||
allInputs = map[string]string{}
|
||||
fm1Node = loginPage.Find("#credentials").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
|
||||
|
||||
// 处理密码
|
||||
allInputs["password"] = cryptoutil.MD5(password)
|
||||
|
||||
// 解决常规图片验证码
|
||||
captchaUrl := getAhjzuCaptchaUrl(loginResponseUrl)
|
||||
captchaResult := ResolveOcrCaptcha(ctx, captchaUrl, c.client)
|
||||
|
||||
allInputs["validateCode"] = captchaResult
|
||||
|
||||
// 登录提交
|
||||
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 getAhjzuLoginPageResponse(ctx context.Context, client *resty.Client, loginUrl string) *resty.Response {
|
||||
res1, err := client.R().Get(loginUrl)
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.
|
||||
With("loginUrl", loginUrl).
|
||||
WithContext(ctx).
|
||||
Wrap(err)
|
||||
})
|
||||
if res1.StatusCode() != http.StatusFound && res1.StatusCode() != http.StatusMovedPermanently {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "今日校园登录失败: 登录页面获取失败"), func(err error) error {
|
||||
return oops.
|
||||
With("loginUrl", loginUrl).
|
||||
With("statusCode", res1.StatusCode()).
|
||||
WithContext(ctx).
|
||||
Wrap(err)
|
||||
})
|
||||
}
|
||||
location := res1.RawResponse.Header.Get("Location")
|
||||
res2, err := client.R().Get(location)
|
||||
if res2.StatusCode() != http.StatusFound && res2.StatusCode() != http.StatusMovedPermanently {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "今日校园登录失败: 登录页面获取失败"), func(err error) error {
|
||||
return oops.
|
||||
With("loginUrl", loginUrl).
|
||||
With("statusCode", res2.StatusCode()).
|
||||
WithContext(ctx).
|
||||
Wrap(err)
|
||||
})
|
||||
}
|
||||
location = res2.RawResponse.Header.Get("Location")
|
||||
res3, err := client.R().Get(location)
|
||||
if res3.StatusCode() != http.StatusFound && res3.StatusCode() != http.StatusMovedPermanently {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "今日校园登录失败: 登录页面获取失败"), func(err error) error {
|
||||
return oops.
|
||||
With("loginUrl", loginUrl).
|
||||
With("statusCode", res3.StatusCode()).
|
||||
WithContext(ctx).
|
||||
Wrap(err)
|
||||
})
|
||||
}
|
||||
location = res3.RawResponse.Header.Get("Location")
|
||||
res4, err := client.R().Get(location)
|
||||
if res4.StatusCode() != http.StatusFound && res3.StatusCode() != http.StatusMovedPermanently {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "今日校园登录失败: 登录页面获取失败"), func(err error) error {
|
||||
return oops.
|
||||
With("loginUrl", loginUrl).
|
||||
With("statusCode", res4.StatusCode()).
|
||||
WithContext(ctx).
|
||||
Wrap(err)
|
||||
})
|
||||
}
|
||||
location = res4.RawResponse.Header.Get("Location")
|
||||
res5, err := client.R().Get(location)
|
||||
if err != nil {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "今日校园登录失败: 登录页面获取失败"), func(err error) error {
|
||||
return oops.
|
||||
With("loginUrl", loginUrl).
|
||||
With("statusCode", res4.StatusCode()).
|
||||
WithContext(ctx).
|
||||
Wrap(err)
|
||||
})
|
||||
}
|
||||
return res5
|
||||
}
|
||||
func getAhjzuCaptchaUrl(url *url.URL) string {
|
||||
return fmt.Sprintf("%s://%s/sso/code/code.jsp", url.Scheme, url.Host)
|
||||
}
|
||||
|
||||
func (c CpdailyAhjzuLogin) GetCookieJar() http.CookieJar {
|
||||
return c.client.CookieJar()
|
||||
}
|
||||
|
||||
func (c CpdailyAhjzuLogin) GetCookieString() string {
|
||||
cookieJar := c.GetCookieJar()
|
||||
serializableJar := cookieJar.(*cookiejar.PersistCookieJar)
|
||||
return serializableJar.Serialize()
|
||||
}
|
||||
|
||||
func (c CpdailyAhjzuLogin) GetClient() *resty.Client {
|
||||
return c.client
|
||||
}
|
||||
|
||||
func NewAhjzu(ctx context.Context) *CpdailyAhjzuLogin {
|
||||
return &CpdailyAhjzuLogin{
|
||||
client: httpclient.NewCpdailyClient(),
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,15 @@ import (
|
||||
|
||||
func GetCpdailyLoginBySchoolInfo(ctx context.Context, info *school.SchoolDetailInfo) CpdailyLogin {
|
||||
switch info.Id {
|
||||
// 南昌理工学院
|
||||
case "d6e8f2ca-10b2-4599-bdb8-4b33d419658d":
|
||||
return NewNut(ctx)
|
||||
// 安徽建筑大学
|
||||
case "ahjzu":
|
||||
return NewAhjzu(ctx)
|
||||
// 河北石油职业技术大学
|
||||
case "01e270d1-00e2-4ce9-b0af-5d5fbd462b8a":
|
||||
return NewIap(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"
|
||||
@@ -37,7 +39,7 @@ type CpdailyCasLogin struct {
|
||||
|
||||
func NewCas(ctx context.Context) CpdailyLogin {
|
||||
return &CpdailyCasLogin{
|
||||
client: httpclient.Cpdaily.Clone(ctx),
|
||||
client: httpclient.NewCpdailyClient(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
@@ -155,11 +157,11 @@ func (c *CpdailyCasLogin) Login(ctx context.Context, schoolDetail *school.School
|
||||
allInputs[name] = value
|
||||
}
|
||||
} else {
|
||||
utils.OkOrPanic(oops.With("schoolDetail", schoolDetail).
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "页面中未找到登录表单"), oops.With("schoolDetail", schoolDetail).
|
||||
With("username", username).
|
||||
With("password", password).
|
||||
WithContext(ctx).
|
||||
Wrap(errors.NewBusinessError(503, "页面中未找到登录表单")))
|
||||
Wrap)
|
||||
}
|
||||
|
||||
// 修改部分参数
|
||||
@@ -189,19 +191,36 @@ func (c *CpdailyCasLogin) Login(ctx context.Context, schoolDetail *school.School
|
||||
allInputs["password"] = encryptCasPassword(password, salt)
|
||||
}
|
||||
|
||||
// 声明需要滑块验证码的学校id
|
||||
var SliderCaptchaIds = []string{"1018764144934965", "sdhyxy"}
|
||||
// 判断是否需要验证码
|
||||
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().
|
||||
@@ -270,7 +289,7 @@ func (c *CpdailyCasLogin) ConfirmAdd(ctx context.Context, sc *school.SchoolDetai
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
// 提交确认添加
|
||||
client := httpclient.Cpdaily.Clone(ctx)
|
||||
client := httpclient.NewCpdailyClient()
|
||||
client.SetCookieJar(cookieJar)
|
||||
post, err := client.R().SetFormData(map[string]string{
|
||||
"service": service,
|
||||
@@ -334,7 +353,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 +418,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).
|
||||
@@ -431,11 +450,16 @@ func submitCasKickOut(ctx context.Context, requestUrl string, doc *goquery.Docum
|
||||
}
|
||||
|
||||
func getCasValidLoginUrl(info *school.SchoolDetailInfo) string {
|
||||
if strings.Contains(info.AmpUrl, "campusphere") {
|
||||
// 特殊学校单独处理
|
||||
// 甘肃农业大学
|
||||
if info.Id == "gsau" {
|
||||
return info.IdsUrl
|
||||
}
|
||||
if strings.Contains(info.AmpUrl, "campusphere") || strings.Contains(info.AmpUrl, "wec-portal-mobile") {
|
||||
return info.AmpUrl
|
||||
} else if strings.Contains(info.AmpUrl2, "campusphere") {
|
||||
} else if strings.Contains(info.AmpUrl2, "campusphere") || strings.Contains(info.AmpUrl2, "wec-portal-mobile") {
|
||||
return info.AmpUrl2
|
||||
} else if strings.Contains(info.AmpUrl3, "campusphere") {
|
||||
} else if strings.Contains(info.AmpUrl3, "campusphere") || strings.Contains(info.AmpUrl3, "wec-portal-mobile") {
|
||||
return info.AmpUrl3
|
||||
}
|
||||
|
||||
@@ -529,3 +553,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,12 @@ import (
|
||||
)
|
||||
|
||||
func ActivateSession(ctx context.Context, host string, client *resty.Client) {
|
||||
client = client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
|
||||
// 处理cookie,干掉path
|
||||
jar := client.CookieJar()
|
||||
realJar := jar.(*cookiejar.PersistCookieJar)
|
||||
realJar.RemoveCookieInfo()
|
||||
client.SetCookieJar(realJar)
|
||||
client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
|
||||
|
||||
host = strings.TrimSuffix(host, "/")
|
||||
|
||||
@@ -78,19 +83,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()
|
||||
@@ -105,7 +110,8 @@ func CookieEnabled(ctx context.Context, host string, client *resty.Client) bool
|
||||
func GetClient(ctx context.Context, cookie string) *resty.Client {
|
||||
cookieJar := cookiejar.NewPersistCookieJar()
|
||||
cookieJar.Deserialize(cookie)
|
||||
client := httpclient.Cpdaily.Clone(ctx)
|
||||
// 不使用 Clone,创建全新的独立 client 实例避免状态共享
|
||||
client := httpclient.NewCpdailyClient()
|
||||
client.SetCookieJar(cookieJar)
|
||||
return client
|
||||
}
|
||||
|
||||
@@ -5,25 +5,29 @@ import (
|
||||
"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/samber/oops"
|
||||
"github.com/tidwall/gjson"
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
const iapRsaPubKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCpbRy8ZoyQvRPpUDIXycglTwVDcYrLcv7P9HI4E7/TAPZiI3GN0ckTUVVWRvoo/re3uCOOJK+F+Ufh19XiRIdoPYCULsSyEjcLLiyAS09mVOzhAQco84E/lM24T7rRTVID7LIgWPewyN8Nd5vzHET4Q3qTthoL8n82BwC1iXuVQIDAQAB"
|
||||
|
||||
type CpdailyIapLogin struct {
|
||||
client *resty.Client
|
||||
}
|
||||
|
||||
func NewIap(ctx context.Context) *CpdailyIapLogin {
|
||||
return &CpdailyIapLogin{
|
||||
client: httpclient.Cpdaily.Clone(ctx),
|
||||
client: httpclient.NewCpdailyClient(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,14 +38,13 @@ 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().
|
||||
WithContext(ctx).
|
||||
SetBody(map[string]any{}).
|
||||
Post(fmt.Sprintf("%s/iap/security/lt", loginUrl))
|
||||
|
||||
// 获取基础lt
|
||||
preLoginUrl := loginUrl + "/portal/login"
|
||||
c.client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
|
||||
loginRedirectResponse, err := c.client.R().Get(preLoginUrl)
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.
|
||||
In("CpdailyIapLogin.Login").
|
||||
@@ -50,6 +53,25 @@ func (c *CpdailyIapLogin) Login(ctx context.Context, schoolDetail *school.School
|
||||
With("password", password).
|
||||
Wrap(err)
|
||||
})
|
||||
u := loginRedirectResponse.RawResponse.Request.URL
|
||||
oldLt := strings.Split(u.String(), "=")[1]
|
||||
c.client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||
ltInfoResponse, err := c.client.R().
|
||||
WithContext(ctx).
|
||||
SetFormData(map[string]string{
|
||||
"lt": oldLt,
|
||||
}).
|
||||
Post(fmt.Sprintf("%s/iap/security/lt", loginUrl))
|
||||
rsa, _ := cryptoutil.NewRSAKeyPairFromPublicKey(iapRsaPubKey)
|
||||
password, _ = rsa.Encrypt(password)
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.
|
||||
In("CpdailyIapLogin.Login").
|
||||
With("schoolDetail", schoolDetail).
|
||||
With("username", username).
|
||||
With("password", "{rsa}"+password).
|
||||
Wrap(err)
|
||||
})
|
||||
|
||||
defer ltInfoResponse.Body.Close()
|
||||
|
||||
@@ -64,8 +86,9 @@ func (c *CpdailyIapLogin) Login(ctx context.Context, schoolDetail *school.School
|
||||
"rememberMe": "true",
|
||||
"dllt": "",
|
||||
"mobile": "",
|
||||
"captcha": "",
|
||||
"username": username,
|
||||
"password": password,
|
||||
"password": "{rsa}" + password,
|
||||
}
|
||||
|
||||
if isIapNeedCaptcha(ctx, loginUrl, username, c.client) {
|
||||
@@ -103,7 +126,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 +146,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 {
|
||||
@@ -142,9 +165,18 @@ func (c *CpdailyIapLogin) Login(ctx context.Context, schoolDetail *school.School
|
||||
case "CAPTCHA_NOTMATCH":
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "Iap登录失败, 验证码识别失败"))
|
||||
case "FAIL_UPNOTMATCH":
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "Iap登录失败, 用户名或密码错误"))
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "Iap登录失败, 用户名或密码错误"), func(err error) error {
|
||||
return oops.With("username", username).
|
||||
With("password", password).
|
||||
With("userId", utils.GetUserIdNotPanic(ctx)).
|
||||
In("Cpdaily.Login").New("Iap登录失败, 用户名或密码错误")
|
||||
})
|
||||
default:
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, fmt.Sprintf("Iap登录失败, 状态识别码: %s", resultCode)))
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, fmt.Sprintf("Iap登录失败, 状态识别码: %s", resultCode)),
|
||||
oops.With("username", username).
|
||||
With("password", password).
|
||||
With("userId", utils.GetUserIdNotPanic(ctx)).
|
||||
In("Cpdaily.Login").Wrap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.NewCpdailyClient(),
|
||||
}
|
||||
}
|
||||
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,84 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"image"
|
||||
"io"
|
||||
"likedream-goframe/internal/application/httpclient"
|
||||
"likedream-goframe/internal/common/cst"
|
||||
"likedream-goframe/internal/utils"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"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().
|
||||
byteData := DownCode2Byte(ctx, url, client)
|
||||
//img, err := BytesToImage(byteData)
|
||||
//utils.OkOrPanic(err, func(err error) error {
|
||||
// return oops.
|
||||
// With("BytesToImage", url).
|
||||
// WithContext(ctx).
|
||||
// Wrap(err)
|
||||
//})
|
||||
//data := ddddocr.AutoOCRVerification(img)
|
||||
//return data
|
||||
return ApiAutoOCRVerification(Bytes2Base64(byteData))
|
||||
}
|
||||
|
||||
func Bytes2Base64(data []byte) string {
|
||||
return base64.StdEncoding.EncodeToString(data)
|
||||
}
|
||||
|
||||
func ApiAutoOCRVerification(data string) string {
|
||||
ctx := gctx.GetInitCtx()
|
||||
baseUrl := g.Config().MustGetWithEnv(ctx, cst.ConfigCaptchaBaseUrl).String() + "/ocr"
|
||||
client := httpclient.Cpdaily.Clone(ctx)
|
||||
client.SetDoNotParseResponse(false)
|
||||
post, err := client.R().SetBody(map[string]any{
|
||||
"base64": data,
|
||||
}).Post(baseUrl)
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.
|
||||
With("AutoOCRVerification", baseUrl).
|
||||
WithContext(ctx).
|
||||
Wrap(err)
|
||||
})
|
||||
result := post.String()
|
||||
return gjson.Get(result, "data").String()
|
||||
}
|
||||
|
||||
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 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")
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
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服务返回数据为空"))
|
||||
}
|
||||
|
||||
return data
|
||||
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,90 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"likedream-goframe/internal/application/httpclient"
|
||||
"likedream-goframe/internal/common/cst"
|
||||
"likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/utils"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/samber/oops"
|
||||
"github.com/tidwall/gjson"
|
||||
"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)
|
||||
width := ApiVerifySliderWidthByBase64(captcha.BigImage, captcha.SmallImage)
|
||||
res2, err2 := client.R().
|
||||
WithContext(ctx).
|
||||
SetFormData(map[string]string{
|
||||
"canvasLength": "280",
|
||||
"moveLength": 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, "滑块解析错误")))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func ApiVerifySliderWidthByBase64(bigImage string, smallImage string) string {
|
||||
ctx := gctx.GetInitCtx()
|
||||
baseUrl := g.Config().MustGetWithEnv(ctx, cst.ConfigCaptchaBaseUrl).String() + "/slider"
|
||||
client := httpclient.Cpdaily.Clone(ctx)
|
||||
client.SetDoNotParseResponse(false)
|
||||
post, err := client.R().SetBody(map[string]any{
|
||||
"big_image": bigImage,
|
||||
"small_image": smallImage,
|
||||
}).Post(baseUrl)
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.
|
||||
With("AutoOCRVerification", baseUrl).
|
||||
WithContext(ctx).
|
||||
Wrap(err)
|
||||
})
|
||||
result := post.String()
|
||||
return gjson.Get(result, "data").String()
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package map_info
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"likedream-goframe/internal/application/httpclient"
|
||||
"likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/utils"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/samber/oops"
|
||||
)
|
||||
|
||||
func SearchAddressLongitudeAndLatitude(ctx context.Context, address string) []MapPoi {
|
||||
client := httpclient.Cpdaily.Clone(ctx).SetDoNotParseResponse(false)
|
||||
// 首先访问获取ak
|
||||
response, err := client.R().Get("https://lbsyun.baidu.com/jsdemo/demo/webgl0_5.htm")
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.WithContext(ctx).With("url", "https://lbsyun.baidu.com/jsdemo/demo/webgl0_5.htm").In("SearchAddressLongitudeAndLatitude.GetAk").Wrap(err)
|
||||
})
|
||||
ak := ParseAkString(response.String())
|
||||
timestamp := fmt.Sprintf("%d", time.Now().UnixMilli())
|
||||
mapResponse, err := client.R().
|
||||
SetQueryParams(map[string]string{
|
||||
"qt": "placesug",
|
||||
"query": address,
|
||||
"callback": "BMapGL._rd._cbk25241",
|
||||
"region": "131",
|
||||
"city_limit": "false",
|
||||
"t": timestamp,
|
||||
"from": "jsapi",
|
||||
"ak": ak,
|
||||
"v": "gl",
|
||||
"timeStamp": timestamp,
|
||||
}).
|
||||
SetHeaders(map[string]string{
|
||||
"Accept": "*/*",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||
"Connection": "keep-alive",
|
||||
"Referer": "https://lbsyun.baidu.com/",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36",
|
||||
}).
|
||||
Get("https://api.map.baidu.com/")
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.WithContext(ctx).With("address", address).In("SearchAddressLongitudeAndLatitude.QueryMap").Wrap(err)
|
||||
})
|
||||
|
||||
// 从JSONP响应中提取JSON数据
|
||||
str := ParseJsonpToJson(mapResponse.String())
|
||||
var jsonData MapJSONData
|
||||
sonic.Unmarshal([]byte(str), &jsonData)
|
||||
return jsonData.Result.PoiList
|
||||
}
|
||||
|
||||
// 正则提取ak // <script type="text/javascript" src="//api.map.baidu.com/api?type=webgl&v=1.0&ak=E4805d16520de693a3fe707cdc962045"></script>
|
||||
func ParseAkString(data string) string {
|
||||
reg := regexp.MustCompile(`ak=([^"]+)`)
|
||||
matches := reg.FindStringSubmatch(data)
|
||||
if len(matches) < 2 {
|
||||
utils.OkOrPanic(errors.NewBusinessError(500, "获取ak出错"))
|
||||
}
|
||||
return matches[1]
|
||||
}
|
||||
|
||||
// 从JSONP响应中提取JSON数据
|
||||
// 输入: /**/BMapGL._rd._cbk25241 && BMapGL._rd._cbk25241({...})
|
||||
// 输出: {...}
|
||||
func ParseJsonpToJson(jsonpData string) string {
|
||||
// 匹配 callback(json) 格式,提取括号中的内容
|
||||
reg := regexp.MustCompile(`\((\{.*\})\)`)
|
||||
matches := reg.FindStringSubmatch(jsonpData)
|
||||
if len(matches) < 2 {
|
||||
utils.OkOrPanic(errors.NewBusinessError(500, "解析JSONP数据出错"))
|
||||
}
|
||||
return matches[1]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package map_info
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
)
|
||||
|
||||
func TestMapSearch(t *testing.T) {
|
||||
res := SearchAddressLongitudeAndLatitude(gctx.New(), "河北石油职业技术大学c座")
|
||||
marshal, _ := json.Marshal(res)
|
||||
t.Log(string(marshal))
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package map_info
|
||||
|
||||
type MapJSONData struct {
|
||||
Status int `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Result MapResult `json:"result"`
|
||||
}
|
||||
type MapLocation struct {
|
||||
Lat float64 `json:"lat"`
|
||||
Lng float64 `json:"lng"`
|
||||
}
|
||||
type MapPoi struct {
|
||||
Name string `json:"name"`
|
||||
Location MapLocation `json:"location"`
|
||||
UID string `json:"uid"`
|
||||
Province string `json:"province"`
|
||||
City string `json:"city"`
|
||||
District string `json:"district"`
|
||||
Business string `json:"business"`
|
||||
Cityid string `json:"cityid"`
|
||||
Tag string `json:"tag"`
|
||||
Address string `json:"address"`
|
||||
Children []interface{} `json:"children"`
|
||||
Adcode string `json:"adcode"`
|
||||
}
|
||||
type MapResult struct {
|
||||
Q string `json:"q"`
|
||||
PoiList []MapPoi `json:"poi_list"`
|
||||
}
|
||||
@@ -1,22 +1,27 @@
|
||||
package school
|
||||
|
||||
import "C"
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
cryptoutil2 "likedream-goframe/internal/application/cpdaily/cryptoutil"
|
||||
"likedream-goframe/internal/application/httpclient"
|
||||
cst2 "likedream-goframe/internal/common/cst"
|
||||
"likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/framework/redis"
|
||||
"likedream-goframe/internal/utils"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/samber/oops"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
var lock = sync.RWMutex{}
|
||||
|
||||
func GetSchoolList(ctx context.Context) []CpdailySchoolBriefInfo {
|
||||
response, err := httpclient.Cpdaily.R().
|
||||
response, err := httpclient.Cpdaily.Clone(ctx).R().
|
||||
WithContext(ctx).
|
||||
Get("https://static.campushoy.com/apicache/tenantListSort")
|
||||
|
||||
@@ -52,7 +57,17 @@ func GetSchoolList(ctx context.Context) []CpdailySchoolBriefInfo {
|
||||
}
|
||||
|
||||
func GetSchoolDetail(ctx context.Context, tenantId string) SchoolDetailInfo {
|
||||
response, err := httpclient.Cpdaily.R().Clone(ctx).
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
client := redis.GetClient()
|
||||
redisResult := client.Get(ctx, cst2.RedisSchoolInfo+tenantId)
|
||||
if redisResult.Err() == nil {
|
||||
var ret SchoolDetailInfo
|
||||
err := sonic.Unmarshal([]byte(redisResult.Val()), &ret)
|
||||
utils.OkOrPanic(err)
|
||||
return ret
|
||||
}
|
||||
response, err := httpclient.Cpdaily.Clone(ctx).R().
|
||||
SetHeaders(map[string]string{
|
||||
"deviceType": "2",
|
||||
"CpdailyClientType": "CPDAILY",
|
||||
@@ -89,6 +104,6 @@ func GetSchoolDetail(ctx context.Context, tenantId string) SchoolDetailInfo {
|
||||
WithContext(ctx).
|
||||
Wrap(errors.NewBusinessError(503, "学校ID不存在").Decorate("empty school detail")))
|
||||
}
|
||||
|
||||
client.Set(ctx, cst2.RedisSchoolInfo+tenantId, ret[0], time.Duration(cst2.RedisSchoolInfoExpire)*time.Second)
|
||||
return ret[0]
|
||||
}
|
||||
|
||||
@@ -1,181 +1,182 @@
|
||||
package submitcaptcha
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"likedream-goframe/internal/application/cpdaily/types"
|
||||
"likedream-goframe/internal/application/recognizer/classifier"
|
||||
"likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/utils"
|
||||
"strings"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
type AIResolver struct {
|
||||
client *resty.Client
|
||||
host string
|
||||
deviceId string
|
||||
taskType types.TaskType
|
||||
}
|
||||
|
||||
func NewAIResolver(client *resty.Client, host, deviceId string, taskType types.TaskType) SubmitCaptchaResolver {
|
||||
return &AIResolver{
|
||||
client: client,
|
||||
host: host,
|
||||
deviceId: deviceId,
|
||||
taskType: taskType,
|
||||
}
|
||||
}
|
||||
func (r *AIResolver) GetValidationInfo(ctx context.Context) ValidationInfo {
|
||||
// GetValidationInfo 获取验证信息
|
||||
reqData := map[string]string{
|
||||
"deviceId": r.deviceId,
|
||||
}
|
||||
|
||||
resp, err := r.client.R().
|
||||
SetBody(reqData).
|
||||
Post(r.getValidateUrl())
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
apiResp := utils.ReadJson[ApiResponse](resp.Body)
|
||||
|
||||
if apiResp.Datas == nil {
|
||||
message := "获取提交验证码信息失败"
|
||||
if apiResp.Message != "" {
|
||||
message = apiResp.Message
|
||||
}
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, message))
|
||||
}
|
||||
|
||||
var validationInfo ValidationInfo
|
||||
err = sonic.Unmarshal(apiResp.Datas, &validationInfo)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
return validationInfo
|
||||
|
||||
}
|
||||
func (r *AIResolver) GetCaptchaTicket(ctx context.Context, info ValidationInfo) string {
|
||||
captchaInfo := r.createCaptcha(info)
|
||||
codes := r.getResult(captchaInfo)
|
||||
return r.submitCaptcha(info, captchaInfo, codes)
|
||||
}
|
||||
|
||||
func (r *AIResolver) getResult(data CpSubmitCaptchaForAI) []string {
|
||||
name := data.Name
|
||||
infos := data.ImageInfos
|
||||
var result []string
|
||||
for _, item := range infos {
|
||||
url := item.Path
|
||||
get, err := r.client.R().Get(url)
|
||||
utils.OkOrPanic(err)
|
||||
defer get.Body.Close()
|
||||
bytes, _ := io.ReadAll(get.Body)
|
||||
//todo 将图片保存方便后面训练
|
||||
//os.WriteFile(item.Code+".png", bytes, 0666)
|
||||
if classifier.ClassifyImage(bytes, name) {
|
||||
result = append(result, item.Code)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// getValidateUrl 根据任务类型获取验证URL
|
||||
func (r *AIResolver) getValidateUrl() string {
|
||||
baseUrl := strings.TrimSuffix(r.host, "/")
|
||||
|
||||
switch r.taskType {
|
||||
case types.TaskTypeCollection:
|
||||
return baseUrl + "/wec-counselor-collector-apps/stu/collector/checkValidation"
|
||||
case types.TaskTypeSign:
|
||||
return baseUrl + "/wec-counselor-sign-apps/stu/sign/checkValidation"
|
||||
case types.TaskTypeSleepCheck:
|
||||
return baseUrl + "/wec-counselor-attendance-apps/student/attendance/checkValidation"
|
||||
default:
|
||||
utils.OkOrPanic(errors.NewBusinessError(400, "不支持的任务类型"))
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// createCaptcha 创建验证码
|
||||
func (r *AIResolver) createCaptcha(info ValidationInfo) CpSubmitCaptchaForAI {
|
||||
baseUrl := strings.TrimSuffix(r.host, "/")
|
||||
url := baseUrl + "/captcha-open-api/v1/captcha/create/scenesImage"
|
||||
|
||||
resp, err := r.client.R().
|
||||
SetHeader("deviceId", r.deviceId).
|
||||
SetMultipartFormData(map[string]string{
|
||||
"accountKey": info.AccountKey,
|
||||
"sceneCode": info.SceneCode,
|
||||
"tenantId": info.TenantId,
|
||||
"userId": info.UserId,
|
||||
}).
|
||||
Post(url)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
apiResp := utils.ReadJson[ApiResponse](resp.Body)
|
||||
|
||||
if apiResp.Result == nil {
|
||||
message := "获取提交验证码失败"
|
||||
if apiResp.Message != "" {
|
||||
message = apiResp.Message
|
||||
}
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, message))
|
||||
}
|
||||
|
||||
var result CpSubmitCaptchaForAI
|
||||
err = sonic.Unmarshal(apiResp.Result, &result)
|
||||
utils.OkOrPanic(err)
|
||||
return result
|
||||
}
|
||||
|
||||
// submitCaptcha 提交验证码
|
||||
func (r *AIResolver) submitCaptcha(info ValidationInfo, submitCaptcha CpSubmitCaptchaForAI, codes []string) string {
|
||||
baseUrl := strings.TrimSuffix(r.host, "/")
|
||||
url := baseUrl + "/captcha-open-api/v1/captcha/validate/scenesImage"
|
||||
|
||||
formData := map[string]string{
|
||||
"accountKey": info.AccountKey,
|
||||
"sceneCode": info.SceneCode,
|
||||
"tenantId": info.TenantId,
|
||||
"userId": info.UserId,
|
||||
"scenesImageCode": submitCaptcha.Code,
|
||||
}
|
||||
|
||||
// 创建multipart form request
|
||||
req := r.client.R().SetHeader("deviceId", r.deviceId)
|
||||
|
||||
// 添加基本表单数据
|
||||
for key, value := range formData {
|
||||
req = req.SetMultipartFormData(map[string]string{key: value})
|
||||
}
|
||||
|
||||
// 添加验证码列表
|
||||
for _, code := range codes {
|
||||
req = req.SetMultipartField("scenesImageCodes", "", "", strings.NewReader(code))
|
||||
}
|
||||
|
||||
resp, err := req.Post(url)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
apiResp := utils.ReadJson[ApiResponse](resp.Body)
|
||||
|
||||
if apiResp.Code != "200" {
|
||||
message := "提交验证码失败"
|
||||
if apiResp.Message != "" {
|
||||
message = apiResp.Message
|
||||
}
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, message))
|
||||
}
|
||||
|
||||
if apiResp.Result == nil {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "今日校园返回空验证码结果"))
|
||||
}
|
||||
|
||||
var result string
|
||||
err = sonic.Unmarshal(apiResp.Result, &result)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
return result
|
||||
}
|
||||
//
|
||||
//import (
|
||||
// "context"
|
||||
// "io"
|
||||
// "likedream-goframe/internal/application/cpdaily/types"
|
||||
// "likedream-goframe/internal/application/recognizer/classifier"
|
||||
// "likedream-goframe/internal/common/errors"
|
||||
// "likedream-goframe/internal/utils"
|
||||
// "strings"
|
||||
//
|
||||
// "github.com/bytedance/sonic"
|
||||
// "resty.dev/v3"
|
||||
//)
|
||||
//
|
||||
//type AIResolver struct {
|
||||
// client *resty.Client
|
||||
// host string
|
||||
// deviceId string
|
||||
// taskType types.TaskType
|
||||
//}
|
||||
//
|
||||
//func NewAIResolver(client *resty.Client, host, deviceId string, taskType types.TaskType) SubmitCaptchaResolver {
|
||||
// return &AIResolver{
|
||||
// client: client,
|
||||
// host: host,
|
||||
// deviceId: deviceId,
|
||||
// taskType: taskType,
|
||||
// }
|
||||
//}
|
||||
//func (r *AIResolver) GetValidationInfo(ctx context.Context) ValidationInfo {
|
||||
// // GetValidationInfo 获取验证信息
|
||||
// reqData := map[string]string{
|
||||
// "deviceId": r.deviceId,
|
||||
// }
|
||||
//
|
||||
// resp, err := r.client.R().
|
||||
// SetBody(reqData).
|
||||
// Post(r.getValidateUrl())
|
||||
// utils.OkOrPanic(err)
|
||||
//
|
||||
// apiResp := utils.ReadJson[ApiResponse](resp.Body)
|
||||
//
|
||||
// if apiResp.Datas == nil {
|
||||
// message := "获取提交验证码信息失败"
|
||||
// if apiResp.Message != "" {
|
||||
// message = apiResp.Message
|
||||
// }
|
||||
// utils.OkOrPanic(errors.NewBusinessError(503, message))
|
||||
// }
|
||||
//
|
||||
// var validationInfo ValidationInfo
|
||||
// err = sonic.Unmarshal(apiResp.Datas, &validationInfo)
|
||||
// utils.OkOrPanic(err)
|
||||
//
|
||||
// return validationInfo
|
||||
//
|
||||
//}
|
||||
//func (r *AIResolver) GetCaptchaTicket(ctx context.Context, info ValidationInfo) string {
|
||||
// captchaInfo := r.createCaptcha(info)
|
||||
// codes := r.getResult(captchaInfo)
|
||||
// return r.submitCaptcha(info, captchaInfo, codes)
|
||||
//}
|
||||
//
|
||||
//func (r *AIResolver) getResult(data CpSubmitCaptchaForAI) []string {
|
||||
// name := data.Name
|
||||
// infos := data.ImageInfos
|
||||
// var result []string
|
||||
// for _, item := range infos {
|
||||
// url := item.Path
|
||||
// get, err := r.client.R().Get(url)
|
||||
// utils.OkOrPanic(err)
|
||||
// defer get.Body.Close()
|
||||
// bytes, _ := io.ReadAll(get.Body)
|
||||
// //todo 将图片保存方便后面训练
|
||||
// //os.WriteFile(item.Code+".png", bytes, 0666)
|
||||
// if classifier.ClassifyImage(bytes, name) {
|
||||
// result = append(result, item.Code)
|
||||
// }
|
||||
// }
|
||||
// return result
|
||||
//}
|
||||
//
|
||||
//// getValidateUrl 根据任务类型获取验证URL
|
||||
//func (r *AIResolver) getValidateUrl() string {
|
||||
// baseUrl := strings.TrimSuffix(r.host, "/")
|
||||
//
|
||||
// switch r.taskType {
|
||||
// case types.TaskTypeCollection:
|
||||
// return baseUrl + "/wec-counselor-collector-apps/stu/collector/checkValidation"
|
||||
// case types.TaskTypeSign:
|
||||
// return baseUrl + "/wec-counselor-sign-apps/stu/sign/checkValidation"
|
||||
// case types.TaskTypeSleepCheck:
|
||||
// return baseUrl + "/wec-counselor-attendance-apps/student/attendance/checkValidation"
|
||||
// default:
|
||||
// utils.OkOrPanic(errors.NewBusinessError(400, "不支持的任务类型"))
|
||||
// return ""
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//// createCaptcha 创建验证码
|
||||
//func (r *AIResolver) createCaptcha(info ValidationInfo) CpSubmitCaptchaForAI {
|
||||
// baseUrl := strings.TrimSuffix(r.host, "/")
|
||||
// url := baseUrl + "/captcha-open-api/v1/captcha/create/scenesImage"
|
||||
//
|
||||
// resp, err := r.client.R().
|
||||
// SetHeader("deviceId", r.deviceId).
|
||||
// SetMultipartFormData(map[string]string{
|
||||
// "accountKey": info.AccountKey,
|
||||
// "sceneCode": info.SceneCode,
|
||||
// "tenantId": info.TenantId,
|
||||
// "userId": info.UserId,
|
||||
// }).
|
||||
// Post(url)
|
||||
// utils.OkOrPanic(err)
|
||||
//
|
||||
// apiResp := utils.ReadJson[ApiResponse](resp.Body)
|
||||
//
|
||||
// if apiResp.Result == nil {
|
||||
// message := "获取提交验证码失败"
|
||||
// if apiResp.Message != "" {
|
||||
// message = apiResp.Message
|
||||
// }
|
||||
// utils.OkOrPanic(errors.NewBusinessError(503, message))
|
||||
// }
|
||||
//
|
||||
// var result CpSubmitCaptchaForAI
|
||||
// err = sonic.Unmarshal(apiResp.Result, &result)
|
||||
// utils.OkOrPanic(err)
|
||||
// return result
|
||||
//}
|
||||
//
|
||||
//// submitCaptcha 提交验证码
|
||||
//func (r *AIResolver) submitCaptcha(info ValidationInfo, submitCaptcha CpSubmitCaptchaForAI, codes []string) string {
|
||||
// baseUrl := strings.TrimSuffix(r.host, "/")
|
||||
// url := baseUrl + "/captcha-open-api/v1/captcha/validate/scenesImage"
|
||||
//
|
||||
// formData := map[string]string{
|
||||
// "accountKey": info.AccountKey,
|
||||
// "sceneCode": info.SceneCode,
|
||||
// "tenantId": info.TenantId,
|
||||
// "userId": info.UserId,
|
||||
// "scenesImageCode": submitCaptcha.Code,
|
||||
// }
|
||||
//
|
||||
// // 创建multipart form request
|
||||
// req := r.client.R().SetHeader("deviceId", r.deviceId)
|
||||
//
|
||||
// // 添加基本表单数据
|
||||
// for key, value := range formData {
|
||||
// req = req.SetMultipartFormData(map[string]string{key: value})
|
||||
// }
|
||||
//
|
||||
// // 添加验证码列表
|
||||
// for _, code := range codes {
|
||||
// req = req.SetMultipartField("scenesImageCodes", "", "", strings.NewReader(code))
|
||||
// }
|
||||
//
|
||||
// resp, err := req.Post(url)
|
||||
// utils.OkOrPanic(err)
|
||||
//
|
||||
// apiResp := utils.ReadJson[ApiResponse](resp.Body)
|
||||
//
|
||||
// if apiResp.Code != "200" {
|
||||
// message := "提交验证码失败"
|
||||
// if apiResp.Message != "" {
|
||||
// message = apiResp.Message
|
||||
// }
|
||||
// utils.OkOrPanic(errors.NewBusinessError(503, message))
|
||||
// }
|
||||
//
|
||||
// if apiResp.Result == nil {
|
||||
// utils.OkOrPanic(errors.NewBusinessError(503, "今日校园返回空验证码结果"))
|
||||
// }
|
||||
//
|
||||
// var result string
|
||||
// err = sonic.Unmarshal(apiResp.Result, &result)
|
||||
// utils.OkOrPanic(err)
|
||||
//
|
||||
// return result
|
||||
//}
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
type RuoliSubmitCaptchaResolver struct {
|
||||
type NormalSubmitCaptchaResolver struct {
|
||||
client *resty.Client
|
||||
host string
|
||||
deviceId string
|
||||
@@ -24,12 +24,12 @@ type RuoliSubmitCaptchaResolver struct {
|
||||
itstarUrl string
|
||||
}
|
||||
|
||||
var _ SubmitCaptchaResolver = (*RuoliSubmitCaptchaResolver)(nil)
|
||||
var _ SubmitCaptchaResolver = (*NormalSubmitCaptchaResolver)(nil)
|
||||
|
||||
// NewRuoliResolver 创建若李验证码解析器
|
||||
func NewRuoliResolver(client *resty.Client, host, deviceId string, taskType types.TaskType) *RuoliSubmitCaptchaResolver {
|
||||
baseUrl := g.Config().MustGetWithEnv(gctx.GetInitCtx(), cst.ConfigRuoliCaptchaBaseUrl).String()
|
||||
return &RuoliSubmitCaptchaResolver{
|
||||
// NewNormalResolver 创建若李验证码解析器
|
||||
func NewNormalResolver(client *resty.Client, host, deviceId string, taskType types.TaskType) *NormalSubmitCaptchaResolver {
|
||||
baseUrl := g.Config().MustGetWithEnv(gctx.GetInitCtx(), cst.ConfigCaptchaBaseUrl).String() + "/classifier"
|
||||
return &NormalSubmitCaptchaResolver{
|
||||
client: client,
|
||||
host: host,
|
||||
deviceId: deviceId,
|
||||
@@ -39,12 +39,12 @@ func NewRuoliResolver(client *resty.Client, host, deviceId string, taskType type
|
||||
}
|
||||
|
||||
// getITStarUrl 获取ITStar服务URL
|
||||
func (r *RuoliSubmitCaptchaResolver) getITStarUrl() string {
|
||||
func (r *NormalSubmitCaptchaResolver) getITStarUrl() string {
|
||||
if r.itstarUrl != "" {
|
||||
return r.itstarUrl
|
||||
}
|
||||
|
||||
r.itstarUrl = g.Config().MustGetWithEnv(gctx.GetInitCtx(), cst.ConfigRuoliCaptchaBaseUrl).String()
|
||||
r.itstarUrl = g.Config().MustGetWithEnv(gctx.GetInitCtx(), cst.ConfigCaptchaBaseUrl).String() + "/classifier"
|
||||
if r.itstarUrl == "" {
|
||||
utils.OkOrPanic(errors.NewBusinessError(500, "未配置ITStarUrl"))
|
||||
}
|
||||
@@ -53,7 +53,7 @@ func (r *RuoliSubmitCaptchaResolver) getITStarUrl() string {
|
||||
}
|
||||
|
||||
// getValidateUrl 根据任务类型获取验证URL
|
||||
func (r *RuoliSubmitCaptchaResolver) getValidateUrl() string {
|
||||
func (r *NormalSubmitCaptchaResolver) getValidateUrl() string {
|
||||
baseUrl := strings.TrimSuffix(r.host, "/")
|
||||
|
||||
switch r.taskType {
|
||||
@@ -70,7 +70,7 @@ func (r *RuoliSubmitCaptchaResolver) getValidateUrl() string {
|
||||
}
|
||||
|
||||
// GetValidationInfo 获取验证信息
|
||||
func (r *RuoliSubmitCaptchaResolver) GetValidationInfo(ctx context.Context) ValidationInfo {
|
||||
func (r *NormalSubmitCaptchaResolver) GetValidationInfo(ctx context.Context) ValidationInfo {
|
||||
reqData := map[string]string{
|
||||
"deviceId": r.deviceId,
|
||||
}
|
||||
@@ -98,14 +98,14 @@ func (r *RuoliSubmitCaptchaResolver) GetValidationInfo(ctx context.Context) Vali
|
||||
}
|
||||
|
||||
// GetCaptchaTicket 获取验证码票据
|
||||
func (r *RuoliSubmitCaptchaResolver) GetCaptchaTicket(ctx context.Context, info ValidationInfo) string {
|
||||
func (r *NormalSubmitCaptchaResolver) GetCaptchaTicket(ctx context.Context, info ValidationInfo) string {
|
||||
captchaInfo := r.createCaptcha(info)
|
||||
codes := r.getResultFromItStar(captchaInfo.RawResponseJson)
|
||||
return r.submitCaptcha(info, captchaInfo, codes)
|
||||
}
|
||||
|
||||
// createCaptcha 创建验证码
|
||||
func (r *RuoliSubmitCaptchaResolver) createCaptcha(info ValidationInfo) CpSubmitCaptcha {
|
||||
func (r *NormalSubmitCaptchaResolver) createCaptcha(info ValidationInfo) CpSubmitCaptcha {
|
||||
baseUrl := strings.TrimSuffix(r.host, "/")
|
||||
url := baseUrl + "/captcha-open-api/v1/captcha/create/scenesImage"
|
||||
|
||||
@@ -139,7 +139,7 @@ func (r *RuoliSubmitCaptchaResolver) createCaptcha(info ValidationInfo) CpSubmit
|
||||
}
|
||||
|
||||
// getResultFromItStar 从ITStar获取识别结果
|
||||
func (r *RuoliSubmitCaptchaResolver) getResultFromItStar(rawResponse json.RawMessage) []string {
|
||||
func (r *NormalSubmitCaptchaResolver) getResultFromItStar(rawResponse json.RawMessage) []string {
|
||||
// 解析原始响应为map以便添加authKey
|
||||
var responseMap map[string]any
|
||||
err := sonic.Unmarshal(rawResponse, &responseMap)
|
||||
@@ -165,7 +165,7 @@ func (r *RuoliSubmitCaptchaResolver) getResultFromItStar(rawResponse json.RawMes
|
||||
}
|
||||
|
||||
// submitCaptcha 提交验证码
|
||||
func (r *RuoliSubmitCaptchaResolver) submitCaptcha(info ValidationInfo, submitCaptcha CpSubmitCaptcha, codes []string) string {
|
||||
func (r *NormalSubmitCaptchaResolver) submitCaptcha(info ValidationInfo, submitCaptcha CpSubmitCaptcha, codes []string) string {
|
||||
baseUrl := strings.TrimSuffix(r.host, "/")
|
||||
url := baseUrl + "/captcha-open-api/v1/captcha/validate/scenesImage"
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -15,4 +15,5 @@ type CpTaskFormItem struct {
|
||||
Sort uint `json:"sort,omitempty"`
|
||||
CacheKey string `json:"cacheKey,omitempty"`
|
||||
Show bool `json:"show,omitempty"`
|
||||
Tip string `json:"tip,omitempty"`
|
||||
}
|
||||
|
||||
@@ -8,4 +8,5 @@ const (
|
||||
ItemTypeMulti
|
||||
ItemTypeImage
|
||||
ItemTypeAddress
|
||||
ItemTypeLatAndLon
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
@@ -360,19 +359,27 @@ func (c *CpdailyAutoSignTask) ParseFormJson(ctx context.Context, rawJson map[str
|
||||
var list []*formtype.CpTaskFormItem
|
||||
|
||||
// 经纬度
|
||||
//dto := &formtype.CpTaskFormItem{
|
||||
// Title: "经度",
|
||||
// ColName: "lon",
|
||||
// Type: formtype.ItemTypeInput,
|
||||
// Required: true,
|
||||
//}
|
||||
//list = append(list, dto)
|
||||
//
|
||||
//dto = &formtype.CpTaskFormItem{
|
||||
// Title: "纬度",
|
||||
// ColName: "lat",
|
||||
// Type: formtype.ItemTypeInput,
|
||||
// Required: true,
|
||||
//}
|
||||
//list = append(list, dto)
|
||||
dto := &formtype.CpTaskFormItem{
|
||||
Title: "经度",
|
||||
ColName: "lon",
|
||||
Type: formtype.ItemTypeInput,
|
||||
Required: true,
|
||||
}
|
||||
list = append(list, dto)
|
||||
|
||||
dto = &formtype.CpTaskFormItem{
|
||||
Title: "纬度",
|
||||
ColName: "lat",
|
||||
Type: formtype.ItemTypeInput,
|
||||
Title: "经纬度",
|
||||
ColName: "latAndLon",
|
||||
Type: formtype.ItemTypeLatAndLon,
|
||||
Required: true,
|
||||
Tip: "您可以通过地址搜索签到经纬度,如果觉得不够准确可以自行修改",
|
||||
}
|
||||
list = append(list, dto)
|
||||
|
||||
@@ -381,21 +388,19 @@ func (c *CpdailyAutoSignTask) ParseFormJson(ctx context.Context, rawJson map[str
|
||||
ColName: "address",
|
||||
Type: formtype.ItemTypeInput,
|
||||
Required: true,
|
||||
Tip: "这个就是指签到完毕后查看时最底部那个最详细的地址信息哦",
|
||||
}
|
||||
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,
|
||||
Tip: "您可以上传多张图片,会顺序轮换更新的哦~",
|
||||
}
|
||||
list = append(list, dto)
|
||||
current++
|
||||
}
|
||||
|
||||
// 是否范围外签到
|
||||
@@ -404,7 +409,6 @@ func (c *CpdailyAutoSignTask) ParseFormJson(ctx context.Context, rawJson map[str
|
||||
ColName: "isMalposition",
|
||||
Type: formtype.ItemTypeSingle,
|
||||
Required: true,
|
||||
Number: current,
|
||||
Options: []formtype.CpTaskFormItemOption{
|
||||
{
|
||||
Content: "是",
|
||||
@@ -416,9 +420,9 @@ func (c *CpdailyAutoSignTask) ParseFormJson(ctx context.Context, rawJson map[str
|
||||
IsOtherItems: false,
|
||||
},
|
||||
},
|
||||
Tip: "一般在学校就是选否,如果在外实习、请假之类的才选是",
|
||||
}
|
||||
list = append(list, dto)
|
||||
current++
|
||||
|
||||
// 范围外签到原因
|
||||
dto = &formtype.CpTaskFormItem{
|
||||
@@ -427,16 +431,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 +448,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 +477,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 +518,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)
|
||||
@@ -543,7 +531,7 @@ func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[strin
|
||||
}
|
||||
|
||||
// 处理验证码
|
||||
submitCaptchaResolver := submitcaptcha.NewRuoliResolver(c.client, c.host, deviceId, types.TaskTypeSign)
|
||||
submitCaptchaResolver := submitcaptcha.NewNormalResolver(c.client, c.host, deviceId, types.TaskTypeSign)
|
||||
captchaInfo := submitCaptchaResolver.GetValidationInfo(ctx)
|
||||
|
||||
if captchaInfo.Validation {
|
||||
@@ -554,7 +542,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 +567,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 +577,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 +587,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 +598,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").
|
||||
@@ -440,19 +435,27 @@ func (c *CpdailySleepCheckTask) ParseFormJson(ctx context.Context, rawJson map[s
|
||||
var list []*formtype.CpTaskFormItem
|
||||
|
||||
// 经纬度
|
||||
//dto := &formtype.CpTaskFormItem{
|
||||
// Title: "经度",
|
||||
// ColName: "lon",
|
||||
// Type: formtype.ItemTypeInput,
|
||||
// Required: true,
|
||||
//}
|
||||
//list = append(list, dto)
|
||||
//
|
||||
//dto = &formtype.CpTaskFormItem{
|
||||
// Title: "纬度",
|
||||
// ColName: "lat",
|
||||
// Type: formtype.ItemTypeInput,
|
||||
// Required: true,
|
||||
//}
|
||||
//list = append(list, dto)
|
||||
dto := &formtype.CpTaskFormItem{
|
||||
Title: "经度",
|
||||
ColName: "lon",
|
||||
Type: formtype.ItemTypeInput,
|
||||
Required: true,
|
||||
}
|
||||
list = append(list, dto)
|
||||
|
||||
dto = &formtype.CpTaskFormItem{
|
||||
Title: "纬度",
|
||||
ColName: "lat",
|
||||
Type: formtype.ItemTypeInput,
|
||||
Title: "经纬度",
|
||||
ColName: "latAndLon",
|
||||
Type: formtype.ItemTypeLatAndLon,
|
||||
Required: true,
|
||||
Tip: "您可以通过地址搜索签到经纬度,如果觉得不够准确可以自行修改",
|
||||
}
|
||||
list = append(list, dto)
|
||||
dto = &formtype.CpTaskFormItem{
|
||||
@@ -460,21 +463,20 @@ func (c *CpdailySleepCheckTask) ParseFormJson(ctx context.Context, rawJson map[s
|
||||
ColName: "address",
|
||||
Type: formtype.ItemTypeInput,
|
||||
Required: true,
|
||||
Tip: "这个就是指签到完毕后查看时最底部那个最详细的地址信息哦",
|
||||
}
|
||||
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,
|
||||
Tip: "您可以上传多张图片,会顺序轮换更新的哦~",
|
||||
}
|
||||
list = append(list, dto)
|
||||
current++
|
||||
|
||||
}
|
||||
|
||||
// 是否范围外签到
|
||||
@@ -483,7 +485,6 @@ func (c *CpdailySleepCheckTask) ParseFormJson(ctx context.Context, rawJson map[s
|
||||
ColName: "isMalposition",
|
||||
Type: formtype.ItemTypeSingle,
|
||||
Required: true,
|
||||
Number: current,
|
||||
Options: []formtype.CpTaskFormItemOption{
|
||||
{
|
||||
Content: "是",
|
||||
@@ -495,9 +496,9 @@ func (c *CpdailySleepCheckTask) ParseFormJson(ctx context.Context, rawJson map[s
|
||||
IsOtherItems: false,
|
||||
},
|
||||
},
|
||||
Tip: "一般在学校就是选否,如果在外实习、请假之类的才选是",
|
||||
}
|
||||
list = append(list, dto)
|
||||
current++
|
||||
|
||||
for key, item := range list {
|
||||
list[key].Show = item.LogicWid == ""
|
||||
@@ -506,7 +507,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
|
||||
|
||||
// 填充必要变量
|
||||
@@ -524,6 +525,12 @@ func (c *CpdailySleepCheckTask) FillCustomForm(ctx context.Context, form map[str
|
||||
lon = v.Content
|
||||
case "address":
|
||||
address = v.Content
|
||||
case "latAndLon":
|
||||
{
|
||||
res := strings.Split(v.Content, ",")
|
||||
lat, lon = res[0], res[1]
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -533,8 +540,8 @@ func (c *CpdailySleepCheckTask) FillCustomForm(ctx context.Context, form map[str
|
||||
resultForm["uaIsCpadaily"] = gvar.New(true)
|
||||
|
||||
// 处理验证码
|
||||
//submitCaptchaResolver := submitcaptcha.NewRuoliResolver(c.client, c.host, deviceId, types.TaskTypeSleepCheck)
|
||||
submitCaptchaResolver := submitcaptcha.NewAIResolver(c.client, c.host, deviceId, types.TaskTypeSleepCheck)
|
||||
submitCaptchaResolver := submitcaptcha.NewNormalResolver(c.client, c.host, deviceId, types.TaskTypeSleepCheck)
|
||||
//submitCaptchaResolver := submitcaptcha.NewAIResolver(c.client, c.host, deviceId, types.TaskTypeSleepCheck)
|
||||
captchaInfo := submitCaptchaResolver.GetValidationInfo(ctx)
|
||||
|
||||
if captchaInfo.Validation {
|
||||
@@ -566,27 +573,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)
|
||||
@@ -666,11 +657,11 @@ func (c *CpdailySleepCheckTask) Submit(ctx context.Context, form map[string]*gva
|
||||
|
||||
// 构造提交数据
|
||||
submitData := map[string]*gvar.Var{
|
||||
"lon": form["longitude"],
|
||||
"version": gvar.New(cryptoutil.EncryptVersion),
|
||||
"calVersion": gvar.New(cpdailyInfo.CalVersion),
|
||||
"deviceId": extensionForm["deviceId"],
|
||||
"userId": gvar.New(username),
|
||||
"lon": form["longitude"],
|
||||
"version": gvar.New(cryptoutil.EncryptVersion),
|
||||
"calVersion": gvar.New(cpdailyInfo.CalVersion),
|
||||
"deviceId": extensionForm["deviceId"],
|
||||
//"userId": gvar.New(username),
|
||||
"systemName": gvar.New("android"),
|
||||
"bodyString": gvar.New(bodyString),
|
||||
"lat": form["latitude"],
|
||||
|
||||
@@ -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 {
|
||||
// 按照指定顺序构建哈希数据
|
||||
|
||||
@@ -10,11 +10,12 @@ import (
|
||||
|
||||
var Cpdaily = resty.New().
|
||||
SetCookieJar(nil).
|
||||
SetTimeout(30 * time.Second).
|
||||
SetTimeout(30*time.Second).
|
||||
SetDoNotParseResponse(true).
|
||||
SetTLSClientConfig(&tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
})
|
||||
}).
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36")
|
||||
|
||||
func init() {
|
||||
if !utils.IsDebug() {
|
||||
@@ -23,3 +24,25 @@ func init() {
|
||||
Cpdaily.SetProxy("http://127.0.0.1:9000")
|
||||
}
|
||||
}
|
||||
|
||||
// NewCpdailyClient 创建一个全新的独立 client 实例,避免 Clone 可能的浅拷贝问题
|
||||
// 每个实例都是完全独立的,适用于并发场景,不会共享状态
|
||||
func NewCpdailyClient() *resty.Client {
|
||||
client := resty.New().
|
||||
SetCookieJar(nil).
|
||||
SetTimeout(30*time.Second).
|
||||
SetDoNotParseResponse(true).
|
||||
SetTLSClientConfig(&tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}).
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36")
|
||||
|
||||
// 根据调试模式设置代理和调试选项
|
||||
if !utils.IsDebug() {
|
||||
client.SetDebug(false)
|
||||
} else {
|
||||
client.SetProxy("http://127.0.0.1:9000")
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
@@ -24,3 +24,25 @@ func init() {
|
||||
Xuexitong.SetProxy("http://127.0.0.1:9000")
|
||||
}
|
||||
}
|
||||
|
||||
// NewXuexitongClient 创建一个全新的独立 client 实例,避免 Clone 可能的浅拷贝问题
|
||||
// 每个实例都是完全独立的,适用于并发场景,不会共享状态
|
||||
func NewXuexitongClient() *resty.Client {
|
||||
client := resty.New().
|
||||
SetCookieJar(nil).
|
||||
SetTimeout(30*time.Second).
|
||||
SetDoNotParseResponse(true).
|
||||
SetTLSClientConfig(&tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}).
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36")
|
||||
|
||||
// 根据调试模式设置代理和调试选项
|
||||
if !utils.IsDebug() {
|
||||
client.SetDebug(false)
|
||||
} else {
|
||||
client.SetProxy("http://127.0.0.1:9000")
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,196 +0,0 @@
|
||||
package classifier
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"os"
|
||||
|
||||
ort "github.com/yalue/onnxruntime_go"
|
||||
)
|
||||
|
||||
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) {
|
||||
// 加载类别名称
|
||||
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,8 +0,0 @@
|
||||
package classifier
|
||||
|
||||
// ClassifyImage 图片数据 & 类别名称
|
||||
// return 是否匹配
|
||||
func ClassifyImage(imgData []byte, className string) bool {
|
||||
name, _, _ := imageClassifier.PredictFromBytes(imgData)
|
||||
return className == name
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package classifier
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"likedream-goframe/internal/application/recognizer"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
//go:embed static/classes.txt
|
||||
var classesBytes []byte
|
||||
|
||||
//go:embed static/model.onnx
|
||||
var modelBytes []byte
|
||||
var imageClassifier *ImageClassifier
|
||||
|
||||
func init() {
|
||||
if !recognizer.OnnxIsInit {
|
||||
recognizer.InitOnnx()
|
||||
}
|
||||
modelPath, classesPath := initModel()
|
||||
classifier, err := NewImageClassifier(modelPath, classesPath)
|
||||
if err != nil {
|
||||
logger.Logger.Panic("初始化ai模型失败", zap.Error(err))
|
||||
}
|
||||
imageClassifier = classifier
|
||||
}
|
||||
func initModel() (modelPath, classesPath string) {
|
||||
// 检测文件是否存在
|
||||
exePath := strings.ToLower(os.Args[0])
|
||||
// 获取绝对路径
|
||||
absPath, _ := filepath.Abs(exePath)
|
||||
staticPath := filepath.Join(filepath.Dir(absPath), "static")
|
||||
if _, err := os.Stat(staticPath); os.IsNotExist(err) {
|
||||
os.Mkdir(staticPath, 0755)
|
||||
}
|
||||
modelPath = filepath.Join(staticPath, "model.onnx")
|
||||
if _, err := os.Stat(modelPath); os.IsNotExist(err) {
|
||||
os.WriteFile(modelPath, modelBytes, 0644)
|
||||
}
|
||||
classesPath = filepath.Join(staticPath, "classes.txt")
|
||||
if _, err := os.Stat(classesPath); os.IsNotExist(err) {
|
||||
os.WriteFile(classesPath, classesBytes, 0644)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -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,23 +0,0 @@
|
||||
公交车
|
||||
向日葵
|
||||
多肉
|
||||
摩托车
|
||||
樱桃
|
||||
汽车
|
||||
满天星
|
||||
狗
|
||||
狮子
|
||||
猫
|
||||
老虎
|
||||
自行车
|
||||
芒果
|
||||
苹果
|
||||
荷花
|
||||
葡萄
|
||||
西瓜
|
||||
豹子
|
||||
长颈鹿
|
||||
青桔
|
||||
飞机
|
||||
香蕉
|
||||
高铁
|
||||
Binary file not shown.
@@ -1,109 +0,0 @@
|
||||
package ddddocr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/thedevsaddam/gojsonq"
|
||||
ort "github.com/yalue/onnxruntime_go"
|
||||
"image"
|
||||
"log"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// SemiOCRVerification 半自动验证码识别(要自己传入outputShape值)
|
||||
func SemiOCRVerification(img image.Image, outputShape ort.Shape) string {
|
||||
img1 := ResizeImage(img, uint(64*img.Bounds().Dx()/img.Bounds().Dy()), 64)
|
||||
imgGray := ConvertToGray(img1)
|
||||
|
||||
inputData := ImageToGrayFloatArray(imgGray)
|
||||
inputShape := ort.NewShape(1, 1, 64, int64(imgGray.Bounds().Dx()))
|
||||
inputTensor, err := ort.NewTensor[float32](inputShape, inputData)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
defer inputTensor.Destroy()
|
||||
// This hypothetical network maps a 2x5 input -> 2x3x4 output.
|
||||
outputTensor, err := ort.NewEmptyTensor[int64](outputShape)
|
||||
defer outputTensor.Destroy()
|
||||
onnxPath := initModel()
|
||||
session, err := ort.NewAdvancedSession(onnxPath,
|
||||
[]string{"input1"}, []string{"output"},
|
||||
[]ort.Value{inputTensor}, []ort.Value{outputTensor}, nil)
|
||||
defer session.Destroy()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = session.Run()
|
||||
if err != nil {
|
||||
fmt.Errorf(err.Error())
|
||||
}
|
||||
|
||||
outputData := outputTensor.GetData()
|
||||
codeResult := ""
|
||||
for i := 0; i < len(outputData); i++ {
|
||||
if outputData[i] != 0 {
|
||||
codeResult += gojsonq.New().JSONString(getCharCode()).Find("[" + strconv.Itoa(int(outputData[i])) + "]").(string)
|
||||
}
|
||||
}
|
||||
return codeResult
|
||||
}
|
||||
|
||||
// AutoOCRVerification 全自动
|
||||
func AutoOCRVerification(img image.Image) string {
|
||||
img1 := ResizeImage(img, uint(64*img.Bounds().Dx()/img.Bounds().Dy()), 64)
|
||||
imgGray := ConvertToGray(img1)
|
||||
inputData := ImageToGrayFloatArray(imgGray)
|
||||
inputShape := ort.NewShape(1, 1, 64, int64(imgGray.Bounds().Dx()))
|
||||
inputTensor, err := ort.NewTensor[float32](inputShape, inputData)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
defer inputTensor.Destroy()
|
||||
var outputShape = ort.NewShape(1, 16)
|
||||
codeResult := ""
|
||||
//失败一次获取输出纬度
|
||||
for {
|
||||
outputTensor, err := ort.NewEmptyTensor[int64](outputShape)
|
||||
defer outputTensor.Destroy()
|
||||
onnxPath := initModel()
|
||||
session, err := ort.NewAdvancedSession(onnxPath,
|
||||
[]string{"input1"}, []string{"output"},
|
||||
[]ort.Value{inputTensor}, []ort.Value{outputTensor}, nil)
|
||||
defer session.Destroy()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err1 := session.Run()
|
||||
if err1 != nil {
|
||||
if strings.Contains(err1.Error(), "OrtValue shape verification failed. Current shape:") {
|
||||
regex := regexp.MustCompile(`Requested shape:{(\d+),(\d+)}`)
|
||||
matches := regex.FindStringSubmatch(err1.Error())
|
||||
if len(matches) == 3 {
|
||||
c1, _ := strconv.Atoi(matches[1])
|
||||
c2, _ := strconv.Atoi(matches[2])
|
||||
outputShape = ort.NewShape(int64(c1), int64(c2))
|
||||
continue
|
||||
}
|
||||
}
|
||||
fmt.Errorf(err1.Error())
|
||||
return codeResult
|
||||
}
|
||||
|
||||
outputData := outputTensor.GetData()
|
||||
|
||||
for i := 0; i < len(outputData); i++ {
|
||||
if outputData[i] != 0 {
|
||||
codeResult += gojsonq.New().JSONString(getCharCode()).Find("[" + strconv.Itoa(int(outputData[i])) + "]").(string)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
return codeResult
|
||||
}
|
||||
@@ -1,870 +0,0 @@
|
||||
package ddddocr
|
||||
|
||||
// 获取映射字符串
|
||||
func getCharCode() string {
|
||||
return `["", "掀", "袜", "顧", "徕", "榱", "荪", "浡", "其", "炎", "玉", "恩", "劣", "徽", "廉", "桂", "拂",
|
||||
"鳊", "撤",
|
||||
"赏", "哮", "侄", "蓮", "И", "进", "饭", "饱", "优", "楸", "礻", "蜉", "營", "伙", "杌", "修", "榜",
|
||||
"准", "铒",
|
||||
"戏", "赭", "襟", "彘", "彩", "雁", "闽", "坎", "聂", "氡", "辜", "苁", "潆", "摁", "月", "稇", "而",
|
||||
"醴", "簉",
|
||||
"卑", "妖", "埽", "嘡", "醛", "見", "煎", "汪", "秽", "迄", "噭", "焉", "钌", "瑕", "玻", "仙", "蹑",
|
||||
"钀", "翦",
|
||||
"丰", "矗", "2", "胚", "镊", "镡", "鍊", "帖", "僰", "淀", "吒", "冲", "挡", "粼", "螈", "缵", "孺",
|
||||
"侦", "曷",
|
||||
"渐", "敷", "投", "宸", "祉", "柳", "尖", "梃", "淘", "臁", "躇", "撖", "惭", "狄", "聢", "官", "狴",
|
||||
"诬", "骄",
|
||||
"跻", "場", "姻", "钎", "藥", "綉", "驾", "舻", "黢", "鲦", "蜣", "渖", "绹", "佰", "怜", "三", "痪",
|
||||
"眍", "养",
|
||||
"角", "薜", "濑", "劳", "戟", "傎", "纫", "徉", "收", "稍", "虫", "螋", "鬲", "捌", "陡", "蓟", "邳",
|
||||
"蹢", "涉",
|
||||
"煋", "端", "懷", "椤", "埶", "廊", "免", "秫", "猢", "睐", "臺", "擀", "布", "麃", "彗", "汊", "芄",
|
||||
"遣", "胙",
|
||||
"另", "癯", "徭", "疢", "茆", "忡", "'", "烃", "笕", "薤", "肆", "熛", "過", "盖", "跷", "呷", "痿",
|
||||
"沖", "魍",
|
||||
"讣", "庤", "弑", "诩", "庵", "履", "暮", "始", "滟", "矅", "蛹", "鸿", "啃", "铋", "沿", "鐾", "酆",
|
||||
"團", "恙",
|
||||
"閥", "聒", "讵", "颠", "沾", "堅", "踣", "陴", "覃", "滙", "浐", "钇", "脆", "炙", "亮", "觌", "産",
|
||||
"汩", "鸭",
|
||||
"斄", "堆", "掭", "揞", "鹂", "郫", "瘅", "蚂", "揩", "学", "组", "浸", "腙", "耀", "嗛", "局", "蠓",
|
||||
"肠", "昏",
|
||||
"I", "岑", "镯", "憧", "油", "泸", "鸟", "潇", "蕻", "褒", "瞧", "旸", "昭", "庐", "鞒", "内", "痈",
|
||||
"己", "曙",
|
||||
"怠", "锟", "晞", "耢", "鲢", "醦", "糕", "療", "寇", "梵", "黾", "呻", "苒", "ü", "校", "嘏", "昃",
|
||||
"Ⅰ", "蕰",
|
||||
"凖", "嵛", "裨", "筏", "匜", "咋", "乏", "婵", "镂", "珰", "感", "蔗", "蚵", "庞", "弢", "槟", "口",
|
||||
"漉", "﹒",
|
||||
"咂", "俩", "增", "硐", "襙", "绉", "卿", "距", "璱", "猖", "铚", "郚", "嬖", "缒", "阃", "扞", "V",
|
||||
"望", "最",
|
||||
"浔", "骜", "赃", "闻", "砍", "奸", "灶", "以", "获", "鳎", "浦", "罐", "孓", "纭", "瘀", "普", "氰",
|
||||
"塮", "症",
|
||||
"顷", "们", "螓", "蛸", "鵰", "册", "美", "萨", "沘", "犰", "嫌", "名", ")", "懦", "滇", "F", "垡",
|
||||
"声", "毅",
|
||||
"隅", "鲎", "煨", "萦", "宜", "唇", "鯨", "邛", "杲", "赜", "长", "魂", "桠", "锇", "搓", "俘", "仰",
|
||||
"膘", "宦",
|
||||
"歹", "遁", "猃", "噉", "幂", "糜", "嗤", "周", "剂", "曦", "暧", "焖", "髻", "釐", "泰", "窟", "檎",
|
||||
"旧", "犀",
|
||||
"镄", "百", "取", "岍", "逗", "叽", "呃", "鲪", "萬", "陈", "7", "習", "区", "逄", "宏", "罡", "漭",
|
||||
"盗", "郿",
|
||||
"般", "谢", "倪", "纵", "婶", "砧", "揖", "扪", "濒", "愤", "茓", "浞", "子", "揄", "旌", "趄", "樊",
|
||||
"醑", "遄",
|
||||
"婚", "汶", "矩", "裈", "弊", "呱", "铳", "勿", "蚴", "忿", "褓", "缚", "酱", "璞", "庆", "除", "礌",
|
||||
"珩", "榨",
|
||||
"鼢", "逞", "容", "圯", "猛", "陌", "-", "嚯", "镘", "鱾", "睚", "猬", "杜", "鳓", "燈", "計", "咣",
|
||||
"炜", "睁",
|
||||
"箱", "邮", "略", "馇", "逐", "雀", "僬", "髯", "奖", "俱", "-", "绗", "犏", "辱", "忑", "挽", "康",
|
||||
"蝼", "栏",
|
||||
"模", "辒", "•", "儋", "罱", "墈", "会", "秀", "栈", "缔", "醜", "蚣", "阮", "鼗", "眼", "湧", "沁",
|
||||
"夥", "毕",
|
||||
"媚", "瘳", "痣", "搴", "闿", "遍", "焰", "岣", "舱", "埌", "麿", "嘿", "靽", "体", "想", "霓", "钛",
|
||||
"摽", "苑",
|
||||
"芳", "技", "綮", "钅", "燠", "栾", "年", "悱", "腹", "员", "呕", "闇", "嗫", "檩", "荒", "溱", "舨",
|
||||
"峙", "卒",
|
||||
"洑", "预", "弯", "蔷", "叵", "锯", "慈", "牧", "患", "贇", "偷", "鲜", "锓", "躔", "嚬", "烈", "娌",
|
||||
"嘲", "详",
|
||||
"麺", "舒", "厨", "徵", "葹", "只", "篦", "鹀", "剕", "驳", "聍", "黧", "砾", "暅", "褫", "呈", "森",
|
||||
"结", "龛",
|
||||
"钲", "轧", "扔", "蕹", "赵", "涒", "冯", "渲", "缭", "坚", "趼", "鲑", "倫", "门", "班", "垚", "鞍",
|
||||
"菘", "畐",
|
||||
"僇", "侉", "禢", "轳", "饦", "兽", "呯", "捂", "樨", "卧", "栝", "豭", "冶", "鉰", "申", "蜈", "印",
|
||||
"缨", "镫",
|
||||
"蕾", "圜", "扑", "娉", "烦", "缳", "广", "峄", "獒", "铔", "奁", "醚", "倥", "蹇", "阚", "镆", "煺",
|
||||
"德", "颉",
|
||||
"嗅", "绷", "蒯", "祺", "崧", "往", "枨", "涡", "鲲", "瓅", "岌", "肘", "飔", "缘", "千", "棱", "溶",
|
||||
"窣", "篼",
|
||||
"代", "捡", "送", "咡", "术", "滑", "茜", "晾", "挤", "曳", "糈", "G", "翊", "殴", "妹", "溥", "璆",
|
||||
"烩", "拙",
|
||||
"襄", "几", "嘴", "D", "驮", "淙", "蹐", "合", "環", "剑", "怪", "褂", "畑", "燏", "订", "珪", "≥",
|
||||
"瘟", "耷",
|
||||
"槑", "衷", "猕", "迁", "霎", "槜", "﹖", "鋈", "苹", "嫣", "祜", "李", "鄒", "噢", "萄", "仝", "纨",
|
||||
"直", "悛",
|
||||
"拣", "远", "诏", "圧", "躬", "蝟", "總", "眆", "筻", "硇", "鳁", "眠", "钆", "泞", "猱", "宾", "酞",
|
||||
"募", "螳",
|
||||
"腴", "念", "宠", "唯", "怊", "勃", "M", "兿", "蟑", "妁", "掸", "拌", "铸", "讼", "诟", "锺", "Ω",
|
||||
"竟", "羚",
|
||||
"剽", "C", "苦", "煳", "罢", "跨", "~", "豸", "±", "俬", "捺", "彦", "钣", "鋆", "用", "缤", "搁",
|
||||
"徼", "谦",
|
||||
"筘", "嗨", "扮", "旇", "折", "咯", "昆", "叟", "垂", "箐", "捻", "燕", "島", "瞀", "鮮", "屡", "點",
|
||||
"瘭", "恚",
|
||||
"旚", "丟", "捽", "菁", "瀑", "炕", "蹩", "芒", "r", "是", "媾", "鹝", "囵", "萤", "拷", "频", "埴",
|
||||
"课", "癍",
|
||||
"袱", "螯", "谘", "榛", "Y", "缣", "裔", "憩", "相", "觀", "晗", "坳", "炔", "勉", "汆", "钡", "舐",
|
||||
"衫", "疫",
|
||||
"鲙", "蘩", "穈", "殁", "九", "泻", "咤", "構", "谆", "陕", "装", "蔡", "画", "介", "苋", "務", "敝",
|
||||
"俟", "帇",
|
||||
"鸺", "贸", "茗", "肃", "滪", "输", "瘗", "菽", "饹", "诉", "遐", "浑", "扎", "卟", "铀", "邗", "觋",
|
||||
"嘎", "塑",
|
||||
"潏", "金", "姘", "潋", "逵", "鲻", "逯", "炮", "甄", "髡", "剩", "嗬", "芴", "屋", "改", "骣", "芪",
|
||||
"邠", "痋",
|
||||
"珑", "帆", "狙", "八", "奔", "族", "轵", "氖", "雕", "痧", "眊", "胛", "酉", "鲼", "砣", "猸", "餮",
|
||||
"郇", "沫",
|
||||
"跖", "蝉", "屑", "辘", "閣", "涑", "邡", "篃", "交", "笼", "颇", "贻", "魄", "黡", "劂", "糠", "炅",
|
||||
"帨", "苍",
|
||||
"瓴", "粤", "莎", "朿", "埔", "绸", "齁", "鱿", "惨", "腢", "郡", "棠", "猫", "脑", "風", "蚱", "捐",
|
||||
"嵌", "胱",
|
||||
"馗", "竽", "泥", "辍", "怖", "雾", "絮", "淼", "筝", "碲", "悼", "龀", "の", "珥", "忐", "溲", "昕",
|
||||
"荔", "掂",
|
||||
"瘦", "僭", "蔌", "抺", "椅", "誉", "扯", "僜", "停", "衉", "汇", "赔", "眄", "呙", "咙", "剿", "次",
|
||||
"蛟", "嗓",
|
||||
"』", "汕", "詈", "帘", "踧", "姁", "血", "堪", "喜", "滩", "璎", "胄", "俨", "眚", "凌", "拽", "滔",
|
||||
"⑿", "嬃",
|
||||
"―", "汐", "潭", "阡", "呓", "婷", "执", "妊", "恂", "妥", "鳘", "蔫", "设", "睒", "笪", "謇", "鞋",
|
||||
"谍", "黯",
|
||||
"虍", "馬", "蚧", "骑", "峤", "舾", "儀", "駡", "β", "蓑", "柏", "痒", "蒇", "痕", "妍", "熠", "僻",
|
||||
"爬", "迭",
|
||||
"畫", "绰", "湯", "凭", "菼", "懈", "顒", "午", "箪", "糙", "址", "钼", "堵", "佘", "侍", "卤", "(",
|
||||
"榚", "泽",
|
||||
"溘", "蟹", "b", "燁", "颂", "菠", "榉", "鲡", "埸", "荛", "歘", "断", "邸", "贡", "礞", "蔼", "脸",
|
||||
"爪", "帜",
|
||||
"翡", "仟", "皎", "辆", "滫", "昔", "™", "柬", "弓", "遇", "杪", "侨", "娓", "镪", "觑", "一", "踌",
|
||||
"牟", "褡",
|
||||
"厩", "晌", "每", "娘", "渤", "c", "咫", "成", "颏", "孩", "鼓", "瞌", "槁", "捒", "阉", "伉", "癣",
|
||||
"胞", "鲟",
|
||||
"瓤", "杅", "紡", "喂", "掠", "镜", "镧", "侞", "赦", "貝", "丕", "臧", "L", "池", "彷", "棓", "锽",
|
||||
"渊", "食",
|
||||
"饨", "堡", "玥", "氣", "讽", "敬", "闺", "帡", "携", "哫", "珈", "魆", "哄", "旁", "喻", "泄", "畎",
|
||||
"郁", "唅",
|
||||
"葜", "繪", "飐", "谶", "聆", "斝", "谥", "辉", "髅", "進", "吧", "蹀", "铛", "笛", "睥", "楼", "凝",
|
||||
"況", "鸷",
|
||||
"苠", "饺", "沙", "缴", "块", "梢", "慝", "珐", "鄏", "霰", "迸", "氆", "趵", "棣", "鳔", "祆", "☆",
|
||||
"苯", "恁",
|
||||
"螨", "庭", "缠", "槠", "津", "髋", "诔", "葶", "蜾", "坻", "蒹", "摔", "向", "垩", "蹭", "淇", "筛",
|
||||
"滬", "玡",
|
||||
"铺", "逼", "劵", "绲", "团", "鳀", "常", "玖", "擢", "株", "铵", "樽", "弭", "醇", "糨", "璈", "曩",
|
||||
"潔", "祘",
|
||||
"磨", "希", "鲅", "擂", "谗", "唳", "欷", "欧", "绋", "庙", "琬", "稳", "糊", "拥", "霪", "浼", "翎",
|
||||
"俜", "摸",
|
||||
"筚", "巯", "墼", "苫", "缩", "镚", "婪", "圹", "咚", "儿", "蒽", "婆", "鲐", "雹", "霞", "嶪", "濠",
|
||||
"琉", "澌",
|
||||
"媢", "禤", "摺", "掏", "矢", "艄", "围", "呸", "寺", "拤", "氐", "柝", "跎", "僖", "挢", "茨", "涮",
|
||||
"缫", "撸",
|
||||
"荨", "嶷", "廋", "魋", "付", "喋", "蜗", "邙", "棹", "璪", "倡", "鞭", "游", "錦", "眬", "抒", "眈",
|
||||
"培", "夏",
|
||||
"黔", "獐", "皋", "戛", "鲀", "垒", "耽", "纤", "漩", "铈", "握", "窝", "芋", "濞", "截", "零", "敖",
|
||||
"眸", "怦",
|
||||
"噎", "簋", "掳", "妣", "湃", "璠", "殄", "觞", "桅", "笋", "鲞", "踯", "傀", "犨", "抵", "疰", "暌",
|
||||
"耖", "供",
|
||||
"枳", "怂", "娶", "鸩", "捣", "庸", "逡", "懋", "颃", "長", "鼫", "姮", "蹈", "耵", "乂", "骐", "殇",
|
||||
"膏", "仳",
|
||||
"冥", "梭", "洵", "碣", "昝", "仉", "軒", "隍", "更", "な", "嵕", "拜", "粑", "鲴", "吇", "秃", "尕",
|
||||
"魃", "狨",
|
||||
"臛", "蟥", "胨", "注", "谁", "张", "才", "尸", "派", "矮", "洳", "舟", "溺", "锴", "寓", "籴", "夕",
|
||||
"叭", "荠",
|
||||
"澼", "劃", "久", "私", "炉", "娟", "麤", "稂", "河", "纴", "夺", "亏", "焙", "。", "塗", "蜩", "栌",
|
||||
"渡", "薰",
|
||||
"崋", "揿", "漤", "啾", "郏", "舣", "卉", "爱", "牚", "撵", "钺", "再", "企", "笺", "疾", "承", "俾",
|
||||
"瞈", "邰",
|
||||
"汾", "瘛", "檫", "蒎", "觅", "绀", "掎", "U", "赓", "匳", "聘", "蛤", "跤", "嗜", "洼", "歔", "弟",
|
||||
"飕", "莼",
|
||||
"嫉", "那", "滈", "践", "僦", "偎", "扢", "绚", "乕", "旳", "招", "饯", "®", "攸", "鞁", "囫", "铨",
|
||||
"陒", "鷄",
|
||||
"畀", "韨", "經", "纾", "萸", "肴", "→", "宗", "迳", "鳞", "亚", "搂", "喀", "狮", "坦", "瞥", "采",
|
||||
"姝", "钳",
|
||||
"□", "剌", "維", "葸", "鼩", "公", "刀", "沩", "喔", "泺", "哉", "徨", "篝", "掊", "沕", "运", "偆",
|
||||
"浒", "语",
|
||||
"乇", "仪", "萝", "疍", "踽", "碡", "熰", "荞", "嚓", "天", "饰", "泵", "械", "孑", "蛰", "荟", "源",
|
||||
"峡", "矜",
|
||||
"睬", "噬", "腆", "婉", "‘", "等", "誓", "辀", "岖", "琖", "碜", "霍", "怼", "唛", "弈", "淑", "疆",
|
||||
"晴", "镴",
|
||||
"鸡", "埚", "焕", "芦", "唻", "踅", "吴", "殡", "唏", "吨", "寡", "鹉", "絲", "坉", "會", "埭", "Ⅲ",
|
||||
"捏", "墅",
|
||||
"卓", "叙", "徇", "柜", "各", "荭", "J", "恝", "囐", "蓉", "犋", "叡", "莺", "颌", "蒸", "饸", "疋",
|
||||
"玊", "兢",
|
||||
"鱽", "藍", "杳", "辂", "獘", "拔", "侪", "湍", "膂", "渔", "瘊", "雉", "稁", "職", "僤", "鄳", "祁",
|
||||
"稱", "I",
|
||||
"裴", "锉", "曹", "鲶", "挨", "哑", "鷪", "鏠", "煞", "师", "蛲", "牁", "琅", "告", "媒", "祭", "确",
|
||||
"荚", "亰",
|
||||
"蝗", "阗", "歩", "疲", "f", "唣", "愛", "郾", "棍", "山", "狲", "纽", "蚡", "栂", "馓", "诊", "猴",
|
||||
"喤", "来",
|
||||
"继", "桎", "嬛", "骞", "邴", "暄", "贼", "昴", "廿", "克", "耔", "彤", "鹭", "葓", "骢", "龁", "鏡",
|
||||
"瀚", "赅",
|
||||
"韩", "譄", "榷", "殚", "膛", "须", "、", "砖", "唶", "番", "蛘", "畴", "铠", "亢", "氓", "铰", "炻",
|
||||
"筫", "迢",
|
||||
"兰", "玺", "砻", "积", "莜", "吸", "监", "膦", "迪", "迷", "冷", "哀", "贳", "瞄", "器", "鹡", "惺",
|
||||
"徐", "酢",
|
||||
"寒", "Ⓡ", "倾", "飞", "楽", "涢", "队", "舆", "赤", "璩", "戳", "殳", "掮", "舴", "蜷", "宄", "拴",
|
||||
"癌", "舛",
|
||||
"婀", "抟", "靡", "骍", "揸", "思", "慧", "平", "橘", "臭", "硖", "卬", "畈", "兠", "茸", "脂", "魚",
|
||||
"晩", "御",
|
||||
"龋", "涣", "罨", "爍", "糌", "汧", "缐", "贽", "要", "祀", "鲊", "爼", "獯", "瀣", "棋", "肈", "佣",
|
||||
"娣", "柩",
|
||||
"枸", "偃", "v", "唷", "劍", "榴", "槐", "漫", "洽", "蒡", "籼", "魔", "峋", "第", "歙", "萧", "谮",
|
||||
"埯", "撮",
|
||||
"马", "绡", "裘", "鹋", "蓬", "显", "噶", "倒", "镳", "艽", "窬", "拳", "樯", "跋", "詹", "钥", "心",
|
||||
"嶽", "嚋",
|
||||
"戎", "吕", "涂", "悃", "麦", "骋", "推", "箩", "硚", "匆", "村", "五", "杨", "凑", "鞫", "镰", "伥",
|
||||
"诒", "纣",
|
||||
"崃", "鸻", "翰", "辌", "廛", "證", "舢", "盼", "腿", "圳", "贱", "皿", "隆", "屈", "龏", "瓒", "顏",
|
||||
"↓", "赈",
|
||||
"煙", "窍", "韧", "壁", "莰", "箬", "蹋", "褰", "峥", "悚", "坜", "环", "回", "疼", "渍", "蝄", "东",
|
||||
"臂", "坩",
|
||||
"走", "痍", "或", "蜀", "熳", "蜻", "佐", "懿", "嚅", "紗", "螭", "忖", "顶", "狡", "吲", "洣", "帛",
|
||||
"呶", "柞",
|
||||
"柫", "酿", "粥", "琢", "呵", "踝", "榀", "呲", "價", "鼋", "欺", "此", "背", "猎", "昱", "濡", "稚",
|
||||
"欠", "暇",
|
||||
"茬", "牙", "迹", "尼", "氛", "膠", "缯", "娼", "骚", "姒", "鬟", "霁", "鲔", "者", "驰", "倩", "馉",
|
||||
"工", "芬",
|
||||
"烙", "卦", "C", "裂", "垲", "摆", "珮", "缏", "杞", "绘", "司", "如", "姞", "荆", "挖", "跗", "伍",
|
||||
"氚", "钘",
|
||||
"郢", "轱", "篆", "吭", "夡", "鹫", "讷", "轺", "!", "匈", "待", "聱", "黏", "海", "蹶", "趋", "鎮",
|
||||
"觊", "江",
|
||||
"咸", "富", "艴", "稗", "钜", "搏", "壶", "鲮", "薪", "猞", "轰", "踪", "赣", "循", "序", "噻", "若",
|
||||
"裾", "许",
|
||||
"癞", "吓", "判", "踔", "查", "蚀", "[", "樓", "坌", "岳", "榄", "役", "倜", "⒂", "旭", "溆", "惯",
|
||||
"咀", "跫",
|
||||
"选", "囱", "污", "镶", "⒁", "淠", "氮", "酯", "寅", "芼", "炊", "夯", "郪", "农", "褲", "嘬", "蹻",
|
||||
"烔", "罄",
|
||||
"开", "靴", "镇", "杯", "羰", "硪", "籍", "摘", "馀", "餐", "眯", "⑴", "呗", "巫", "幤", "蒤", "蒗",
|
||||
"镥", "檵",
|
||||
"盛", "純", "娃", "●", "耿", "巡", "婴", "槔", "i", "颊", "Ⅳ", "栅", "绅", "邘", "冉", "碧", "使",
|
||||
"熨", "羞",
|
||||
"扼", "漳", "觯", "楊", "励", "逑", "咄", "之", "斤", "嘣", "鹰", "媸", "鲂", "褚", "磚", "琨", "聪",
|
||||
"牖", "太",
|
||||
"蓍", "涫", "≤", "虽", "鸽", "燧", "褊", "聿", "壬", "然", "疚", "莲", "悴", "簃", "颓", "坠", "瞬",
|
||||
"汳", "l",
|
||||
"登", "瘼", "窳", "桤", "縯", "匣", "坡", "↑", "愦", "攘", "渭", "嬢", "鲰", "性", "楚", "澈", "赪",
|
||||
"達", "鄯",
|
||||
"罅", "帽", "茠", "底", "嫜", "奏", "浅", "荽", "楹", "鼍", "枵", "嗔", "滍", "椴", "嵩", "氤", "搠",
|
||||
"两", "榔",
|
||||
"树", "吝", "基", "峂", "栎", "侮", "舸", "遂", "颡", "锷", "杼", "酔", "幄", "哽", "睢", "陔", "※",
|
||||
"嚆", "宬",
|
||||
"宽", "髦", "笾", "保", "蹊", "榕", "咏", "椋", "丧", "裤", "骛", "逧", "弇", "崆", "樘", "疤", "鸤",
|
||||
"伞", "抚",
|
||||
"诎", "诵", "豢", "佳", "差", "埝", "极", "黍", "煜", "曰", "阱", "悞", "叹", "垤", "藁", "嗵", "崔",
|
||||
"卫", "珂",
|
||||
"憯", "蔬", "菜", "碑", "扈", "铆", "夹", "衡", "弱", "挈", "徜", "疠", "丶", "遠", "提", "斧", "炟",
|
||||
"肺", "B",
|
||||
"她", "晟", "谎", "邱", "粳", "酽", "爨", "鬼", "伧", "兹", "嶓", "谤", "饕", "揶", "谱", "歡", "髪",
|
||||
"餍", "泳",
|
||||
"郞", "谣", "汉", "褐", "非", "刽", "缅", "饴", "齐", "兴", "涯", "芫", "凡", "褶", "晡", "努", "蚶",
|
||||
"彥", "皤",
|
||||
"砌", "黼", "吹", "指", "㙟", "蓁", "鹜", "話", "拊", "辨", "盎", "肌", "旘", "软", "颍", "甏", "滚",
|
||||
"旦", "滨",
|
||||
"间", "尴", "对", "鄘", "称", "镗", "咅", "璐", "怔", "垛", "洎", "瓮", "绨", "脚", "遒", "吊", "纸",
|
||||
"蹅", "经",
|
||||
"泉", "武", "汀", "歪", "败", "拾", "铪", "吼", "邹", "磊", "论", "岛", "厍", "锛", "芎", "芭", "音",
|
||||
"澧", "镕",
|
||||
"锒", "宙", "牵", "忱", "嫔", "麯", "澉", "擐", "砥", "撞", "痴", "盹", "畿", "厾", "酸", "俑", "脽",
|
||||
"鸈", "枷",
|
||||
"咨", "蔹", "诂", "胰", "董", "脶", "黩", "髓", "鉵", "澎", "鲽", "梧", "樱", "诜", "鲯", "跂", "盂",
|
||||
"浴", "苻",
|
||||
"锅", "實", "碁", "嘛", "氕", "艮", "涟", "绢", "姿", "茝", "砘", "簿", "穷", "镃", "∈", "抽", "事",
|
||||
"誜", "窅",
|
||||
"瀘", "鲹", "兖", "嵎", "陧", "榍", "轶", "柿", "藤", "薏", "娆", "骷", "梅", "摒", "睪", "剪", "羸",
|
||||
"忧", "邝",
|
||||
"跺", "旆", "堕", "伫", "绍", "疵", "樟", "–", "绾", "蜴", "靸", "侃", "瘘", "珧", "遨", "縠", "信",
|
||||
"充", "桔",
|
||||
"黇", "劬", "脒", "良", "俵", "颙", "轹", "犿", "屐", "牾", "4", "兮", "澝", "汗", "沼", "铲", "濋",
|
||||
"鹬", "丝",
|
||||
"妫", "重", "蒺", "磲", "曚", "尔", "国", "桐", "俣", "剐", "哼", "恹", "哧", "藔", "谓", "轨", "眩",
|
||||
"痞", "添",
|
||||
"鬯", "库", "梱", "婕", "蜢", "贿", "敕", "泯", "羟", "龇", "垸", "左", "肖", "辎", "鞣", "谄", "可",
|
||||
"腺", "末",
|
||||
"狞", "贷", "嗌", "仕", "楞", "膻", "臻", "欻", "洲", "所", "檀", "抔", "罹", "牒", "仫", "芨", "柄",
|
||||
"嫩", "酒",
|
||||
"祙", "渠", "的", "笨", "鳐", "楡", "过", "苡", "核", "拖", "阢", "莒", "凤", "锋", "` + "`" + `", "硎", "弁",
|
||||
"鬶", "朐",
|
||||
"忏", "於", "昊", "剟", "咳", "湘", "日", "满", "哨", "螵", "餪", "放", "佶", "葵", "硷", "c", "抱",
|
||||
"锥", "芮",
|
||||
"啻", "惊", "峁", "琊", "嶲", "撺", "煅", "屏", "袗", "鄞", "梓", "鹌", "宅", "赂", "鱼", "洱", "騳",
|
||||
"E", "物",
|
||||
"觏", "雙", "瑀", "上", "淩", "愀", "❋", "鄙", "憝", "沛", "硫", "产", "垯", "亁", "枭", "堰", "赑",
|
||||
"趾", "庹",
|
||||
"腭", "迨", "拚", "晒", "蜇", "扣", "纰", "闵", "窭", "椽", "菏", "嘁", "伛", "郸", "素", "殷", "表",
|
||||
"躞", "笸",
|
||||
"耻", "荧", "辛", "篑", "馈", "壮", "耩", "宛", "慰", "盡", "塆", "铯", "苏", "王", "桕", "⑧", "°",
|
||||
"浚", "栉",
|
||||
"朘", "虚", "骆", "坂", "秤", "鲋", "蕊", "渝", "呦", "潼", "驱", "诼", "峇", "盤", "趴", "肄", "笑",
|
||||
"讹", "貋",
|
||||
"穂", "啼", "趟", "暽", "傣", "蜎", "挎", "陳", "勖", "戴", "旃", "瞎", "舌", "幻", "喾", "赁", "E",
|
||||
"播", "诀",
|
||||
"蟛", "鹛", "骶", "輸", "連", "醳", "逅", "奉", "崖", "娩", "幔", "佃", "扅", "阔", "生", "贬", "疯",
|
||||
"珀", "苶",
|
||||
"屯", "裣", "蹯", "蝮", "解", "陂", "疝", "茈", "帑", "议", "仲", "埙", "竺", "峰", "遮", "涎", "穸",
|
||||
"阂", "潵",
|
||||
"镱", "例", "荑", "u", "脎", "衍", "轲", "⑵", "虾", "颚", "钞", "²", "伴", "根", "沣", "腌", "户",
|
||||
"~", "辙",
|
||||
"愧", "噤", "觥", "波", "铗", "纂", "鲺", "僚", "毐", "〇", "桼", "祗", "慢", "啵", "坏", "吗", "嗞",
|
||||
"甬", "曈",
|
||||
"徹", "灏", "混", "渌", "括", "脖", "汝", "現", "訇", "紅", "飘", "虢", "腱", "旄", "嬴", "昨", "孀",
|
||||
"蚁", "呛",
|
||||
"讳", "病", ",", "喈", "蒋", "镭", "葩", "耲", "鳈", "锄", "喘", "返", "傕", "咆", "享", "枥", "瓠",
|
||||
"茳", "铱",
|
||||
"脘", "暹", "廒", "爝", "橹", "瞑", "铎", "岢", "叁", "翏", "捭", "賀", "悉", "帝", "芥", "牀", "闌",
|
||||
"毯", "亍",
|
||||
"弧", "锆", "币", "祊", "纔", "齑", "肟", "绤", "獨", "翚", "颢", "係", "鍪", "粉", "统", "诗", "娜",
|
||||
"褥", "鈺",
|
||||
"湔", "呤", "犸", "湨", "泣", "蟾", "犾", "烛", "斐", "朦", "室", "诨", "榭", "煦", "醺", "敞", "燮",
|
||||
"糅", "衽",
|
||||
"孔", "猄", "疭", "辰", "钽", "胁", "釆", "钉", "胤", "涧", "弼", "濯", "汨", "颖", "茫", "皑", "遏",
|
||||
"捃", "坭",
|
||||
"燴", "肩", "滞", "玢", "巽", "砺", "蜿", "毁", "億", "骥", "本", "忽", "肚", "搽", "靰", "郴", "跆",
|
||||
"客", "酣",
|
||||
"α", "屎", "辩", "殂", "垝", "紫", "秦", "喇", "凶", "傧", "铐", "蘊", "補", "贤", "竿", "途", "慗",
|
||||
"榖", "券",
|
||||
"莠", "逆", "鳇", "误", "崟", "妇", "磷", "捧", "莸", "⇋", "绺", "稻", "填", "逋", "侈", "隶", "侵",
|
||||
"翥", "惘",
|
||||
"惧", "鸥", "赠", "壳", "芯", "巩", "獗", "硅", "搎", "鲛", "9", "夸", "穆", "缜", "诓", "观", "薛",
|
||||
"咎", "杧",
|
||||
"页", "饫", "瑟", "率", "礤", "悭", "畔", "匯", "匮", "鼠", "犒", "芡", "傍", "嫂", "啸", "鄉", "哭",
|
||||
"鄱", "捷",
|
||||
"靺", "嚒", "嘀", "哒", "#", "拼", "钚", "魁", "霣", "眶", "郊", "死", "愁", "箭", "鼙", "签", "害",
|
||||
"斛", "睑",
|
||||
"蟜", "余", "墨", "様", "读", "養", "貉", "较", "浆", "翩", "徂", "冕", "铧", "列", "诈", "穝", "缑",
|
||||
"纲", "志",
|
||||
"舀", "甾", "举", "馁", "ä", "畹", "榼", "垢", "襁", "麟", "灭", "佴", "镩", "酝", "柒", "梯", "傈",
|
||||
"萭", "悫",
|
||||
"莨", "搞", "+", "兄", "偲", "攀", "曝", "嵝", "喳", "从", "遶", "撴", ".", "鄄", "欲", "挺", "娡",
|
||||
"发", "速",
|
||||
"胲", "褀", "态", "行", "蚓", "坼", "适", "厦", "寐", "带", "緃", "醤", "珽", "‧", "溍", "斋", "鐀",
|
||||
"朝", "欢",
|
||||
"传", "築", "咪", "据", "蹜", "医", "妄", "肇", "囝", "怡", "镎", "桩", "轩", "岔", "腐", "矽", "媵",
|
||||
"搒", "菔",
|
||||
"拘", "D", "欃", "唧", "瞒", "郈", "绦", "吟", "撝", "醉", "镣", "匝", "拎", "砒", "顸", "袁", "驼",
|
||||
"愔", "实",
|
||||
"國", "奧", "胩", "府", "逾", "愕", "廷", "碌", "锖", "狩", "褴", "镢", "芷", "娥", "唤", "┌", "云",
|
||||
"О", "檔",
|
||||
"驴", "躯", "驺", "洃", "檑", "窴", "(", "腕", "立", "楯", "齮", "〔", "漆", "k", "芍", "蹽", "鬓",
|
||||
"概", "楣",
|
||||
"唐", "闲", "糗", "旱", "幸", "腽", "嗄", "迂", "镠", "顿", "扥", "圃", "烜", "馍", "佝", "岷", "童",
|
||||
"悦", "┐",
|
||||
"铌", "袈", "靓", "骸", "和", "乔", "灸", "泓", "临", "睿", "掖", "偿", "鐘", "犁", "祓", "鈴", "搌",
|
||||
"授", "鹳",
|
||||
"赢", "怅", "絪", "硬", "芙", "螅", "”", "傢", "避", "裕", "歁", "全", "衰", "仃", "媛", "鬻", "跽",
|
||||
"沌", "急",
|
||||
"猷", "激", "巉", "哝", "渣", "笫", "跳", "螫", "熜", "Z", "筷", "佩", "啶", "萃", "頫", "荙", "出",
|
||||
"孽", "钟",
|
||||
"戡", "釉", "咬", "滦", "鹇", "贯", "鹮", "具", "翁", "机", "濱", "谳", "釣", "懑", "葛", "袯", "谭",
|
||||
"质", "胴",
|
||||
"誊", "侗", "⑩", "静", "蚜", "溋", "嫪", "嗲", "瑭", "座", "舫", "靶", "棘", "泊", "嵖", "摧", "勋",
|
||||
"僡", "藉",
|
||||
"疖", "巂", "随", "罾", "崚", "猹", "憨", "苘", "斓", "鼷", "利", "谲", "剔", "艺", "箓", "蛀", "鲚",
|
||||
"搐", "裟",
|
||||
"捶", "绌", "揪", "帮", "缥", "匍", "冀", "杻", "逛", "邑", "禾", "郰", "黜", "丘", "樂", "滌", "緣",
|
||||
"胃", "苄",
|
||||
"巾", "瑜", "元", "蝶", "层", "烧", "级", "岭", "蘭", "繇", "蝓", "洞", "奢", "则", "政", "矾", "啭",
|
||||
"瘠", "碴",
|
||||
"忤", "身", "匠", "警", "饩", "犬", "皲", "箔", "豕", "虑", "草", "喟", "芤", "逭", "艳", "幡", "姚",
|
||||
"賓", "饪",
|
||||
"卯", "敌", "烽", "嫚", "黝", "豺", "㭗", "教", "偕", "板", "茹", "孤", "人", "狻", "寰", "厕", "玲",
|
||||
"璨", "锵",
|
||||
"搛", "勍", "匾", "聃", "奘", "垃", "焓", "喽", "嫫", "貌", "瘐", "嚰", "孟", "衔", "郎", "账", "础",
|
||||
"电", "黑",
|
||||
"骁", "拨", "濆", "圉", "刮", "闭", "竣", "铅", "羔", "硌", "筑", "难", "管", "苕", "眺", "嫄", "竖",
|
||||
"榟", "崴",
|
||||
"摭", "狐", "娑", "②", "罽", "谊", "←", "狳", "铫", "凯", "狉", "9", "肪", "崤", "莊", "妨", "缶",
|
||||
"滃", "瀦",
|
||||
"揉", "肫", "恧", "糯", "嵬", "5", "裆", "嚷", "稣", "隐", "仂", "て", "驹", "籽", "肢", "尘", "苈",
|
||||
"撷", "镲",
|
||||
"趹", "晤", "唱", "鉏", "篌", "驩", "雍", "闳", "拄", "藜", "朴", "伺", "诳", "房", "吱", "Й", "鳄",
|
||||
"罿", "祧",
|
||||
"酩", "郅", "耎", "尜", "绝", "禅", "揠", "鎏", "慕", "麥", "呜", "鸫", "党", "尝", "砑", "牌", "踉",
|
||||
"刨", "襻",
|
||||
"㾄", "螽", "谌", "止", "抑", "爻", "磬", "铄", "蓠", "委", "汲", "鹑", "╱", "嚣", "彝", "穄", "穹",
|
||||
"態", "醋",
|
||||
"⒀", "叼", "婳", "簌", "渥", "很", "甸", "帅", "锏", "与", "樾", "泷", "棼", "湲", "越", "祥", "短",
|
||||
"顼", "阘",
|
||||
"宋", "馘", "鈉", "未", "囍", "浏", "叻", "箜", "鑽", "法", "曲", "淤", "僮", "做", "强", "析", "磕",
|
||||
"谠", "染",
|
||||
"促", "朊", "隼", "铉", "莆", "蝣", "孛", "薮", "s", "惴", "秘", "妩", "訄", "蔓", "喷", "诡", "犷",
|
||||
"酐", "酇",
|
||||
"刹", "壅", "甫", "史", "孃", "髌", "螬", "擤", "漏", "寞", "奡", "悢", "颔", "岁", "耄", ";", "又",
|
||||
"锭", "鲤",
|
||||
"癔", "杰", "孥", "酲", "蓐", "耋", "捆", "庖", "面", "鹈", "殊", "剡", "峪", "识", "锨", "归", "茴",
|
||||
"—", "菤",
|
||||
"汁", "攝", "液", "鼐", "示", "讠", "男", "凍", "ò", "明", "莓", "砜", "崎", "蜂", "斡", "榫", "娅",
|
||||
"钪", "昙",
|
||||
"胜", "欣", "怨", "◆", "粗", "秷", "节", "市", "贩", "祟", "弍", "蒟", "烁", "糧", "蠃", "編", "黙",
|
||||
"壕", "戚",
|
||||
"犊", "桥", "仺", "孳", "怯", "皓", "倆", "垮", "扩", "诮", "钝", "脯", "晏", "帔", "葫", "瑾", "運",
|
||||
"孬", "跄",
|
||||
"掣", "癜", "掌", "墀", "禇", "耸", "蜓", "鹆", "鄢", "攰", "瘢", "暝", "鸣", "峧", "遵", "笃", "畚",
|
||||
"帧", "晨",
|
||||
"镔", "搜", "靠", "咐", "韓", "绮", "觉", "拦", "斲", "疽", "掐", "尽", "許", "矶", "镉", "豹", "粞",
|
||||
"袋", "酵",
|
||||
"蛙", "戕", "劉", "髀", "彭", "玎", "囿", "郐", "善", "睃", "結", "拧", "邯", "讧", "召", "椭", "瑪",
|
||||
"痼", "庼",
|
||||
"反", "疱", "屠", "荣", "君", "胍", "乙", "臬", "头", "诰", "讪", "席", "晁", ":", "理", "槿", "璘",
|
||||
"禧", "呢",
|
||||
"蹙", "擒", "鸲", "丐", "苓", "壑", "滥", "⑾", "炗", "礴", "耕", "卅", "唿", "苛", "寵", "窖", "麻",
|
||||
"蕨", "沤",
|
||||
"氢", "虔", "癃", "及", "崛", "爽", "蛔", "颤", "膲", "桢", "坐", "蟞", "儇", "葚", "骤", "誤", "寝",
|
||||
"嘭", "灰",
|
||||
"汹", "韂", "铮", "慒", "寶", "肽", "摅", "紧", "亞", "潸", "悯", "橛", "檗", "闹", "愿", "担", "袄",
|
||||
"棚", "垟",
|
||||
"塄", "婞", "麈", "麸", "暗", "咦", "跞", "谡", "盈", "磐", "慎", "瘰", "掼", "憔", "研", "被", "贮",
|
||||
"莛", "至",
|
||||
"呀", "庑", "矫", "摛", "怃", "缙", "磺", "即", "驻", "瘤", "偏", "℃", "嫘", "癫", "汈", "鹟", "搅",
|
||||
"辅", "璀",
|
||||
"阊", "绻", "瑙", "蓂", "棺", "孢", "铊", "鼒", "果", "砮", "飾", "凰", "Я", "遗", "祛", "纮", "劲",
|
||||
"霹", "骃",
|
||||
"绔", "薅", "瀵", "垅", "?", "轻", "惇", "怕", "啥", "哙", "燎", "缆", "匡", "怫", "卞", "朋", "酏",
|
||||
"阑", "爾",
|
||||
"伏", "敏", "埼", "罩", "菹", "艋", "肭", "鯭", "杋", "裀", "撬", "蕺", "惠", "大", "爇", "笈", "絷",
|
||||
"琳", "谫",
|
||||
"诛", "糇", "袢", "倓", "髃", "觽", "埏", "寖", "個", "筴", "外", "漯", "樭", "喁", "杀", "臑", "缇",
|
||||
"裸", "巅",
|
||||
"毹", "茅", "忆", "琼", "唑", "烷", "项", "隋", "约", "排", "吮", "谂", "宝", "牲", "瘫", "娄", "沂",
|
||||
"醫", "拭",
|
||||
"纺", "蹰", "哞", "风", "霆", "值", "酺", "侠", "螾", "埂", "育", "夷", "鮼", "怍", "鸠", "Θ", "瞳",
|
||||
"阇", "耥",
|
||||
"羝", "伽", "洴", "記", "楔", "颼", "沪", "邢", "冰", "昀", "阙", "洌", "嫦", "杂", "仔", "芑", "潴",
|
||||
"痄", "桨",
|
||||
"连", "碓", "塈", "F", "昇", "何", "桦", "晥", "驵", "旋", "药", "银", "奋", "灣", "俐", "絡", "嫁",
|
||||
"浮", "为",
|
||||
"鞅", "科", "颦", "潽", "镍", "鸨", "粵", "骂", "拱", "韫", "盆", "赎", "尿", "钿", "坍", "唁", "秧",
|
||||
"昌", "曆",
|
||||
"颋", "遭", "秭", "褔", "腋", "〉", "吉", "漓", "臆", "焘", "已", "制", "钹", "鴨", "咖", "莘", "P",
|
||||
"碥", "互",
|
||||
"治", "标", "膝", "伪", "浿", "纛", "郗", "看", "佧", "糖", "篓", "亡", "´", "骙", "澡", "影", "窂",
|
||||
"紬", "镅",
|
||||
"慌", "框", "晋", "説", "丢", "凹", "卖", "巧", "蹉", "乾", "莫", "Z", "谔", "矧", "铑", "暴", "庄",
|
||||
"湿", "活",
|
||||
"穿", "腩", "筣", "水", "6", "琦", "迈", "伯", "洄", "抡", "▪", "酋", "荤", "雒", "粕", "簠", "菰",
|
||||
"髁", "枇",
|
||||
"陲", "多", "仗", "央", "滁", "胸", "梏", "痉", "姑", "襞", "﹑", "齿", "弩", "花", "吆", "赫", "岵",
|
||||
"佪", "谑",
|
||||
"锤", "轴", "盐", "馄", "臜", "戢", "涠", "鸸", "糟", "孪", "禁", "蒲", "化", "疏", "痰", "脾", "刈",
|
||||
"應", "珍",
|
||||
"膺", "扌", "廙", "汜", "牍", "虐", "婿", "啕", "彻", "赝", "陶", "蠲", ">", "位", "屁", "醍", "粢",
|
||||
"挪", "臌",
|
||||
"滹", "遴", "馨", "n", "稼", "徊", "酌", "轸", "债", "朰", "程", "辞", "痊", "插", "鹩", "郄", "铝",
|
||||
"狱", "叱",
|
||||
"同", "寄", "搪", "蚯", "魭", "舍", "旷", "闰", "涝", "民", "嗡", "苌", "馕", "姥", "屉", "啧", "枢",
|
||||
"❤", "窕",
|
||||
"钊", "矬", "菂", "佑", "≠", "獬", "桁", "墟", "皖", "鼻", "它", "歇", "独", "好", "晕", "蚝", "锞",
|
||||
"颈", "豚",
|
||||
"聖", "裉", "扫", "岿", "悒", "佥", "苗", "妞", "晚", "圭", "茼", "脲", "摊", "窠", "狸", "抻", "场",
|
||||
"呼", "囟",
|
||||
"噗", "狺", "困", "瀹", "削", "衬", "谰", "蛆", "訓", "鉄", "痃", "炱", "蝻", "我", "暨", "骓", "馋",
|
||||
"埤", "脞",
|
||||
"晃", "螟", "洮", "泛", "掾", "穑", "米", "蕲", "玦", "讙", "逢", "劐", "袭", "凫", "僳", "畛", "晷",
|
||||
"鳕", "Ë",
|
||||
"愬", "坫", "鳡", "鞯", "叔", "胂", "囚", "筋", "青", "度", "涕", "琰", "﹔", "径", "陇", "睛", "链",
|
||||
"状", "逶",
|
||||
"蘅", "“", "庇", "邽", "纥", "踶", "爺", "狭", "钫", "桃", "弛", "淳", "办", "茕", "砸", "喱", "仅",
|
||||
"潞", "杈",
|
||||
"得", "咕", "俞", "检", "借", "恋", "驿", "倌", "钢", "琐", "哆", "撙", "箫", "川", "猥", "牢", "蹁",
|
||||
"城", "馏",
|
||||
"锡", "楝", "蛱", "奈", "瑶", "桺", "耆", "翟", "阒", "稲", "橐", "萱", "惹", "蘼", "主", "擦", "蟒",
|
||||
"台", "佬",
|
||||
"荫", "廖", "笏", "铕", "衣", "洇", "炒", "瀍", "崭", "圻", "洚", "契", "嫱", "倏", "晶", "了", "堠",
|
||||
"勰", "椎",
|
||||
"询", "梗", "飒", "锰", "览", "溇", "寻", "蓅", "【", "碇", "井", "露", "顔", "堌", "庳", "踩", "i",
|
||||
"饷", "俊",
|
||||
"楫", "條", "搭", "奍", "羽", "憋", "岘", "毡", "曜", "乃", "′", "针", "羲", "菓", "吩", "咩", "鞘",
|
||||
"尊", "宫",
|
||||
"舜", "啖", "惗", "北", "懊", "骇", "阄", "躅", "权", "缲", "肥", "铜", "《", "录", "也", "棬", "煮",
|
||||
"舄", "厮",
|
||||
"'", "順", "受", "霜", "新", "售", "牞", "圣", "妗", "犴", "宥", "哦", "陀", "卺", "冚", "蹒", "亸",
|
||||
"禮", "骰",
|
||||
"瑢", "弒", "抛", "谷", "嫰", "動", "嘌", "惩", "枣", "忌", "茡", "爵", "嘚", "郧", "丨", "敲", "帚",
|
||||
"沭", "槊",
|
||||
"⑶", "專", "毶", "圄", "磅", "蛭", "由", "蠹", "剜", "诫", "秆", "愠", "藓", "母", "请", "衩", "忸",
|
||||
"蜕", "饽",
|
||||
"晦", "倔", "腠", "痛", "品", "簧", "父", "锐", "描", "蓰", "蛴", "箍", "兕", "苜", "饼", "奚", "泗",
|
||||
"裥", "皂",
|
||||
"嵚", ",", "澶", "蠖", "沅", "馎", "籀", "菝", "眵", "糥", "铽", "痤", "颟", "淄", "作", "抉", "俄",
|
||||
"么", "郑",
|
||||
"耒", "佛", "1", "纡", "鸢", "④", "鎚", "壖", "遢", "鬈", "拢", "托", "哈", "節", "橦", "冼", "六",
|
||||
"耗", "樵",
|
||||
"涔", "舳", "龌", "衿", "婧", "栓", "椹", "嘘", "膊", "茁", "丹", "螃", "剖", "洧", "珞", "潺", "孱",
|
||||
"呐", "萩",
|
||||
"刷", "引", "说", "熟", "/", "靖", "酷", "耠", "饬", "菌", "洙", "荃", "饲", "酾", "阁", "陬", "铿",
|
||||
"倻", "牮",
|
||||
"鞡", "撕", "倘", "盒", "曺", "襦", "辄", "算", "塬", "潢", "羖", "湾", "续", "△", "疙", "谖", "嘅",
|
||||
"遑", "篚",
|
||||
"筮", "氍", "递", "尧", "G", "{", "分", "埒", "@", "蜍", "荼", "襆", "槭", "檠", "縢", "濉", "梆",
|
||||
"隔", "镛",
|
||||
"倞", "润", "瓯", "瓢", "蟊", "沐", "啷", "砚", "皱", "剅", "儙", "错", "幌", "滓", "砗", "郤", "喧",
|
||||
"峣", "簸",
|
||||
"毖", "踏", "锕", "…", "悖", "谧", "醵", "加", "镐", "泐", "傫", "胪", "缄", "卩", "蓼", "丸", "垌",
|
||||
"汞", "宴",
|
||||
"膙", "圊", "矻", "嚏", "漾", "幕", "駕", "葒", "绪", "袪", "镋", "杭", "澴", "鬃", "粟", "偻", "饳",
|
||||
"抨", "亟",
|
||||
"温", "韶", "轿", "罟", "际", "诖", "复", "坯", "骗", "*", "副", "裢", "憬", "邾", "崇", "蕈", "疮",
|
||||
"粽", "炝",
|
||||
"珲", "莅", "衾", "爲", "枯", "汛", "仁", "熏", "馥", "㎡", "檐", "锦", "竭", "颁", "遽", "瘙", "样",
|
||||
"遛", "殍",
|
||||
"湄", "消", "鳌", "痫", "鳏", "瓶", "窈", "谚", "麒", "鸹", "蟋", "横", "唠", "瘪", "媪", "侔", "鐵",
|
||||
"系", "杖",
|
||||
"m", "叉", "沟", "衢", "寘", "■", "弗", "建", "疣", "珣", "綦", "劈", "道", "嘈", "先", "芝", "降",
|
||||
"滕", "邵",
|
||||
"邺", "給", ")", "廨", "郛", "势", "氇", "坤", "昂", "焼", "奕", "闱", "朓", "毽", "还", "坨", "銭",
|
||||
"龂", "銎",
|
||||
"壽", "矸", "窒", "①", "玷", "蝽", "泃", "烀", "魈", "★", "慶", "K", "嘶", "酶", "呖", "殿", "乡",
|
||||
"䄂", "阳",
|
||||
"轪", "碱", "譬", "摩", "鳖", "刳", "地", "包", "貊", "悝", "圩", "今", "嚭", "凳", "谕", "馃", "捎",
|
||||
"佯", "侬",
|
||||
"愆", "微", "涤", "舔", "蛇", "筲", "助", "锾", "剧", "缧", "簪", "惚", "柢", "庾", "虹", "雪", "猡",
|
||||
"脔", "亶",
|
||||
"烨", "T", "锗", "芈", "女", "动", "偬", "琥", "县", "诣", "精", "嬗", "栀", "艨", "智", "冗", "闼",
|
||||
"嗝", "z",
|
||||
"夢", "拿", "鹲", "尤", "啮", "﹐", "ɔ", "钓", "施", "萼", "邻", "竞", "碶", "艰", "》", "翻", "馆",
|
||||
"橪", "逝",
|
||||
"臀", "淫", "枉", "羿", "拇", "溷", "徒", "涓", "關", "聋", "嵊", "殖", "叛", "敫", "舵", "亊", "诽",
|
||||
"菱", "苎",
|
||||
"破", "腚", "A", "嵋", "扊", "挂", "篷", "棂", "碟", "復", "劾", "韪", "疔", "粒", "鲵", "毙", "店",
|
||||
"锻", "衮",
|
||||
"寳", "◎", "斯", "倦", "醢", "曾", "茚", "荐", "隗", "芊", "豪", "亻", "哂", "堃", "宇", "桑", "匋",
|
||||
"植", "亥",
|
||||
"撂", "棒", "蟠", "W", "迟", "蚋", "溊", "缌", "鞚", "蚤", "適", "赌", "卣", "厚", "鲾", "匙", "槃",
|
||||
"郎", "鬏",
|
||||
"玳", "龄", "丈", "圮", "冑", "院", "葬", "嵐", "瓦", "孵", "漶", "星", "吐", "獍", "藠", "萍", "振",
|
||||
"潜", "龉",
|
||||
"匦", "粹", "諾", "畵", "峦", "&", "埕", "朵", "戒", "炳", "酪", "绂", "篁", "测", "殆", "涌", "业",
|
||||
"盏", "醊",
|
||||
"笆", "孰", "骊", "湛", "踰", "汎", "哲", "澙", "鲷", "√", "鄣", "亿", "螺", "吠", "伟", "凛", "骡",
|
||||
"恻", "巨",
|
||||
"扶", "泡", "峯", "韵", "腎", "睦", "栖", "}", "笙", "疌", "绶", "忒", "哥", "价", "纻", "薨", "漂",
|
||||
"濮", "缮",
|
||||
"勐", "妮", "傩", "陛", "陷", "柆", "瞭", "鲳", "烬", "喉", "固", "桡", "聊", "逦", "猊", "梻", "涵",
|
||||
"栒", "逍",
|
||||
"饥", "凼", "早", "姣", "蕤", "塌", "桀", "亳", "虻", "鹨", "典", "情", "怄", "商", "钍", "赚", "塥",
|
||||
"煽", "垱",
|
||||
"蝴", "乓", "籁", "帷", "锢", "圪", "快", "赘", "杵", "漠", "滴", "斩", "拈", "蚕", "陽", "篡", "郦",
|
||||
"瞻", "郯",
|
||||
"鳍", "幽", "旅", "乖", "鹖", "斫", "痂", "肸", "右", "锂", "永", "泾", "茎", "觱", "彼", "擎", "䨱",
|
||||
"翱", "徝",
|
||||
"醅", "求", "湫", "転", "溴", "師", "瓣", "蝠", "铭", "社", "苞", "仇", "噌", "你", "嗾", "雳", "榧",
|
||||
"駹", "雯",
|
||||
"叨", "遫", "氏", "航", "辗", "溢", "历", "楷", "诱", "雏", "梳", "藕", "屺", "槎", "钐", "燘", "棽",
|
||||
"驸", "褪",
|
||||
"清", "十", "廰", "移", "筌", "揾", "瞠", "姽", "馑", "恢", "逸", "p", "瑚", "茄", "鹧", "俗", "璟",
|
||||
"栊", "买",
|
||||
"瀛", "镒", "球", "氲", "缛", "講", "胀", "焒", "悲", "翕", "拗", "T", "桌", "脓", "闪", "稀", "狎",
|
||||
"火", "柁",
|
||||
"琴", "澍", "嗟", "龚", "楮", "噼", "隽", "栩", "焻", "哩", "藻", "瘸", "含", "偶", "界", "嘃", "昶",
|
||||
"澄", "頤",
|
||||
"绒", "鲁", "麝", "决", "撒", "岙", "季", "刿", "肝", "蒉", "蓇", "财", "完", "蠔", "脉", "肱", "谙",
|
||||
"蜮", "郭",
|
||||
"慨", "晔", "髂", "蛏", "眨", "钗", "葺", "惆", "娈", "瞵", "踞", "棁", "蝢", "嚎", "猝", "必", "剞",
|
||||
"关", "咛",
|
||||
"劫", "闸", "肯", "№", "莩", "哇", "蛑", "镬", "羡", "驊", "茂", "塍", "沓", "筱", "杉", "战", "茧",
|
||||
"耙", "击",
|
||||
"需", "腊", "酎", "畦", "葙", "鹘", "韭", "嚚", "争", "域", "伢", "鞲", "哳", "栲", "某", "翌", "哗",
|
||||
"焚", "螗",
|
||||
"懲", "躲", "約", "镖", "凿", "饶", "够", "剁", "铥", "应", "署", "杮", "蒂", " ", "坷", "礅", "款",
|
||||
"梁", "鄜",
|
||||
"髹", "選", "伤", "路", "З", "亲", "野", "啦", "捯", "憷", "鲩", "札", "怏", "塘", "绊", "愍", "簦",
|
||||
"牦", "黥",
|
||||
"鳜", "唉", "W", "沱", "蚺", "甪", "摉", "协", "耨", "娱", "桄", "仆", "类", "搡", "滤", "岗", "休",
|
||||
"坶", "谒",
|
||||
"忭", "飨", "闷", "菟", "鲣", "驷", "湜", "疡", "蚩", "萊", "䝉", "硒", "贺", "弃", "徘", "陨", "否",
|
||||
"遥", "妒",
|
||||
"X", "間", "觜", "跬", "夬", "羮", "喙", "赇", "鹗", "『", "砀", "残", "绿", "小", "勘", "瀌", "扉",
|
||||
"耧", "衅",
|
||||
"挟", "乐", "鹏", "墁", "澜", "噍", "坊", "術", "嗖", "知", "盉", "圆", "嗈", "蘖", "资", "爭", "=",
|
||||
"刑", "裒",
|
||||
"〈", "淸", "定", "袒", "戗", "钤", "吵", "旯", "蓝", "裎", "溅", "贰", "荏", "甥", "悌", "勤", "炽",
|
||||
"换", "躜",
|
||||
"!", "薄", "痱", "双", "匕", "肷", "挥", "茑", "船", "砝", "煤", "荜", "弘", "▏", "陆", "稔", "朽",
|
||||
"冤", "頉",
|
||||
"遊", "砰", "迎", "碎", "唪", "醪", "稆", "练", "锸", "阵", "皇", "香", "镀", "嫡", "持", "桶", "垄",
|
||||
"阍", "戥",
|
||||
"臣", "琛", "涘", "惶", "赙", "葆", "住", "舊", "枝", "媲", "蓣", "龅", "搦", "_", "图", "力", "纪",
|
||||
"悍", "麗",
|
||||
"戽", "腧", "绣", "跟", "哕", "打", "蝰", "Φ", "吞", "功", "夀", "劓", "沇", "熔", "占", "隰", "命",
|
||||
"佻", "豁",
|
||||
"苣", "楦", "掇", "蛛", "唢", "郜", "霉", "鲏", "予", "沸", "殻", "俯", "探", "篪", "荇", "邈", "烯",
|
||||
"忮", "伸",
|
||||
"岬", "×", "锧", "窸", "毪", "纩", "蛋", "讯", "骼", "叶", "楂", "犟", "站", "盘", "隈", "喝", "儣",
|
||||
"兵", "尚",
|
||||
"孙", "爿", "芜", "羁", "旖", "溽", "迩", "京", "7", "龃", "狝", "缦", "缁", "鲃", "怒", "故", "據",
|
||||
"枫", "髙",
|
||||
"亭", "耳", "飚", "O", "编", "箸", "幼", "氘", "鞮", "匐", "祯", "臃", "辫", "磋", "溝", "墙", "诚",
|
||||
"阻", "档",
|
||||
"歆", "璃", "悻", "婤", "映", "瑞", "牂", "话", "忠", "潘", "惋", "冬", "氦", "腔", "胬", "盔", "\"",
|
||||
"饮", "贶",
|
||||
"嚄", "儆", "溜", "砷", "樇", "跏", "泩", "馌", "埃", "莙", "革", "珙", "乌", "鍋", "穴", "石", "珺",
|
||||
"熹", "诞",
|
||||
"<", "腉", "姊", "钧", "罪", "拆", "赊", "殒", "堇", "仑", "掺", "塃", "獴", "迥", "盦", "檬", "益",
|
||||
"居", "鼑",
|
||||
"异", "嘻", "悔", "旮", "况", "時", "阋", "洛", "線", "#", "型", "迕", "睇", "橱", "笊", "蛞", "愚",
|
||||
"茉", "镈",
|
||||
"镞", "垭", "扁", "泫", "搬", "古", "书", "疸", "痨", "黟", "墉", "料", "并", "ㆍ", "裳", "鞑", "湮",
|
||||
"柠", "颐",
|
||||
"形", "━", "逹", "硁", "置", "韦", "瓞", "象", "殽", "均", "浓", "瞓", "椐", "洨", "乱", "襜", "终",
|
||||
"優", "睹",
|
||||
"敦", "鼬", "唆", "佼", "財", "瘃", "H", "痳", "勺", "依", "虎", "蕖", "玄", "缓", "滢", "^", "骅",
|
||||
"诘", "弋",
|
||||
":", "∩", "廪", "缈", "造", "蕉", "孖", "嫒", "寨", "意", "岽", "庶", "罗", "瞢", "酹", "蔟", "赴",
|
||||
"烂", "栋",
|
||||
"格", "矛", "驯", "词", "嗦", "剀", "蓓", "期", "鏢", "羑", "奴", "椱", "A", "狗", "烟", "蹬", "案",
|
||||
"记", "讴",
|
||||
"鳑", "侯", "霏", "焜", "沬", "份", "酦", "芗", "庚", "瑗", "鹎", "穗", "鲠", "肛", "厄", "蜔", "學",
|
||||
"伊", "⑥",
|
||||
"琪", "邒", "少", "霖", "蓖", "猜", "塾", "肾", "罃", "伐", "钩", "骈", "溟", "饵", "莉", "é", "刖",
|
||||
"洯", "堉",
|
||||
"锝", "趔", "七", "萁", "竹", "憾", "蚨", "离", "柔", "替", "侑", "飙", "气", "震", "厥", "备", "刻",
|
||||
"顽", "瞽",
|
||||
"腄", "雄", "燃", "旬", "简", "翠", "熥", "◇", "吃", "囡", "玙", "铷", "暖", "配", "傻", "窄", "皈",
|
||||
"夼", "舂",
|
||||
"乜", "苩", "攉", "雠", "茇", "锈", "酰", "粮", "祝", "考", "堍", "鳅", "彬", "▲", "孝", "蠊", "顇",
|
||||
"娲", "腥",
|
||||
"$", "珠", "厂", "诠", "蹓", "轼", "嵫", "捩", "硗", "胺", "证", "膀", "」", "胯", "钷", "毂", "柙",
|
||||
"深", "沄",
|
||||
"匹", "8", "爷", "礳", "秏", "窜", "魑", "d", "转", "烆", "屿", "眙", "極", "袤", "護", "V", "狂",
|
||||
"柑", "玠",
|
||||
"氩", "’", "馊", "玛", "坢", "%", "燔", "颗", "舅", "暂", "艾", "芹", "溏", "晰", "件", "琚", "仿",
|
||||
"祾", "酤",
|
||||
"騠", "揳", "鲫", "蜥", "仨", "牺", "步", "讓", "港", "煲", "铴", "腦", "鳝", "危", "鋪", "冠", "正",
|
||||
"柽", "抍",
|
||||
"掘", "控", "娴", "娀", "離", "手", "臾", "酗", "筼", "煸", "弹", "照", "哎", "毒", "颀", "诙", "刚",
|
||||
"搢", "䧳",
|
||||
"峒", "滋", "\\", "匀", "黉", "毓", "娠", "床", "浪", "祐", "铟", "4", "?", "凄", "飗", "蚍", "葑",
|
||||
"抗", "鹞",
|
||||
"糸", "红", "英", "违", "橡", "眷", "防", "缬", "龠", "察", "仍", "辇", "减", "闫", "箴", "龍", "館",
|
||||
"屙", "翙",
|
||||
"媽", "涴", "到", "旻", "删", "瞾", "鏖", "咭", "豨", "荘", "炭", "畼", "构", "锘", "鉫", "候", "扇",
|
||||
"繄", "猩",
|
||||
"瘵", "恺", "贵", "榦", "息", "恽", "胎", "狰", "雜", "辋", "璜", "硈", "泠", "呔", "蹿", "踹", "摄",
|
||||
"炀", "坞",
|
||||
"蹄", "裝", "赛", "蝥", "塔", "靳", "荬", "找", "仡", "淮", "比", "淆", "义", "淝", "卢", "辟", "寂",
|
||||
"庒", "鳯",
|
||||
"暲", "景", "邪", "腻", "赍", "甍", "讲", "哌", "嶝", "鎌", "总", "缱", "问", "磛", "谅", "拉", "靈",
|
||||
"奭", "沆",
|
||||
"茔", "羅", "鄠", "網", "吏", "懵", "鑫", "歌", "黹", "嵘", "涞", "碳", "崂", "婥", "赞", "镑", "購",
|
||||
"幺", "鸰",
|
||||
"饟", "蝌", "忝", "懒", "禺", "梽", "齉", "恳", "拯", "弥", "荡", "芾", "幪", "厌", "馒", "蜘", "欸",
|
||||
"吣", "1",
|
||||
"却", "榻", "碾", "袂", "錎", "钬", "無", "嬉", "笞", "蹴", "视", "雇", "创", "椟", "6", "瘁", "斜",
|
||||
"傥", "喃",
|
||||
"炷", "秾", "嘱", "茀", "犄", "窑", "庀", "潍", "伦", "䀲", "凉", "Р", "撻", "萜", "二", "倨", "蔑",
|
||||
"捕", "勚",
|
||||
"士", "鈇", "踺", "啤", "彧", "缪", "述", "傅", "颅", "畸", "畜", "滗", "慭", "琎", "斌", "参", "胳",
|
||||
"骖", "稠",
|
||||
"汰", "铻", "闯", "留", "蘘", "沏", "亦", "择", "華", "禽", "砟", "祼", "狃", "噫", "狼", "寤", "跪",
|
||||
"浠", "·",
|
||||
"费", "瓘", "鼹", "锪", "箢", "垣", "慊", "虏", "秩", "偉", "镏", "钯", "恐", "鹃", "菇", "炸", "潮",
|
||||
"蟀", "硂",
|
||||
"偌", "哏", "验", "桉", "阴", "初", "掴", "鹺", "峨", "赋", "舉", "裹", "赶", "土", "淋", "瘌", "沔",
|
||||
"r", "赀",
|
||||
"淖", "茯", "怛", "谜", "洗", "似", "舡", "纳", "晓", "R", "诐", "痹", "漪", "顺", "挛", "阎", "贝",
|
||||
"钰", "惬",
|
||||
"疬", "菀", "埘", "怙", "部", "译", "鲭", "窋", "敢", "夜", "撰", "珅", "特", "襕", "癖", "胡", "⒃",
|
||||
"附", "擘",
|
||||
"痢", "尬", "鉴", "瞋", "膨", "阽", "挲", "⒄", "骎", "帕", "缕", "计", "障", "鳆", "隹", "朔", "碹",
|
||||
"当", "迦",
|
||||
"氙", "蘑", "妓", "炬", "苊", "萎", "浈", "沥", "绯", "壤", "噱", "蹾", "驶", "葱", "孕", "羹", "钻",
|
||||
"農", "勝",
|
||||
"膈", "灿", "赆", "靿", "耱", "陪", "忙", "缰", "奶", "儒", "个", "朱", "燹", "琮", "轷", "錾", "箅",
|
||||
"澳", "嗥",
|
||||
"攥", "没", "匿", "鲆", "|", "矣", "他", "鸶", "芸", "B", "髑", "街", "巿", "廣", "盯", "監", "鲸",
|
||||
"胭", "凬",
|
||||
"寿", "挝", "绽", "+", "劝", "究", "眢", "集", "衙", "卷", "j", "跶", "牡", "畯", "貅", "销", "發",
|
||||
"咱", "蓊",
|
||||
"揣", "咝", "琶", "荦", "阌", "盅", "嘹", "苟", "醮", "洪", "鲧", "钒", "柱", "氨", "旰", "冽", "茭",
|
||||
"嵇", "粲",
|
||||
"蛾", "訾", "辔", "N", "尹", "趿", "蹲", "疟", "祠", "段", "車", "网", "⒉", "舷", "廐", "侣", "棵",
|
||||
"粜", "觐",
|
||||
"铼", "锁", "兒", "舁", "时", "垦", "版", "摈", "扳", "见", "腮", "嫖", "痭", "呆", "簖", "伋", "鳙",
|
||||
"珊", "麂",
|
||||
"既", "谴", "热", "超", "蠕", "铞", "e", "殓", "因", "锿", "文", "禊", "皙", "鑙", "爹", "鋼", "忻",
|
||||
"秣", "镁",
|
||||
"奠", "橉", "畺", "笮", "疹", "湝", "龟", "殃", "毵", "溃", "勢", "索", "砉", "阼", "堞", "酥", "冁",
|
||||
"喊", "¥",
|
||||
"幛", "娇", "锲", "蕃", "铘", "铍", "鴿", "响", "傲", "脏", "杓", "罕", "笥", "弦", "但", "缃", "扬",
|
||||
"盲", "碚",
|
||||
"幢", "鎖", "缺", "钋", "麽", "禳", "浃", "啄", "昧", "蒴", "帙", "琏", "咧", "舰", "亵", "浊", "豳",
|
||||
"衲", "俏",
|
||||
"镵", "浩", "勾", "槛", "榈", "徙", "鹤", "洹", "铂", "揎", "棕", "挦", "挫", "阆", "衹", "甚", "近",
|
||||
"】", "簏",
|
||||
"汽", "踮", "淌", "檇", "痔", "谝", "钙", "蕞", "蔯", "兆", "蔽", "后", "蚬", "谸", "芟", "枞", "叫",
|
||||
"栗", "餘",
|
||||
"营", "郝", "氯", "㺃", "狍", "冏", "庛", "纱", "泼", "碍", "认", "邓", "茵", "饧", "闟", "惝", "裙",
|
||||
"噙", "忘",
|
||||
"虬", "群", "S", "佗", "恼", "坟", "肮", "皮", "玃", "在", "赧", "孚", "偾", "镨", "恨", "葡", "西",
|
||||
"缞", "挠",
|
||||
"逃", "吾", "膪", "焦", "翘", "桧", "变", "渗", "繁", "際", "痘", "撼", "筅", "坑", "前", "玑", "数",
|
||||
"融", "鲌",
|
||||
"讦", "窃", "鄌", "伾", "众", "攻", "彪", "锎", "焐", "殛", "锊", "嗉", "枓", "抢", "鞠", "掩", "贾",
|
||||
"搔", "皁",
|
||||
"拶", "朗", "渺", "跛", "㛃", "鏾", "慥", "杆", "沈", "戍", "豫", "楠", "爆", "汤", "昉", "耘", "缡",
|
||||
".", "允",
|
||||
"揜", "责", "艟", "裁", "喬", "砹", "鹣", "裼", "啉", "蛳", "酮", "听", "维", "阪", "獾", "浣", "訂",
|
||||
"瘿", "蜡",
|
||||
"泖", "蔚", "貔", "致", "禨", "尓", "糺", "绐", "遯", "笄", "邦", "圈", "洟", "缟", "槲", "桹", "镓",
|
||||
"骒", "髫",
|
||||
"暾", "像", "縻", "戊", "飧", "驽", "干", "万", "绕", "披", "雅", "桊", "卡", "贲", "吡", "沧", "鳟",
|
||||
"堂", "扺",
|
||||
"岱", "封", "鄭", "螣", "瞩", "幞", "邕", "睫", "涩", "自", "趱", "愣", "威", "酊", "罂", "慑", "袴",
|
||||
"架", "烘",
|
||||
"现", "灞", "钔", "股", "興", "乍", "噜", "济", "碛", "兀", "诅", "柴", "瓿", "[", "怿", "竦", "白",
|
||||
"黄", "阶",
|
||||
"务", "榮", "澹", "谏", "垓", "跸", "繻", "窿", "紊", "陟", "劁", "嗑", "牯", "厉", "敛", "鮕", "嘉",
|
||||
"蔻", "鼎",
|
||||
"恒", "硝", "溉", "骘", "窘", "任", "裱", "处", "旨", "舶", "缸", "囹", "笠", "讥", "泜", "脊", "煊",
|
||||
"淦", "牝",
|
||||
"硕", "胧", "泚", "溪", "贪", "牛", "答", "瘴", "Q", "炯", "⑤", "篾", "銀", "乩", "杶", "垆", "蛐",
|
||||
"苔", "啪",
|
||||
"y", "玮", "琫", "寮", "邂", "後", "僵", "贴", "硭", "枚", "姆", "乎", "讶", "醭", "橥", "脱", "蒈",
|
||||
"擞", "忪",
|
||||
"顾", "柚", "褿", "忲", "辖", "铡", "螠", "殉", "喆", "爡", "轮", "棰", "鲉", "跃", "韬", "睡", "嘧",
|
||||
"袅", "圗",
|
||||
"檄", "踊", "阀", "题", "桫", "林", "沉", "禚", "散", "麇", "沦", "秋", "导", "斑", "宰", "嘞", "暑",
|
||||
"笱", "搋",
|
||||
"擅", "镤", "锶", "L", "厣", "有", "猗", "袆", "绞", "甭", "歧", "跣", "潦", "専", "绑", "飱", "廓",
|
||||
"磔", "接",
|
||||
"腓", "窎", "瑁", "飓", "蟪", "俎", "П", "缉", "䘵", "夙", "潟", "桷", "淡", "虺", "恶", "|", "驭",
|
||||
"怀", "邋",
|
||||
"辢", "逻", "晖", "蜃", "蜊", "溻", "冢", "尻", "礼", "厝", "亘", "酴", "饔", "悸", "戆", "什", "玚",
|
||||
"馔", "哔",
|
||||
"沃", "竑", "葭", "垞", "鏂", "抃", "弄", "去", "焊", "焌", "x", "苇", "與", "炼", "蛄", "莴", "阏",
|
||||
"薷", "禀",
|
||||
"鸯", "栽", "冒", "姓", "0", "尃", "蜞", "毗", "骟", "秸", "荸", "柈", "恬", "赡", "侏", "兑", "蝤",
|
||||
"荷", "徳",
|
||||
"押", "挣", "腰", "宣", "鸵", "葳", "遘", "讨", "狒", "涿", "囤", "邃", "蒜", "疑", "脍", "嘟", "鹠",
|
||||
"吻", "鄹",
|
||||
"耦", "华", "霸", "侥", "勒", "挞", "臊", "尺", "让", "榆", "阝", "鳚", "灾", "鲬", "艿", "Ⅱ", "锩",
|
||||
"攮", "蚊",
|
||||
"蔁", "唝", "涅", "挹", "淏", "鏊", "氵", "鹕", "律", "對", "粱", "恫", "挻", "滏", "叮", "‰", "鼯",
|
||||
"绫", "秉",
|
||||
"怩", "質", "岐", "菊", "佤", "帏", "骺", "爰", "珉", "耪", "乞", "郕", "鲱", "雷", "蒿", "不", "啐",
|
||||
"侓", "郓",
|
||||
"歼", "拒", "胗", "寕", "旒", "勁", "婢", "诌", "蹂", "姐", "媳", "歃", "拐", "辐", "拟", "醯", "雌",
|
||||
"点", "玟",
|
||||
"您", "鲨", "载", "藩", "罔", "噀", "抹", "萏", "补", "蝎", "辈", "劢", "乚", "唔", "瓜", "恿", "蟭",
|
||||
"涸", "纬",
|
||||
"睽", "樣", "帻", "蓥", "谯", "柯", "渴", "脁", "诲", "福", "③", "熊", "羌", "疴", "袖", "虿", "杏",
|
||||
"覌", "易",
|
||||
"醾", "筒", "肤", "苷", "柃", "榇", "酬", "癸", "啰", "眛", "稷", "展", "鸾", "祢", "蝾", "敵", "毛",
|
||||
"痦", "老",
|
||||
"赐", "单", "淹", "畋", "符", "奎", "绥", "轾", "鄺", "濩", "眦", "觎", "忾", "璋", "刘", "翳", "菪",
|
||||
"簟", "胼",
|
||||
"孫", "機", "獻", "低", "o", "捅", "鳥", "⑨", "卸", "废", "启", "呋", "窥", "巢", "揭", "咴", "趺",
|
||||
"鲥", "空",
|
||||
"膄", "崮", "锜", "络", "納", "恤", "刭", "批", "霭", "氧", "钮", "甑", "祲", "粘", "辏", "∶", "臨",
|
||||
"│", "粪",
|
||||
"惰", "肼", "浉", "橄", "5", "東", "8", "漁", "浜", "忍", "奂", "遹", "扃", "扦", "入", "欤", "豆",
|
||||
"悠", "蕴",
|
||||
"萑", "媄", "龈", "÷", "磙", "鸬", "缎", "嗯", "浕", "木", "陋", "柰", "瘩", "箨", "松", "躁", "鲇",
|
||||
"彰", "恕",
|
||||
"楗", "姨", "撅", "诹", "戮", "桓", "棉", "束", "嗍", "庋", "瞿", "郂", "哪", "町", "a", "铁", "洫",
|
||||
"失", "栳",
|
||||
"篇", "鳗", "眇", "椿", "義", "就", "都", "镌", "阖", "夭", "拃", "跚", "業", "圬", "演", "篥", "3",
|
||||
"昼", "從",
|
||||
"瞍", "蓦", "鼾", "坪", "觫", "鲍", "馿", "妾", "密", "奥", "耰", "佟", "嘤", "貘", "薯", "稽", "届",
|
||||
"褛", "钠",
|
||||
"猿", "佈", "倍", "铩", "铙", "踵", "o", "捱", "鹐", "當", "狷", "写", "遆", "钱", "姗", "寸", "综",
|
||||
"挑", "礶",
|
||||
"靼", "溯", "湟", "漱", "碰", "职", "味", "Λ", "璁", "壴", "给", "碘", "恭", "苴", "酚", "套", "宕",
|
||||
"辽", "窀",
|
||||
"催", "踢", "惟", "璧", "翔", "稿", "癀", "霈", "光", "膑", "妆", "庡", "圾", "躺", "惮", "切", "目",
|
||||
"梦", "岫",
|
||||
"飑", "叠", "累", "鴻", "透", "量", "妻", "沨", "俪", "屣", "挚", "w", "厅", "货", "箧", "漦", "隙",
|
||||
"瘥", "妃",
|
||||
"門", "鹦", "隳", "龆", "瞪", "跑", "帼", "刺", "起", "鞴", "岸", "渫", "莽", "髭", "耜", "瑛", "葉",
|
||||
"宵", "绠",
|
||||
"眭", "畅", "茱", "卜", "濂", "浍", "冈", "脬", "厘", "夔", "纟", "槚", "哐", "萋", "曼", "膳", "潲",
|
||||
"啜", "啊",
|
||||
"猾", "捉", "箕", "㐂", "藝", "艘", "園", "妲", "灵", "蚌", "范", "&", "疥", "陑", "洒", "唾", "a",
|
||||
"婄", "谋",
|
||||
"t", "唬", "宓", "瓷", "︰", "焗", "默", "噔", "菡", "恃", "亩", "边", "啟", "颞", "啬", "盟", "墩",
|
||||
"汭", "缝",
|
||||
"魇", "酡", "疗", "梾", "尾", "鹿", "锔", "溦", "酃", "囊", "掉", "罥", "报", "诤", "喹", "蕙", "割",
|
||||
"蠢", "兼",
|
||||
"俺", "升", "屦", "獠", "辣", "跹", "颧", "宪", "抬", "咿", "沲", "旗", "荩", "傒", "鳳", "變", "偈",
|
||||
"薹", "钭",
|
||||
"垧", "谪", "躐", "谀", "R", "字", "怆", "陵", "追", "迓", "舞", "卲", "捲", "麾", "税", "方", "竴",
|
||||
"壹", "射",
|
||||
"轫", "塞", "僧", "椠", "突", "阕", "惫", "U", "佞", "秒", "友", "視", "莹", "攒", "荻", "俅", "筐",
|
||||
"能", "刓",
|
||||
"拍", "浥", "揽", "谨", "军", "巷", "怵", "额", "恪", "荀", "糍", "湎", "褙", "觇", "效", "估", "噩",
|
||||
"恍", "聚",
|
||||
"涛", "式", "兜", "汔", "祃", "銘", "烺", "瑄", "枘", "丁", "樗", "堙", "窗", "號", "鬣", "阈", "乳",
|
||||
"簇", "绳",
|
||||
"淞", "]", "征", "聩", "凸", "礓", "柘", "懂", "㧎", "纷", "里", "爸", "靥", "莞", "馅", "仞", "芏",
|
||||
"莳", "殪",
|
||||
"煌", "落", "佚", "帱", "诶", "家", "将", "浯", "抿", "讫", "趸", "筢", "绩", "原", "专", "滂", "嗳",
|
||||
"踬", "泱",
|
||||
"體", "蜱", "绵", "伻", "埗", "妤", "蔺", "赳", "嘢", "梨", "鹪", "烤", "镦", "赉", "崞", "蚰", "骝",
|
||||
"幅", "汴",
|
||||
"丽", "访", "叩", "羼", "亓", "恸", "燥", "笤", "丑", "枰", "守", "蜚", "戈", "高", "q", "瓻", "隘",
|
||||
"H", "南",
|
||||
"剎", "扭", "骠", "孜", "园", "锬", "审", "¥", "罚", "购", "裰", "泔", "醌", "醒", "绎", "莪", "掬",
|
||||
"睾", "鹁",
|
||||
"蛊", "票", "剃", "释", "调", "黛", "撩", "篱", "茌", "敓", "蜜", "魉", "鳤", "昽", "俳", "辁", "键",
|
||||
"犯", "嶙",
|
||||
"狈", "邬", "枋", "婊", "憎", "督", "救", "措", "足", "码", "栟", "虼", "棻", "阅", "肊", "坛", "鸱",
|
||||
"侩", "烫",
|
||||
"湉", "脐", "戾", "旎", "膚", "椒", "境", "绛", "濛", "蓄", "章", "压", "窦", "彖", "阜", "咻", "神",
|
||||
"存", "诺",
|
||||
"課", "覺", "鲈", "渚", "「", "缢", "仓", "氅", "筇", "桴", "荥", "些", "纯", "肋", "退", "妪", "别",
|
||||
"書", "租",
|
||||
"嚼", "芘", "笳", "涰", "馐", "熵", "犹", "朕", "猪", "蔸", "參", "觳", "舖", "蝙", "骏", "织", "躏",
|
||||
"纹", "锹",
|
||||
"蛉", "撑", "氽", "茶", "俛", "誠", "胖", "崽", "炫", "乘", "把", "趑", "谵", "骧", "犍", "鸮", "灌",
|
||||
"焯", "倭",
|
||||
"狁", "盱", "踱", "滠", "儡", "诸", "芽", "駆", "蹼", "虱", "祇", " ", "蠡", "衄", "谬", "蒍", "婺",
|
||||
"蘧", "博",
|
||||
"捍", "磁", "慷", "釂", "蛩", "钕", "祎", "呑", "贫", "斗", "菅", "操", "规", "硼", "惕", "丫", "俭",
|
||||
"肿", "骀",
|
||||
"砼", "句", "茛", "闶", "钏", "饻", "圠", "萌", "魏", "铃", "摞", "┅", "伎", "獭", "田", "钴", "峭",
|
||||
"魅", "捋",
|
||||
"唼", "鹅", "祚", "嬷", "圖", "抄", "嶂", "鸳", "溧", "钵", "嗽", "墠", "锌", "愈", "併", "踟", "羯",
|
||||
"翅", "纠",
|
||||
"勻", "岚", "菖", "便", "祈", "毳", "屹", "掰", "倬", "扛", "巴", "拮", "绁", "跌", "飯", "嵯", "翮",
|
||||
"堤", "诃",
|
||||
"腑", "皆", "鄂", "胾", "片", "这", "浙", "雨", "鼱", "袼", "鹊", "厢", "蛣", "摇", "蛮", "揍", "⒅",
|
||||
"啱", "薇",
|
||||
"岈", "兔", "谇", "纶", "肉", "崩", "甩", "涪", "馼", "铣", "锚", "丙", "雲", "烹", "钦", "撄", "铬",
|
||||
"令", "虮",
|
||||
"湊", "禹", "抖", "喏", "旺", "畲", "戬", "嗷", "釜", "车", "缀", "玕", "谐", "啁", "怎", "下", "惑",
|
||||
"恅", "藿",
|
||||
"筰", "帐", "祸", "镝", "喵", "刁", "习", "藏", "墓", "护", "聲", "箦", "严", "按", "谛", "睨", "艚",
|
||||
"歉", "蟮",
|
||||
"胶", "盍", "鳃", "狯", "垫", "杠", "线", "3", "葖", "t", "熬", "虞", "嶶", "篮", "黻", "墒", "氟",
|
||||
"嫠", "漕",
|
||||
"腾", "哟", "玫", "撇", "垍", "靛", "翼", "淅", "省", "斥", "稞", "蠼", "谩", "埠", "蘸", "刊", "烝",
|
||||
"宁", "鹚",
|
||||
"龊", "苪", "袷", "诧", "细", "蒌", "焅", "2", "筹", "扒", "卮", "捞", "净", "菲", "逮", "槍", "蛎",
|
||||
"莱", "黠",
|
||||
"逖", "辚", "剥", "啴", "诿", "楱", "氪", "领", "嗒", "藐", "惜", "甘", "佾", "嵴", "胫", ";", "晳",
|
||||
"锣", "瞅",
|
||||
"缷", "耶", "搤", "策", "咽", "邀", "霾", "悟", "属", "鸪", "牴", "贞", "趁", "丞", "瘆", "豌", "著",
|
||||
"饿", "筠",
|
||||
"划", "璇", "损", "卵", "腒", "畏", "盥", "耐", "圏", "拓", "蒙", "鋫", "劙", "蹦", "熘", "烊", "匏",
|
||||
"咔", "轘",
|
||||
"沽", "菩", "罴", "磉", "炖", "假", "枪", "龙", "俸", "焱", "四", "─", "毫", "涨", "浇", "椰", "賣",
|
||||
"蘇", "真",
|
||||
"安", "坝", "枕", "鸼", "昵", "亨", "苤", "祷", "枧", "赟", "菑", "鳂", "戌", "悄", "種", "鳢", "嗣",
|
||||
"電", "颥",
|
||||
"妯", "谟", "蜒", "训", "泍", "洁", "勇", "哿", "扰", "蟆", "螂", "刃", "絜", "曪", "乒", "湖", "鞨",
|
||||
"懜", "夤",
|
||||
"哓", "胥", "桞", "俇", "肣", "半", "于", "橼", "锑", "熙", "甜", "槌", "盾", "屃", "缗", "共", "碗",
|
||||
"凇", "笔",
|
||||
"阿", "擗", "袍", "敉", "钾", "俶", "蚪", "琤", "凱", "辕", "à", "恣", "皴", "創", "寥", "妳", "腈",
|
||||
"畤", "迤",
|
||||
"垠", "触", "趙", "铤", "逊", "羊", "碉", "锱", "骨", "仄", "斟", "俚", "啡", "芩", "迫", "杷", "种",
|
||||
"鹄", "牗",
|
||||
"耑", "艇", "芰", "整", "慙", "飛", "甓", "岩", "鸦", "黎", "僊", "糁", "谈", "洋", "椑", "健", "〕",
|
||||
"內", "言",
|
||||
"掷", "倚", "姬", "矿", "灯", "阐", "凋", "銮", "豇", "瑰", "抓", "噪", "堋", "吁", "妈", "庥", "彳",
|
||||
"鄗", "闩",
|
||||
"夫", "0", "庠", "悬", "妙", "琵", "着", "首", "熣", "瞰", "揆", "燚", "条", "姜", "滘", "麓", "鳉",
|
||||
"椀", "蓿",
|
||||
"廠", "泌", "蝈", "倕", "丛", "耍", "且", "蝇", "凉", "豐", "泪", "臼", "服", "刍", "織", "渎", "尥",
|
||||
"甙", "埋",
|
||||
"珏", "援", "祖", "彊", "臱", "惦", "葴", "礁", "达", "橋", "钨", "崦", "醐", "巳", "中", "颜", "溠",
|
||||
"铹", "负",
|
||||
"抠", "愎", "罘", "雩", "胝", "冱", "筵", "篙", "材", "肓", "○", "迅", "腼", "橙", "仵", "茏", "慵",
|
||||
"齌", "琯",
|
||||
"疃", "貢", "豉", "瞟", "忉", "禄", "通", "咒", "愫", "秕", "筜", "觖", "州", "渑", "胆", "喑", "張",
|
||||
"衖", "洺",
|
||||
"眉", "榞", "色", "邶", "攫", "堑", "淬", "嗪", "肐", "殣", "辊", "隧", "献", "潤", "蓺", "函", "鹱",
|
||||
"轭", "劭",
|
||||
"椁", "膜", "亹", "侧", "貂", "哚", "磴", "蠋", "囔", "险", "伶", "世", "菥", "莶", "瘾", "燫", "延",
|
||||
"∵", "毋",
|
||||
"羧", "无", "雎", "曛", "沚", "巍", "g", "熄", "恰", "伷", "開", "冻", "颛", "支", "⑦", "鞥", "赖",
|
||||
"试", "泮",
|
||||
"联", "沮", "穰", "è", "峻", "滿", "豊", "刎", "鴈", "覆", "串", "锃", "春", "储", "矍", "哺", "评",
|
||||
"猁", "愉",
|
||||
"疳", "閃", "奄", "甲", "墦", "頭", "锫", "俦", "玩", "搀", "砭", "流", "橇", "泅", "琇", "趣", "∧",
|
||||
"辑", "灊",
|
||||
"貴", "迮", "摹", "霄", "濟", "限", "彀", "匪", "缂", "觚", "奇", "诋", "灼", "萘", "狠", "澥", "岂",
|
||||
"悺", "闾",
|
||||
"麋", "号", "槽", "姹", "陉", "瑯", "尉", "h", "绖", "宿", "戋", "粝", "砂", "该", "鞧", "翯", "釘",
|
||||
"铢", "窨",
|
||||
"設", "⒆"]`
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,31 +0,0 @@
|
||||
package ddddocr
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"likedream-goframe/internal/application/recognizer"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:embed static/common_old.onnx
|
||||
var commonOldBytes []byte
|
||||
|
||||
func initModel() string {
|
||||
if !recognizer.OnnxIsInit {
|
||||
recognizer.InitOnnx()
|
||||
}
|
||||
// 检测文件是否存在
|
||||
exePath := strings.ToLower(os.Args[0])
|
||||
// 获取绝对路径
|
||||
absPath, _ := filepath.Abs(exePath)
|
||||
staticPath := filepath.Join(filepath.Dir(absPath), "static")
|
||||
if _, err := os.Stat(staticPath); os.IsNotExist(err) {
|
||||
os.Mkdir(staticPath, 0755)
|
||||
}
|
||||
modelPath := filepath.Join(staticPath, "common_old.onnx")
|
||||
if _, err := os.Stat(modelPath); os.IsNotExist(err) {
|
||||
os.WriteFile(modelPath, commonOldBytes, 0644)
|
||||
}
|
||||
return modelPath
|
||||
}
|
||||
Binary file not shown.
@@ -1,64 +0,0 @@
|
||||
package recognizer
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
ort "github.com/yalue/onnxruntime_go"
|
||||
"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
|
||||
|
||||
// OnnxIsInit 初始化onnx配置
|
||||
var OnnxIsInit = false
|
||||
|
||||
func InitOnnx() {
|
||||
name, bytes := getOnnxRuntimeName()
|
||||
// 检测文件是否存在
|
||||
exePath := strings.ToLower(os.Args[0])
|
||||
// 获取绝对路径
|
||||
absPath, _ := filepath.Abs(exePath)
|
||||
filePath := filepath.Join(filepath.Dir(absPath), name)
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
os.WriteFile(filePath, bytes, 0644)
|
||||
}
|
||||
ort.SetSharedLibraryPath(filePath)
|
||||
err := ort.InitializeEnvironment()
|
||||
if err != nil {
|
||||
logger.Logger.Panic("初始化onnx:", zap.String("绝对路径", filePath))
|
||||
return
|
||||
}
|
||||
OnnxIsInit = true
|
||||
}
|
||||
|
||||
func getOnnxRuntimeName() (onnxRuntimeFileName string, onnxRuntimeFileBytes []byte) {
|
||||
systemOS := runtime.GOOS
|
||||
switch systemOS {
|
||||
case "windows":
|
||||
onnxRuntimeFileName = "onnxruntime.dll"
|
||||
onnxRuntimeFileBytes = onnxRuntimeFileWindows
|
||||
return
|
||||
case "darwin":
|
||||
onnxRuntimeFileName = "onnxruntime.dylib"
|
||||
onnxRuntimeFileBytes = onnxRuntimeFileDarwin
|
||||
return
|
||||
case "linux":
|
||||
onnxRuntimeFileName = "onnxruntime.so"
|
||||
onnxRuntimeFileBytes = onnxRuntimeFileLinux
|
||||
return
|
||||
}
|
||||
logger.Logger.Panic("暂不支持的操作系统:", zap.String("系统名称", systemOS))
|
||||
return "", nil
|
||||
}
|
||||
+1
-1
@@ -24,7 +24,7 @@ type XuexitongLogin struct {
|
||||
|
||||
func NewXuexitongLogin(ctx context.Context) *XuexitongLogin {
|
||||
return &XuexitongLogin{
|
||||
client: httpclient.Xuexitong.Clone(ctx),
|
||||
client: httpclient.NewXuexitongClient(),
|
||||
}
|
||||
}
|
||||
func (c *XuexitongLogin) Login(ctx context.Context, username, password string, retry int) (err error) {
|
||||
@@ -1,101 +1,102 @@
|
||||
package xuexitong
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"likedream-goframe/internal/application/recognizer/ddddocr"
|
||||
"likedream-goframe/internal/application/xuexitong/entity"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
"likedream-goframe/internal/utils"
|
||||
"strings"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
ort "github.com/yalue/onnxruntime_go"
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
ApiPullCourses = "https://mooc1-api.chaoxing.com/mycourse/backclazzdata"
|
||||
)
|
||||
|
||||
type XuexitongCourse struct {
|
||||
client *resty.Client
|
||||
}
|
||||
|
||||
func NewXuexitongCourse(client *resty.Client) *XuexitongCourse {
|
||||
return &XuexitongCourse{
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
// PullCourseList 拉取学习通所有课程列表并返回
|
||||
func (c *XuexitongCourse) PullCourseList(ctx context.Context, retry int) (courseList []entity.XueXiTCourse, err error) {
|
||||
req := c.client.R()
|
||||
req.Header.Add("User-Agent", GetUA("mobile"))
|
||||
get, err := req.Get(ApiPullCourses)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body := get.String()
|
||||
if strings.Contains(body, "很抱歉,您所浏览的页面不存在") && retry > 0 { //防止学习通抽风
|
||||
return c.PullCourseList(ctx, retry-1)
|
||||
}
|
||||
if strings.Contains(body, "请输入验证码") {
|
||||
logger.Logger.Warn("触发验证码,正在进行AI智能识别绕过.....")
|
||||
//todo ddddocr识别
|
||||
succ := false
|
||||
for i := 0; i < 5; i++ {
|
||||
codePath, err := ApiVerifyCode(c.client, 5)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if codePath == "" { //如果path为空,那么可能是账号问题
|
||||
return nil, errors.New("无法正常获取学习通验证码,请检查对应url是否正常")
|
||||
}
|
||||
img, _ := utils.ReadImg(codePath) //读取验证码图片
|
||||
codeResult := ddddocr.SemiOCRVerification(img, ort.NewShape(1, 23))
|
||||
utils.DeleteFile(codePath) //删除验证码文件、
|
||||
if ApiPassVerifyCode(c.client, codeResult, 5) {
|
||||
succ = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !succ {
|
||||
return nil, errors.New("AI验证码识别失败,请检查对应url是否正常")
|
||||
}
|
||||
}
|
||||
var courseResult entity.XueXiTCourseJson
|
||||
err = sonic.Unmarshal([]byte(body), &courseResult)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, channel := range courseResult.ChannelList {
|
||||
if channel.Content.Course.Data == nil {
|
||||
continue
|
||||
}
|
||||
for _, v := range channel.Content.Course.Data {
|
||||
flag := false
|
||||
course := entity.XueXiTCourse{
|
||||
Cpi: channel.Cpi,
|
||||
Key: strings.Split(strings.Split(v.CourseSquareUrl, "classId=")[1], "&userId")[0],
|
||||
CourseID: strings.Split(strings.Split(v.CourseSquareUrl, "courseId=")[1], "&personId")[0],
|
||||
CourseTeacher: v.Teacherfactor,
|
||||
CourseImage: v.Name,
|
||||
CourseDataID: v.Id,
|
||||
ContentID: channel.Content.Id,
|
||||
IsStart: channel.Content.Isstart,
|
||||
State: channel.Content.State,
|
||||
}
|
||||
for _, subCourse := range courseList {
|
||||
if subCourse.CourseID == course.CourseID {
|
||||
flag = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !flag {
|
||||
courseList = append(courseList, course)
|
||||
}
|
||||
}
|
||||
}
|
||||
return courseList, nil
|
||||
}
|
||||
//
|
||||
//import (
|
||||
// "context"
|
||||
// "errors"
|
||||
// "likedream-goframe/internal/application/recognizer/ddddocr"
|
||||
// "likedream-goframe/internal/application/xuexitong/entity"
|
||||
// "likedream-goframe/internal/framework/logger"
|
||||
// "likedream-goframe/internal/utils"
|
||||
// "strings"
|
||||
//
|
||||
// "github.com/bytedance/sonic"
|
||||
// ort "github.com/yalue/onnxruntime_go"
|
||||
// "resty.dev/v3"
|
||||
//)
|
||||
//
|
||||
//const (
|
||||
// ApiPullCourses = "https://mooc1-api.chaoxing.com/mycourse/backclazzdata"
|
||||
//)
|
||||
//
|
||||
//type XuexitongCourse struct {
|
||||
// client *resty.Client
|
||||
//}
|
||||
//
|
||||
//func NewXuexitongCourse(client *resty.Client) *XuexitongCourse {
|
||||
// return &XuexitongCourse{
|
||||
// client: client,
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//// PullCourseList 拉取学习通所有课程列表并返回
|
||||
//func (c *XuexitongCourse) PullCourseList(ctx context.Context, retry int) (courseList []entity.XueXiTCourse, err error) {
|
||||
// req := c.client.R()
|
||||
// req.Header.Add("User-Agent", GetUA("mobile"))
|
||||
// get, err := req.Get(ApiPullCourses)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// body := get.String()
|
||||
// if strings.Contains(body, "很抱歉,您所浏览的页面不存在") && retry > 0 { //防止学习通抽风
|
||||
// return c.PullCourseList(ctx, retry-1)
|
||||
// }
|
||||
// if strings.Contains(body, "请输入验证码") {
|
||||
// logger.Logger.Warn("触发验证码,正在进行AI智能识别绕过.....")
|
||||
// //todo ddddocr识别
|
||||
// succ := false
|
||||
// for i := 0; i < 5; i++ {
|
||||
// codePath, err := ApiVerifyCode(c.client, 5)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// if codePath == "" { //如果path为空,那么可能是账号问题
|
||||
// return nil, errors.New("无法正常获取学习通验证码,请检查对应url是否正常")
|
||||
// }
|
||||
// img, _ := utils.ReadImg(codePath) //读取验证码图片
|
||||
// codeResult := ddddocr.SemiOCRVerification(img, ort.NewShape(1, 23))
|
||||
// utils.DeleteFile(codePath) //删除验证码文件、
|
||||
// if ApiPassVerifyCode(c.client, codeResult, 5) {
|
||||
// succ = true
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// if !succ {
|
||||
// return nil, errors.New("AI验证码识别失败,请检查对应url是否正常")
|
||||
// }
|
||||
// }
|
||||
// var courseResult entity.XueXiTCourseJson
|
||||
// err = sonic.Unmarshal([]byte(body), &courseResult)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// for _, channel := range courseResult.ChannelList {
|
||||
// if channel.Content.Course.Data == nil {
|
||||
// continue
|
||||
// }
|
||||
// for _, v := range channel.Content.Course.Data {
|
||||
// flag := false
|
||||
// course := entity.XueXiTCourse{
|
||||
// Cpi: channel.Cpi,
|
||||
// Key: strings.Split(strings.Split(v.CourseSquareUrl, "classId=")[1], "&userId")[0],
|
||||
// CourseID: strings.Split(strings.Split(v.CourseSquareUrl, "courseId=")[1], "&personId")[0],
|
||||
// CourseTeacher: v.Teacherfactor,
|
||||
// CourseImage: v.Name,
|
||||
// CourseDataID: v.Id,
|
||||
// ContentID: channel.Content.Id,
|
||||
// IsStart: channel.Content.Isstart,
|
||||
// State: channel.Content.State,
|
||||
// }
|
||||
// for _, subCourse := range courseList {
|
||||
// if subCourse.CourseID == course.CourseID {
|
||||
// flag = true
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// if !flag {
|
||||
// courseList = append(courseList, course)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return courseList, nil
|
||||
//}
|
||||
|
||||
+6
@@ -10,6 +10,12 @@ func TestXuexitongLogin(t *testing.T) {
|
||||
xuexitongLogin := NewXuexitongLogin(context.Background())
|
||||
err := xuexitongLogin.Login(context.Background(), "13551503012", "xiaozhi520", 5)
|
||||
utils.OkOrPanic(err)
|
||||
}
|
||||
|
||||
func TestPullCourse(t *testing.T) {
|
||||
xuexitongLogin := NewXuexitongLogin(context.Background())
|
||||
err := xuexitongLogin.Login(context.Background(), "13551503012", "xiaozhi520", 5)
|
||||
utils.OkOrPanic(err)
|
||||
client := xuexitongLogin.GetClient()
|
||||
courseAction := NewXuexitongCourse(client)
|
||||
list, err := courseAction.PullCourseList(context.Background(), 5)
|
||||
@@ -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"
|
||||
ConfigCaptchaBaseUrl = "captcha.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,13 +3,19 @@ package cst
|
||||
const (
|
||||
RedisUserInfo = "userinfo:"
|
||||
RedisVerifyCode = "verifycode:"
|
||||
RedisResetPassword = "resetpassword:"
|
||||
RedisBalanceMigrate = "balancemigrate:" // 积分转移token
|
||||
RedisCpUserLogin = "cpuserlogin:"
|
||||
RedisCpUserAuthorize = "cpuser:authorize:"
|
||||
RedisSchoolInfo = "cpuser:schoolinfo:"
|
||||
)
|
||||
|
||||
var (
|
||||
RedisUserInfoExpire = int64(86400)
|
||||
RedisVerifyCodeExpire = int64(86400)
|
||||
RedisResetPasswordExpire = int64(3600) // 重置密码链接1小时有效
|
||||
RedisBalanceMigrateExpire = int64(3600) // 积分转移链接1小时有效
|
||||
RedisCpUserLoginExpire = int64(180)
|
||||
RedisCpUserAuthorizeExpire = int64(1800)
|
||||
RedisSchoolInfoExpire = int64(3600) // 学校信息1小时有效
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
@@ -27,4 +31,20 @@ func InitDB() {
|
||||
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表示永久)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package cpdaily
|
||||
import (
|
||||
"context"
|
||||
v1 "likedream-goframe/internal/api/v1"
|
||||
"likedream-goframe/internal/application/cpdaily/map_info"
|
||||
"likedream-goframe/internal/ent"
|
||||
"likedream-goframe/internal/service"
|
||||
"likedream-goframe/internal/utils"
|
||||
@@ -11,6 +12,11 @@ import (
|
||||
type CpTaskCtrlV1 struct {
|
||||
}
|
||||
|
||||
func (c CpTaskCtrlV1) FindLocation(ctx context.Context, req *v1.FindLocationReq) (res *utils.Response[any], err error) {
|
||||
defer utils.Pass(&err)
|
||||
return utils.SuccessWithData[any](map_info.SearchAddressLongitudeAndLatitude(ctx, req.Address)), nil
|
||||
}
|
||||
|
||||
func (c CpTaskCtrlV1) List(ctx context.Context, req *v1.CpTaskListReq) (res *utils.Response[[]*ent.CpTask], err error) {
|
||||
defer utils.Pass(&err)
|
||||
return service.Global.CpTask.List(ctx, req), nil
|
||||
|
||||
@@ -2,9 +2,7 @@ package cpdaily
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
v1 "likedream-goframe/internal/api/v1"
|
||||
"likedream-goframe/internal/application/recognizer/classifier"
|
||||
"likedream-goframe/internal/ent"
|
||||
"likedream-goframe/internal/service"
|
||||
"likedream-goframe/internal/utils"
|
||||
@@ -13,14 +11,6 @@ import (
|
||||
type CpTaskLogCtrlV1 struct {
|
||||
}
|
||||
|
||||
func (c CpTaskLogCtrlV1) OCR(ctx context.Context, req *v1.CpTaskOCRReq) (res *utils.Response[any], err error) {
|
||||
file, _ := req.File.Open()
|
||||
defer file.Close()
|
||||
bytes, _ := io.ReadAll(file)
|
||||
result := classifier.ClassifyImage(bytes, req.Name)
|
||||
return utils.SuccessWithData[any](result), nil
|
||||
}
|
||||
|
||||
func (c CpTaskLogCtrlV1) List(ctx context.Context, req *v1.CpTaskLogListReq) (res *utils.Response[[]*ent.CpTaskLog], err error) {
|
||||
defer utils.Pass(&err)
|
||||
return service.Global.CpTaskLog.List(ctx, req), nil
|
||||
|
||||
@@ -18,4 +18,5 @@ type ICpTaskCtrlV1 interface {
|
||||
Delete(ctx context.Context, req *v1.DeleteTaskReq) (res *utils.Response[any], err error)
|
||||
ExecuteNow(ctx context.Context, req *v1.ExecuteNowReq) (res *utils.Response[any], err error)
|
||||
PauseTask(ctx context.Context, req *v1.PauseTaskReq) (res *utils.Response[any], err error)
|
||||
FindLocation(ctx context.Context, req *v1.FindLocationReq) (res *utils.Response[any], err error)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,4 @@ import (
|
||||
|
||||
type ICpTaskLogCtrlV1 interface {
|
||||
List(ctx context.Context, req *v1.CpTaskLogListReq) (res *utils.Response[[]*ent.CpTaskLog], err error)
|
||||
|
||||
OCR(ctx context.Context, req *v1.CpTaskOCRReq) (res *utils.Response[any], err error)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
OnDelete: schema.Cascade,
|
||||
},
|
||||
{
|
||||
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(),
|
||||
|
||||
@@ -31,7 +31,7 @@ func (CpUser) Fields() []ent.Field {
|
||||
// Edges of the CpUser.
|
||||
func (CpUser) Edges() []ent.Edge {
|
||||
return []ent.Edge{
|
||||
edge.To("cptasks", CpTask.Type),
|
||||
edge.To("cptasks", CpTask.Type).Annotations(entsql.OnDelete(entsql.Cascade)),
|
||||
edge.From("sysuser", SysUser.Type).Unique().Required().Field("user_id").Ref("cpuser"),
|
||||
edge.To("cp_task_logs", CpTaskLog.Type).Annotations(entsql.OnDelete(entsql.Cascade)), // 用户拥有的cp任务日志
|
||||
edge.From("school", CpSchool.Type).
|
||||
|
||||
@@ -12,16 +12,16 @@ import (
|
||||
errors2 "likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/ent"
|
||||
"likedream-goframe/internal/ent/cptask"
|
||||
"likedream-goframe/internal/ent/cpuser"
|
||||
"likedream-goframe/internal/framework/errors"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
"likedream-goframe/internal/framework/model/email"
|
||||
email2 "likedream-goframe/internal/service/email"
|
||||
"likedream-goframe/internal/utils"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/google/uuid"
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/samber/oops"
|
||||
"go.uber.org/zap"
|
||||
@@ -53,7 +53,9 @@ func (c *CpdailyTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task)
|
||||
db.Global.CpTask.UpdateOneID(taskPayload.CpTaskID).SetTaskStatus(cptask.TaskStatusDoing).ExecX(ctx)
|
||||
|
||||
defer utils.HandleError(func(err error) {
|
||||
e := db.Global.CpTask.UpdateOneID(taskPayload.CpTaskID).SetTaskStatus(cptask.TaskStatusFail).Exec(ctx)
|
||||
// 执行失败应该更新下次执行时间
|
||||
executeTime := utils.GetCpdailyNextExecuteTime(targetTask)
|
||||
e := db.Global.CpTask.UpdateOneID(taskPayload.CpTaskID).SetNextExecuteTime(executeTime).SetTaskStatus(cptask.TaskStatusFail).Exec(ctx)
|
||||
utils.LogError(ctx, "设置任务状态失败", oops.With("task_id", taskPayload.CpTaskID).Wrap(e))
|
||||
|
||||
var errorMessage string
|
||||
@@ -63,7 +65,7 @@ func (c *CpdailyTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task)
|
||||
} else if e1, ok := utils.Of[*errors2.BusinessError](err); ok {
|
||||
errorMessage = e1.Message
|
||||
} else {
|
||||
errorMessage = "未知的错误导致任务执行失败"
|
||||
errorMessage = "未知的错误导致任务执行失败" + err.Error()
|
||||
}
|
||||
|
||||
e = db.Global.CpTaskLog.Create().
|
||||
@@ -90,16 +92,18 @@ 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)
|
||||
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)
|
||||
restyClient = login.GetClient(ctx, cookie)
|
||||
}
|
||||
cookie := targetTask.Edges.CpUser.Cookie
|
||||
restyClient := login.GetClient(ctx, cookie)
|
||||
//if !login.CookieEnabled(ctx, schoolDetail.WecloudUrl, restyClient) {
|
||||
// 定时任务强制登录并更新cookie
|
||||
loginExecutor := login.GetCpdailyLoginBySchoolInfo(ctx, &schoolDetail)
|
||||
loginExecutor.Login(ctx, &schoolDetail, targetTask.Edges.CpUser.Username, targetTask.Edges.CpUser.Password)
|
||||
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)
|
||||
|
||||
@@ -120,23 +124,26 @@ func (c *CpdailyTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task)
|
||||
e = sonic.UnmarshalString(targetTask.FormData, &customFormItems)
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).With("task_id", taskPayload.CpTaskID).In("CpdailyTask.ProcessTask").Wrap)
|
||||
|
||||
var deviceID string
|
||||
|
||||
switch targetTask.Edges.CpUser.MobileType {
|
||||
case cpuser.MobileTypeRandom:
|
||||
deviceID = uuid.NewString()
|
||||
case cpuser.MobileTypeAuthorize:
|
||||
deviceID = targetTask.Edges.CpUser.MobileCode
|
||||
default:
|
||||
utils.OkOrPanic(errors.NewBusinessErrorWithDetail(503, "不支持的设备类型", targetTask.Edges.CpUser.MobileType))
|
||||
}
|
||||
|
||||
submitForm := taskExecutor.FillCustomForm(ctx, cpdailyRealTask, customFormItems, deviceID, targetTask.Edges.CpUser.Username)
|
||||
//var deviceID string
|
||||
//
|
||||
//switch targetTask.Edges.CpUser.MobileType {
|
||||
//case cpuser.MobileTypeRandom:
|
||||
// deviceID = uuid.NewString()
|
||||
//case cpuser.MobileTypeAuthorize:
|
||||
// deviceID = targetTask.Edges.CpUser.MobileCode
|
||||
//default:
|
||||
// utils.OkOrPanic(errors.NewBusinessErrorWithDetail(503, "不支持的设备类型", targetTask.Edges.CpUser.MobileType))
|
||||
//}
|
||||
// 不管是否随机应该都从数据库取,因为就算是随机 数据库已经随机了一个设备id了
|
||||
deviceID := targetTask.Edges.CpUser.MobileCode
|
||||
// 固定获取图片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 +163,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
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
cst2 "likedream-goframe/internal/common/cst"
|
||||
"likedream-goframe/internal/common/db"
|
||||
"likedream-goframe/internal/ent"
|
||||
@@ -87,7 +86,7 @@ func (ts *TaskScanner) scanCpdailyTask() {
|
||||
// 只查询30分钟以内的任务
|
||||
minExecuteTime := currentTime.Add(-30 * time.Minute).Unix()
|
||||
|
||||
allTasks := db.Global.Debug().CpTask.Query().
|
||||
allTasks := db.Global.CpTask.Query().
|
||||
Where(
|
||||
cptask.NextExecuteTimeLTE(currentTime.Unix()),
|
||||
cptask.NextExecuteTimeNotNil(),
|
||||
@@ -124,11 +123,10 @@ 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.Unique(5*time.Minute),
|
||||
asynq.MaxRetry(4),
|
||||
asynq.Unique(10*time.Second),
|
||||
asynq.ProcessAt(execTime),
|
||||
asynq.Deadline(execTime.Add(12*time.Minute)),
|
||||
asynq.Timeout(10*time.Minute),
|
||||
asynq.Queue(cst2.TaskQueueCpdaily),
|
||||
)
|
||||
// 更新未开始的任务状态为队列中
|
||||
|
||||
@@ -26,7 +26,7 @@ func initServer(ctx context.Context) {
|
||||
RetryDelayFunc: func(n int, _ error, _ *asynq.Task) time.Duration {
|
||||
return time.Duration(n) * 2 * time.Second
|
||||
},
|
||||
DelayedTaskCheckInterval: 10 * time.Second,
|
||||
DelayedTaskCheckInterval: 2 * time.Second,
|
||||
ErrorHandler: taskErrorHandler{},
|
||||
Queues: map[string]int{
|
||||
cst2.TaskQueueEmail: 2,
|
||||
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
v1 "likedream-goframe/internal/api/v1"
|
||||
"likedream-goframe/internal/application/cpdaily/login"
|
||||
"likedream-goframe/internal/application/cpdaily/school"
|
||||
@@ -25,6 +24,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 +50,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 +111,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 +179,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 +188,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{})
|
||||
@@ -237,7 +220,7 @@ func (v CpTaskServiceV1) Delete(ctx context.Context, req *v1.DeleteTaskReq) *uti
|
||||
utils.OkOrPanic(errors2.NewBusinessError(400, "非法的今日校园账号"))
|
||||
}
|
||||
// 异步终止线程,不管是否成功
|
||||
go schedule.StopTask(context.Background(), cst2.TaskQueueCpdaily, fmt.Sprintf(cst2.TaskCpdailyId, req.Id))
|
||||
//go schedule.StopTask(context.Background(), cst2.TaskQueueCpdaily, fmt.Sprintf(cst2.TaskCpdailyId, req.Id))
|
||||
db2.Global.CpTask.DeleteOne(cpTask).ExecX(ctx)
|
||||
return utils.SuccessWithMsg("删除成功")
|
||||
}
|
||||
@@ -276,13 +259,13 @@ func (v CpTaskServiceV1) ExecuteNow(ctx context.Context, req *v1.ExecuteNowReq)
|
||||
|
||||
// 使用时间戳生成唯一的 TaskID,避免与自动扫描的任务冲突,也避免 Unique 限制
|
||||
// 由于已经检查了任务状态,不需要使用 Unique 来防重复
|
||||
taskID := fmt.Sprintf("cpdaily:task:manual:%d:%d", req.Id, time.Now().UnixNano())
|
||||
//taskID := fmt.Sprintf("cpdaily:task:manual:%d:%d", req.Id, time.Now().UnixNano())
|
||||
e = utils.Catch(func() {
|
||||
queue.AddTask(ctx, asynq.NewTask(cst2.TaskCpdaily, payloadWriter.Bytes()),
|
||||
asynq.TaskID(taskID),
|
||||
asynq.Unique(5*time.Second),
|
||||
asynq.Unique(2*time.Minute),
|
||||
asynq.MaxRetry(0),
|
||||
asynq.Timeout(10*time.Minute),
|
||||
asynq.Deadline(time.Now().Add(10*time.Minute)),
|
||||
asynq.Queue(cst2.TaskQueueCpdaily),
|
||||
)
|
||||
// 更新未开始的任务状态为队列中
|
||||
@@ -309,6 +292,7 @@ func (v CpTaskServiceV1) PauseTask(ctx context.Context, req *v1.PauseTaskReq) *u
|
||||
return utils.SuccessWithMsg("恢复成功")
|
||||
|
||||
} else {
|
||||
//go schedule.StopTask(context.Background(), cst2.TaskQueueCpdaily, fmt.Sprintf(cst2.TaskCpdailyId, req.Id))
|
||||
db2.Global.CpTask.UpdateOne(cpTask).SetTaskStatus(cptask.TaskStatusPause).SetNextExecuteTime(-1).ExecX(ctx)
|
||||
return utils.SuccessWithMsg("暂停成功")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user