Compare commits

...

61 Commits

Author SHA1 Message Date
sugarBecomer b234c87048 ✨ 经纬度更新 2025-11-22 13:33:25 +08:00
sugarBecomer f982aa5e0c 🚀 删除冗余import 2025-11-21 20:32:23 +08:00
sugarBecomer 22823328c0 🚀 Dockerfile 2025-11-21 16:18:13 +08:00
sugarBecomer 323e49e375 🚀 captcha分离 2025-11-21 16:14:19 +08:00
sugarBecomer 526e4ac198 🐛 没有任务id了,因此没法删除asynq里的任务了 2025-11-18 16:05:03 +08:00
sugarBecomer 7469813e37 🐛 浅浅的加个锁 2025-11-18 15:53:57 +08:00
sugarBecomer 8eb4e40b57 🐛 缓存高频访问的学校信息 2025-11-18 15:37:08 +08:00
sugarBecomer 311cf18708 🐛 每次直接创建全新client试试效果 2025-11-18 14:57:37 +08:00
sugarBecomer 1bde0af01d 🐛 小问题修复 2025-11-18 14:34:43 +08:00
sugarBecomer 765ec6fb32 🐛 修复cookie问题 2025-11-17 17:14:49 +08:00
sugarBecomer 5cd8329519 🐛 修改定时任务信息 2025-11-17 12:39:05 +08:00
sugarBecomer 1ea9170d89 Merge remote-tracking branch 'origin/main' 2025-11-17 12:05:37 +08:00
sugarBecomer 29fc69d022 🐛 尝试解决cookie jar的问题 2025-11-17 12:03:47 +08:00
RecordingTheSmile 09e7a82882 fix: iap login failed message 2025-11-16 19:51:35 +08:00
sugarBecomer 1fff983d37 🐛 尝试干掉submitData的userId看看是否解决 2025-11-16 19:14:32 +08:00
RecordingTheSmile 836e61b8b0 fix: duplicate asynq.Unique option. 2025-11-14 22:36:01 +08:00
sugarBecomer 96044b8a88 Merge remote-tracking branch 'origin/main' 2025-11-14 22:35:32 +08:00
sugarBecomer acb865596e 🐛 取消定时任务cookie检测,强制登录 2025-11-14 22:35:15 +08:00
RecordingTheSmile 59559f4201 fix: task id unique 2025-11-14 22:26:10 +08:00
sugarBecomer 4d516adea8 ✨ 新增适配安徽建筑大学登录 2025-11-14 21:48:56 +08:00
sugarBecomer a37368ddcf ✨ 新增适配山东华宇工学院滑块 2025-11-14 20:38:58 +08:00
sugarBecomer 26dcfaf82d 🐛 增加日志信息 2025-11-14 13:09:00 +08:00
sugarBecomer e55a8866b1 🐛 schedule不要任务id唯一 2025-11-13 23:28:32 +08:00
sugarBecomer 0702135db2 🐛 iap新版本登录 2025-11-13 22:39:01 +08:00
sugarBecomer c668a76a99 🐛 关联关系bug 2025-11-13 21:42:26 +08:00
sugarBecomer 2a1cb90766 🐛 紧急修复漏洞 2025-11-13 21:20:07 +08:00
sugarBecomer ce536eb5bc 🐛 cookie问题 2025-11-13 16:53:59 +08:00
sugarBecomer 0244e330fd 🐛 cookie问题 2025-11-13 16:49:36 +08:00
sugarBecomer 035bac10cc 🐛 尝试调整登录cookie校验 2025-11-13 11:46:44 +08:00
sugarBecomer cb808f9c68 ✨ 适配南昌理工学院 2025-11-13 00:17:49 +08:00
sugarBecomer 22735aeaae ✨ 随机图片改为顺序图片 2025-11-12 19:59:02 +08:00
sugarBecomer dd6eeb5dfe 🐛 opencv 2025-11-12 15:54:52 +08:00
sugarBecomer ac6409f3e2 🐛 opencv 2025-11-12 15:39:20 +08:00
sugarBecomer 36a6932056 🐛 opencv 2025-11-12 15:19:15 +08:00
sugarBecomer 72dab0e968 🐛 opencv 2025-11-12 15:01:33 +08:00
sugarBecomer b716434f62 🐛 opencv 2025-11-12 14:45:25 +08:00
sugarBecomer 8ed7dd77e4 🐛 opencv 2025-11-12 14:30:03 +08:00
sugarBecomer ae1c9c695e 🐛 签到问题 & opencv 2025-11-12 14:12:47 +08:00
sugarBecomer 66995c0c78 🐛 cas滑块验证码 2025-11-12 13:35:17 +08:00
sugarBecomer 120e690795 🐛 cas滑块验证码 2025-11-12 13:31:54 +08:00
sugarBecomer bc2bb25578 🐛 签到bug 2025-11-11 22:34:06 +08:00
RecordingTheSmile b37fc5cb24 fix: 签到额外信息填写问题 2025-11-11 22:29:01 +08:00
sugarBecomer 614dd9917f 🐛 签到bug 2025-11-11 22:07:57 +08:00
sugarBecomer 00bc3432bd 🐛 修复onnx环境 2025-11-11 21:48:08 +08:00
sugarBecomer ffec10d1cc ✨ 迁移积分 2025-11-11 21:31:49 +08:00
sugarBecomer 3993fa9b32 ✨ 忘记密码 & 重发邮件 2025-11-11 14:08:07 +08:00
sugarBecomer 20b92b7d1c 🐛 注册验证码 2025-11-11 12:55:53 +08:00
sugarBecomer 05b9a94d41 🐛 ai初始化 & 错误处理 2025-11-11 11:57:50 +08:00
sugarBecomer 49f4475991 🐛 干掉global id 2025-11-10 22:53:05 +08:00
sugarBecomer 76bfdd03dd 🐛 cookie更新 2025-11-10 22:20:34 +08:00
sugarBecomer c18f2fdcf7 🐛 细节问题 2025-11-10 21:51:08 +08:00
sugarBecomer 74c6154c59 🐛 验证码处理 & 查询条件 2025-11-10 21:36:59 +08:00
sugarBecomer 3dc9a5e2a4 🐛 错误的判定 2025-11-10 15:40:40 +08:00
sugarBecomer ca30fd89cb 🐛 错误的判定 2025-11-10 15:40:24 +08:00
sugarBecomer b56e18b8ce ✨ 实现ai识别提交验证码问题 2025-11-10 15:39:38 +08:00
sugarBecomer b031157f98 ✨ 调整文件结构 2025-11-10 14:46:01 +08:00
sugarBecomer 7262023382 ✨ 学习通获取课程列表 2025-11-10 14:40:38 +08:00
sugarBecomer dde5825d30 ✨ 集成ddddocr服务 2025-11-10 13:57:17 +08:00
sugarBecomer da4bc164e2 🚚 优化ai模型初始化方式 2025-11-10 13:38:41 +08:00
sugarBecomer ae95a871c8 ✨ 学习通 2025-11-09 22:18:03 +08:00
sugarBecomer 4a29772de0 🐛 下载地址返回 2025-11-09 00:58:59 +08:00
126 changed files with 4077 additions and 1051 deletions
+5 -8
View File
@@ -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
View File
@@ -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)
+4 -4
View File
@@ -4,7 +4,7 @@ import (
"context"
"likedream-goframe/internal/api"
"likedream-goframe/internal/application/filestorage"
"likedream-goframe/internal/db"
db2 "likedream-goframe/internal/common/db"
_ "likedream-goframe/internal/ent/runtime"
"likedream-goframe/internal/framework/redis"
"likedream-goframe/internal/framework/schedule"
@@ -17,10 +17,10 @@ import (
)
func Run() {
db.InitDB()
db2.InitDB()
// 如果没有数据,进行数据初始化
if !db.CheckHasData() {
db.GenerateData()
if !db2.CheckHasData() {
db2.GenerateData()
}
ctx, cancel := context.WithCancel(gctx.GetInitCtx())
+15 -8
View File
@@ -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
+16 -9
View File
@@ -1,6 +1,6 @@
module likedream-goframe
go 1.24
go 1.24.0
require (
entgo.io/ent v0.14.5
@@ -28,10 +28,11 @@ require (
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.9.0
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
)
@@ -69,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
@@ -98,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
)
+33 -18
View File
@@ -111,6 +111,18 @@ github.com/hashicorp/hcl/v2 v2.18.1 h1:6nxnOJFku1EuSawSD81fuviYUV8DxFr3fp2dUi3ZY
github.com/hashicorp/hcl/v2 v2.18.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE=
github.com/hibiken/asynq v0.25.1 h1:phj028N0nm15n8O2ims+IvJ2gz4k2auvermngh9JhTw=
github.com/hibiken/asynq v0.25.1/go.mod h1:pazWNOLBu0FEynQRBvHA26qdIKRSmfdIfUm4HdsLmXg=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joomcode/errorx v1.2.0 h1:7Y/fguon+9r6a/75Rv3nrUwS7nXNEcJjLShjCvz00Og=
github.com/joomcode/errorx v1.2.0/go.mod h1:Mbz68VA9hsQLT50iCQQUZ2Z1XYAKYB4EoFkFCTFyiJM=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
@@ -134,8 +146,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
@@ -177,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=
@@ -202,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.9.0 h1:AhgkpBjphJZsHT5karKt93xPkPFNP0Iz6ENUbNAFQU4=
github.com/yalue/onnxruntime_go v1.9.0/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=
@@ -236,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=
@@ -246,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=
@@ -257,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=
@@ -267,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=
@@ -286,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=
@@ -308,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=
@@ -318,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=
@@ -331,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=
+1 -1
View File
@@ -2,6 +2,7 @@ package api
import (
"context"
"likedream-goframe/internal/common/cst"
"likedream-goframe/internal/controller/agent"
"likedream-goframe/internal/controller/card"
"likedream-goframe/internal/controller/common"
@@ -11,7 +12,6 @@ import (
"likedream-goframe/internal/controller/role"
"likedream-goframe/internal/controller/user"
"likedream-goframe/internal/controller/user_login"
"likedream-goframe/internal/cst"
"likedream-goframe/internal/middleware"
"likedream-goframe/internal/utils"
+5
View File
@@ -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"`
}
+51 -9
View File
@@ -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
}
@@ -0,0 +1,282 @@
package cookiejar
import (
"bytes"
"encoding/gob"
"github.com/bytedance/sonic"
"io"
"likedream-goframe/internal/utils"
"net/http"
"net/url"
"strings"
"sync"
)
type domainMap map[string]cookieMap
// AllSavePersistCookieJar 支持泛域名的 Cookie Jar
// 不同的二级域名会共享相同主域名的 cookies
// 例如: passport2.chaoxing.com 和 mooc1-api.chaoxing.com 共享 .chaoxing.com 的 cookies
type AllSavePersistCookieJar struct {
domainMap domainMap
*sync.RWMutex
}
func NewAllSavePersistCookieJar() *AllSavePersistCookieJar {
return &AllSavePersistCookieJar{
domainMap: make(domainMap),
RWMutex: &sync.RWMutex{},
}
}
// extractMainDomain 提取主域名
// 例如: passport2.chaoxing.com -> chaoxing.com
//
// mooc1-api.chaoxing.com -> chaoxing.com
// www.example.co.uk -> example.co.uk
func extractMainDomain(host string) string {
// 移除端口号
if colonIndex := strings.Index(host, ":"); colonIndex != -1 {
host = host[:colonIndex]
}
parts := strings.Split(host, ".")
if len(parts) <= 2 {
// 已经是主域名或单级域名
return host
}
// 处理特殊的二级顶级域名,如 .co.uk, .com.cn 等
// 简化处理:取最后两部分
// 更复杂的情况可以使用 publicsuffix 包
return strings.Join(parts[len(parts)-2:], ".")
}
func (a *AllSavePersistCookieJar) SetCookies(u *url.URL, cookies []*http.Cookie) {
a.Lock()
defer a.Unlock()
// 提取主域名
mainDomain := extractMainDomain(u.Host)
if _, exists := a.domainMap[mainDomain]; !exists {
a.domainMap[mainDomain] = make(cookieMap)
}
for _, cookie := range cookies {
if a.domainMap[mainDomain] == nil {
a.domainMap[mainDomain] = make(cookieMap)
}
// 查找是否存在相同 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)
}
}
}
func (a *AllSavePersistCookieJar) Cookies(u *url.URL) []*http.Cookie {
a.RLock()
defer a.RUnlock()
// 提取主域名
mainDomain := extractMainDomain(u.Host)
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
}
return nil
}
func (a *AllSavePersistCookieJar) Serialize() string {
a.RLock()
defer a.RUnlock()
json, err := sonic.Marshal(a.domainMap)
utils.OkOrPanic(err)
return string(json)
}
func (a *AllSavePersistCookieJar) Deserialize(content string) {
a.Lock()
defer a.Unlock()
if content == "" {
a.domainMap = make(domainMap)
return
}
var domainMap domainMap
err := sonic.Unmarshal([]byte(content), &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 := 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
}
func (a *AllSavePersistCookieJar) SerializeBytes() []byte {
a.RLock()
defer a.RUnlock()
ret := make([]byte, 0)
retWriter := bytes.NewBuffer(ret)
err := gob.NewEncoder(retWriter).
Encode(a.domainMap)
utils.OkOrPanic(err)
return retWriter.Bytes()
}
func (a *AllSavePersistCookieJar) DeserializeBytes(content []byte) {
a.Lock()
defer a.Unlock()
if len(content) == 0 {
a.domainMap = make(domainMap)
return
}
retReader := bytes.NewBuffer(content)
var domainMap domainMap
err := gob.NewDecoder(retReader).
Decode(&domainMap)
// 如果反序列化失败,尝试从旧格式转换
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
}
func (a *AllSavePersistCookieJar) DeserializeReader(reader io.Reader) {
a.Lock()
defer a.Unlock()
// 先读取所有数据,以便在失败时可以重试
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
}
+234 -16
View File
@@ -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())
}
@@ -5,8 +5,8 @@ import (
"encoding/json"
"likedream-goframe/internal/application/cpdaily/types"
"likedream-goframe/internal/application/filestorage"
"likedream-goframe/internal/common/db"
"likedream-goframe/internal/common/errors"
"likedream-goframe/internal/db"
"likedream-goframe/internal/ent"
"likedream-goframe/internal/ent/cpimage"
"likedream-goframe/internal/utils"
+2 -1
View File
@@ -2,8 +2,9 @@ package cpdaily
import (
"context"
"likedream-goframe/internal/common/cst"
"github.com/gogf/gf/v2/frame/g"
"likedream-goframe/internal/cst"
)
type Info struct {
+209
View File
@@ -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,12 @@ 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)
default:
if info.IsIap() {
return NewIap(ctx)
+53 -20
View File
@@ -9,17 +9,19 @@ import (
"likedream-goframe/internal/application/cookiejar"
"likedream-goframe/internal/application/cpdaily/school"
"likedream-goframe/internal/application/httpclient"
"likedream-goframe/internal/common/cst"
"likedream-goframe/internal/common/errors"
"likedream-goframe/internal/cst"
"likedream-goframe/internal/framework/redis"
"likedream-goframe/internal/utils"
"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)
}
+12 -6
View File
@@ -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
}
+44 -12
View File
@@ -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)
}
}
}
+213
View File
@@ -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)
}
+54 -42
View File
@@ -1,72 +1,84 @@
package login
import (
"bytes"
"context"
"encoding/base64"
"image"
"io"
"likedream-goframe/internal/application/httpclient"
"likedream-goframe/internal/cst"
"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]
}
@@ -0,0 +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
//}
@@ -20,6 +20,15 @@ type CpSubmitCaptcha struct {
RawResponseJson json.RawMessage `json:"-"`
}
type CpSubmitCaptchaForAI struct {
Code string `json:"code"`
Name string `json:"name"`
ImageInfos []struct {
Code string `json:"code"`
Path string `json:"path"`
} `json:"imageInfos"`
}
// ApiResponse 通用API响应结构体
type ApiResponse struct {
Code json.Number `json:"code"`
@@ -5,8 +5,8 @@ import (
"encoding/json"
"fmt"
"likedream-goframe/internal/application/cpdaily/types"
"likedream-goframe/internal/common/cst"
"likedream-goframe/internal/common/errors"
"likedream-goframe/internal/cst"
"likedream-goframe/internal/utils"
"strings"
@@ -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"
+6 -11
View File
@@ -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)
}
+40 -49
View File
@@ -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 {
+44 -48
View File
@@ -10,10 +10,9 @@ import (
"likedream-goframe/internal/application/cpdaily/task/formtype"
"likedream-goframe/internal/application/cpdaily/taskhelper"
"likedream-goframe/internal/application/cpdaily/types"
"likedream-goframe/internal/common/cst"
"likedream-goframe/internal/common/errors"
"likedream-goframe/internal/cst"
"likedream-goframe/internal/utils"
"math/rand/v2"
"strconv"
"strings"
"time"
@@ -436,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{
@@ -456,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++
}
// 是否范围外签到
@@ -479,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: "是",
@@ -491,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 == ""
@@ -502,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
// 填充必要变量
@@ -520,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]
}
}
}
@@ -529,7 +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.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 {
@@ -539,9 +551,10 @@ func (c *CpdailySleepCheckTask) FillCustomForm(ctx context.Context, form map[str
return utils.Catch(func() {
ticket = submitCaptchaResolver.GetCaptchaTicket(ctx, captchaInfo)
})
}, retry.MaxDelay(15*time.Second), retry.Attempts(5), retry.Context(ctx))
utils.OkOrPanic(errors.NewBusinessError(503, "提交验证码识别失败").WithInnerError(e), oops.WithContext(ctx).Wrap)
}, retry.Delay(15*time.Second), retry.Attempts(5), retry.Context(ctx))
if e != nil {
utils.OkOrPanic(errors.NewBusinessError(503, "提交验证码识别失败").WithInnerError(e), oops.WithContext(ctx).Wrap)
}
resultForm["ticket"] = gvar.New(ticket)
} else {
resultForm["ticket"] = gvar.New("")
@@ -560,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)
@@ -660,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"],
@@ -674,7 +671,6 @@ func (c *CpdailySleepCheckTask) Submit(ctx context.Context, form map[string]*gva
}
sign := taskhelper.GetSubmitSign(submitData)
submitData["sign"] = gvar.New(sign)
response, e := c.client.R().
SetBody(submitData).
@@ -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 {
// 按照指定顺序构建哈希数据
+2 -1
View File
@@ -2,8 +2,9 @@ package filestorage
import (
"context"
"likedream-goframe/internal/common/cst"
"github.com/gogf/gf/v2/frame/g"
"likedream-goframe/internal/cst"
)
var GlobalS3 *S3
+25 -2
View File
@@ -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
}
@@ -0,0 +1,48 @@
package httpclient
import (
"crypto/tls"
"likedream-goframe/internal/application/cookiejar"
"likedream-goframe/internal/utils"
"time"
"resty.dev/v3"
)
var Xuexitong = resty.New().
SetCookieJar(cookiejar.NewAllSavePersistCookieJar()).
SetRedirectPolicy(resty.FlexibleRedirectPolicy(20)).
SetTimeout(30 * time.Second).
SetTLSClientConfig(&tls.Config{
InsecureSkipVerify: true,
})
func init() {
if !utils.IsDebug() {
Xuexitong.SetDebug(false)
} else {
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
}
+2 -7
View File
@@ -46,7 +46,7 @@ func (c *Downloader) Search(ctx context.Context, keyword string) (result []Music
return
}
func (c *Downloader) Download(ctx context.Context, url string) (result []byte) {
func (c *Downloader) Download(ctx context.Context, url string) string {
client := httpclient.Music.Clone(ctx)
response, err := client.R().Get("https://www.gequbao.net" + url)
if err != nil {
@@ -64,10 +64,5 @@ func (c *Downloader) Download(ctx context.Context, url string) (result []byte) {
download_url := gjson.Get(post.String(), "data.url").String()
// 下载音频文件
downloadResp, err := client.R().Get(download_url)
if err != nil {
utils.OkOrPanic(errors.NewBusinessError(503, "下载出错3"), oops.WithContext(ctx).Wrap)
}
return downloadResp.Bytes()
return download_url
}
@@ -0,0 +1,37 @@
package cryptoutil
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"likedream-goframe/internal/framework/logger"
"go.uber.org/zap"
)
const (
KEY = "u2oh6Vu^HWe4_AES"
)
func LoginEncrypt(str string) (string, error) {
key := []byte(KEY)
block, err := aes.NewCipher(key)
if err != nil {
logger.Logger.Error("aes.NewCipher err:", zap.Error(err))
return "", err
}
paddedData := pad([]byte(str), block.BlockSize())
ciphertext := make([]byte, len(paddedData))
mode := cipher.NewCBCEncrypter(block, key)
mode.CryptBlocks(ciphertext, paddedData)
data := base64.StdEncoding.EncodeToString(ciphertext)
return data, nil
}
// pad 确保数据长度是块大小的整数倍,以便符合块加密算法的要求
func pad(src []byte, blockSize int) []byte {
padding := blockSize - len(src)%blockSize
padText := bytes.Repeat([]byte{byte(padding)}, padding)
return append(src, padText...)
}
@@ -0,0 +1,72 @@
package entity
// XueXiTCourse 课程所有信息
type XueXiTCourseJson struct {
Result int `json:"result"`
Msg string `json:"msg"`
ChannelList []ChannelItem `json:"channelList"`
Mcode string `json:"mcode"`
Createcourse int `json:"createcoursed"`
TeacherEndCourse int `json:"teacherEndCourse"`
ShowEndCourse int `json:"showEndCourse"`
HasMore bool `json:"hasMore"`
StuEndCourse int `json:"stuEndCourse"`
}
// ChannelItem 课程列表
type ChannelItem struct {
Cfid int `json:"cfid"`
Norder int `json:"norder"`
CataName string `json:"cataName"`
Cataid string `json:"cataid"`
Id int `json:"id"`
Cpi int `json:"cpi"`
Key any `json:"key"`
Content struct {
Studentcount int `json:"studentcount"`
Chatid string `json:"chatid"`
IsFiled int `json:"isFiled"`
Isthirdaq int `json:"isthirdaq"`
Isstart bool `json:"isstart"`
Isretire int `json:"isretire"`
Name string `json:"name"`
Course struct {
Data []struct {
BelongSchoolId string `json:"belongSchoolId"`
Coursestate int `json:"coursestate"`
Teacherfactor string `json:"teacherfactor"`
IsCourseSquare int `json:"isCourseSquare"`
Schools string `json:"schools"`
CourseSquareUrl string `json:"courseSquareUrl"`
Imageurl string `json:"imageurl"`
AppInfo string `json:"appInfo"`
Name string `json:"name"`
DefaultShowCatalog int `json:"defaultShowCatalog"`
Id int `json:"id"`
AppData int `json:"appData"`
} `json:"data"`
} `json:"course"`
Roletype int `json:"roletype"`
Id int `json:"id"`
State int `json:"state"`
Cpi int `json:"cpi"`
Bbsid string `json:"bbsid"`
IsSquare int `json:"isSquare"`
} `json:"content"`
Topsign int `json:"topsign"`
}
type XueXiTCourse struct {
Cpi int `json:"cpi"` // 用户唯一标识
Key string `json:"key"` // classID 在课程API中为key
CourseID string `json:"courseId"` // 课程ID
ChatID string `json:"chatId"`
CourseTeacher string `json:"courseTeacher"` // 课程老师
CourseName string `json:"courseName"` //课程名
CourseImage string `json:"courseImage"`
// 两个标识 暂时不知道有什么用
CourseDataID int `json:"courseDataId"`
ContentID int `json:"ContentID"`
IsStart bool `json:"isstart"` //是否开课了,开课了为true,没开课为false。一般来说开课才能刷,不然刷不了的
State int `json:"state"` //课程状态,0为正常,1为课程已经结束
}
@@ -0,0 +1,76 @@
package xuexitong
import (
"context"
"likedream-goframe/internal/application/cookiejar"
"likedream-goframe/internal/application/httpclient"
"likedream-goframe/internal/application/xuexitong/cryptoutil"
"likedream-goframe/internal/framework/logger"
"net/http"
"strings"
"time"
"go.uber.org/zap"
"resty.dev/v3"
)
const (
ApiLoginWeb = "https://passport2.chaoxing.com/fanyalogin"
)
type XuexitongLogin struct {
client *resty.Client
}
func NewXuexitongLogin(ctx context.Context) *XuexitongLogin {
return &XuexitongLogin{
client: httpclient.NewXuexitongClient(),
}
}
func (c *XuexitongLogin) Login(ctx context.Context, username, password string, retry int) (err error) {
usernameEncrypted, err := cryptoutil.LoginEncrypt(username)
if err != nil {
logger.Logger.Error("LoginEncrypt err:", zap.Error(err))
return err
}
passwordEncrypted, err := cryptoutil.LoginEncrypt(password)
if err != nil {
logger.Logger.Error("LoginEncrypt err:", zap.Error(err))
return err
}
resp, err := c.client.R().SetFormData(map[string]string{
"fid": "-1",
"uname": usernameEncrypted,
"password": passwordEncrypted,
"refer": "http%3A%2F%2Fi.mooc.chaoxing.com",
"t": "true",
"forbidotherlogin": "0",
"validate": "",
"doubleFactorLogin": "0",
"independentId": "0",
"independentNameId": "0",
}).Post(ApiLoginWeb)
if err != nil {
logger.Logger.Error("LoginPost err:", zap.Error(err))
return err
}
body := resp.String()
//触发风控直接重新登录
if strings.Contains(body, "很抱歉,您所浏览的页面暂时不能访问!") && retry > 0 {
time.Sleep(time.Duration(retry*5) * time.Second)
return c.Login(ctx, username, password, retry-1)
}
return nil
}
func (c *XuexitongLogin) GetCookieJar() http.CookieJar {
return c.client.CookieJar()
}
func (c *XuexitongLogin) GetCookieString() string {
cookieJar := c.GetCookieJar()
serializableJar := cookieJar.(*cookiejar.PersistCookieJar)
return serializableJar.Serialize()
}
func (c *XuexitongLogin) GetClient() *resty.Client {
return c.client.SetRedirectPolicy(resty.NoRedirectPolicy())
}
@@ -0,0 +1,86 @@
package xuexitong
import (
"fmt"
"io"
"likedream-goframe/internal/utils"
"log"
"math/rand"
"os"
"strings"
"time"
"resty.dev/v3"
)
var randChar []string = []string{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f", "A", "B", "C", "D", "E", "F"}
func ApiVerifyCode(client *resty.Client, retry int) (string, error) {
if retry < 0 {
return "", fmt.Errorf("retry limit exceeded")
}
urlStr := "https://mooc1-api.chaoxing.com/processVerifyPng.ac?t=" + fmt.Sprintf("%d", time.Now().UnixMilli())
req := client.R()
req.Header.Add("User-Agent", GetUA("mobile"))
req.Header.Add("Accept", "*/*")
req.Header.Add("Host", "mooc1-api.chaoxing.com")
req.Header.Add("Connection", "keep-alive")
res, err := req.Get(urlStr)
if err != nil {
if retry > 0 {
return ApiVerifyCode(client, retry-1)
} else {
return res.String(), err
}
}
codeFileName := "code" + randChar[rand.Intn(len(randChar))] //生成验证码文件名称
for i := 0; i < 10; i++ {
codeFileName += randChar[rand.Intn(len(randChar))]
}
codeFileName += ".png"
utils.PathExistForCreate("./assets/code/") //检测是否存在路径,如果不存在则创建
filepath := fmt.Sprintf("./assets/code/%s", codeFileName)
file, err := os.Create(filepath)
if err != nil {
res.Body.Close() //立即释放
log.Println(err)
return "", err
}
_, err = io.Copy(file, res.Body)
if err != nil {
res.Body.Close() //立即释放
log.Println(err)
return "", err
}
file.Close()
if utils.IsBadImg(filepath) {
res.Body.Close() //立即释放
utils.DeleteFile(filepath) //删除坏的文件
return ApiVerifyCode(client, retry-1)
}
return filepath, nil
}
func ApiPassVerifyCode(client *resty.Client, code string, retry int) bool {
if retry < 0 {
return false
}
urlStr := "https://mooc1-api.chaoxing.com/html/processVerify.ac"
client.SetRedirectPolicy(resty.NoRedirectPolicy())
req := client.R()
req.SetMultipartField("ucode", "", "", strings.NewReader(code))
req.SetMultipartField("app", "", "", strings.NewReader("0"))
req.Header.Add("User-Agent", GetUA("mobile"))
req.Header.Add("Accept", "*/*")
req.Header.Add("Host", "mooc1-api.chaoxing.com")
req.Header.Add("Connection", "keep-alive")
res, err := req.Post(urlStr)
if err != nil {
return ApiPassVerifyCode(client, code, retry-1)
}
client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
return res.StatusCode() == 302
}
@@ -0,0 +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
//}
@@ -0,0 +1,24 @@
package xuexitong
import (
"context"
"likedream-goframe/internal/utils"
"testing"
)
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)
utils.OkOrPanic(err)
t.Log(list)
}
@@ -0,0 +1,77 @@
package xuexitong
import (
"crypto/md5"
"encoding/hex"
"fmt"
"math/rand"
"strings"
)
const (
APP_VERSION = "6.6.4"
DEVICE_VENDOR = "MI10"
BUILD = "10831_263"
//BUILD = "10903_285"
ANDROID_VERSION = "Android 11"
)
var IMEI = TokenHex(16)
// GetUA 构建并获取 UA
func GetUA(uaType string) string {
switch uaType {
case "mobile":
//return strings.Join([]string{
// fmt.Sprintf("Dalvik/2.1.0 (Linux; U; %s; %s Build/SKQ1.210216.001)", ANDROID_VERSION, DEVICE_VENDOR),
// fmt.Sprintf("(schild:%s)", MobileUASign(DEVICE_VENDOR, "zh_CN", APP_VERSION, BUILD, IMEI)),
// fmt.Sprintf("(device:%s)", DEVICE_VENDOR),
// "Language/zh_CN",
// fmt.Sprintf("com.chaoxing.mobile/ChaoXingStudy_3_%s_android_phone_%s", APP_VERSION, BUILD),
// //APP_VERSION,
// fmt.Sprintf("(@Kalimdor)_%s", IMEI),
//}, " ")
return strings.Join([]string{
fmt.Sprintf("Mozilla/5.0 (Linux; %s; %s Build/OPM1.171019.019; wv)", ANDROID_VERSION, DEVICE_VENDOR),
fmt.Sprintf("AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36"),
fmt.Sprintf("(schild:%s)", MobileUASign(DEVICE_VENDOR, "zh_CN", APP_VERSION, BUILD, IMEI)),
fmt.Sprintf("(device:%s)", DEVICE_VENDOR),
"Language/zh_CN",
fmt.Sprintf("com.chaoxing.mobile/ChaoXingStudy_3_%s_android_phone_%s", APP_VERSION, BUILD),
fmt.Sprintf("(@Kalimdor)_%s", IMEI),
}, " ")
case "web":
return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.35"
default:
return ""
}
}
// 用于生成IMEI
func TokenHex(len int) string {
b := make([]byte, 16)
_, err := rand.Read(b)
if err != nil {
panic(err)
}
token := hex.EncodeToString(b)
return token
}
func MobileUASign(model, locale, version, build, imei string) string {
// 拼接字符串,和 Python 的 " ".join 相同
str := strings.Join([]string{
"(schild:ipL$TkeiEmfy1gTXb2XHrdLN0a@7c^vu)",
fmt.Sprintf("(device:%s)", model),
fmt.Sprintf("Language/%s", locale),
fmt.Sprintf("com.chaoxing.mobile/ChaoXingStudy_3_%s_android_phone_%s", version, build),
fmt.Sprintf("(@Kalimdor)_%s", imei),
}, " ")
// 计算 md5
hash := md5.Sum([]byte(str))
// 转换为小写 hex 字符串
return fmt.Sprintf("%x", hash)
}
+47
View File
@@ -0,0 +1,47 @@
package cst
import "time"
const (
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"
ConfigTaskConcurrency = "task.concurrency"
// Email配置常量
ConfigEmailSMTPHost = "email.smtpHost"
ConfigEmailSMTPPort = "email.smtpPort"
ConfigEmailSMTPUsername = "email.smtpUsername"
ConfigEmailSMTPPassword = "email.smtpPassword"
ConfigEmailFromEmail = "email.fromEmail"
ConfigEmailFromName = "email.fromName"
)
const (
// 应用程序常量
AppName = "若梦系统"
AppCopyright = "© 2025 若梦系统. 保留所有权利."
)
var (
TimeZoneShangHai, _ = time.LoadLocation("Asia/Shanghai")
)
+21
View File
@@ -0,0 +1,21 @@
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小时有效
)
@@ -5,6 +5,8 @@ const (
TaskEmail = "task:email"
TaskResetDaily = "task:reset:daily"
TaskCpdailyId = "cpdaily:task:schedule:%d"
TaskQueueCpdaily = "queue:cpdaily"
TaskQueueEmail = "queue:email"
TaskQueueDefault = "default"
@@ -2,17 +2,18 @@ package db
import (
"context"
"github.com/gogf/gf/v2/os/gctx"
"github.com/shopspring/decimal"
"likedream-goframe/internal/cst"
"likedream-goframe/internal/common/cst"
"likedream-goframe/internal/ent/sysmenu"
"likedream-goframe/internal/ent/sysrole"
"likedream-goframe/internal/ent/sysuser"
"likedream-goframe/internal/utils"
"github.com/gogf/gf/v2/os/gctx"
"github.com/shopspring/decimal"
)
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
})
@@ -35,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").
@@ -49,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).
@@ -65,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).
@@ -76,7 +77,7 @@ func generateMenus() {
SetParent(sysM).
SaveX(ctx)
// 新增 编辑 删除按钮
Global.SysMenu.Create().
Global.Debug().SysMenu.Create().
SetName("新增").
SetLabel("add").
SetType(sysmenu.TypeButton).
@@ -84,7 +85,7 @@ func generateMenus() {
SetOrder(1).
SetParent(userM).
SaveX(ctx)
Global.SysMenu.Create().
Global.Debug().SysMenu.Create().
SetName("编辑").
SetLabel("edit").
SetType(sysmenu.TypeButton).
@@ -93,7 +94,7 @@ func generateMenus() {
SetParent(userM).
SaveX(ctx)
// 删除按钮
Global.SysMenu.Create().
Global.Debug().SysMenu.Create().
SetName("删除").
SetLabel("delete").
SetType(sysmenu.TypeButton).
@@ -102,7 +103,7 @@ func generateMenus() {
SetParent(userM).
SaveX(ctx)
// 列表
Global.SysMenu.Create().
Global.Debug().SysMenu.Create().
SetName("列表").
SetLabel("list").
SetType(sysmenu.TypeButton).
@@ -110,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).
@@ -121,7 +122,7 @@ func generateMenus() {
SetParent(sysM).
SaveX(ctx)
// 新增 编辑 删除按钮
Global.SysMenu.Create().
Global.Debug().SysMenu.Create().
SetName("新增").
SetLabel("add").
SetType(sysmenu.TypeButton).
@@ -129,7 +130,7 @@ func generateMenus() {
SetOrder(1).
SetParent(roleM).
SaveX(ctx)
Global.SysMenu.Create().
Global.Debug().SysMenu.Create().
SetName("编辑").
SetLabel("edit").
SetType(sysmenu.TypeButton).
@@ -138,7 +139,7 @@ func generateMenus() {
SetParent(roleM).
SaveX(ctx)
// 删除按钮
Global.SysMenu.Create().
Global.Debug().SysMenu.Create().
SetName("删除").
SetLabel("delete").
SetType(sysmenu.TypeButton).
@@ -146,7 +147,7 @@ func generateMenus() {
SetOrder(3).
SetParent(roleM).
SaveX(ctx)
Global.SysMenu.Create().
Global.Debug().SysMenu.Create().
SetName("列表").
SetLabel("list").
SetType(sysmenu.TypeButton).
@@ -154,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).
@@ -165,7 +166,7 @@ func generateMenus() {
SetParent(sysM).
SaveX(ctx)
// 新增 编辑 删除按钮
Global.SysMenu.Create().
Global.Debug().SysMenu.Create().
SetName("新增").
SetLabel("add").
SetType(sysmenu.TypeButton).
@@ -173,7 +174,7 @@ func generateMenus() {
SetOrder(1).
SetParent(menuM).
SaveX(ctx)
Global.SysMenu.Create().
Global.Debug().SysMenu.Create().
SetName("编辑").
SetLabel("edit").
SetType(sysmenu.TypeButton).
@@ -182,7 +183,7 @@ func generateMenus() {
SetParent(menuM).
SaveX(ctx)
// 删除按钮
Global.SysMenu.Create().
Global.Debug().SysMenu.Create().
SetName("删除").
SetLabel("delete").
SetType(sysmenu.TypeButton).
@@ -190,7 +191,7 @@ func generateMenus() {
SetOrder(3).
SetParent(menuM).
SaveX(ctx)
Global.SysMenu.Create().
Global.Debug().SysMenu.Create().
SetName("列表").
SetLabel("list").
SetType(sysmenu.TypeButton).
@@ -203,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)
}
+50
View File
@@ -0,0 +1,50 @@
package db
import (
"context"
"likedream-goframe/internal/common/cst"
"likedream-goframe/internal/ent"
"likedream-goframe/internal/framework/logger"
"likedream-goframe/internal/utils"
_ "github.com/go-sql-driver/mysql"
"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)
dbDsn, err := g.Cfg().Get(gctx.New(), cst.ConfigDatabaseDsn)
utils.OkOrPanic(err)
client, err := ent.Open(dbType.String(), dbDsn.String())
utils.OkOrPanic(err)
// 自动迁移
if err := client.Schema.Create(context.Background()); err != nil {
logger.Logger.Fatal("failed creating schema resources: %v", zap.Error(err))
}
Global = client
dbDsn2, err := g.Cfg().Get(gctx.New(), cst.ConfigDatabaseDsn2)
utils.OkOrPanic(err)
GlobalGorm, err = gorm.Open(postgres.Open(dbDsn2.String()), &gorm.Config{})
if err != nil {
logger.Logger.Fatal("连接pg数据库失败: %v", zap.Error(err))
}
// 可选:配置连接池
sqlDB, err := GlobalGorm.DB()
if err != nil {
logger.Logger.Fatal("获取数据库实例失败: %v", zap.Error(err))
}
// 设置连接池参数
sqlDB.SetMaxIdleConns(10) // 设置空闲连接池中连接的最大数量
sqlDB.SetMaxOpenConns(100) // 设置打开数据库连接的最大数量
sqlDB.SetConnMaxLifetime(0) // 设置连接可复用的最大时间(0表示永久)
}
@@ -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/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)
}
+3 -24
View File
@@ -2,12 +2,9 @@ package music
import (
"context"
"fmt"
v1 "likedream-goframe/internal/api/v1"
"likedream-goframe/internal/application/music"
"likedream-goframe/internal/utils"
"github.com/gogf/gf/v2/net/ghttp"
)
type MusicCtrlV1 struct {
@@ -25,25 +22,7 @@ func (c *MusicCtrlV1) Search(ctx context.Context, req *v1.SearchMusicReq) (res *
func (c *MusicCtrlV1) Download(ctx context.Context, req *v1.DownloadMusicReq) (res *utils.Response[any], err error) {
defer utils.Pass(&err)
// 获取音频二进制数据
audioData := music.MusicDownloader.Download(ctx, req.Path)
// 从 context 获取 ghttp.Request
r := ghttp.RequestFromCtx(ctx)
// 构建文件名:Name - Singer.mp3
filename := fmt.Sprintf("music.mp3")
// 设置响应头
r.Response.Header().Set("Content-Type", "audio/mpeg")
r.Response.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
r.Response.Header().Set("Content-Length", fmt.Sprintf("%d", len(audioData)))
// 写入二进制数据
r.Response.Write(audioData)
// 标记已处理,防止框架再次处理响应
r.ExitAll()
return nil, nil
// 获取音频下载地址
downloadUrl := music.MusicDownloader.Download(ctx, req.Path)
return utils.SuccessWithData[any](downloadUrl), nil
}
@@ -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
}
+54 -1
View File
@@ -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
}
-44
View File
@@ -1,44 +0,0 @@
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"
ConfigRedisUrl = "redis.url"
ConfigTaskConcurrency = "task.concurrency"
// Email配置常量
ConfigEmailSMTPHost = "email.smtpHost"
ConfigEmailSMTPPort = "email.smtpPort"
ConfigEmailSMTPUsername = "email.smtpUsername"
ConfigEmailSMTPPassword = "email.smtpPassword"
ConfigEmailFromEmail = "email.fromEmail"
ConfigEmailFromName = "email.fromName"
)
const (
// 应用程序常量
AppName = "今日校园系统"
AppCopyright = "© 2024 今日校园系统. 保留所有权利."
)
var (
TimeZoneShangHai, _ = time.LoadLocation("Asia/Shanghai")
)
-15
View File
@@ -1,15 +0,0 @@
package cst
const (
RedisUserInfo = "userinfo:"
RedisVerifyCode = "verifycode:"
RedisCpUserLogin = "cpuserlogin:"
RedisCpUserAuthorize = "cpuser:authorize:"
)
var (
RedisUserInfoExpire = int64(86400)
RedisVerifyCodeExpire = int64(86400)
RedisCpUserLoginExpire = int64(180)
RedisCpUserAuthorizeExpire = int64(1800)
)
-30
View File
@@ -1,30 +0,0 @@
package db
import (
"context"
"likedream-goframe/internal/cst"
"likedream-goframe/internal/ent"
"likedream-goframe/internal/framework/logger"
"likedream-goframe/internal/utils"
_ "github.com/go-sql-driver/mysql"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
"go.uber.org/zap"
)
var Global *ent.Client
func InitDB() {
dbType, err := g.Cfg().Get(gctx.New(), cst.ConfigDatabaseType)
utils.OkOrPanic(err)
dbDsn, err := g.Cfg().Get(gctx.New(), cst.ConfigDatabaseDsn)
utils.OkOrPanic(err)
client, err := ent.Open(dbType.String(), dbDsn.String())
utils.OkOrPanic(err)
// 自动迁移
if err := client.Schema.Create(context.Background()); err != nil {
logger.Logger.Fatal("failed creating schema resources: %v", zap.Error(err))
}
Global = client
}
+12 -1
View File
@@ -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(", ")
+8
View File
@@ -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()
+80
View File
@@ -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))
+78
View File
@@ -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) {
+52
View File
@@ -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)
}
+6
View File
@@ -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))
-5
View File
@@ -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
+10 -30
View File
@@ -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
+74 -1
View File
@@ -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
+1
View File
@@ -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(),
+1 -1
View File
@@ -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).
+1 -1
View File
@@ -2,7 +2,7 @@ package redis
import (
"context"
"likedream-goframe/internal/cst"
"likedream-goframe/internal/common/cst"
"likedream-goframe/internal/utils"
"time"
+54 -27
View File
@@ -8,20 +8,20 @@ import (
"likedream-goframe/internal/application/cpdaily/school"
task2 "likedream-goframe/internal/application/cpdaily/task"
"likedream-goframe/internal/application/cpdaily/task/formtype"
"likedream-goframe/internal/common/db"
errors2 "likedream-goframe/internal/common/errors"
"likedream-goframe/internal/db"
"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) {
+3 -1
View File
@@ -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
}
+1 -1
View File
@@ -2,7 +2,7 @@ package schedule
import (
"context"
"likedream-goframe/internal/cst"
"likedream-goframe/internal/common/cst"
"likedream-goframe/internal/framework/redis"
"likedream-goframe/internal/utils"
+1 -1
View File
@@ -2,7 +2,7 @@ package schedule
import (
"context"
"likedream-goframe/internal/db"
"likedream-goframe/internal/common/db"
"likedream-goframe/internal/ent/cptask"
"likedream-goframe/internal/utils"
+20 -14
View File
@@ -5,9 +5,8 @@ import (
"context"
"encoding/gob"
"errors"
"fmt"
"likedream-goframe/internal/cst"
"likedream-goframe/internal/db"
cst2 "likedream-goframe/internal/common/cst"
"likedream-goframe/internal/common/db"
"likedream-goframe/internal/ent"
"likedream-goframe/internal/ent/cptask"
"likedream-goframe/internal/framework/queue"
@@ -17,6 +16,7 @@ import (
"strings"
"time"
"entgo.io/ent/dialect/sql"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
"github.com/hibiken/asynq"
@@ -79,16 +79,23 @@ func (ts *TaskScanner) StopScan() {
case <-ts.stopSign:
}
}
func (ts *TaskScanner) scanCpdailyTask() {
currentTime := time.Now().In(cst.TimeZoneShangHai)
currentTime := time.Now().In(cst2.TimeZoneShangHai)
// 只查询30分钟以内的任务
minExecuteTime := currentTime.Add(-30 * time.Minute).Unix()
allTasks := db.Global.CpTask.Query().
Where(cptask.NextExecuteTimeLTE(currentTime.Unix()), cptask.NextExecuteTimeNotNil(), cptask.NextExecuteTimeGT(0), cptask.StepEQ(cptask.StepComplete), cptask.TaskStatusEQ(cptask.TaskStatusNotStart)).
Where(
cptask.NextExecuteTimeLTE(currentTime.Unix()),
cptask.NextExecuteTimeNotNil(),
cptask.NextExecuteTimeGT(0),
cptask.StepEQ(cptask.StepComplete),
cptask.TaskStatusEQ(cptask.TaskStatusNotStart),
func(selector *sql.Selector) {
selector.Where(sql.ExprP("success_count < frequency"))
}).
Select(cptask.FieldID, cptask.FieldExecuteHour, cptask.FieldExecuteMinute, cptask.FieldNextExecuteTime).
AllX(ts.ctx)
@@ -97,7 +104,7 @@ func (ts *TaskScanner) scanCpdailyTask() {
for _, task := range allTasks {
// 30分钟之前的任务不再执行,但是要重置下次执行时间
if *task.NextExecuteTime < minExecuteTime {
originExecuteTime := time.Unix(*task.NextExecuteTime, 0).In(cst.TimeZoneShangHai).Format("2006-01-02 15:04:05")
originExecuteTime := time.Unix(*task.NextExecuteTime, 0).In(cst2.TimeZoneShangHai).Format("2006-01-02 15:04:05")
g.Log().Debugf(ts.ctx, "任务ID %d 已过期,原来预计执行时间:%s,跳过执行", task.ID, originExecuteTime)
continue
}
@@ -112,16 +119,15 @@ func (ts *TaskScanner) scanCpdailyTask() {
utils.OkOrPanic(e, oops.WithContext(ts.ctx).With("payload", payload).In("CpdailyTaskScanner.scanCpdailyTask").Wrap)
year, month, day := currentTime.Date()
execTime := time.Date(year, month, day, *task.ExecuteHour, *task.ExecuteMinute, 0, 0, cst.TimeZoneShangHai)
execTime := time.Date(year, month, day, *task.ExecuteHour, *task.ExecuteMinute, 0, 0, cst2.TimeZoneShangHai)
e = utils.Catch(func() {
queue.AddTask(ts.ctx, asynq.NewTask(cst.TaskCpdaily, payloadWriter.Bytes()),
asynq.TaskID(fmt.Sprintf("cpdaily:task:schedule:%d", task.ID)),
asynq.MaxRetry(3),
asynq.Unique(10*time.Second),
queue.AddTask(ts.ctx, asynq.NewTask(cst2.TaskCpdaily, payloadWriter.Bytes()),
asynq.Unique(5*time.Minute),
asynq.MaxRetry(4),
asynq.ProcessAt(execTime),
asynq.Deadline(execTime.Add(12*time.Minute)),
asynq.Queue(cst.TaskQueueCpdaily),
asynq.Timeout(10*time.Minute),
asynq.Queue(cst2.TaskQueueCpdaily),
)
// 更新未开始的任务状态为队列中
db.Global.CpTask.Update().Where(cptask.ID(task.ID), cptask.TaskStatusEQ(cptask.TaskStatusNotStart)).SetTaskStatus(cptask.TaskStatusQueue).Exec(ts.ctx)
+10 -10
View File
@@ -2,7 +2,7 @@ package schedule
import (
"context"
"likedream-goframe/internal/cst"
cst2 "likedream-goframe/internal/common/cst"
"likedream-goframe/internal/framework/redis"
"likedream-goframe/internal/utils"
"time"
@@ -17,7 +17,7 @@ var globalServer *asynq.Server
func initServer(ctx context.Context) {
cfg := g.Cfg()
taskConcurrency := cfg.MustGetWithEnv(ctx, cst.ConfigTaskConcurrency).Int()
taskConcurrency := cfg.MustGetWithEnv(ctx, cst2.ConfigTaskConcurrency).Int()
globalServer = asynq.NewServerFromRedisClient(redis.GetClient(), asynq.Config{
Concurrency: taskConcurrency,
@@ -26,20 +26,20 @@ 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{
cst.TaskQueueEmail: 2,
cst.TaskQueueCpdaily: 10,
cst.TaskQueueDefault: 1,
cst2.TaskQueueEmail: 2,
cst2.TaskQueueCpdaily: 10,
cst2.TaskQueueDefault: 1,
},
})
mux := asynq.NewServeMux()
mux.Handle(cst.TaskCpdaily, &CpdailyTaskHandler{})
mux.Handle(cst.TaskEmail, &EmailTaskHandler{})
mux.Handle(cst.TaskResetDaily, &ResetTaskHandler{})
mux.Handle(cst2.TaskCpdaily, &CpdailyTaskHandler{})
mux.Handle(cst2.TaskEmail, &EmailTaskHandler{})
mux.Handle(cst2.TaskResetDaily, &ResetTaskHandler{})
e := globalServer.Start(mux)
utils.OkOrPanic(e)
@@ -49,7 +49,7 @@ type taskErrorHandler struct {
}
func (t taskErrorHandler) HandleError(ctx context.Context, task *asynq.Task, err error) {
if task.Type() == cst.TaskCpdaily {
if task.Type() == cst2.TaskCpdaily {
g.Log().Errorf(ctx, "今日校园任务执行失败: %+v", err)
}
}
+6 -3
View File
@@ -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)
}
+7 -7
View File
@@ -3,7 +3,7 @@ package middleware
import (
"context"
"errors"
"likedream-goframe/internal/cst"
cst2 "likedream-goframe/internal/common/cst"
"likedream-goframe/internal/ent"
"likedream-goframe/internal/framework/redis"
"likedream-goframe/internal/service"
@@ -28,7 +28,7 @@ func AuthHandler(r *ghttp.Request) {
r.Middleware.Next()
return
}
token := r.Request.Header.Get(cst.AuthorizationKey)
token := r.Request.Header.Get(cst2.AuthorizationKey)
if token == "" || !strings.HasPrefix(token, "Bearer ") {
r.Response.ClearBuffer()
r.Response.WriteJson(utils.NotLogin())
@@ -46,7 +46,7 @@ func AuthHandler(r *ghttp.Request) {
userIdF64 := claims["userId"].(float64)
userId := int64(userIdF64)
userIdStr := strconv.FormatInt(userId, 10)
infoCmd := redis.GetClient().Get(r.GetCtx(), cst.RedisUserInfo+userIdStr)
infoCmd := redis.GetClient().Get(r.GetCtx(), cst2.RedisUserInfo+userIdStr)
if infoCmd.Err() != nil && !errors.Is(infoCmd.Err(), redis.Nil) {
r.Response.ClearBuffer()
r.Response.WriteJson(utils.FailBusiness("验证服务异常"))
@@ -58,7 +58,7 @@ func AuthHandler(r *ghttp.Request) {
var userInfo *ent.SysUser
if errors.Is(infoCmd.Err(), redis.Nil) {
userInfo = service.Global.UserLogin.UserInfo(userId, r.GetCtx())
redis.GetClient().SetEx(r.GetCtx(), cst.RedisUserInfo+userIdStr, userInfo, time.Duration(cst.RedisUserInfoExpire)*time.Second)
redis.GetClient().SetEx(r.GetCtx(), cst2.RedisUserInfo+userIdStr, userInfo, time.Duration(cst2.RedisUserInfoExpire)*time.Second)
} else {
e := sonic.UnmarshalString(info, &userInfo)
if e != nil {
@@ -68,7 +68,7 @@ func AuthHandler(r *ghttp.Request) {
return
}
}
ctx := context.WithValue(r.GetCtx(), cst.CtxUserInfo, userInfo)
ctx := context.WithValue(r.GetCtx(), cst2.CtxUserInfo, userInfo)
// 存储权限信息
roles := userInfo.Edges.Roles
@@ -76,7 +76,7 @@ func AuthHandler(r *ghttp.Request) {
if len(roles) > 0 {
for _, role := range roles {
// admin 拥有所有的权限
if role.Label == cst.KeyRoleAdmin {
if role.Label == cst2.KeyRoleAdmin {
permissionList = append(permissionList, "*")
}
for _, menu := range role.Edges.Menus {
@@ -84,7 +84,7 @@ func AuthHandler(r *ghttp.Request) {
}
}
}
ctx = context.WithValue(ctx, cst.CtxPermissionList, permissionList)
ctx = context.WithValue(ctx, cst2.CtxPermissionList, permissionList)
r.SetCtx(ctx)
r.Middleware.Next()
}
+10 -6
View File
@@ -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 {
+1 -1
View File
@@ -1,7 +1,7 @@
package middleware
import (
"likedream-goframe/internal/cst"
"likedream-goframe/internal/common/cst"
"likedream-goframe/internal/utils"
"github.com/gogf/gf/v2/net/ghttp"

Some files were not shown because too many files have changed in this diff Show More