diff --git a/cmd/import_from_jwc/import_from_jwc.go b/cmd/import_from_jwc/import_from_jwc.go index 3374424..68995ca 100644 --- a/cmd/import_from_jwc/import_from_jwc.go +++ b/cmd/import_from_jwc/import_from_jwc.go @@ -57,6 +57,7 @@ func readRawCSV(filename string) [][]string { func main() { initDB() data := readRawCSV(fmt.Sprintf("./data/%s.csv", Semester)) + // 课程号,课程名称,学时,合上教师,任课教师,开课院系,课程安排,教学班名称,选课人数,学分,教室,授课语言,是否通识课,通识课归属模块,年级 // init queryAllBaseCourse() @@ -174,6 +175,9 @@ func queryAllBaseCourse() { func parseMainTeacherFromLine(line []string) po.TeacherPO { teacherInfo := strings.Split(line[4], "|") + if len(teacherInfo) <= 1 { + return po.TeacherPO{} + } teacher := po.TeacherPO{ Name: teacherInfo[1], Code: teacherInfo[0], diff --git a/cmd/load/trainingplan/extend_training_plan.go b/cmd/load/trainingplan/extend_training_plan.go index 694a2b4..ba1ec8e 100644 --- a/cmd/load/trainingplan/extend_training_plan.go +++ b/cmd/load/trainingplan/extend_training_plan.go @@ -6,6 +6,8 @@ import ( "os" "strconv" + "gorm.io/gorm/clause" + "github.com/joho/godotenv" "jcourse_go/dal" @@ -44,7 +46,9 @@ func main() { MajorCode: tp.Code, MajorClass: tp.MajorClass, } - result := db.Model(po.TrainingPlanPO{}).Create(&tp_po) + result := db.Model(po.TrainingPlanPO{}). + Clauses(clause.OnConflict{DoNothing: true}). + Create(&tp_po) if result.Error != nil { log.Fatalf("In create training plan %#v:%#v", tp, result.Error) } @@ -65,7 +69,10 @@ func main() { SuggestSemester: c.SuggestSemester, // Department: c.Department, } - cresult = db.Model(po.TrainingPlanCoursePO{}).Create(&tpc_po) + // 已有记录则跳过 + cresult = db.Model(po.TrainingPlanCoursePO{}). + Clauses(clause.OnConflict{DoNothing: true}). + Create(&tpc_po) if cresult.Error != nil { if !errors.Is(cresult.Error, gorm.ErrRecordNotFound) { log.Fatalf("In bind course %#v totraining plan %#v:%#v", c, tp, cresult.Error) diff --git a/constant/userpoint.go b/constant/userpoint.go new file mode 100644 index 0000000..d6fb77c --- /dev/null +++ b/constant/userpoint.go @@ -0,0 +1,6 @@ +package constant + +const ( + HandleFeeRateKey = "point_handling_fee_rate" + DefaultHandleFeeRate = 0.01 +) diff --git a/dal/redis.go b/dal/redis.go index e2a9e06..31f2b35 100644 --- a/dal/redis.go +++ b/dal/redis.go @@ -16,11 +16,13 @@ func GetRedisDSN() string { port := util.GetRedisPort() return fmt.Sprintf("%s:%s", host, port) } - +func GetRedisPassWord() string { + return util.GetRedisPassword() +} func InitRedisClient() { rdb = redis.NewClient(&redis.Options{ Addr: GetRedisDSN(), - Password: "", + Password: GetRedisPassWord(), DB: 0, }) } diff --git a/go.mod b/go.mod index 9d96ea8..80ac993 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,12 @@ module jcourse_go -go 1.22.0 +go 1.23 -toolchain go1.22.6 +toolchain go1.23.0 require ( github.com/SJTU-jCourse/password_hasher v0.0.0-20240731144855-1f64f055ff5c - github.com/bytedance/sonic v1.12.1 + github.com/bytedance/sonic v1.12.3 github.com/gin-gonic/contrib v0.0.0-20240508051311-c1c6bf0061b0 github.com/gin-gonic/gin v1.10.0 github.com/glebarez/sqlite v1.11.0 @@ -18,74 +18,76 @@ require ( github.com/joho/godotenv v1.5.1 github.com/lib/pq v1.10.9 github.com/mozillazg/go-pinyin v0.20.0 - github.com/redis/go-redis/v9 v9.5.3 + github.com/pkg/errors v0.9.1 + github.com/redis/go-redis/v9 v9.7.0 github.com/stretchr/testify v1.9.0 github.com/tmc/langchaingo v0.1.12 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gorm.io/driver/postgres v1.5.9 - gorm.io/gorm v1.25.10 + gorm.io/gorm v1.25.12 ) require ( github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect - github.com/bytedance/sonic/loader v0.2.0 // indirect + github.com/bytedance/sonic/loader v0.2.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/dlclark/regexp2 v1.10.0 // indirect + github.com/dlclark/regexp2 v1.11.4 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gabriel-vasile/mimetype v1.4.6 // indirect github.com/gin-contrib/sse v0.1.0 // indirect - github.com/glebarez/go-sqlite v1.21.2 // indirect + github.com/glebarez/go-sqlite v1.22.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.20.0 // indirect - github.com/goccy/go-json v0.10.2 // indirect + github.com/go-playground/validator/v10 v10.22.1 // indirect + github.com/goccy/go-json v0.10.3 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/gomodule/redigo v2.0.0+incompatible // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/context v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect - github.com/gorilla/sessions v1.3.0 // indirect + github.com/gorilla/sessions v1.4.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect - github.com/jackc/pgx/v5 v5.5.5 // indirect - github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.1 // 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/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect - github.com/pgvector/pgvector-go v0.1.1 // indirect - github.com/pkoukk/tiktoken-go v0.1.6 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pgvector/pgvector-go v0.2.2 // indirect + github.com/pkoukk/tiktoken-go v0.1.7 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/robfig/cron/v3 v3.0.1 // indirect - github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/cast v1.7.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect github.com/vcaesar/cedar v0.20.2 // indirect - golang.org/x/arch v0.8.0 // indirect - golang.org/x/crypto v0.27.0 // indirect - golang.org/x/net v0.29.0 // indirect + golang.org/x/arch v0.11.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect + golang.org/x/net v0.30.0 // indirect golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.25.0 // indirect - golang.org/x/text v0.18.0 // indirect - golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.25.0 // indirect - google.golang.org/protobuf v1.34.2 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect + golang.org/x/time v0.7.0 // indirect + google.golang.org/protobuf v1.35.1 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - modernc.org/libc v1.22.5 // indirect - modernc.org/mathutil v1.5.0 // indirect - modernc.org/memory v1.5.0 // indirect - modernc.org/sqlite v1.23.1 // indirect + modernc.org/libc v1.61.0 // indirect + modernc.org/mathutil v1.6.0 // indirect + modernc.org/memory v1.8.0 // indirect + modernc.org/sqlite v1.33.1 // indirect ) // 本地调试修改 diff --git a/go.sum b/go.sum index d514f8d..b361ff6 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,8 @@ cloud.google.com/go/vertexai v0.12.0 h1:zTadEo/CtsoyRXNx3uGCncoWAP1H2HakGqwznt+i cloud.google.com/go/vertexai v0.12.0/go.mod h1:8u+d0TsvBfAAd2x5R6GMgbYhsLgo3J7lmP4bR8g2ig8= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +entgo.io/ent v0.13.1 h1:uD8QwN1h6SNphdCCzmkMN3feSUzNnVvV/WIkHKMbzOE= +entgo.io/ent v0.13.1/go.mod h1:qCEmo+biw3ccBn9OyL4ZK5dfpwg++l1Gxwac5B1206A= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= @@ -41,11 +43,11 @@ github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdb github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= -github.com/bytedance/sonic v1.12.1 h1:jWl5Qz1fy7X1ioY74WqO0KjAMtAGQs4sYnjiEBiyX24= -github.com/bytedance/sonic v1.12.1/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= +github.com/bytedance/sonic v1.12.3 h1:W2MGa7RCU1QTeYRTPE3+88mVC0yXmsRQRChiyVocVjU= +github.com/bytedance/sonic v1.12.3/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= -github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM= -github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E= +github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= @@ -71,8 +73,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= -github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= +github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/docker/docker v25.0.5+incompatible h1:UmQydMduGkrD5nQde1mecF/YnSbTOaPeFIeP5C4W+DE= github.com/docker/docker v25.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= @@ -87,16 +89,16 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= -github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc= +github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/contrib v0.0.0-20240508051311-c1c6bf0061b0 h1:EUFmvQ8ffefnSAmaUZd9HZYZSw9w/bFjp3FiNaJ5WmE= github.com/gin-gonic/contrib v0.0.0-20240508051311-c1c6bf0061b0/go.mod h1:iqneQ2Df3omzIVTkIfn7c1acsVnMGiSLn4XF5Blh3Yg= github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= -github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= -github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= +github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= +github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= github.com/go-ego/gse v0.80.3 h1:YNFkjMhlhQnUeuoFcUEd1ivh6SOB764rT8GDsEbDiEg= @@ -117,12 +119,12 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= -github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= +github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-redis/redismock/v9 v9.2.0 h1:ZrMYQeKPECZPjOj5u9eyOjg8Nnb0BS9lkVIZ6IpsKLw= github.com/go-redis/redismock/v9 v9.2.0/go.mod h1:18KHfGDK4Y6c2R0H38EUGWAdc7ZQS9gfYxc94k7rWT0= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= @@ -142,8 +144,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= -github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -164,8 +166,8 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+ github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= -github.com/gorilla/sessions v1.3.0 h1:XYlkq7KcpOB2ZhHBPv5WpjMIxrQosiZanfoy1HLZFzg= -github.com/gorilla/sessions v1.3.0/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= +github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= +github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= github.com/gwatts/gin-adapter v1.0.0 h1:TsmmhYTR79/RMTsfYJ2IQvI1F5KZ3ZFJxuQSYEOpyIA= github.com/gwatts/gin-adapter v1.0.0/go.mod h1:44AEV+938HsS0mjfXtBDCUZS9vONlF2gwvh8wu4sRYc= github.com/hibiken/asynq v0.24.1 h1:+5iIEAyA9K/lcSPvx3qoPtsKJeKI5u9aOIvUmSsazEw= @@ -176,16 +178,18 @@ github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= 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-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= -github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= -github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= -github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +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.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= +github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= +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/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -193,8 +197,8 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= -github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= +github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -234,6 +238,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mozillazg/go-pinyin v0.20.0 h1:BtR3DsxpApHfKReaPO1fCqF4pThRwH9uwvXzm+GnMFQ= github.com/mozillazg/go-pinyin v0.20.0/go.mod h1:iR4EnMMRXkfpFVV5FMi4FNB6wGq9NV6uDWbUuPhP4Yc= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/nikolalohinski/gonja v1.5.3 h1:GsA+EEaZDZPGJ8JtpeGN78jidhOlxeJROpqMT9fTj9c= github.com/nikolalohinski/gonja v1.5.3/go.mod h1:RmjwxNiXAEqcq1HeK5SSMmqFJvKOfTfXhkJv6YBtPa4= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= @@ -246,22 +252,21 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= -github.com/pgvector/pgvector-go v0.1.1 h1:kqJigGctFnlWvskUiYIvJRNwUtQl/aMSUZVs0YWQe+g= -github.com/pgvector/pgvector-go v0.1.1/go.mod h1:wLJgD/ODkdtd2LJK4l6evHXTuG+8PxymYAVomKHOWac= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pgvector/pgvector-go v0.2.2 h1:Q/oArmzgbEcio88q0tWQksv/u9Gnb1c3F1K2TnalxR0= +github.com/pgvector/pgvector-go v0.2.2/go.mod h1:u5sg3z9bnqVEdpe1pkTij8/rFhTaMCMNyQagPDLK8gQ= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw= -github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= +github.com/pkoukk/tiktoken-go v0.1.7 h1:qOBHXX4PHtvIvmOtyg1EeKlwFRiMKAcoMp4Q+bLQDmw= +github.com/pkoukk/tiktoken-go v0.1.7/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/redis/go-redis/v9 v9.0.3/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= -github.com/redis/go-redis/v9 v9.5.3 h1:fOAp1/uJG+ZtcITgZOfYFmTKPE7n4Vclj1wZFgRciUU= -github.com/redis/go-redis/v9 v9.5.3/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= -github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= +github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= @@ -277,19 +282,17 @@ github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFR github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 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= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/testcontainers/testcontainers-go v0.31.0 h1:W0VwIhcEVhRflwL9as3dhY6jXjVCA27AkmbnZ+UTh3U= @@ -345,14 +348,14 @@ go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 h1:Ss6D3hLXTM0KobyBYEAygXzFfG go.starlark.net v0.0.0-20230302034142-4b1e35fe2254/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= -golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= -golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4= +golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= -golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw= -golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= @@ -361,8 +364,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -377,22 +380,22 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= -golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -409,8 +412,8 @@ google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -429,18 +432,34 @@ 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.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8= gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= -gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s= -gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= mellium.im/sasl v0.3.1 h1:wE0LW6g7U83vhvxjC1IY8DnXM+EU095yeo8XClvCdfo= mellium.im/sasl v0.3.1/go.mod h1:xm59PUYpZHhgQ9ZqoJ5QaCqzWMi8IeS49dhp6plPCzw= -modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= -modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= -modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= -modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= -modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= -modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= -modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= +modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= +modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= +modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4= +modernc.org/ccgo/v4 v4.21.0/go.mod h1:h6kt6H/A2+ew/3MW/p6KEoQmrq/i3pr0J/SiwiaF/g0= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M= +modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= +modernc.org/libc v1.61.0 h1:eGFcvWpqlnoGwzZeZe3PWJkkKbM/3SUGyk1DVZQ0TpE= +modernc.org/libc v1.61.0/go.mod h1:DvxVX89wtGTu+r72MLGhygpfi3aUGgZRdAYGCAVVud0= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= +modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= +modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM= +modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/handler/admin.go b/handler/admin.go index aeec617..55bf9f9 100644 --- a/handler/admin.go +++ b/handler/admin.go @@ -1,6 +1,9 @@ package handler import ( + "jcourse_go/model/model" + "jcourse_go/service" + "log" "net/http" "github.com/gin-gonic/gin" @@ -33,3 +36,17 @@ func AdminGetUserList(c *gin.Context) { c.JSON(http.StatusOK, response) */ } +func AdminChangeUserPoint(c *gin.Context) { + var request dto.ChangeUserPointRequest + if err := c.ShouldBindJSON(&request); err != nil { + c.JSON(http.StatusBadRequest, dto.BaseResponse{Message: "参数错误"}) + return + } + err := service.ChangeUserPoints(c, request.UserID, model.PointEventAdminChange, request.Value, "") + if err != nil { + c.JSON(http.StatusInternalServerError, dto.BaseResponse{Message: "用户积分更新失败。"}) + log.Printf("ChangeUserPointHandler: %v", err) + return + } + c.JSON(http.StatusOK, dto.BaseResponse{Message: "用户积分更新成功。"}) +} diff --git a/handler/auth.go b/handler/auth.go index a43508d..5e5370d 100644 --- a/handler/auth.go +++ b/handler/auth.go @@ -2,6 +2,7 @@ package handler import ( "errors" + "jcourse_go/util" "net/http" "github.com/gin-gonic/contrib/sessions" @@ -85,7 +86,12 @@ func SendVerifyCodeHandler(c *gin.Context) { return } - err = service.SendRegisterCodeEmail(c, request.Email) + if util.IsDebug() { + err = service.SendRegisterCodeEmailMock(c, request.Email) + } else { + err = service.SendRegisterCodeEmail(c, request.Email) + } + if err != nil { c.JSON(http.StatusInternalServerError, dto.BaseResponse{Message: "验证码发送失败,请稍后重试。"}) return diff --git a/handler/user.go b/handler/user.go index f0a55c7..6a3e5d2 100644 --- a/handler/user.go +++ b/handler/user.go @@ -2,15 +2,14 @@ package handler import ( "errors" - "net/http" - "strconv" - "jcourse_go/constant" "jcourse_go/middleware" "jcourse_go/model/converter" "jcourse_go/model/dto" "jcourse_go/model/model" "jcourse_go/service" + "net/http" + "strconv" "github.com/gin-gonic/gin" ) diff --git a/handler/userpoint.go b/handler/userpoint.go new file mode 100644 index 0000000..d474768 --- /dev/null +++ b/handler/userpoint.go @@ -0,0 +1,107 @@ +package handler + +import ( + "jcourse_go/middleware" + "jcourse_go/model/dto" + "jcourse_go/model/model" + "jcourse_go/service" + "net/http" + "time" + + "github.com/gin-gonic/gin" +) + +func TransferUserPointHandler(c *gin.Context) { + var request dto.TransferUserPointRequest + if err := c.ShouldBind(&request); err != nil { + c.JSON(http.StatusBadRequest, dto.BaseResponse{Message: "参数错误"}) + return + } + user := middleware.GetCurrentUser(c) + if user.ID == request.Receiver { + c.JSON(http.StatusBadRequest, dto.BaseResponse{Message: "不能给自己转账。"}) + return + } + err := service.TransferUserPoints(c, user.ID, request.Receiver, request.Value) + if err != nil { + c.JSON(http.StatusInternalServerError, dto.BaseResponse{Message: "用户积分转账失败。"}) + return + } + c.JSON(http.StatusOK, dto.BaseResponse{Message: "用户积分转账成功。"}) +} +func AdminTransferUserPoint(c *gin.Context) { + var request dto.TransferUserPointAdminRequest + if err := c.ShouldBind(&request); err != nil { + c.JSON(http.StatusBadRequest, dto.BaseResponse{Message: "参数错误"}) + return + } + if request.Sender == request.Receiver { + c.JSON(http.StatusBadRequest, dto.BaseResponse{Message: "不能给自己转账。"}) + return + } + err := service.TransferUserPoints(c, request.Sender, request.Receiver, request.Value) + if err != nil { + c.JSON(http.StatusInternalServerError, dto.BaseResponse{Message: "用户积分转账失败。"}) + return + } + c.JSON(http.StatusOK, dto.BaseResponse{Message: "用户积分转账成功。"}) +} + +func GetUserPointDetailListHandler(c *gin.Context) { + var request dto.UserPointDetailListRequest + if err := c.ShouldBind(&request); err != nil { + c.JSON(http.StatusBadRequest, dto.BaseResponse{Message: "参数错误"}) + return + } + user := middleware.GetCurrentUser(c) + filter := model.UserPointDetailFilter{ + UserID: user.ID, + StartTime: time.Unix(request.StartTime, 0), + EndTime: time.Unix(request.EndTime, 0), + } + userPointDetails, err := service.GetUserPointDetailList(c, filter) + if err != nil { + c.JSON(http.StatusInternalServerError, dto.BaseResponse{Message: "内部错误。"}) + } + + total, _ := service.GetUserPointDetailCount(c, filter) + if err != nil { + c.JSON(http.StatusInternalServerError, dto.BaseResponse{Message: "内部错误。"}) + } + response := dto.UserPointDetailListResponse{ + Page: request.Page, + PageSize: request.PageSize, + Total: total, + Data: userPointDetails, + } + c.JSON(http.StatusOK, response) +} + +func AdminGetUserPointDetailList(c *gin.Context) { + var request dto.UserPointDetailListAdminRequest + if err := c.ShouldBind(&request); err != nil { + c.JSON(http.StatusBadRequest, dto.BaseResponse{Message: "参数错误"}) + return + } + filter := model.UserPointDetailFilter{ + UserID: request.UserID, + StartTime: time.Unix(request.StartTime, 0), + EndTime: time.Unix(request.EndTime, 0), + } + userPointDetails, err := service.GetUserPointDetailList(c, filter) + if err != nil { + c.JSON(http.StatusInternalServerError, dto.BaseResponse{Message: "内部错误。"}) + } + + total, _ := service.GetUserPointDetailCount(c, filter) + if err != nil { + c.JSON(http.StatusInternalServerError, dto.BaseResponse{Message: "内部错误。"}) + } + response := dto.UserPointDetailListResponse{ + Page: request.Page, + PageSize: request.PageSize, + Total: total, + Data: userPointDetails, + } + c.JSON(http.StatusOK, response) +} diff --git a/middleware/session.go b/middleware/session.go index 23c3c34..56d2434 100644 --- a/middleware/session.go +++ b/middleware/session.go @@ -14,7 +14,7 @@ import ( func InitSession(r *gin.Engine) { secret := util.GetSessionSecret() - store, err := sessions.NewRedisStore(10, "tcp", dal.GetRedisDSN(), "", []byte(secret)) + store, err := sessions.NewRedisStore(10, "tcp", dal.GetRedisDSN(), dal.GetRedisPassWord(), []byte(secret)) if err != nil { panic(err) } diff --git a/model/converter/user.go b/model/converter/user.go index 32374e5..81ab2d2 100644 --- a/model/converter/user.go +++ b/model/converter/user.go @@ -16,6 +16,7 @@ func ConvertUserDetailFromPO(po po.UserPO) model.UserDetail { Department: po.Department, Major: po.Major, Grade: po.Grade, + Points: po.Points, } } diff --git a/model/converter/userpoint.go b/model/converter/userpoint.go new file mode 100644 index 0000000..5c598ca --- /dev/null +++ b/model/converter/userpoint.go @@ -0,0 +1,15 @@ +package converter + +import ( + "jcourse_go/model/model" + "jcourse_go/model/po" + "jcourse_go/util" +) + +func ConvertUserPointDetailItemFromPO(po po.UserPointDetailPO) model.UserPointDetailItem { + return model.UserPointDetailItem{ + Time: po.CreatedAt.Format(util.GoTimeLayout), + Value: po.Value, + Description: po.Description, + } +} diff --git a/model/dto/userpoint.go b/model/dto/userpoint.go new file mode 100644 index 0000000..70ed0b3 --- /dev/null +++ b/model/dto/userpoint.go @@ -0,0 +1,32 @@ +package dto + +import "jcourse_go/model/model" + +type UserPointDetailListRequest struct { + StartTime int64 `json:"start_time" form:"start_time"` // unix timestamp, 单位秒 + EndTime int64 `json:"end_time" form:"end_time"` + model.PaginationFilterForQuery +} +type UserPointDetailListAdminRequest struct { + UserID int64 `json:"user_id" form:"user_id"` + UserPointDetailListRequest +} + +type UserPointDetailListResponse = BasePaginateResponse[model.UserPointDetailItem] + +type ChangeUserPointRequest struct { + UserID int64 `json:"user_id" form:"user_id" binding:"required"` + Value int64 `json:"value" form:"value" binding:"required"` +} +type ChangeUserPointResponse = BaseResponse + +type TransferUserPointRequest struct { + // sender is the current user + Receiver int64 `json:"receiver" form:"receiver" binding:"required"` + Value int64 `json:"value" form:"value" binding:"required"` +} + +type TransferUserPointAdminRequest struct { + Sender int64 `json:"sender" form:"sender" binding:"required"` + TransferUserPointRequest +} diff --git a/model/model/user.go b/model/model/user.go index 507f262..08d4e62 100644 --- a/model/model/user.go +++ b/model/model/user.go @@ -27,6 +27,7 @@ type UserDetail struct { Department string `json:"department"` Major string `json:"major"` Grade string `json:"grade"` + Points int64 `json:"points"` } type UserActivity struct { diff --git a/model/model/userpoint.go b/model/model/userpoint.go new file mode 100644 index 0000000..48d0b14 --- /dev/null +++ b/model/model/userpoint.go @@ -0,0 +1,33 @@ +package model + +import "time" + +type PointEventType = string + +const ( + PointEventReview PointEventType = "review" + PointEventLike PointEventType = "like" + PointEventBeLiked PointEventType = "be_liked" + PointEventAdminChange PointEventType = "admin_change" + PointEventInit PointEventType = "init" + PointEventTransfer PointEventType = "transfer" + PointEventReward PointEventType = "reward" + PointEventPunish PointEventType = "punish" + PointEventWithdraw PointEventType = "withdraw" + PointEventConsume PointEventType = "consume" + PointEventRedeem PointEventType = "redeem" // 兑换积分 +) + +// 用户积分明细 +type UserPointDetailItem struct { + Time string `json:"time"` + Value int64 `json:"value"` // 积分变化值: +1, -3 + Description string `json:"description"` +} +type UserPointDetailFilter struct { + PaginationFilterForQuery + UserPointDetailID int64 + UserID int64 + StartTime time.Time + EndTime time.Time +} diff --git a/model/po/user.go b/model/po/user.go index 674ae7a..9e10010 100644 --- a/model/po/user.go +++ b/model/po/user.go @@ -20,6 +20,7 @@ type UserPO struct { Degree string // 学位 Grade string // 年级 Bio string // 个人介绍 + Points int64 // 积分 LastSeenAt time.Time } diff --git a/model/po/userpoint.go b/model/po/userpoint.go new file mode 100644 index 0000000..9e236dd --- /dev/null +++ b/model/po/userpoint.go @@ -0,0 +1,16 @@ +package po + +import "gorm.io/gorm" + +type PointEvent struct { + EventType string `gorm:"index"` + Description string `gorm:"index"` + Value int64 // 积分变动值 +} +type UserPointDetailPO struct { + gorm.Model + PointEvent // 积分事件 + UserID int64 `gorm:"index"` // 用户ID +} + +func (po *UserPointDetailPO) TableName() string { return "user_point_details" } diff --git a/repository/migrate.go b/repository/migrate.go index c9639e5..fd677e8 100644 --- a/repository/migrate.go +++ b/repository/migrate.go @@ -12,7 +12,7 @@ func Migrate(db *gorm.DB) error { &po.OfferedCoursePO{}, &po.OfferedCourseTeacherPO{}, &po.TrainingPlanPO{}, &po.TrainingPlanCoursePO{}, &po.ReviewPO{}, &po.RatingPO{}, - &po.SettingPO{}) + &po.SettingPO{}, &po.UserPointDetailPO{}) if err != nil { return err } diff --git a/repository/option.go b/repository/option.go index 0c62eb7..415ada8 100644 --- a/repository/option.go +++ b/repository/option.go @@ -1,6 +1,12 @@ package repository -import "gorm.io/gorm" +import ( + "fmt" + "time" + + "gorm.io/gorm" + "gorm.io/gorm/clause" +) type DBOption func(*gorm.DB) *gorm.DB @@ -9,6 +15,38 @@ func WithUserIDs(userIDs []int64) DBOption { return db.Where("user_id in ?", userIDs) } } +func WithForUpdateLock() DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Clauses(clause.Locking{Strength: "UPDATE"}) // for update lock + } +} +func WithOptimisticLock(column string, version interface{}) DBOption { + return func(db *gorm.DB) *gorm.DB { + query := fmt.Sprintf("%s = ?", column) + return db.Where(query, version) + } +} +func WithRawOptimisticLock(query interface{}, args ...interface{}) DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Where(query, args) + } +} + +func WithTimeBetween(start, end time.Time) DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Where("created_at between ? and ?", start, end) + } +} +func WithTimeAfter(start time.Time) DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Where("created_at >= ?", start) + } +} +func WithTimeBefore(end time.Time) DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Where("created_at <= ?", end) + } +} func WithUserID(id int64) DBOption { return func(db *gorm.DB) *gorm.DB { diff --git a/repository/transaction.go b/repository/transaction.go new file mode 100644 index 0000000..1468063 --- /dev/null +++ b/repository/transaction.go @@ -0,0 +1,113 @@ +package repository + +import ( + "context" + + "gorm.io/gorm" +) + +type DBOperation func(repo IRepository) error +type IRepository interface { + NewUserPointQuery() IUserPointDetailQuery + NewUserQuery() IUserQuery + NewReviewQuery() IReviewQuery + NewBaseCourseQuery() IBaseCourseQuery + NewCourseQuery() ICourseQuery + NewOfferedCourseQuery() IOfferedCourseQuery + NewRatingQuery() IRatingQuery + NewSettingQuery() ISettingQuery + NewTeacherQuery() ITeacherQuery + NewTrainingPlanQuery() ITrainingPlanQuery + NewTrainingPlanCourseQuery() ITrainingPlanCourseQuery + InTransaction(ctx context.Context, operation DBOperation) error +} +type Repository struct { + db *gorm.DB +} + +func (r *Repository) NewUserPointQuery() IUserPointDetailQuery { + return &UserPointDetailQuery{ + db: r.db, + } +} + +func (r *Repository) NewReviewQuery() IReviewQuery { + return &ReviewQuery{ + db: r.db, + } +} + +func (r *Repository) NewBaseCourseQuery() IBaseCourseQuery { + return &BaseCourseQuery{ + db: r.db, + } +} + +func (r *Repository) NewCourseQuery() ICourseQuery { + return &CourseQuery{ + db: r.db, + } +} + +func (r *Repository) NewOfferedCourseQuery() IOfferedCourseQuery { + return &OfferedCourseQuery{ + db: r.db, + } +} + +func (r *Repository) NewRatingQuery() IRatingQuery { + return &RatingQuery{ + db: r.db, + } +} + +func (r *Repository) NewSettingQuery() ISettingQuery { + return &SettingQuery{ + db: r.db, + } +} + +func (r *Repository) NewTeacherQuery() ITeacherQuery { + return &TeacherQuery{ + db: r.db, + } +} + +func (r *Repository) NewTrainingPlanQuery() ITrainingPlanQuery { + return &TrainingPlanQuery{ + db: r.db, + } +} + +func (r *Repository) NewTrainingPlanCourseQuery() ITrainingPlanCourseQuery { + return &TrainingPlanCourseQuery{ + db: r.db, + } +} + +var _ IRepository = &Repository{} + +func (r *Repository) NewUserQuery() IUserQuery { + return &UserQuery{ + db: r.db, + } +} +func (r *Repository) NewUserPointDetailQuery() IUserPointDetailQuery { + return &UserPointDetailQuery{ + db: r.db, + } +} +func (r *Repository) InTransaction(ctx context.Context, operation DBOperation) error { + db := r.db.WithContext(ctx) + tx := db.Begin() + if err := operation(NewRepository(tx)); err != nil { + tx.Rollback() + return err + } + tx.Commit() + return nil +} + +func NewRepository(db *gorm.DB) IRepository { + return &Repository{db: db} +} diff --git a/repository/transaction_test.go b/repository/transaction_test.go new file mode 100644 index 0000000..0e96367 --- /dev/null +++ b/repository/transaction_test.go @@ -0,0 +1,264 @@ +package repository + +import ( + "context" + "fmt" + + "jcourse_go/model/model" + "jcourse_go/model/po" + "log" + "testing" + + "github.com/glebarez/sqlite" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "gorm.io/gorm" +) + +func InitTestDB(t *testing.T) (IRepository, *gorm.DB) { + db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{}) + if err != nil { + t.Fatalf("failed to connect database: %v", err) + } + repo := NewRepository(db) + err = Migrate(db) + if err != nil { + t.Fatal(err) + } + return repo, db +} +func ClearTestDB(db *gorm.DB) error { + var tables []string + // Retrieve the list of tables + if err := db.Raw("SELECT name FROM sqlite_master WHERE type='table' AND name != 'sqlite_sequence'").Scan(&tables).Error; err != nil { + return err + } + // Drop each table + for _, table := range tables { + if err := db.Exec("DROP TABLE IF EXISTS " + table).Error; err != nil { + return err + } + } + return nil +} +func InitTestUser(t *testing.T, repo IRepository, n int) ([]po.UserPO, error) { + ctx := context.Background() + userQuery := repo.NewUserQuery() + userPOs := make([]po.UserPO, 0) + for i := 0; i < n; i++ { + email := fmt.Sprintf("test:transaction:%d@example.com", i) + password := fmt.Sprintf("test:transaction:%d", i) + users, err := userQuery.GetUser(ctx, WithEmail(email)) + var user *po.UserPO + if err == nil && len(users) != 0 { + user = &users[0] + user.Points = 1000 + err = userQuery.UpdateUser(ctx, *user) + if err != nil { + return nil, err + } + } else { + user, err = userQuery.CreateUser(ctx, email, password) + if user == nil || err != nil { + return nil, err + } + user.Points = 1000 + err := userQuery.UpdateUser(ctx, *user) + if err != nil { + return nil, err + } + + } + userPOs = append(userPOs, *user) + } + for _, user := range userPOs { + assert.Equal(t, int64(1000), user.Points) + } + return userPOs, nil +} +func ResetUserPoints(users []po.UserPO) { + for _, users := range users { + users.Points = 1000 + } +} +func QueryUser(repo IRepository, idx int) (po.UserPO, []po.UserPointDetailPO, error) { + userQuery := repo.NewUserQuery() + userPointDetailQuery := repo.NewUserPointQuery() + ctx := context.Background() + email := fmt.Sprintf("test:transaction:%d@example.com", idx) + userPOs, err := userQuery.GetUser(ctx, WithEmail(email)) + if err != nil { + return po.UserPO{}, nil, err + } + if len(userPOs) == 0 { + return po.UserPO{}, nil, errors.Errorf("user not found") + } + userPO := userPOs[0] + userPointDetails, err := userPointDetailQuery.GetUserPointDetail(ctx, WithUserID(int64(userPO.ID))) + if err != nil { + return userPO, nil, err + } + return userPO, userPointDetails, nil +} + +func TestInTransAction(t *testing.T) { + ctx := context.Background() + + TransferOpsSucceed := func(repo IRepository) error { + userPOs, err := InitTestUser(t, repo, 2) + assert.Nil(t, err) + assert.Len(t, userPOs, 2) + user1 := userPOs[0] + user2 := userPOs[1] + userQuery := repo.NewUserQuery() + userPointDetailQuery := repo.NewUserPointQuery() + user1.Points -= 100 + t.Logf("user1 points: %d\n", user1.Points) + user2.Points += 99 + t.Logf("user2 points: %d\n", user2.Points) + err = userQuery.UpdateUser(ctx, user1) + if err != nil { + return err + } + err = userQuery.UpdateUser(ctx, user2) + if err != nil { + return err + } + err = userPointDetailQuery.CreateUserPointDetail(ctx, int64(user1.ID), model.PointEventTransfer, -100, "test") + if err != nil { + return err + } + err = userPointDetailQuery.CreateUserPointDetail(ctx, int64(user2.ID), model.PointEventTransfer, 99, "test") + if err != nil { + return err + } + return nil + } + TransferOpsRollback1 := func(repo IRepository) error { + userPOs, err := InitTestUser(t, repo, 2) + assert.Nil(t, err) + assert.Len(t, userPOs, 2) + user1 := userPOs[0] + user2 := userPOs[1] + userQuery := repo.NewUserQuery() + userPointDetailQuery := repo.NewUserPointQuery() + user1.Points -= 100 + t.Logf("user1 points: %d\n", user1.Points) + user2.Points += 99 + t.Logf("user2 points: %d\n", user2.Points) + err = userQuery.UpdateUser(ctx, user1) + if err != nil { + return err + } + err = userQuery.UpdateUser(ctx, user2) + if err != nil { + return err + } + err = userPointDetailQuery.CreateUserPointDetail(ctx, int64(user1.ID), model.PointEventTransfer, -100, "test") + if err != nil { + return err + } + err = userPointDetailQuery.CreateUserPointDetail(ctx, int64(user2.ID), model.PointEventTransfer, 99, "test") + if err != nil { + return err + } + return errors.New("test rollback at end") + } + TransferOpsRollback2 := func(repo IRepository) error { + userPOs, err := InitTestUser(t, repo, 2) + if err != nil { + log.Printf("InitTestUser error: %v", err) + } + assert.Nil(t, err) + assert.Len(t, userPOs, 2) + user1 := userPOs[0] + user2 := userPOs[1] + userQuery := repo.NewUserQuery() + userPointDetailQuery := repo.NewUserPointQuery() + user1.Points -= 100 + t.Logf("user1 points: %d\n", user1.Points) + user2.Points += 99 + t.Logf("user2 points: %d\n", user2.Points) + err = userQuery.UpdateUser(ctx, user1) + if err != nil { + return err + } + err = userQuery.UpdateUser(ctx, user2) + if err != nil { + return err + } + err = errors.New("test rollback in mid") + ign := userPointDetailQuery.CreateUserPointDetail(ctx, int64(user1.ID), model.PointEventTransfer, -100, "test") + if ign != nil { + return err + } + ign = userPointDetailQuery.CreateUserPointDetail(ctx, int64(user2.ID), model.PointEventTransfer, 99, "test") + if ign != nil { + return err + } + return err + } + tests := []struct { + name string + ops DBOperation + wantErr assert.ErrorAssertionFunc + }{ + // TODO: Add test cases. + {name: "transfer", ops: TransferOpsSucceed, wantErr: assert.NoError}, + {name: "rollback1", ops: TransferOpsRollback1, wantErr: assert.Error}, + {name: "rollback2", ops: TransferOpsRollback2, wantErr: assert.Error}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + repo, db := InitTestDB(t) + defer func() { + err := ClearTestDB(db) + if err != nil { + t.Fatal(err) + } + }() + + terr := repo.InTransaction(ctx, tt.ops) + tt.wantErr(t, terr, fmt.Sprintf("InTransAction(%v)", tt.ops)) + + if terr == nil { + // succeed + user1PO, userPointDetails, err := QueryUser(repo, 0) + if err != nil { + t.Fatal(err) + return + } + if len(userPointDetails) == 0 { + t.Fatal("user1 point details not found") + return + } + assert.Equal(t, int64(900), user1PO.Points) + assert.Len(t, userPointDetails, 1) + assert.Equal(t, int64(user1PO.ID), userPointDetails[0].UserID) + assert.Equal(t, model.PointEventTransfer, userPointDetails[0].EventType) + assert.Equal(t, int64(-100), userPointDetails[0].Value) + user2PO, userPointDetails, err := QueryUser(repo, 1) + if err != nil { + t.Fatal(err) + return + } + if len(userPointDetails) == 0 { + t.Fatal("user1 point details not found") + return + } + assert.Equal(t, int64(1099), user2PO.Points) + assert.Len(t, userPointDetails, 1) + assert.Equal(t, int64(user2PO.ID), userPointDetails[0].UserID) + assert.Equal(t, model.PointEventTransfer, userPointDetails[0].EventType) + assert.Equal(t, int64(99), userPointDetails[0].Value) + } else { + // rollback + // rollback create user + _, _, err := QueryUser(repo, 0) + assert.Error(t, err) + _, _, err = QueryUser(repo, 1) + assert.Error(t, err) + } + }) + } +} diff --git a/repository/user.go b/repository/user.go index 08a1242..3e072c8 100644 --- a/repository/user.go +++ b/repository/user.go @@ -4,17 +4,23 @@ import ( "context" "time" + "github.com/pkg/errors" + "gorm.io/gorm" "jcourse_go/constant" "jcourse_go/model/po" ) +const ( + UpdateNotFoundErr = "No rows affected" +) + type IUserQuery interface { GetUser(ctx context.Context, opts ...DBOption) ([]po.UserPO, error) GetUserCount(ctx context.Context, opts ...DBOption) (int64, error) GetUserByIDs(ctx context.Context, userIDs []int64) (map[int64]po.UserPO, error) - UpdateUser(ctx context.Context, user po.UserPO) error + UpdateUser(ctx context.Context, user po.UserPO, opts ...DBOption) error CreateUser(ctx context.Context, email string, password string) (*po.UserPO, error) ResetUserPassword(ctx context.Context, userID int64, password string) error } @@ -71,9 +77,13 @@ func (q *UserQuery) GetUserCount(ctx context.Context, opts ...DBOption) (int64, return count, nil } -func (q *UserQuery) UpdateUser(ctx context.Context, user po.UserPO) error { - result := q.optionDB(ctx, WithID(int64(user.ID))).Updates(&user).Error - return result +func (q *UserQuery) UpdateUser(ctx context.Context, user po.UserPO, opts ...DBOption) error { + opts = append(opts, WithID(int64(user.ID))) + result := q.optionDB(ctx, opts...).Updates(&user) + if result.RowsAffected == 0 { + return errors.New(UpdateNotFoundErr) + } + return result.Error } func (q *UserQuery) CreateUser(ctx context.Context, email string, passwordStore string) (*po.UserPO, error) { diff --git a/repository/user_test.go b/repository/user_test.go new file mode 100644 index 0000000..0ab5b6e --- /dev/null +++ b/repository/user_test.go @@ -0,0 +1,135 @@ +package repository + +import ( + "context" + + "jcourse_go/dal" + "jcourse_go/model/po" + "log" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestUserQuery_UpdateUser(t *testing.T) { + ctx := context.Background() + db := dal.GetDBClient() + err := Migrate(db) + if err != nil { + return + } + query := NewUserQuery(db) + t.Run("none", func(t *testing.T) { + err := query.UpdateUser(ctx, po.UserPO{}, WithID(-1)) + log.Printf("err: %v", err) + assert.Errorf(t, err, "error: %v", err) + }) + + t.Run("concurrently_opt_lock", func(t *testing.T) { + var wg sync.WaitGroup + const numRoutines = 100 + testUserEmail := "update_concurrent_test_concurrently_opt_lock@example.com" + users, err := query.GetUser(ctx, WithEmail(testUserEmail)) + assert.Nil(t, err) + var user po.UserPO + if len(users) != 0 { + user = users[0] + } else { + pUser, err := query.CreateUser(ctx, testUserEmail, "password") + if err != nil || pUser == nil { + log.Printf("err: %v", err) + return + } + user = *pUser + assert.Nil(t, err) + } + + if err != nil { + return + } + + originalPoints := user.Points + errChan := make(chan error, numRoutines) + for range numRoutines { + wg.Add(1) + go func() { + defer wg.Done() + time.Sleep(100 * time.Millisecond) + newUser := user + newUser.Points += 100 + conErr := query.UpdateUser(ctx, newUser, WithOptimisticLock("points", originalPoints)) + if conErr != nil { + errChan <- conErr + } + }() + } + + wg.Wait() + close(errChan) + errCount := 0 + for conErr := range errChan { + log.Printf("err: %v", conErr) + errCount += 1 + } + log.Printf("%v routine failed to update", errCount) + assert.Equal(t, 99, errCount) + users, err = query.GetUser(ctx, WithEmail(testUserEmail)) + assert.Nil(t, err) + comp := func() bool { return len(users) > 0 } + assert.Condition(t, comp) + assert.Equal(t, users[0].Points, originalPoints+100) + }) + t.Run("concurrently_no_lock", func(t *testing.T) { + var wg sync.WaitGroup + const numRoutines = 100 + testUserEmail := "update_concurrent_test_concurrently_no_lock@example.com" + users, err := query.GetUser(ctx, WithEmail(testUserEmail)) + assert.Nil(t, err) + var user po.UserPO + if len(users) != 0 { + user = users[0] + } else { + pUser, err := query.CreateUser(ctx, testUserEmail, "password") + if err != nil || pUser == nil { + log.Printf("err: %v", err) + return + } + user = *pUser + assert.Nil(t, err) + } + + if err != nil { + return + } + errChan := make(chan error, numRoutines) + for i := range numRoutines { + wg.Add(1) + go func() { + defer wg.Done() + time.Sleep(100 * time.Millisecond) + newUser := user + newUser.Points += int64(i) + conErr := query.UpdateUser(ctx, newUser) + if conErr != nil { + errChan <- conErr + } + }() + } + + wg.Wait() + close(errChan) + errCount := 0 + for conErr := range errChan { + log.Printf("err: %v", conErr) + errCount += 1 + } + log.Printf("%v routine failed to update", errCount) + assert.Equal(t, 0, errCount) + users, err = query.GetUser(ctx, WithEmail(testUserEmail)) + assert.Nil(t, err) + log.Printf("points: %d", users[0].Points) + // 大概率不是100 + }) +} diff --git a/repository/userpoint.go b/repository/userpoint.go new file mode 100644 index 0000000..f1f59c3 --- /dev/null +++ b/repository/userpoint.go @@ -0,0 +1,66 @@ +package repository + +import ( + "context" + "jcourse_go/model/po" + + "gorm.io/gorm" +) + +type IUserPointDetailQuery interface { + GetUserPointDetail(ctx context.Context, opts ...DBOption) ([]po.UserPointDetailPO, error) + GetUserPointDetailCount(ctx context.Context, opts ...DBOption) (int64, error) + CreateUserPointDetail(ctx context.Context, userID int64, eventType string, value int64, description string) error +} + +type UserPointDetailQuery struct { + db *gorm.DB +} + +func (q *UserPointDetailQuery) optionDB(ctx context.Context, opts ...DBOption) *gorm.DB { + db := q.db.WithContext(ctx).Model(po.UserPointDetailPO{}) + for _, opt := range opts { + db = opt(db) + } + return db +} + +func (q *UserPointDetailQuery) GetUserPointDetail(ctx context.Context, opts ...DBOption) ([]po.UserPointDetailPO, error) { + db := q.optionDB(ctx, opts...) + userPointDetailPOs := make([]po.UserPointDetailPO, 0) + + result := db.Find(&userPointDetailPOs) + if result.Error != nil { + return userPointDetailPOs, result.Error + } + return userPointDetailPOs, nil +} + +func (q *UserPointDetailQuery) GetUserPointDetailCount(ctx context.Context, opts ...DBOption) (int64, error) { + db := q.optionDB(ctx, opts...) + var count int64 + result := db.Count(&count) + if result.Error != nil { + return 0, result.Error + } + return count, nil +} + +func (q *UserPointDetailQuery) CreateUserPointDetail(ctx context.Context, userID int64, eventType string, value int64, description string) error { + userPointDetail := po.UserPointDetailPO{ + UserID: userID, + PointEvent: po.PointEvent{ + EventType: eventType, + Value: value, + Description: description, + }, + } + result := q.optionDB(ctx).Create(&userPointDetail) + return result.Error +} + +func NewUserPointDetailQuery(db *gorm.DB) IUserPointDetailQuery { + return &UserPointDetailQuery{ + db: db, + } +} diff --git a/router.go b/router.go index 7130107..7097ba2 100644 --- a/router.go +++ b/router.go @@ -24,7 +24,6 @@ func registerRouter(r *gin.Engine) { if !util.IsNoLoginMode() { needAuthGroup.Use(middleware.RequireAuth()) } - needAuthGroup.GET("/common", handler.GetCommonInfo) teacherGroup := needAuthGroup.Group("/teacher") @@ -72,11 +71,16 @@ func registerRouter(r *gin.Engine) { // userGroup.POST("/:userID/unwatch", handler.UnWatchUserHandler) userGroup.PUT("/:userID/profile", handler.UpdateUserProfileHandler) + userPointGroup := userGroup.Group("/point") + userPointGroup.GET("", handler.GetUserPointDetailListHandler) + userPointGroup.POST("/transfer", handler.TransferUserPointHandler) + adminGroup := needAuthGroup.Group("/admin") adminGroup.Use(middleware.RequireAdmin()) adminGroup.GET("/user", handler.AdminGetUserList) - - adminGroup.GET("") + adminGroup.POST("/user/point/change", handler.AdminChangeUserPoint) + adminGroup.GET("/user/point/detail", handler.AdminGetUserPointDetailList) + adminGroup.GET("/user/point/transfer", handler.AdminTransferUserPoint) llmGroup := needAuthGroup.Group(("/llm")) llmGroup.GET("/review/opt", handler.OptCourseReviewHandler) @@ -86,4 +90,5 @@ func registerRouter(r *gin.Engine) { if util.IsDebug() { llmGroup.GET("/vectorize/:courseID", handler.VectorizeCourseHandler) } + } diff --git a/service/auth.go b/service/auth.go index b054fe1..4e7be08 100644 --- a/service/auth.go +++ b/service/auth.go @@ -111,15 +111,36 @@ func SendRegisterCodeEmail(ctx context.Context, email string) error { body := fmt.Sprintf(constant.EmailBodyVerifyCode, code) // nolint: gosimple err = repository.StoreVerifyCode(ctx, email, code) if err != nil { + fmt.Printf("StoreVerifyCode error: %v\n", err) return err } err = rpc.SendMail(ctx, email, constant.EmailTitleVerifyCode, body) if err != nil { + fmt.Printf("SendMail error: %v\n", err) return err } err = repository.StoreSendVerifyCodeHistory(ctx, email) return err } +func SendRegisterCodeEmailMock(ctx context.Context, email string) error { + recentSent := repository.GetSendVerifyCodeHistory(ctx, email) + if recentSent { + return errors.New("recently sent code") + } + code, err := generateVerifyCode() + if err != nil { + return err + } + body := fmt.Sprintf(constant.EmailBodyVerifyCode, code) // nolint: gosimple + err = repository.StoreVerifyCode(ctx, email, code) + if err != nil { + fmt.Printf("StoreVerifyCode error: %v\n", err) + return err + } + fmt.Printf("[HINT] Send email to %s, title: %s, body: %s\n", email, constant.EmailTitleVerifyCode, body) + err = repository.StoreSendVerifyCodeHistory(ctx, email) + return err +} func ValidateEmail(email string) bool { // 1. validate basic email format diff --git a/service/user.go b/service/user.go index 05286ea..9056aa4 100644 --- a/service/user.go +++ b/service/user.go @@ -4,10 +4,9 @@ import ( "context" "jcourse_go/dal" + "jcourse_go/model/converter" "jcourse_go/model/dto" "jcourse_go/model/model" - - "jcourse_go/model/converter" "jcourse_go/repository" ) @@ -68,8 +67,8 @@ func GetUserList(ctx context.Context, filter model.UserFilterForQuery) ([]model. } result := make([]model.UserMinimal, 0) - for _, po := range userPOs { - result = append(result, converter.ConvertUserMinimalFromPO(po)) + for _, userPO := range userPOs { + result = append(result, converter.ConvertUserMinimalFromPO(userPO)) } return result, nil } @@ -91,3 +90,22 @@ func UpdateUserProfileByID(ctx context.Context, userProfileDTO dto.UserProfileDT } return nil } + +func buildUserPointDetailDBOptionFromFilter(query repository.IUserPointDetailQuery, filter model.UserPointDetailFilter) []repository.DBOption { + opts := make([]repository.DBOption, 0) + if filter.UserPointDetailID > 0 { + opts = append(opts, repository.WithID(filter.UserPointDetailID)) + } + if filter.UserID > 0 { + opts = append(opts, repository.WithUserID(filter.UserID)) + } + opts = append(opts, repository.WithPaginate(filter.Page, filter.PageSize)) + if !filter.StartTime.IsZero() && !filter.EndTime.IsZero() { + opts = append(opts, repository.WithTimeBetween(filter.StartTime, filter.EndTime)) + } else if !filter.StartTime.IsZero() { + opts = append(opts, repository.WithTimeAfter(filter.StartTime)) + } else if !filter.EndTime.IsZero() { + opts = append(opts, repository.WithTimeBefore(filter.EndTime)) + } + return opts +} diff --git a/service/userpoint.go b/service/userpoint.go new file mode 100644 index 0000000..bd6c55c --- /dev/null +++ b/service/userpoint.go @@ -0,0 +1,140 @@ +package service + +import ( + "context" + "fmt" + "jcourse_go/constant" + "jcourse_go/dal" + "jcourse_go/model/converter" + "jcourse_go/model/model" + "jcourse_go/model/po" + "jcourse_go/repository" + + "github.com/pkg/errors" +) + +func GetUserPointDetailList(ctx context.Context, fileter model.UserPointDetailFilter) ([]model.UserPointDetailItem, error) { + userPointDetailQuery := repository.NewUserPointDetailQuery(dal.GetDBClient()) + opts := buildUserPointDetailDBOptionFromFilter(userPointDetailQuery, fileter) + userPointDetailPOs, err := userPointDetailQuery.GetUserPointDetail(ctx, opts...) + if err != nil { + return nil, err + } + result := make([]model.UserPointDetailItem, 0) + for _, detailPO := range userPointDetailPOs { + result = append(result, converter.ConvertUserPointDetailItemFromPO(detailPO)) + } + return result, nil +} +func GetUserPointDetailCount(ctx context.Context, filter model.UserPointDetailFilter) (int64, error) { + userPointDetailQuery := repository.NewUserPointDetailQuery(dal.GetDBClient()) + filter.Page, filter.PageSize = 0, 0 + opts := buildUserPointDetailDBOptionFromFilter(userPointDetailQuery, filter) + return userPointDetailQuery.GetUserPointDetailCount(ctx, opts...) +} + +// HINT: 以下的几个UserPoint相关函数都是并发安全的, 但不保证成功,事务失败时需要上层自行处理 +func ChangeUserPoints(ctx context.Context, userID int64, eventType model.PointEventType, value int64, description string) error { + repo := repository.NewRepository(dal.GetDBClient()) + userQuery := repo.NewUserQuery() + userPOs, err := userQuery.GetUser(ctx, repository.WithID(userID)) + if err != nil { + return err + } + if len(userPOs) == 0 { + return errors.Errorf("user %d not found", userID) + } + user := userPOs[0] + if user.Points+value < 0 { + return errors.Errorf("user %d has not enough points", userID) + } + originalPoints := user.Points + user.Points += value + userPointDetailQuery := repo.NewUserPointQuery() + operation := func(repo repository.IRepository) error { + err := userQuery.UpdateUser(ctx, user, repository.WithOptimisticLock("points", originalPoints)) + if err != nil { + return err + } + err = userPointDetailQuery.CreateUserPointDetail(ctx, userID, eventType, value, description) + if err != nil { + return err + } + return nil + } + return repo.InTransaction(ctx, operation) +} + +func calcHandlingFee(ctx context.Context, value int64) int64 { + siteQuery := repository.NewSettingQuery(dal.GetDBClient()) + siteSetting, err := siteQuery.GetSetting(ctx, constant.HandleFeeRateKey) + if err != nil || siteSetting == nil { + return int64(float64(value) * (1 - constant.DefaultHandleFeeRate)) + } + handlerFeeRate := siteSetting.GetValue().(float64) + return int64(float64(value) * (1 - handlerFeeRate)) +} + +const ( + TransferDescriptionFormat = "用户%d转账给用户%d %d分" +) + +func TransferUserPoints(ctx context.Context, senderID int64, receiverID int64, value int64) error { + userQuery := repository.NewUserQuery(dal.GetDBClient()) + + // 合并到一次查询 + ids := []int64{senderID, receiverID} + userPOs, err := userQuery.GetUser(ctx, repository.WithIDs(ids)) + if err != nil { + return err + } + var senderPO *po.UserPO = nil + var receiverPO *po.UserPO = nil + for _, user := range userPOs { + if user.ID == uint(senderID) { + senderPO = &user + } + if user.ID == uint(receiverID) { + receiverPO = &user + } + } + if senderPO == nil { + return errors.New("sender not found") + } + if receiverPO == nil { + return errors.New("receiver not found") + } + if senderPO.Points < value { + return errors.New("sender has not enough points") + } + receivedValue := value - calcHandlingFee(ctx, value) + senderOriginalPoints := senderPO.Points + receiverOriginalPoints := receiverPO.Points + senderPO.Points -= value + receiverPO.Points += receivedValue + repo := repository.NewRepository(dal.GetDBClient()) + description := fmt.Sprintf(TransferDescriptionFormat, senderID, receiverID, value) + operations := func(repo repository.IRepository) error { + userQuery := repo.NewUserQuery() + userPointDetailQuery := repo.NewUserPointQuery() + err := userQuery.UpdateUser(ctx, *senderPO, repository.WithOptimisticLock("points", senderOriginalPoints)) + if err != nil { + return err + } + err = userQuery.UpdateUser(ctx, *receiverPO, repository.WithOptimisticLock("points", receiverOriginalPoints)) + if err != nil { + return err + } + err = userPointDetailQuery.CreateUserPointDetail(ctx, senderID, model.PointEventTransfer, -value, description) + if err != nil { + return err + } + err = userPointDetailQuery.CreateUserPointDetail(ctx, receiverID, model.PointEventTransfer, receivedValue, description) + if err != nil { + return err + } + return nil + } + err = repo.InTransaction(ctx, operations) + return err +} diff --git a/util/env.go b/util/env.go index 7b04429..c7f172f 100644 --- a/util/env.go +++ b/util/env.go @@ -51,6 +51,9 @@ func GetRedisHost() string { func GetRedisPort() string { return GetEnvDefault("REDIS_PORT", "6379") } +func GetRedisPassword() string { + return GetEnvDefault("REDIS_PASSWORD", "") +} // === SMTP === diff --git a/util/timeutil.go b/util/timeutil.go new file mode 100644 index 0000000..d5bff5f --- /dev/null +++ b/util/timeutil.go @@ -0,0 +1,13 @@ +package util + +import ( + "time" +) + +const ( + GoTimeLayout = "2006-01-02 15:04:05" +) + +func ParseTime(timeStr string) (time.Time, error) { + return time.Parse(GoTimeLayout, timeStr) +}