Compare commits

...

11 Commits

Author SHA1 Message Date
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
39 changed files with 899 additions and 560 deletions
+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)
+6 -4
View File
@@ -4,14 +4,16 @@ url = "https://devocr.likedream.xyz"
[database]
type = "mysql"
dsn = "test_likedream_go:EpXckXmxawDEQGxJ@tcp(database.out:3306)/test_likedream_go?parseTime=true"
dsn2 = "host=database.out user=dev password=JPmKjmpcaG3EYAJZ dbname=dev port=5432 sslmode=disable TimeZone=Asia/Shanghai"
# host=localhost user=gorm password=gorm dbname=gorm port=9920 sslmode=disable TimeZone=Asia/Shanghai
[server]
port = 8090
url = "https://devocr.likedream.xyz"
openapiPath= "/api.json"
swaggerPath= "/swagger"
emailConfirmBaseUrl = "https://dev.likedream.xyz/confirm/"
emailConfirmBaseUrl = "http://localhost:3200/confirm/"
emailResetPasswordUrl = "http://localhost:3200/reset-password/"
balanceMigrateUrl = "http://localhost:3200/balance-migrate/"
[env]
# 可选值: dev, prod
mode = "dev"
@@ -30,7 +32,7 @@ smtpUsername = "demo@likedream.xyz"
smtpPassword = "baca122..."
# 发件人邮箱和显示名称
fromEmail = "demo@likedream.xyz"
fromName = "今日校园系统"
fromName = "若梦系统"
[s3]
+16 -8
View File
@@ -1,6 +1,6 @@
module likedream-goframe
go 1.24
go 1.24.0
require (
entgo.io/ent v0.14.5
@@ -33,7 +33,9 @@ require (
github.com/yalue/onnxruntime_go v1.12.1
go.uber.org/automaxprocs v1.6.0
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.39.0
golang.org/x/crypto v0.43.0
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.31.1
resty.dev/v3 v3.0.0-beta.3
)
@@ -71,6 +73,12 @@ require (
github.com/gorilla/websocket v1.5.3 // indirect
github.com/grokify/html-strip-tags-go v0.1.0 // indirect
github.com/hashicorp/hcl/v2 v2.18.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.9 // indirect
@@ -100,13 +108,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 -16
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/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
@@ -179,6 +191,7 @@ github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqe
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -240,8 +253,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs=
golang.org/x/image v0.29.0 h1:HcdsyR4Gsuys/Axh0rDEmlBmB68rW1U9BUdB3UVHsas=
golang.org/x/image v0.29.0/go.mod h1:RVJROnf3SLK8d26OW91j4FrIHGbsJ8QnbEocVTOWQDA=
@@ -250,8 +263,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -261,8 +274,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -271,8 +284,8 @@ golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -290,8 +303,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -312,8 +325,8 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -322,8 +335,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
@@ -335,6 +348,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=
+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#验证码不能为空"`
}
@@ -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
}
+9 -14
View File
@@ -384,18 +384,14 @@ func (c *CpdailyAutoSignTask) ParseFormJson(ctx context.Context, rawJson map[str
}
list = append(list, dto)
var current uint = 1
if isPhoto := rows["isPhoto"]; isPhoto != nil && isPhoto.Int() == 1 {
dto = &formtype.CpTaskFormItem{
Title: "签到照片",
ColName: "signImg",
Type: formtype.ItemTypeImage,
Required: true,
Number: current,
}
list = append(list, dto)
current++
}
// 是否范围外签到
@@ -404,7 +400,6 @@ func (c *CpdailyAutoSignTask) ParseFormJson(ctx context.Context, rawJson map[str
ColName: "isMalposition",
Type: formtype.ItemTypeSingle,
Required: true,
Number: current,
Options: []formtype.CpTaskFormItemOption{
{
Content: "是",
@@ -418,7 +413,6 @@ func (c *CpdailyAutoSignTask) ParseFormJson(ctx context.Context, rawJson map[str
},
}
list = append(list, dto)
current++
// 范围外签到原因
dto = &formtype.CpTaskFormItem{
@@ -427,16 +421,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 +438,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(),
@@ -577,8 +571,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 +581,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
}
@@ -463,18 +463,15 @@ func (c *CpdailySleepCheckTask) ParseFormJson(ctx context.Context, rawJson map[s
}
list = append(list, dto)
var current uint = 1
if isPhoto := rows["isPhoto"]; isPhoto != nil && isPhoto.Int() == 1 {
dto = &formtype.CpTaskFormItem{
Title: "查寝照片",
ColName: "signImg",
Type: formtype.ItemTypeImage,
Required: true,
Number: current,
}
list = append(list, dto)
current++
}
// 是否范围外签到
@@ -483,7 +480,6 @@ func (c *CpdailySleepCheckTask) ParseFormJson(ctx context.Context, rawJson map[s
ColName: "isMalposition",
Type: formtype.ItemTypeSingle,
Required: true,
Number: current,
Options: []formtype.CpTaskFormItemOption{
{
Content: "是",
@@ -497,7 +493,6 @@ func (c *CpdailySleepCheckTask) ParseFormJson(ctx context.Context, rawJson map[s
},
}
list = append(list, dto)
current++
for key, item := range list {
list[key].Show = item.LogicWid == ""
@@ -1,9 +1,11 @@
package classifier
import (
"embed"
_ "embed"
"likedream-goframe/internal/application/recognizer"
"likedream-goframe/internal/framework/logger"
"likedream-goframe/internal/utils"
"os"
"path/filepath"
"strings"
@@ -11,11 +13,8 @@ import (
"go.uber.org/zap"
)
//go:embed static/classes.txt
var classesBytes []byte
//go:embed static/model.onnx
var modelBytes []byte
//go:embed static/*
var fs embed.FS
var imageClassifier *ImageClassifier
func init() {
@@ -39,10 +38,14 @@ func initModel() (modelPath, classesPath string) {
os.Mkdir(staticPath, 0755)
}
modelPath = filepath.Join(staticPath, "model.onnx")
modelBytes, err := fs.ReadFile("static/model.onnx")
utils.OkOrPanic(err)
if _, err := os.Stat(modelPath); os.IsNotExist(err) {
os.WriteFile(modelPath, modelBytes, 0644)
}
classesPath = filepath.Join(staticPath, "classes.txt")
classesBytes, err := fs.ReadFile("static/classes.txt")
utils.OkOrPanic(err)
if _, err := os.Stat(classesPath); os.IsNotExist(err) {
os.WriteFile(classesPath, classesBytes, 0644)
}
@@ -1,6 +1,7 @@
package ddddocr
import (
"embed"
_ "embed"
"likedream-goframe/internal/application/recognizer"
"os"
@@ -8,8 +9,8 @@ import (
"strings"
)
//go:embed static/common_old.onnx
var commonOldBytes []byte
//go:embed static/*
var fs embed.FS
func initModel() string {
if !recognizer.OnnxIsInit {
@@ -24,8 +25,9 @@ func initModel() string {
os.Mkdir(staticPath, 0755)
}
modelPath := filepath.Join(staticPath, "common_old.onnx")
modelBytes, _ := fs.ReadFile("static/common_old.onnx")
if _, err := os.Stat(modelPath); os.IsNotExist(err) {
os.WriteFile(modelPath, commonOldBytes, 0644)
os.WriteFile(modelPath, modelBytes, 0644)
}
return modelPath
}
+6 -11
View File
@@ -1,6 +1,7 @@
package recognizer
import (
"embed"
_ "embed"
"likedream-goframe/internal/framework/logger"
"os"
@@ -12,14 +13,8 @@ import (
"go.uber.org/zap"
)
//go:embed assets/onnxruntime.dll
var onnxRuntimeFileWindows []byte
//go:embed assets/libonnxruntime.1.23.2.dylib
var onnxRuntimeFileDarwin []byte
//go:embed assets/libonnxruntime.so.1.23.2
var onnxRuntimeFileLinux []byte
//go:embed static/*
var fs embed.FS
// OnnxIsInit 初始化onnx配置
var OnnxIsInit = false
@@ -48,15 +43,15 @@ func getOnnxRuntimeName() (onnxRuntimeFileName string, onnxRuntimeFileBytes []by
switch systemOS {
case "windows":
onnxRuntimeFileName = "onnxruntime.dll"
onnxRuntimeFileBytes = onnxRuntimeFileWindows
onnxRuntimeFileBytes, _ = fs.ReadFile("static/onnxruntime.dll")
return
case "darwin":
onnxRuntimeFileName = "onnxruntime.dylib"
onnxRuntimeFileBytes = onnxRuntimeFileDarwin
onnxRuntimeFileBytes, _ = fs.ReadFile("static/libonnxruntime.1.23.2.dylib")
return
case "linux":
onnxRuntimeFileName = "onnxruntime.so"
onnxRuntimeFileBytes = onnxRuntimeFileLinux
onnxRuntimeFileBytes, _ = fs.ReadFile("static/libonnxruntime.so.1.23.2")
return
}
logger.Logger.Panic("暂不支持的操作系统:", zap.String("系统名称", systemOS))
+21 -18
View File
@@ -3,22 +3,25 @@ package cst
import "time"
const (
ConfigOcrServerUrl = "ocr.url"
ConfigEnvMode = "env.mode"
ConfigDatabaseType = "database.type"
ConfigDatabaseDsn = "database.dsn"
ConfigServerPort = "server.port"
ConfigEmailConfirmBaseUrl = "server.emailConfirmBaseUrl"
ConfigAuthorizationKey = "server.authorization"
ConfigJwtKey = "server.jwt"
ConfigRuoliCaptchaBaseUrl = "ruoliCaptcha.baseUrl"
ConfigS3AccessKey = "s3.accessKey"
ConfigS3SecretKey = "s3.secretKey"
ConfigS3BaseUrl = "s3.baseUrl"
ConfigS3Bucket = "s3.bucket"
ConfigCpdailyAppVersion = "cpdaily.appVersion"
ConfigCpdailySystemVersion = "cpdaily.systemVersion"
ConfigCpdailyCalVersion = "cpdaily.calVersion"
ConfigOcrServerUrl = "ocr.url"
ConfigEnvMode = "env.mode"
ConfigDatabaseType = "database.type"
ConfigDatabaseDsn = "database.dsn"
ConfigDatabaseDsn2 = "database.dsn2"
ConfigServerPort = "server.port"
ConfigEmailConfirmBaseUrl = "server.emailConfirmBaseUrl"
ConfigEmailResetPasswordUrl = "server.emailResetPasswordUrl"
ConfigBalanceMigrateUrl = "server.balanceMigrateUrl"
ConfigAuthorizationKey = "server.authorization"
ConfigJwtKey = "server.jwt"
ConfigRuoliCaptchaBaseUrl = "ruoliCaptcha.baseUrl"
ConfigS3AccessKey = "s3.accessKey"
ConfigS3SecretKey = "s3.secretKey"
ConfigS3BaseUrl = "s3.baseUrl"
ConfigS3Bucket = "s3.bucket"
ConfigCpdailyAppVersion = "cpdaily.appVersion"
ConfigCpdailySystemVersion = "cpdaily.systemVersion"
ConfigCpdailyCalVersion = "cpdaily.calVersion"
ConfigRedisUrl = "redis.url"
@@ -35,8 +38,8 @@ const (
const (
// 应用程序常量
AppName = "今日校园系统"
AppCopyright = "© 2024 今日校园系统. 保留所有权利."
AppName = "若梦系统"
AppCopyright = "© 2025 若梦系统. 保留所有权利."
)
var (
+4
View File
@@ -3,6 +3,8 @@ package cst
const (
RedisUserInfo = "userinfo:"
RedisVerifyCode = "verifycode:"
RedisResetPassword = "resetpassword:"
RedisBalanceMigrate = "balancemigrate:" // 积分转移token
RedisCpUserLogin = "cpuserlogin:"
RedisCpUserAuthorize = "cpuser:authorize:"
)
@@ -10,6 +12,8 @@ const (
var (
RedisUserInfoExpire = int64(86400)
RedisVerifyCodeExpire = int64(86400)
RedisResetPasswordExpire = int64(3600) // 重置密码链接1小时有效
RedisBalanceMigrateExpire = int64(3600) // 积分转移链接1小时有效
RedisCpUserLoginExpire = int64(180)
RedisCpUserAuthorizeExpire = int64(1800)
)
+26 -26
View File
@@ -13,7 +13,7 @@ import (
)
func CheckHasData() bool {
count, err := Global.SysUser.Query().Count(context.Background())
count, err := Global.Debug().SysUser.Query().Count(context.Background())
utils.OkOrPanic(err, func(err error) error {
return err
})
@@ -36,7 +36,7 @@ func GenerateData() {
// 生成用户
func generateUser() {
ctx := gctx.New()
Global.SysUser.Create().
Global.Debug().SysUser.Create().
SetUsername("admin").
SetPassword(utils.PasswordBcryptHash("123456")).
SetEmail("admin@qq.com").
@@ -50,15 +50,15 @@ func generateUser() {
// 生成角色
func generateRoles() {
ctx := gctx.New()
Global.SysRole.Create().SetName("管理员").SetLabel(cst.KeyRoleAdmin).SetDescription("超级管理员角色").SetOrder(1).ExecX(ctx)
Global.SysRole.Create().SetName("代理").SetLabel(cst.KeyRoleAgent).SetDescription("代理角色").SetOrder(2).ExecX(ctx)
Global.SysRole.Create().SetName("普通用户").SetLabel(cst.KeyRoleUser).SetDescription("普通用户角色").SetOrder(3).ExecX(ctx)
Global.Debug().SysRole.Create().SetName("管理员").SetLabel(cst.KeyRoleAdmin).SetDescription("超级管理员角色").SetOrder(1).ExecX(ctx)
Global.Debug().SysRole.Create().SetName("代理").SetLabel(cst.KeyRoleAgent).SetDescription("代理角色").SetOrder(2).ExecX(ctx)
Global.Debug().SysRole.Create().SetName("普通用户").SetLabel(cst.KeyRoleUser).SetDescription("普通用户角色").SetOrder(3).ExecX(ctx)
}
// 生成菜单
func generateMenus() {
ctx := gctx.New()
sysM := Global.SysMenu.Create().
sysM := Global.Debug().SysMenu.Create().
SetName("系统管理").
SetLabel("system").
SetType(sysmenu.TypeGroup).
@@ -66,7 +66,7 @@ func generateMenus() {
SetRoute("/system").
SetOrder(1).
SaveX(ctx)
userM := Global.SysMenu.Create().
userM := Global.Debug().SysMenu.Create().
SetName("用户管理").
SetLabel("user").
SetType(sysmenu.TypeMenu).
@@ -77,7 +77,7 @@ func generateMenus() {
SetParent(sysM).
SaveX(ctx)
// 新增 编辑 删除按钮
Global.SysMenu.Create().
Global.Debug().SysMenu.Create().
SetName("新增").
SetLabel("add").
SetType(sysmenu.TypeButton).
@@ -85,7 +85,7 @@ func generateMenus() {
SetOrder(1).
SetParent(userM).
SaveX(ctx)
Global.SysMenu.Create().
Global.Debug().SysMenu.Create().
SetName("编辑").
SetLabel("edit").
SetType(sysmenu.TypeButton).
@@ -94,7 +94,7 @@ func generateMenus() {
SetParent(userM).
SaveX(ctx)
// 删除按钮
Global.SysMenu.Create().
Global.Debug().SysMenu.Create().
SetName("删除").
SetLabel("delete").
SetType(sysmenu.TypeButton).
@@ -103,7 +103,7 @@ func generateMenus() {
SetParent(userM).
SaveX(ctx)
// 列表
Global.SysMenu.Create().
Global.Debug().SysMenu.Create().
SetName("列表").
SetLabel("list").
SetType(sysmenu.TypeButton).
@@ -111,7 +111,7 @@ func generateMenus() {
SetOrder(4).
SetParent(userM).
SaveX(ctx)
roleM := Global.SysMenu.Create().
roleM := Global.Debug().SysMenu.Create().
SetName("角色管理").
SetLabel("role").
SetType(sysmenu.TypeMenu).
@@ -122,7 +122,7 @@ func generateMenus() {
SetParent(sysM).
SaveX(ctx)
// 新增 编辑 删除按钮
Global.SysMenu.Create().
Global.Debug().SysMenu.Create().
SetName("新增").
SetLabel("add").
SetType(sysmenu.TypeButton).
@@ -130,7 +130,7 @@ func generateMenus() {
SetOrder(1).
SetParent(roleM).
SaveX(ctx)
Global.SysMenu.Create().
Global.Debug().SysMenu.Create().
SetName("编辑").
SetLabel("edit").
SetType(sysmenu.TypeButton).
@@ -139,7 +139,7 @@ func generateMenus() {
SetParent(roleM).
SaveX(ctx)
// 删除按钮
Global.SysMenu.Create().
Global.Debug().SysMenu.Create().
SetName("删除").
SetLabel("delete").
SetType(sysmenu.TypeButton).
@@ -147,7 +147,7 @@ func generateMenus() {
SetOrder(3).
SetParent(roleM).
SaveX(ctx)
Global.SysMenu.Create().
Global.Debug().SysMenu.Create().
SetName("列表").
SetLabel("list").
SetType(sysmenu.TypeButton).
@@ -155,7 +155,7 @@ func generateMenus() {
SetOrder(4).
SetParent(roleM).
SaveX(ctx)
menuM := Global.SysMenu.Create().
menuM := Global.Debug().SysMenu.Create().
SetName("菜单管理").
SetLabel("menu").
SetType(sysmenu.TypeMenu).
@@ -166,7 +166,7 @@ func generateMenus() {
SetParent(sysM).
SaveX(ctx)
// 新增 编辑 删除按钮
Global.SysMenu.Create().
Global.Debug().SysMenu.Create().
SetName("新增").
SetLabel("add").
SetType(sysmenu.TypeButton).
@@ -174,7 +174,7 @@ func generateMenus() {
SetOrder(1).
SetParent(menuM).
SaveX(ctx)
Global.SysMenu.Create().
Global.Debug().SysMenu.Create().
SetName("编辑").
SetLabel("edit").
SetType(sysmenu.TypeButton).
@@ -183,7 +183,7 @@ func generateMenus() {
SetParent(menuM).
SaveX(ctx)
// 删除按钮
Global.SysMenu.Create().
Global.Debug().SysMenu.Create().
SetName("删除").
SetLabel("delete").
SetType(sysmenu.TypeButton).
@@ -191,7 +191,7 @@ func generateMenus() {
SetOrder(3).
SetParent(menuM).
SaveX(ctx)
Global.SysMenu.Create().
Global.Debug().SysMenu.Create().
SetName("列表").
SetLabel("list").
SetType(sysmenu.TypeButton).
@@ -204,14 +204,14 @@ func generateMenus() {
// 关联角色
func relationRoles() {
ctx := gctx.New()
admin := Global.SysUser.Query().Where(sysuser.UsernameEQ("admin")).OnlyX(ctx)
role := Global.SysRole.Query().Where(sysrole.LabelEQ(cst.KeyRoleAdmin)).AllX(ctx)
Global.SysUser.UpdateOne(admin).AddRoles(role...).ExecX(ctx)
admin := Global.Debug().SysUser.Query().Where(sysuser.UsernameEQ("admin")).OnlyX(ctx)
role := Global.Debug().SysRole.Query().Where(sysrole.LabelEQ(cst.KeyRoleAdmin)).AllX(ctx)
Global.Debug().SysUser.UpdateOne(admin).AddRoles(role...).ExecX(ctx)
}
// 关联菜单
func relationMenus() {
ctx := gctx.New()
adminRole := Global.SysRole.Query().Where(sysrole.LabelEQ(cst.KeyRoleAdmin)).OnlyX(ctx)
Global.SysRole.UpdateOne(adminRole).AddMenus(Global.SysMenu.Query().AllX(ctx)...).ExecX(ctx)
adminRole := Global.Debug().SysRole.Query().Where(sysrole.LabelEQ(cst.KeyRoleAdmin)).OnlyX(ctx)
Global.Debug().SysRole.UpdateOne(adminRole).AddMenus(Global.Debug().SysMenu.Query().AllX(ctx)...).ExecX(ctx)
}
+21 -1
View File
@@ -11,10 +11,14 @@ import (
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
"go.uber.org/zap"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
var Global *ent.Client
var GlobalGorm *gorm.DB
func InitDB() {
dbType, err := g.Cfg().Get(gctx.New(), cst.ConfigDatabaseType)
utils.OkOrPanic(err)
@@ -23,8 +27,24 @@ func InitDB() {
client, err := ent.Open(dbType.String(), dbDsn.String())
utils.OkOrPanic(err)
// 自动迁移
if err := client.Schema.Create(context.Background()); err != nil {
if err := client.Debug().Schema.Create(context.Background()); err != nil {
logger.Logger.Fatal("failed creating schema resources: %v", zap.Error(err))
}
Global = client
dbDsn2, err := g.Cfg().Get(gctx.New(), cst.ConfigDatabaseDsn2)
utils.OkOrPanic(err)
GlobalGorm, err = gorm.Open(postgres.Open(dbDsn2.String()), &gorm.Config{})
if err != nil {
logger.Logger.Fatal("连接pg数据库失败: %v", zap.Error(err))
}
// 可选:配置连接池
sqlDB, err := GlobalGorm.DB()
if err != nil {
logger.Logger.Fatal("获取数据库实例失败: %v", zap.Error(err))
}
// 设置连接池参数
sqlDB.SetMaxIdleConns(10) // 设置空闲连接池中连接的最大数量
sqlDB.SetMaxOpenConns(100) // 设置打开数据库连接的最大数量
sqlDB.SetConnMaxLifetime(0) // 设置连接可复用的最大时间(0表示永久)
}
@@ -25,4 +25,7 @@ type IUserInfoV1 interface {
InviteUserList(ctx context.Context, req *v1.CommonPageReq) (*utils.Response[[]*v1.UserInviteRes], error)
// 生成邀请码
GenerateInviteCode(ctx context.Context, req *v1.CommonPostReq) (*utils.Response[any], error)
RequestBalanceMigrate(ctx context.Context, req *v1.RequestBalanceMigrateReq) (*utils.Response[any], error)
BalanceMigrate(ctx context.Context, req *v1.BalanceMigrateReq) (*utils.Response[any], error)
}
@@ -13,4 +13,8 @@ type IUserLoginV1 interface {
Login(ctx context.Context, req *v1.UserLoginReq) (*utils.Response[any], error)
Register(ctx context.Context, req *v1.UserRegisterReq) (*utils.Response[any], error)
Confirm(ctx context.Context, req *v1.UserConfirmReq) (*utils.Response[any], error)
ResendVerification(ctx context.Context, req *v1.ResendVerificationReq) (*utils.Response[any], error)
ForgotPassword(ctx context.Context, req *v1.ForgotPasswordReq) (*utils.Response[any], error)
ResetPassword(ctx context.Context, req *v1.ForgetResetPasswordReq) (*utils.Response[any], error)
GetResetPasswordInfo(ctx context.Context, req *v1.ResetPasswordInfoReq) (*utils.Response[any], error)
}
@@ -4,8 +4,12 @@ import (
"context"
v1 "likedream-goframe/internal/api/v1"
"likedream-goframe/internal/ent"
"likedream-goframe/internal/framework/captcha"
"likedream-goframe/internal/framework/redis"
"likedream-goframe/internal/service"
"likedream-goframe/internal/utils"
"github.com/bytedance/sonic"
)
type UserInfoV1 struct {
@@ -60,3 +64,34 @@ func (c *UserInfoV1) GenerateInviteCode(ctx context.Context, req *v1.CommonPostR
defer utils.Pass(&err)
return service.Global.UserInfo.GenerateInviteCode(ctx, req), nil
}
func (c *UserInfoV1) RequestBalanceMigrate(ctx context.Context, req *v1.RequestBalanceMigrateReq) (res *utils.Response[any], err error) {
defer utils.Pass(&err)
client := redis.GetClient()
dotsStr := client.GetDel(ctx, req.CaptchaKey).Val()
if dotsStr == "" {
return utils.Fail(10003, "验证码错误"), nil
}
var dots captcha.ConfirmData
err = sonic.Unmarshal([]byte(dotsStr), &dots)
utils.OkOrPanic(err)
if !captcha.VerifyCaptcha(dots, req.Dots) {
return utils.Fail(10003, "验证码错误"), nil
}
return service.Global.UserInfo.RequestBalanceMigrate(ctx, req), nil
}
func (c *UserInfoV1) BalanceMigrate(ctx context.Context, req *v1.BalanceMigrateReq) (res *utils.Response[any], err error) {
defer utils.Pass(&err)
client := redis.GetClient()
dotsStr := client.GetDel(ctx, req.CaptchaKey).Val()
if dotsStr == "" {
return utils.Fail(10003, "验证码错误"), nil
}
var dots captcha.ConfirmData
err = sonic.Unmarshal([]byte(dotsStr), &dots)
utils.OkOrPanic(err)
if !captcha.VerifyCaptcha(dots, req.Dots) {
return utils.Fail(10003, "验证码错误"), nil
}
return service.Global.UserInfo.BalanceMigrate(ctx, req), nil
}
+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
}
-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
+6 -27
View File
@@ -380,56 +380,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
@@ -90,16 +90,17 @@ func (c *CpdailyTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task)
// 查询学校详情
schoolDetail := school.GetSchoolDetail(ctx, targetTask.Edges.CpUser.SchoolID)
restyClient := login.GetClient(ctx, targetTask.Edges.CpUser.Cookie)
cookie := targetTask.Edges.CpUser.Cookie
restyClient := login.GetClient(ctx, cookie)
if !login.CookieEnabled(ctx, schoolDetail.WecloudUrl, restyClient) {
loginExecutor := login.GetCpdailyLoginBySchoolInfo(ctx, &schoolDetail)
loginExecutor.Login(ctx, &schoolDetail, targetTask.Edges.CpUser.Username, targetTask.Edges.CpUser.Password)
cookie := loginExecutor.GetCookieString()
cookie = loginExecutor.GetCookieString()
db.Global.CpUser.UpdateOne(targetTask.Edges.CpUser).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, cookie, targetTask.Edges.CpUser.SchoolID)
cpdailyRealTask := taskExecutor.QueryFormByTitle(ctx, targetTask.Title)
+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
@@ -87,7 +87,7 @@ func (ts *TaskScanner) scanCpdailyTask() {
// 只查询30分钟以内的任务
minExecuteTime := currentTime.Add(-30 * time.Minute).Unix()
allTasks := db.Global.Debug().CpTask.Query().
allTasks := db.Global.CpTask.Query().
Where(
cptask.NextExecuteTimeLTE(currentTime.Unix()),
cptask.NextExecuteTimeNotNil(),
+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 {
+11 -27
View File
@@ -25,6 +25,7 @@ import (
"time"
"entgo.io/ent/dialect/sql"
"github.com/bytedance/sonic"
"github.com/gogf/gf/v2/frame/g"
"github.com/hibiken/asynq"
"github.com/samber/oops"
@@ -111,7 +112,7 @@ func (v CpTaskServiceV1) TaskFormData(ctx context.Context, req *v1.GetCpTaskForm
}
if cpTask.FormData != "" {
var formData []*formtype.CpTaskFormItem
err := json.Unmarshal([]byte(cpTask.FormData), &formData)
err := sonic.Unmarshal([]byte(cpTask.FormData), &formData)
utils.OkOrPanic(err)
return utils.SuccessWithData[any](formData)
}
@@ -178,9 +179,6 @@ func (v CpTaskServiceV1) PostExecute(ctx context.Context, req *v1.PostCpTaskExec
utils.OkOrPanic(errors2.NewBusinessError(400, "执行次数应不低于原有次数"))
}
// 记录原始任务状态,用于判断是否需要立即扫描
originalStatus := cpTask.TaskStatus
//扣积分
basePoint := decimal.NewFromInt(cst2.BasePointCpdaily)
if user.Edges.Agent != nil {
@@ -190,35 +188,20 @@ func (v CpTaskServiceV1) PostExecute(ctx context.Context, req *v1.PostCpTaskExec
balance.BalanceService.ChangeUserBalance(ctx, user.ID, needPoint.Neg())
// 更新任务
e := db2.WithTx(ctx, func(ctx context.Context, tx *ent.Tx) {
cpTask := db2.Global.CpTask.UpdateOne(cpTask).
update := db2.Global.CpTask.UpdateOne(cpTask).
SetStep(cptask.StepComplete).
SetFrequency(req.Frequency).
SetTaskStatus(cptask.TaskStatusNotStart).
SetNotificationType(req.NotificationType).
SetExecuteHour(req.ExecuteHour).
SetExecuteMinute(req.ExecuteMinute).
SaveX(ctx)
// 计算下次执行时间
var nextExecuteAt int64
// 只有在原始状态为未开始时,才考虑5分钟窗口立即扫描
if originalStatus == cptask.TaskStatusNotStart {
currentTime := time.Now().In(cst2.TimeZoneShangHai)
executeTime := time.Date(currentTime.Year(), currentTime.Month(), currentTime.Day(), req.ExecuteHour, req.ExecuteMinute, 0, 0, cst2.TimeZoneShangHai)
timeDiff := executeTime.Sub(currentTime)
if timeDiff >= -5*time.Minute && timeDiff <= 5*time.Minute {
// 执行时间在当前时间前后5分钟内,设置为当前时间,让扫描器立即扫描到
nextExecuteAt = currentTime.Unix()
} else {
// 否则使用正常的计算逻辑
nextExecuteAt = utils.GetCpdailyNextExecuteTime(cpTask)
}
} else {
// 其他状态(暂停、失败等),使用正常的计算逻辑
nextExecuteAt = utils.GetCpdailyNextExecuteTime(cpTask)
SetExecuteMinute(req.ExecuteMinute)
if cpTask.Step == cptask.StepExecute {
update.SetTaskStatus(cptask.TaskStatusNotStart)
}
cpTask := update.SaveX(ctx)
// 其他状态(暂停、失败等),使用正常的计算逻辑
nextExecuteAt := utils.GetCpdailyNextExecuteTime(cpTask)
db2.Global.CpTask.UpdateOne(cpTask).SetNextExecuteTime(nextExecuteAt).ExecX(ctx)
}, &sql.TxOptions{})
@@ -283,6 +266,7 @@ func (v CpTaskServiceV1) ExecuteNow(ctx context.Context, req *v1.ExecuteNowReq)
asynq.Unique(5*time.Second),
asynq.MaxRetry(0),
asynq.Timeout(10*time.Minute),
asynq.Deadline(time.Now().Add(10*time.Minute)),
asynq.Queue(cst2.TaskQueueCpdaily),
)
// 更新未开始的任务状态为队列中
+2 -2
View File
@@ -2,7 +2,6 @@ package cpdaily
import (
"context"
"encoding/json"
v1 "likedream-goframe/internal/api/v1"
"likedream-goframe/internal/application/cpdaily/login"
"likedream-goframe/internal/application/cpdaily/school"
@@ -17,6 +16,7 @@ import (
"strconv"
"time"
"github.com/bytedance/sonic"
"github.com/samber/oops"
)
@@ -53,7 +53,7 @@ func (v CpUserServiceV1) ConfirmAdd(ctx context.Context, req *v1.CpUserConfirmAd
Wrap(err)
})
var sc school.SchoolDetailInfo
json.Unmarshal([]byte(schoolInfo), &sc)
sonic.Unmarshal([]byte(schoolInfo), &sc)
loginExecutor := login.GetCpdailyLoginBySchoolInfo(ctx, &sc)
loginExecutor.(*login.CpdailyCasLogin).ConfirmAdd(ctx, &sc, req.Username, req.Password, req.Key, req.DynamicCode)
db.Global.CpUser.Create().
+93 -1
View File
@@ -101,6 +101,29 @@ func (s *EmailServiceV1) SendTaskNotificationEmail(ctx context.Context, toEmail,
return utils.SuccessWithMsg("通知邮件发送成功")
}
// SendResetPasswordEmail 发送重置密码邮件
func (s *EmailServiceV1) SendResetPasswordEmail(ctx context.Context, toEmail, username string, token string) *utils.Response[any] {
if s.config.SMTPUsername == "" || s.config.SMTPPassword == "" {
logger.Logger.Error("邮件服务未配置")
return utils.FailBusiness("邮件服务未配置")
}
subject := fmt.Sprintf("重置%s账户密码", cst2.AppName)
body, err := s.renderResetPasswordEmailTemplate(username, token)
if err != nil {
logger.Logger.Error("邮件模板渲染失败")
return utils.FailBusiness(fmt.Sprintf("邮件模板渲染失败: %v", err))
}
err = s.sendEmail(ctx, toEmail, subject, body)
if err != nil {
logger.Logger.Error("邮件发送失败")
return utils.FailBusiness(fmt.Sprintf("邮件发送失败: %v", err))
}
return utils.SuccessWithMsg("重置密码邮件发送成功")
}
// sendEmail 发送邮件的底层方法
func (s *EmailServiceV1) sendEmail(ctx context.Context, to, subject, body string) error {
@@ -123,7 +146,7 @@ func (s *EmailServiceV1) sendEmail(ctx context.Context, to, subject, body string
asynq.MaxRetry(3),
asynq.Timeout(10*time.Minute),
)
logger.Logger.Info("添加任务: ", zap.String("task_id", task.ID))
logger.Logger.Info("添加任务: ", zap.String("task_id", task.ID), zap.String("邮箱", to))
return nil
}
@@ -190,3 +213,72 @@ func (s *EmailServiceV1) renderTaskNotificationTemplate(taskName string, status
return buf.String(), nil
}
// renderResetPasswordEmailTemplate 渲染重置密码邮件模板
func (s *EmailServiceV1) renderResetPasswordEmailTemplate(username string, token string) (string, error) {
// 单独解析重置密码模板
tmpl, err := template.ParseFS(templateFiles, "templates/layout.gohtml", "templates/reset_password.gohtml")
if err != nil {
return "", fmt.Errorf("解析重置密码邮件模板失败: %v", err)
}
get, err := g.Cfg().Get(gctx.New(), cst2.ConfigEmailResetPasswordUrl)
if err != nil {
return "", err
}
data := NewResetPasswordTemplateData(username, cst2.AppName, cst2.AppCopyright, get.String()+token)
var buf bytes.Buffer
err = tmpl.ExecuteTemplate(&buf, "reset_password.gohtml", data)
if err != nil {
return "", fmt.Errorf("渲染重置密码邮件模板失败: %v", err)
}
return buf.String(), nil
}
// SendBalanceMigrateEmail 发送积分转移邮件
func (s *EmailServiceV1) SendBalanceMigrateEmail(ctx context.Context, toEmail, username string, balance int, token string) *utils.Response[any] {
if s.config.SMTPUsername == "" || s.config.SMTPPassword == "" {
logger.Logger.Error("邮件服务未配置")
return utils.FailBusiness("邮件服务未配置")
}
subject := fmt.Sprintf("%s积分转移通知", cst2.AppName)
body, err := s.renderBalanceMigrateEmailTemplate(username, balance, token)
if err != nil {
logger.Logger.Error("邮件模板渲染失败")
return utils.FailBusiness(fmt.Sprintf("邮件模板渲染失败: %v", err))
}
err = s.sendEmail(ctx, toEmail, subject, body)
if err != nil {
logger.Logger.Error("邮件发送失败")
return utils.FailBusiness(fmt.Sprintf("邮件发送失败: %v", err))
}
return utils.SuccessWithMsg("积分转移邮件发送成功")
}
// renderBalanceMigrateEmailTemplate 渲染积分转移邮件模板
func (s *EmailServiceV1) renderBalanceMigrateEmailTemplate(username string, balance int, token string) (string, error) {
// 单独解析积分转移模板
tmpl, err := template.ParseFS(templateFiles, "templates/layout.gohtml", "templates/balance_migrate.gohtml")
if err != nil {
return "", fmt.Errorf("解析积分转移邮件模板失败: %v", err)
}
get, err := g.Cfg().Get(gctx.New(), cst2.ConfigBalanceMigrateUrl)
if err != nil {
return "", err
}
data := NewBalanceMigrateTemplateData(username, cst2.AppName, cst2.AppCopyright, balance, get.String()+token)
var buf bytes.Buffer
err = tmpl.ExecuteTemplate(&buf, "balance_migrate.gohtml", data)
if err != nil {
return "", fmt.Errorf("渲染积分转移邮件模板失败: %v", err)
}
return buf.String(), nil
}
+50
View File
@@ -64,12 +64,62 @@ func NewTaskNotificationTemplateData(taskName string, status email.TaskStatus, d
}
}
// ResetPasswordTemplateData 重置密码邮件模板数据
type ResetPasswordTemplateData struct {
BaseTemplateData
Username string `json:"username"` // 用户名
ResetURL string `json:"reset_url"` // 重置密码链接
}
// NewResetPasswordTemplateData 创建重置密码邮件模板数据
func NewResetPasswordTemplateData(username, appName, copyright string, resetURL string) *ResetPasswordTemplateData {
return &ResetPasswordTemplateData{
BaseTemplateData: BaseTemplateData{
Title: "重置密码 - " + appName,
HeaderIcon: "🔒",
HeaderTitle: "重置您的密码",
HeaderColor: "#f56565",
AppName: appName,
AppCopyright: copyright,
},
Username: username,
ResetURL: resetURL,
}
}
// BalanceMigrateTemplateData 积分转移邮件模板数据
type BalanceMigrateTemplateData struct {
BaseTemplateData
Username string `json:"username"` // 用户名
Balance int `json:"balance"` // 积分余额
MigrateURL string `json:"migrate_url"` // 转移链接
}
// NewBalanceMigrateTemplateData 创建积分转移邮件模板数据
func NewBalanceMigrateTemplateData(username, appName, copyright string, balance int, migrateURL string) *BalanceMigrateTemplateData {
return &BalanceMigrateTemplateData{
BaseTemplateData: BaseTemplateData{
Title: "积分转移 - " + appName,
HeaderIcon: "💰",
HeaderTitle: "积分转移通知",
HeaderColor: "#ff9800",
AppName: appName,
AppCopyright: copyright,
},
Username: username,
Balance: balance,
MigrateURL: migrateURL,
}
}
// TemplateType 模板类型枚举
type TemplateType string
const (
TemplateTypeSignup TemplateType = "signup" // 注册模板
TemplateTypeTaskNotification TemplateType = "task_notification" // 任务通知模板
TemplateTypeResetPassword TemplateType = "reset_password" // 重置密码模板
TemplateTypeBalanceMigrate TemplateType = "balance_migrate" // 积分转移模板
)
// GetTemplatePath 获取模板文件路径
@@ -0,0 +1,73 @@
{{- /*gotype: likedream-goframe/internal/service/email.BalanceMigrateTemplateData*/ -}}
{{template "layout" .}}
{{define "extra-styles"}}
<style>
.balance-info {
background: linear-gradient(90deg, #fff9f0 0%, #ffe5cc 100%);
border-radius: 8px;
padding: 20px 24px;
border-left: 4px solid #ff9800;
margin: 24px 0;
}
.balance-amount {
font-size: 32px;
font-weight: bold;
color: #ff9800;
text-align: center;
margin: 10px 0;
}
.warning-box {
background: linear-gradient(90deg, #fff5f5 0%, #ffe5e5 100%);
border-radius: 8px;
padding: 20px 24px;
border-left: 4px solid #f56565;
margin: 24px 0;
}
.warning-box p {
margin: 8px 0;
font-size: 14px;
color: #c53030;
}
</style>
{{end}}
{{define "content"}}
<h2>亲爱的 <span class="highlight-text">{{.Username}}</span>,您好!</h2>
<p>我们检测到您在旧系统中有积分余额,现在可以将积分转移到新系统。</p>
<div class="balance-info">
<p style="margin: 0; text-align: center; font-size: 16px;">您的旧系统积分余额</p>
<div class="balance-amount">{{.Balance}} 积分</div>
</div>
<p>点击下面的按钮完成积分转移:</p>
<div style="text-align: center; margin: 30px 0;">
<a href="{{.MigrateURL}}"
style="background-color: #ff9800; color: white; padding: 12px 24px;
text-decoration: none; border-radius: 4px; display: inline-block;">
立即转移积分
</a>
</div>
<p>如果按钮无法点击,请复制以下链接到浏览器地址栏:</p>
<p style="word-break: break-all; color: #666;">{{.MigrateURL}}</p>
<p><strong>链接有效期:24小时</strong></p>
<div class="warning-box">
<p style="margin: 0;"><strong>⚠️ 重要提示</strong></p>
<p>• 积分转移后将从旧系统清空,请确认后再操作</p>
<p>• 每个账户只能转移一次</p>
<p>• 请勿将此链接分享给其他人</p>
</div>
<hr>
<p>如果您在转移过程中遇到任何问题,请随时联系我们的技术支持团队。</p>
<p><strong>祝您使用愉快!</strong> 💰</p>
{{end}}
@@ -0,0 +1,59 @@
{{- /*gotype: likedream-goframe/internal/service/email.ResetPasswordTemplateData*/ -}}
{{template "layout" .}}
{{define "extra-styles"}}
<style>
.warning-box {
background: linear-gradient(90deg, #fff5f5 0%, #ffe5e5 100%);
border-radius: 8px;
padding: 20px 24px;
border-left: 4px solid #f56565;
margin: 24px 0;
}
.warning-box p {
margin: 8px 0;
font-size: 14px;
color: #c53030;
}
.info-box {
background: linear-gradient(90deg, #f0f9ff 0%, #e0f2fe 100%);
border-radius: 8px;
padding: 20px 24px;
border-left: 4px solid #3b82f6;
margin: 24px 0;
}
</style>
{{end}}
{{define "content"}}
<h2>亲爱的 <span class="highlight-text">{{.Username}}</span>,您好!</h2>
<p>我们收到了您重置<strong>{{.AppName}}</strong>账户密码的请求。请点击下面的链接重置您的密码:</p>
<div style="text-align: center; margin: 30px 0;">
<a href="{{.ResetURL}}"
style="background-color: #f56565; color: white; padding: 12px 24px;
text-decoration: none; border-radius: 4px; display: inline-block;">
重置密码
</a>
</div>
<p>如果按钮无法点击,请复制以下链接到浏览器地址栏:</p>
<p style="word-break: break-all; color: #666;">{{.ResetURL}}</p>
<div class="info-box">
<p style="margin: 0; color: #1e40af;"><strong>⏰ 重要提示:链接有效期为 1 小时</strong></p>
</div>
<div class="warning-box">
<p style="margin: 0;"><strong>⚠️ 安全提醒</strong></p>
<p>如果您没有请求重置密码,请忽略此邮件。您的账户仍然是安全的。</p>
<p>请勿将此链接分享给任何人!</p>
</div>
<hr>
<p>如果您在重置密码过程中遇到任何问题,请随时联系我们的技术支持团队。</p>
<p><strong>祝您使用愉快!</strong> 🔒</p>
{{end}}
@@ -2,6 +2,7 @@ package user_login
import (
"context"
"encoding/base64"
"fmt"
v1 "likedream-goframe/internal/api/v1"
"likedream-goframe/internal/common/cst"
@@ -12,8 +13,11 @@ import (
"likedream-goframe/internal/ent/sysrole"
"likedream-goframe/internal/ent/sysuser"
"likedream-goframe/internal/framework/ent2"
"likedream-goframe/internal/framework/redis"
"likedream-goframe/internal/service/balance"
"likedream-goframe/internal/service/email"
"likedream-goframe/internal/utils"
"math/rand"
"strings"
"time"
@@ -249,3 +253,130 @@ func buildPermissionList(list []*ent.SysMenu) []string {
}
return permissionList
}
func (s *UserInfoServiceV1) RequestBalanceMigrate(ctx context.Context, req *v1.RequestBalanceMigrateReq) *utils.Response[any] {
// 获取当前登录用户(新系统)
currentUser := utils.GetUserInfoFromCtx(ctx)
newSystemEmail := currentUser.Email
// 旧系统的邮箱从请求参数获取
oldSystemEmail := req.Email
// 定义旧系统用户结构
type AspNetUser struct {
Email string `gorm:"column:email"`
UserName string `gorm:"column:user_name"`
Balance int `gorm:"column:balance"`
}
// 查询旧系统数据库中是否存在该邮箱的用户
var oldUser AspNetUser
err := db.GlobalGorm.Table("AspNetUsers").Where("email = ?", oldSystemEmail).First(&oldUser).Error
if err != nil {
// 用户不存在或查询失败,为了安全仍返回成功
return utils.SuccessWithMsg("如果该邮箱在旧系统中存在积分,转移链接已发送")
}
// 检查积分是否大于0
if oldUser.Balance <= 0 {
return utils.SuccessWithMsg("如果该邮箱在旧系统中存在积分,转移链接已发送")
}
// 发送积分转移邮件
go func() {
ctx2 := context.Background()
client := redis.GetClient()
// 获取并删除旧的 token(如果存在)
// 使用新系统邮箱作为key,因为token是关联到当前登录用户的
oldTokenKey := cst.RedisBalanceMigrate + "email:" + newSystemEmail
oldToken := client.GetDel(ctx2, oldTokenKey).Val()
if oldToken != "" {
// 删除旧 token 对应的转移记录
client.Del(ctx2, cst.RedisBalanceMigrate+oldToken)
}
// 生成新的 token
token := make([]byte, 32)
_, _ = rand.Read(token)
tokenStr := base64.URLEncoding.EncodeToString(token)
// Token存储格式:token -> "新系统邮箱|旧系统邮箱"
tokenValue := newSystemEmail + "|" + oldSystemEmail
err2 := client.SetEx(ctx2, cst.RedisBalanceMigrate+tokenStr, tokenValue, time.Duration(cst.RedisBalanceMigrateExpire)*time.Second).Err()
utils.OkOrPanic(err2, oops.WithContext(ctx2).Wrap)
// 保存新系统邮箱到 token 的映射,方便下次删除
client.SetEx(ctx2, oldTokenKey, tokenStr, time.Duration(cst.RedisBalanceMigrateExpire)*time.Second)
// 发送积分转移邮件到新系统邮箱
emailService := email.NewV1()
emailService.SendBalanceMigrateEmail(ctx2, oldSystemEmail, currentUser.Username, oldUser.Balance, tokenStr)
}()
// 无论是否满足条件,都返回成功,避免泄露用户信息
return utils.SuccessWithMsg("如果该邮箱在旧系统中存在积分,转移链接已发送")
}
func (s *UserInfoServiceV1) BalanceMigrate(ctx context.Context, req *v1.BalanceMigrateReq) *utils.Response[any] {
client := redis.GetClient()
tokenValue := client.GetDel(ctx, cst.RedisBalanceMigrate+req.Token).Val()
if tokenValue == "" {
return utils.FailParam("无效或已过期的积分转移链接")
}
// 解析 token 值:格式为 "新系统邮箱|旧系统邮箱"
emails := strings.Split(tokenValue, "|")
if len(emails) != 2 {
return utils.FailParam("无效的积分转移链接")
}
newSystemEmail := emails[0]
oldSystemEmail := emails[1]
// 删除新系统邮箱对应的 token 映射
client.Del(ctx, cst.RedisBalanceMigrate+"email:"+newSystemEmail)
// 定义旧系统用户结构
type AspNetUser struct {
Email string `gorm:"column:email"`
UserName string `gorm:"column:user_name"`
Balance int `gorm:"column:balance"`
}
// 查询旧系统用户
var oldUser AspNetUser
err := db.GlobalGorm.Table("AspNetUsers").Where("email = ?", oldSystemEmail).First(&oldUser).Error
if err != nil {
return utils.FailParam("旧系统中未找到该用户或积分已转移")
}
// 检查积分是否大于0
if oldUser.Balance <= 0 {
return utils.FailParam("您的旧系统积分余额为0,无需转移")
}
// 查询新系统用户
newUser, err := db.Global.SysUser.Query().WithAgent().Where(sysuser.EmailEQ(newSystemEmail)).Only(ctx)
if ent.IsNotFound(err) {
return utils.FailParam("新系统中未找到该用户")
}
b := decimal.NewFromInt32(int32(oldUser.Balance))
if newUser.Edges.Agent != nil {
b = utils.TransformBalance(b, newUser.Edges.Agent.Rate)
}
err = balance.BalanceService.ChangeUserBalance(ctx, newUser.ID, b)
if err != nil {
return utils.FailParam("积分转移失败,请重新尝试")
}
// 清空旧系统积分
err = db.GlobalGorm.Table("AspNetUsers").Where("email = ?", oldSystemEmail).Update("balance", 0).Error
if err != nil {
// 旧系统更新失败,记录日志但不影响返回结果(因为新系统已经转移成功)
// 回滚
balance.BalanceService.ChangeUserBalance(ctx, newUser.ID, b.Neg())
// 这里可以考虑回滚新系统的操作,但为了简化流程暂时保留新系统的更新
return utils.FailBusiness("旧系统积分清空失败,请联系管理员")
}
return utils.SuccessWithMsg("积分转移成功!您的积分已转移到新系统")
}
@@ -84,11 +84,14 @@ func (s *UserLoginServiceV1) Register(ctx context.Context, req *v1.UserRegisterR
// 注册成功后发送欢迎邮件(异步发送,不影响注册结果)
go func() {
ctx2 := context.Background()
client := redis.GetClient()
token := make([]byte, 32)
_, _ = rand.Read(token)
tokenStr := base64.URLEncoding.EncodeToString(token)
err2 := redis.GetClient().SetEx(ctx2, cst2.RedisVerifyCode+tokenStr, req.Email, time.Duration(cst2.RedisVerifyCodeExpire)*time.Second).Err()
err2 := client.SetEx(ctx2, cst2.RedisVerifyCode+tokenStr, req.Email, time.Duration(cst2.RedisVerifyCodeExpire)*time.Second).Err()
utils.OkOrPanic(err2, oops.WithContext(ctx2).Wrap)
// 保存邮箱到 token 的映射,方便重新发送时删除旧 token
client.SetEx(ctx2, cst2.RedisVerifyCode+"email:"+req.Email, tokenStr, time.Duration(cst2.RedisVerifyCodeExpire)*time.Second)
// 创建新的context避免原context被取消
emailService := email.NewV1()
emailService.SendSignupEmail(ctx2, req.Email, req.Username, tokenStr)
@@ -110,6 +113,8 @@ func (s *UserLoginServiceV1) Confirm(ctx context.Context, req *v1.UserConfirmReq
if emailStr == "" {
return utils.FailParam("非法的验证")
}
// 删除邮箱对应的 token 映射
client.Del(ctx, cst2.RedisVerifyCode+"email:"+emailStr)
// 查询用户
user := db.Global.SysUser.Query().Where(sysuser.EmailEQ(emailStr)).OnlyX(ctx)
db.Global.SysUser.UpdateOne(user).SetVerify(true).SaveX(ctx)
@@ -118,3 +123,135 @@ func (s *UserLoginServiceV1) Confirm(ctx context.Context, req *v1.UserConfirmReq
r.Response.Header().Set(cst2.AuthorizationKey, utils.GenerateToken(user.ID))
return utils.SuccessWithMsg("验证成功")
}
func (s *UserLoginServiceV1) ResendVerification(ctx context.Context, req *v1.ResendVerificationReq) *utils.Response[any] {
// 查询数据库中是否存在该邮箱的未激活用户
user, err := db.Global.SysUser.Query().Where(
sysuser.EmailEQ(req.Email),
sysuser.VerifyEQ(false),
).Only(ctx)
// 如果用户存在且未激活,则重新发送激活邮件
if err == nil && user != nil {
go func() {
ctx2 := context.Background()
client := redis.GetClient()
// 获取并删除旧的 token(如果存在)
oldTokenKey := cst2.RedisVerifyCode + "email:" + req.Email
oldToken := client.GetDel(ctx2, oldTokenKey).Val()
if oldToken != "" {
// 删除旧 token 对应的验证码记录
client.Del(ctx2, cst2.RedisVerifyCode+oldToken)
}
// 生成新的 token
token := make([]byte, 32)
_, _ = rand.Read(token)
tokenStr := base64.URLEncoding.EncodeToString(token)
// 设置新的 token 到 Redis
err2 := client.SetEx(ctx2, cst2.RedisVerifyCode+tokenStr, req.Email, time.Duration(cst2.RedisVerifyCodeExpire)*time.Second).Err()
utils.OkOrPanic(err2, oops.WithContext(ctx2).Wrap)
// 保存邮箱到 token 的映射,方便下次删除
client.SetEx(ctx2, oldTokenKey, tokenStr, time.Duration(cst2.RedisVerifyCodeExpire)*time.Second)
// 发送激活邮件
emailService := email.NewV1()
emailService.SendSignupEmail(ctx2, req.Email, user.Username, tokenStr)
}()
}
// 无论用户是否存在,都返回成功,避免泄露用户信息
return utils.SuccessWithMsg("如果该邮箱存在未激活账户,验证邮件已重新发送")
}
func (s *UserLoginServiceV1) ForgotPassword(ctx context.Context, req *v1.ForgotPasswordReq) *utils.Response[any] {
// 查询数据库中是否存在该邮箱的已激活用户
user, err := db.Global.SysUser.Query().Where(
sysuser.EmailEQ(req.Email),
sysuser.VerifyEQ(true),
).Only(ctx)
// 如果用户存在且已激活,则发送重置密码邮件
if err == nil && user != nil {
go func() {
ctx2 := context.Background()
client := redis.GetClient()
// 获取并删除旧的 token(如果存在)
oldTokenKey := cst2.RedisResetPassword + "email:" + req.Email
oldToken := client.GetDel(ctx2, oldTokenKey).Val()
if oldToken != "" {
// 删除旧 token 对应的重置密码记录
client.Del(ctx2, cst2.RedisResetPassword+oldToken)
}
// 生成新的 token
token := make([]byte, 32)
_, _ = rand.Read(token)
tokenStr := base64.URLEncoding.EncodeToString(token)
// 设置新的 token 到 Redis
err2 := client.SetEx(ctx2, cst2.RedisResetPassword+tokenStr, req.Email, time.Duration(cst2.RedisResetPasswordExpire)*time.Second).Err()
utils.OkOrPanic(err2, oops.WithContext(ctx2).Wrap)
// 保存邮箱到 token 的映射,方便下次删除
client.SetEx(ctx2, oldTokenKey, tokenStr, time.Duration(cst2.RedisResetPasswordExpire)*time.Second)
// 发送重置密码邮件
emailService := email.NewV1()
emailService.SendResetPasswordEmail(ctx2, req.Email, user.Username, tokenStr)
}()
}
// 无论用户是否存在,都返回成功,避免泄露用户信息
return utils.SuccessWithMsg("如果该邮箱已注册,重置密码邮件已发送")
}
func (s *UserLoginServiceV1) ResetPassword(ctx context.Context, req *v1.ForgetResetPasswordReq) *utils.Response[any] {
client := redis.GetClient()
emailStr := client.GetDel(ctx, cst2.RedisResetPassword+req.Token).Val()
if emailStr == "" {
return utils.FailParam("无效或已过期的重置密码链接")
}
// 删除邮箱对应的 token 映射
client.Del(ctx, cst2.RedisResetPassword+"email:"+emailStr)
// 查询用户
user, err := db.Global.SysUser.Query().Where(sysuser.EmailEQ(emailStr)).Only(ctx)
if ent.IsNotFound(err) {
return utils.FailParam("用户不存在")
}
// 更新密码
db.Global.SysUser.UpdateOne(user).SetPassword(utils.PasswordBcryptHash(req.NewPassword)).SaveX(ctx)
return utils.SuccessWithMsg("密码重置成功,请使用新密码登录")
}
func (s *UserLoginServiceV1) GetResetPasswordInfo(ctx context.Context, req *v1.ResetPasswordInfoReq) *utils.Response[any] {
client := redis.GetClient()
// 使用 Get 而不是 GetDel,不删除 token
emailStr := client.Get(ctx, cst2.RedisResetPassword+req.Token).Val()
if emailStr == "" {
return utils.FailParam("无效或已过期的重置密码链接")
}
// 查询用户
user, err := db.Global.SysUser.Query().Where(sysuser.EmailEQ(emailStr)).Only(ctx)
if ent.IsNotFound(err) {
return utils.FailParam("无效或已过期的重置密码链接")
}
type ResetPasswordInfoRes struct {
Username string `json:"username"` // 用户名
Email string `json:"email"` // 邮箱
}
// 返回用户信息
return utils.SuccessWithData[any](&ResetPasswordInfoRes{
Username: user.Username,
Email: user.Email,
})
}