init: 第一次提交
- 以实现登录获取个人信息和课程 - 实现了获取视频记录 - 实现了学习接口
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Taskfile.yml
|
||||
20
cmd/agent/main.go
Normal file
20
cmd/agent/main.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"ckwk/internal/router"
|
||||
"ckwk/pkg/log"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func main() {
|
||||
router := router.SetupRouter()
|
||||
|
||||
if err := http.ListenAndServe(
|
||||
":8080",
|
||||
router,
|
||||
); err != nil {
|
||||
log.Error("服务启动失败", zap.Error(err))
|
||||
}
|
||||
}
|
||||
5
cmd/server/main.go
Normal file
5
cmd/server/main.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package main
|
||||
|
||||
func main() {
|
||||
|
||||
}
|
||||
47
go.mod
Normal file
47
go.mod
Normal file
@@ -0,0 +1,47 @@
|
||||
module ckwk
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/antchfx/htmlquery v1.3.6
|
||||
github.com/gin-gonic/gin v1.12.0
|
||||
github.com/google/uuid v1.6.0
|
||||
go.uber.org/zap v1.27.1
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
resty.dev/v3 v3.0.0-beta.6
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/antchfx/xpath v1.3.6 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||
github.com/gin-contrib/sse v1.1.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.30.1 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // 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.4 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/arch v0.22.0 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
)
|
||||
172
go.sum
Normal file
172
go.sum
Normal file
@@ -0,0 +1,172 @@
|
||||
github.com/antchfx/htmlquery v1.3.6 h1:RNHHL7YehO5XdO8IM8CynwLKONwRHWkrghbYhQIk9ag=
|
||||
github.com/antchfx/htmlquery v1.3.6/go.mod h1:kcVUqancxPygm26X2rceEcagZFFVkLEE7xgLkGSDl/4=
|
||||
github.com/antchfx/xpath v1.3.6 h1:s0y+ElRRtTQdfHP609qFu0+c6bglDv20pqOViQjjdPI=
|
||||
github.com/antchfx/xpath v1.3.6/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
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.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
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/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
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.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
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/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=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
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.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||
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=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
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/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.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=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
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.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
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/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
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=
|
||||
resty.dev/v3 v3.0.0-beta.6 h1:ghRdNpoE8/wBCv+kTKIOauW1aCrSIeTq7GxtfYgtevU=
|
||||
resty.dev/v3 v3.0.0-beta.6/go.mod h1:NTOerrC/4T7/FE6tXIZGIysXXBdgNqwMZuKtxpea9NM=
|
||||
402
internal/ckwk/api.go
Normal file
402
internal/ckwk/api.go
Normal file
@@ -0,0 +1,402 @@
|
||||
package ckwk
|
||||
|
||||
import (
|
||||
"ckwk/pkg/common"
|
||||
"ckwk/pkg/log"
|
||||
"ckwk/pkg/request"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/antchfx/htmlquery"
|
||||
"go.uber.org/zap"
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
type WK struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Host string `json:"host"`
|
||||
Req *resty.Client
|
||||
|
||||
Cookies []*http.Cookie
|
||||
|
||||
LoginRegexp *regexp.Regexp
|
||||
CourseIDRegexp *regexp.Regexp
|
||||
TimeRegexp *regexp.Regexp
|
||||
}
|
||||
|
||||
func NewWK(username, password, host string, cookies []*http.Cookie) *WK {
|
||||
hasAuth := (username != "" && password != "") || len(cookies) > 0
|
||||
if host == "" || !hasAuth {
|
||||
return nil
|
||||
}
|
||||
|
||||
req := request.NewClient(&request.Config{
|
||||
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0",
|
||||
Proxy: "http://127.0.0.1:9000",
|
||||
VerifySSL: false,
|
||||
})
|
||||
if len(cookies) > 0 {
|
||||
req.SetCookies(cookies)
|
||||
}
|
||||
|
||||
wk := &WK{
|
||||
Username: username,
|
||||
Password: password,
|
||||
Host: host,
|
||||
Req: req,
|
||||
Cookies: cookies,
|
||||
|
||||
LoginRegexp: regexp.MustCompile("var data =(.*?);"),
|
||||
CourseIDRegexp: regexp.MustCompile(`\?courseId=(\d+)`),
|
||||
TimeRegexp: regexp.MustCompile(`\d{4}-\d{2}-\d{2}`),
|
||||
}
|
||||
if len(cookies) == 0 && username != "" {
|
||||
wk.Login()
|
||||
}
|
||||
|
||||
return wk
|
||||
}
|
||||
|
||||
// Cookies: returns cookies
|
||||
func (wk *WK) Cookie() []*http.Cookie {
|
||||
return wk.Cookies
|
||||
}
|
||||
|
||||
// SetCookies: returns cookies
|
||||
func (wk *WK) SetCookies(cs []*http.Cookie) {
|
||||
wk.Cookies = cs
|
||||
wk.Req.SetCookies(cs)
|
||||
}
|
||||
|
||||
// Code: Get Verify Code
|
||||
func (wk *WK) Code() (string, error) {
|
||||
resp, err := wk.Req.
|
||||
R().
|
||||
SetQueryParam("r", fmt.Sprint(common.RandFloat64())).
|
||||
Get(fmt.Sprintf("https://%s/service/code", wk.Host))
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("获取验证码失败: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode() != 200 {
|
||||
return "", fmt.Errorf("获取验证码失败: code: %d", resp.StatusCode())
|
||||
}
|
||||
var result CodeResp
|
||||
_, err = wk.Req.
|
||||
R().
|
||||
SetFormData(map[string]string{
|
||||
"image": base64.StdEncoding.EncodeToString(resp.Bytes()),
|
||||
"probability": "false",
|
||||
"png_fix": "false",
|
||||
}).
|
||||
SetResult(&result).
|
||||
Post("http://localhost:8000/ocr")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("获取验证码验证结果失败: %w", err)
|
||||
}
|
||||
if result.Code != 200 {
|
||||
return "", fmt.Errorf("ocr 失败: code: %d, message: %s", result.Code, result.Message)
|
||||
}
|
||||
return result.Data, nil
|
||||
}
|
||||
|
||||
// Login: Login WebSite
|
||||
func (wk *WK) Login() (bool, error) {
|
||||
yzm := ""
|
||||
for i := 1; i <= 3; i++ {
|
||||
yzm, _ = wk.Code()
|
||||
if yzm != "" {
|
||||
break
|
||||
}
|
||||
log.Warnf("第 %d 次获取验证码失败, 正在重试...\n", i)
|
||||
}
|
||||
if yzm == "" {
|
||||
return false, fmt.Errorf("以达到最大重试次数,验证码获取失败,登录终止。")
|
||||
}
|
||||
|
||||
resp, err := wk.Req.
|
||||
R().
|
||||
SetFormData(map[string]string{
|
||||
"username": wk.Username,
|
||||
"password": wk.Password,
|
||||
"code": yzm,
|
||||
"redirect": "",
|
||||
}).
|
||||
Post(fmt.Sprintf("https://%s/user/login", wk.Host))
|
||||
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("请求登录失败: %w", err)
|
||||
}
|
||||
matchs := wk.LoginRegexp.FindStringSubmatch(string(resp.Bytes()))
|
||||
if len(matchs) <= 1 {
|
||||
return false, fmt.Errorf("没有找到匹配字符串")
|
||||
}
|
||||
|
||||
var result LoginResp
|
||||
err = json.Unmarshal([]byte(matchs[1]), &result)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("解析 json data 失败: %w", err)
|
||||
}
|
||||
|
||||
if !result.Status {
|
||||
return false, fmt.Errorf("登录失败: %s", result.Msg)
|
||||
}
|
||||
wk.SetCookies(resp.Cookies())
|
||||
log.Info("登录成功", zap.Any("cookies", wk.Cookies))
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// CourseParse: 课程解析
|
||||
func (wk *WK) CourseParse(content string) ([]Course, error) {
|
||||
courses := make([]Course, 0)
|
||||
doc, err := htmlquery.Parse(strings.NewReader(content))
|
||||
if err != nil {
|
||||
return courses, fmt.Errorf("解析 HTML 失败: %w", err)
|
||||
}
|
||||
|
||||
itemsNode := htmlquery.Find(doc, `//div[@class="user-course"]/div[@class="item"]`)
|
||||
if len(itemsNode) < 1 {
|
||||
return courses, nil
|
||||
// return courses, fmt.Errorf("未解析到课程")
|
||||
}
|
||||
|
||||
for _, node := range itemsNode {
|
||||
aNode := htmlquery.FindOne(node, `.//div[@class="name"]/a`)
|
||||
if aNode == nil {
|
||||
continue
|
||||
}
|
||||
href := htmlquery.SelectAttr(aNode, "href")
|
||||
|
||||
match := wk.CourseIDRegexp.FindStringSubmatch(href)
|
||||
var id int
|
||||
if len(match) > 1 {
|
||||
idVal, err := strconv.Atoi(match[1])
|
||||
if err == nil {
|
||||
id = idVal
|
||||
} else {
|
||||
log.Warn("ID 转换失败", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
tagsNode := htmlquery.FindOne(node, `.//div[@class="tags"]/span`)
|
||||
progressNode := htmlquery.FindOne(node, `.//div[@class="progress"]/div[@class="txt"]`)
|
||||
|
||||
timeNode := htmlquery.FindOne(node, `.//div[@class="time"]`)
|
||||
var startTime, stopTime string
|
||||
if timeNode != nil {
|
||||
fullText := htmlquery.InnerText(timeNode)
|
||||
dates := wk.TimeRegexp.FindAllString(fullText, -1)
|
||||
|
||||
if len(dates) >= 2 {
|
||||
startTime = dates[0]
|
||||
stopTime = dates[1]
|
||||
}
|
||||
}
|
||||
|
||||
var credit float32
|
||||
creditNode := htmlquery.FindOne(node, `.//div[@class="note"]/div[@class="number"]/span`)
|
||||
creditStr := strings.TrimSpace(htmlquery.InnerText(creditNode))
|
||||
creditVal, err := strconv.ParseFloat(creditStr, 32)
|
||||
if err == nil {
|
||||
credit = float32(creditVal)
|
||||
} else {
|
||||
log.Warn("课程学分转换失败", zap.Error(err))
|
||||
}
|
||||
|
||||
typeNode := htmlquery.FindOne(node, `.//div[@class="note"]/div[@class="kind"]/span`)
|
||||
|
||||
course := Course{
|
||||
ID: id,
|
||||
Name: htmlquery.InnerText(aNode),
|
||||
Teacher: htmlquery.InnerText(tagsNode),
|
||||
Progress: htmlquery.InnerText(progressNode),
|
||||
StartTime: startTime,
|
||||
StopTime: stopTime,
|
||||
Credit: credit,
|
||||
Type: htmlquery.InnerText(typeNode),
|
||||
}
|
||||
courses = append(courses, course)
|
||||
}
|
||||
return courses, nil
|
||||
}
|
||||
|
||||
// CourseGet: 课程获取
|
||||
func (wk *WK) CourseGet(kind CourseKind) ([]Course, error) {
|
||||
var courses []Course
|
||||
resp, err := wk.Req.
|
||||
R().
|
||||
SetQueryParam("kind", string(kind)).
|
||||
Get(fmt.Sprintf("https://%s/user/index", wk.Host))
|
||||
if err != nil {
|
||||
return courses, fmt.Errorf("获取课程失败: %w", err)
|
||||
}
|
||||
courses, err = wk.CourseParse(string(resp.Bytes()))
|
||||
if err != nil {
|
||||
return courses, fmt.Errorf("解析课程失败: %w", err)
|
||||
}
|
||||
|
||||
return courses, nil
|
||||
}
|
||||
|
||||
// UserParse: 用户信息解析
|
||||
func (wk *WK) UserInfoParse(content string) (User, error) {
|
||||
var user User
|
||||
|
||||
doc, err := htmlquery.Parse(strings.NewReader(content))
|
||||
if err != nil {
|
||||
return user, fmt.Errorf("解析用户信息失败: %w", err)
|
||||
}
|
||||
|
||||
userInfoNode := htmlquery.FindOne(doc, `//div[@class="useredata-form"]`)
|
||||
if userInfoNode == nil {
|
||||
return user, fmt.Errorf("未解析到用户信息")
|
||||
}
|
||||
nameNode := htmlquery.FindOne(userInfoNode, `.//div[@class="item"][1]/div[@class="txt"]/text()`)
|
||||
if nameNode != nil {
|
||||
user.Name = strings.TrimSpace(htmlquery.InnerText(nameNode))
|
||||
}
|
||||
idNode := htmlquery.FindOne(userInfoNode, `.//div[@class="item"][2]/div[@class="txt"]`)
|
||||
if idNode != nil {
|
||||
user.ID = strings.TrimSpace(htmlquery.InnerText(idNode))
|
||||
}
|
||||
deptNode := htmlquery.FindOne(userInfoNode, `.//div[@class="item"][3]/div[@class="txt"]`)
|
||||
if deptNode != nil {
|
||||
user.Dept = strings.TrimSpace(htmlquery.InnerText(deptNode))
|
||||
}
|
||||
classNode := htmlquery.FindOne(userInfoNode, `.//div[@class="item"][4]/div[@class="txt"]`)
|
||||
if classNode != nil {
|
||||
user.Class = strings.TrimSpace(htmlquery.InnerText(classNode))
|
||||
}
|
||||
genderNode := htmlquery.FindOne(userInfoNode, `.//select[@id="gender"]/option[@selected="selected"]`)
|
||||
if genderNode != nil {
|
||||
user.Gender = htmlquery.SelectAttr(genderNode, "value")
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// UserGet: 用户信息获取
|
||||
func (wk *WK) UserInfoGet() (User, error) {
|
||||
var user User
|
||||
resp, err := wk.Req.
|
||||
R().
|
||||
Get(fmt.Sprintf("https://%s/user/member", wk.Host))
|
||||
if err != nil {
|
||||
return user, fmt.Errorf("获取用户信息页面失败: %w", err)
|
||||
}
|
||||
user, err = wk.UserInfoParse(string(resp.Bytes()))
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
wk.Username = user.ID
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Online: 保持账号状态
|
||||
func (wk *WK) Online() (bool, error) {
|
||||
resp, err := wk.Req.
|
||||
R().
|
||||
SetHeaders(map[string]string{
|
||||
"x-requested-with": "XMLHttpRequest",
|
||||
"Accept": "application/json, text/javascript, */*; q=0.01",
|
||||
"Accept-Encoding": "gzip, deflate, br, zstd",
|
||||
}).
|
||||
Post(fmt.Sprintf("https://%s/user/online", wk.Host))
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("保持账号状态失败: %w", err)
|
||||
}
|
||||
log.Info("保持账号状态", zap.Any("resp", string(resp.Bytes())))
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Study: 学习
|
||||
func (wk *WK) Study(nodeID, studyID, studyTime string, status StudyStatus) (*StudyResp, error) {
|
||||
var data map[string]string
|
||||
switch status {
|
||||
case StudyStart:
|
||||
yzm := ""
|
||||
for i := 1; i <= 3; i++ {
|
||||
yzm, _ = wk.Code()
|
||||
if yzm != "" {
|
||||
break
|
||||
}
|
||||
log.Warnf("第 %d 次获取验证码失败, 正在重试...\n", i)
|
||||
}
|
||||
if yzm == "" {
|
||||
return nil, fmt.Errorf("以达到最大重试次数,验证码获取失败,登录终止。")
|
||||
}
|
||||
data = map[string]string{
|
||||
"nodeId": nodeID,
|
||||
"studyId": "0",
|
||||
"studyTime": "1",
|
||||
"code": yzm,
|
||||
}
|
||||
case Study:
|
||||
data = map[string]string{
|
||||
"nodeId": nodeID,
|
||||
"studyId": studyID,
|
||||
"studyTime": studyTime,
|
||||
}
|
||||
case StudyOver:
|
||||
data = map[string]string{
|
||||
"nodeId": nodeID,
|
||||
"studyId": studyID,
|
||||
"studyTime": studyTime,
|
||||
"close": "1",
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("传入的学习状态不匹配")
|
||||
}
|
||||
resp, err := wk.Req.
|
||||
R().
|
||||
SetHeaders(map[string]string{
|
||||
"x-requested-with": "XMLHttpRequest",
|
||||
"Accept": "application/json, text/javascript, */*; q=0.01",
|
||||
"Accept-Encoding": "gzip, deflate, br, zstd",
|
||||
}).
|
||||
SetFormData(data).
|
||||
Post(fmt.Sprintf("https://%s/user/node/study", wk.Host))
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("进行学习失败: %w", err)
|
||||
}
|
||||
|
||||
var result StudyResp
|
||||
if err := json.Unmarshal(resp.Bytes(), &result); err != nil {
|
||||
return nil, fmt.Errorf("解析响应失败: %w", err)
|
||||
}
|
||||
|
||||
log.Info("学习响应", zap.Any("resp", result))
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetStudyList: 获取学习记录
|
||||
func (wk *WK) GetStudyList(courseID, page string) (*AllRecordResp[StudyList], error) {
|
||||
return GetRecords[StudyList](wk, RecordStudy, courseID, page)
|
||||
}
|
||||
|
||||
// GetExamList: 获取作业记录
|
||||
// todo
|
||||
func (wk *WK) GetWorkList(courseID, page string) (*AllRecordResp[ExamList], error) {
|
||||
return GetRecords[ExamList](wk, RecordStudy, courseID, page)
|
||||
}
|
||||
|
||||
// GetExamList: 获取考试记录
|
||||
func (wk *WK) GetExamList(courseID, page string) (*AllRecordResp[ExamList], error) {
|
||||
return GetRecords[ExamList](wk, RecordExam, courseID, page)
|
||||
}
|
||||
|
||||
// GetExamList: 获取讨论记录
|
||||
// todo
|
||||
func (wk *WK) GetDiscussList(courseID, page string) (*AllRecordResp[ExamList], error) {
|
||||
return GetRecords[ExamList](wk, RecordStudy, courseID, page)
|
||||
}
|
||||
31
internal/ckwk/api_test.go
Normal file
31
internal/ckwk/api_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package ckwk
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestWK(t *testing.T) {
|
||||
// var cookies []*http.Cookie
|
||||
|
||||
// cookie := http.Cookie{
|
||||
// Name: "token",
|
||||
// Value: "sid.odITb8QqlLLPPoQMpnMKZjx98cUGuC",
|
||||
// // Domain: ".leykeji.com",
|
||||
// Path: "/",
|
||||
// }
|
||||
|
||||
// cookies = append(cookies, &cookie)
|
||||
|
||||
// wk := NewWK("2025301201", "Zhilv666", "cqcst.leykeji.com", cookies)
|
||||
|
||||
// wk.Login()
|
||||
|
||||
// user, err := wk.UserInfoGet()
|
||||
// log.Debug("获取用户信息", zap.Any("user", user), zap.Error(err))
|
||||
|
||||
// courses, err := wk.CourseGet(Finish)
|
||||
// log.Debug("获取课程信息", zap.Any("courses", courses), zap.Error(err))
|
||||
fmt.Printf("time.Now().UnixMilli(): %v\n", time.Now().UnixMilli())
|
||||
}
|
||||
111
internal/ckwk/resp.go
Normal file
111
internal/ckwk/resp.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package ckwk
|
||||
|
||||
// 获取验证码响应
|
||||
type CodeResp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
// 登录响应
|
||||
type LoginResp struct {
|
||||
RefreshCode int `json:"refresh_code"`
|
||||
Status bool `json:"status"`
|
||||
Msg string `json:"msg"`
|
||||
Back string `json:"back"`
|
||||
}
|
||||
|
||||
type StudyResp struct {
|
||||
State int `json:"state"`
|
||||
StudyID int `json:"studyId"`
|
||||
Status bool `json:"status"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
type AllRecordResp[T any] struct {
|
||||
List []T `json:"list"`
|
||||
PageInfo PageInfo `json:"pageInfo"`
|
||||
Status bool `json:"status"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
type PageInfo struct {
|
||||
KeyName string `json:"keyName"`
|
||||
Page int `json:"page"`
|
||||
PageCount int `json:"pageCount"`
|
||||
RecordsCount int `json:"recordsCount"`
|
||||
OnlyCount int `json:"onlyCount"`
|
||||
PageSize int `json:"pageSize"`
|
||||
}
|
||||
|
||||
// 学习记录列表
|
||||
type StudyList struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type interface{} `json:"type"`
|
||||
ChapterID string `json:"chapterId"`
|
||||
CourseID string `json:"courseId"`
|
||||
VideoFile string `json:"videoFile"`
|
||||
VideoDuration string `json:"videoDuration"`
|
||||
VotingPath interface{} `json:"votingPath"`
|
||||
TabVideo string `json:"tabVideo"`
|
||||
TabFile string `json:"tabFile"`
|
||||
TabVote string `json:"tabVote"`
|
||||
TabWork string `json:"tabWork"`
|
||||
TabExam string `json:"tabExam"`
|
||||
Sort string `json:"sort"`
|
||||
VideoMode string `json:"videoMode"`
|
||||
LocalFile string `json:"localFile"`
|
||||
SchoolID string `json:"schoolId"`
|
||||
Lock string `json:"lock"`
|
||||
UnlockTime string `json:"unlockTime"`
|
||||
Bid string `json:"bid"`
|
||||
Duration string `json:"duration"`
|
||||
Progress string `json:"progress"`
|
||||
State string `json:"state"`
|
||||
ViewCount string `json:"viewCount"`
|
||||
FinalTime string `json:"finalTime"`
|
||||
Error int `json:"error"`
|
||||
ErrorMessage string `json:"errorMessage"`
|
||||
BeginTime string `json:"beginTime"`
|
||||
ViewedDuration string `json:"viewedDuration"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// 考试记录列表
|
||||
type ExamList struct {
|
||||
ID string `json:"id"`
|
||||
UserID interface{} `json:"userId"`
|
||||
Title string `json:"title"`
|
||||
TopicNumber string `json:"topicNumber"`
|
||||
Score string `json:"score"`
|
||||
AddTime string `json:"addTime"`
|
||||
NodeID string `json:"nodeId"`
|
||||
CourseID string `json:"courseId"`
|
||||
LimitedTime string `json:"limitedTime"`
|
||||
Sequence string `json:"sequence"`
|
||||
Remarks string `json:"remarks"`
|
||||
PaperID string `json:"paperId"`
|
||||
StartTime string `json:"startTime"`
|
||||
EndTime string `json:"endTime"`
|
||||
CreateUserID string `json:"createUserId"`
|
||||
ClassList string `json:"classList"`
|
||||
IsPrivate string `json:"isPrivate"`
|
||||
TeacherType string `json:"teacherType"`
|
||||
Allow string `json:"allow"`
|
||||
Frequency string `json:"frequency"`
|
||||
HasCollect string `json:"hasCollect"`
|
||||
SchoolID string `json:"schoolId"`
|
||||
Parsing string `json:"parsing"`
|
||||
AddDate string `json:"addDate"`
|
||||
Random string `json:"random"`
|
||||
RandData interface{} `json:"randData"`
|
||||
RandNumber string `json:"randNumber"`
|
||||
Name string `json:"name"`
|
||||
ChapterID string `json:"chapterId"`
|
||||
State string `json:"state"`
|
||||
SubmitTime string `json:"submitTime"`
|
||||
FinalScore string `json:"finalScore"`
|
||||
FinishTime string `json:"finishTime"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
117
internal/ckwk/session_manager.go
Normal file
117
internal/ckwk/session_manager.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package ckwk
|
||||
|
||||
import (
|
||||
"ckwk/pkg/log"
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type SessionManager struct {
|
||||
mu sync.RWMutex
|
||||
sessions map[string]SessionItem
|
||||
userToSession map[string]string
|
||||
}
|
||||
|
||||
type SessionItem struct {
|
||||
Instance *WK
|
||||
LastValue time.Time
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func NewSessionManager() *SessionManager {
|
||||
return &SessionManager{
|
||||
sessions: make(map[string]SessionItem),
|
||||
userToSession: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
// Store: 保存 session 并返回 session id
|
||||
func (m *SessionManager) Store(wk *WK) string {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
userKey := wk.Host + ":" + wk.Username
|
||||
if oldID, exists := m.userToSession[userKey]; exists {
|
||||
item := m.sessions[oldID]
|
||||
item.LastValue = time.Now()
|
||||
item.Instance = wk
|
||||
m.sessions[oldID] = item
|
||||
|
||||
log.Info("用户已存在,复用旧 Session",
|
||||
zap.String("id", oldID),
|
||||
zap.String("user", userKey),
|
||||
)
|
||||
|
||||
return oldID
|
||||
}
|
||||
|
||||
sessionID := uuid.New().String()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
m.userToSession[userKey] = sessionID
|
||||
m.sessions[sessionID] = SessionItem{
|
||||
Instance: wk,
|
||||
LastValue: time.Now(),
|
||||
cancel: cancel,
|
||||
}
|
||||
|
||||
log.Info("创建新 Session", zap.String("id", sessionID))
|
||||
|
||||
go m.KeepAlive(ctx, sessionID, wk)
|
||||
|
||||
return sessionID
|
||||
}
|
||||
|
||||
// Get: 获取指定 session id 的 wk
|
||||
func (m *SessionManager) Get(sessionID string) (*WK, bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
item, ok := m.sessions[sessionID]
|
||||
if ok {
|
||||
return item.Instance, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (m *SessionManager) Del(sessionID string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if item, ok := m.sessions[sessionID]; ok {
|
||||
userKey := item.Instance.Host + ":" + item.Instance.Username
|
||||
|
||||
if item.cancel != nil {
|
||||
item.cancel()
|
||||
}
|
||||
|
||||
delete(m.userToSession, userKey)
|
||||
delete(m.sessions, sessionID)
|
||||
|
||||
log.Info("删除 Session", zap.String("id", sessionID))
|
||||
}
|
||||
}
|
||||
|
||||
func (m *SessionManager) KeepAlive(ctx context.Context, id string, wk *WK) {
|
||||
ticker := time.NewTicker(2 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
log.Info("启动 KeepAlive", zap.String("id", id))
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Info("KeepAlive 已停止", zap.String("id", id))
|
||||
return
|
||||
case <-ticker.C:
|
||||
_, err := wk.Online()
|
||||
if err != nil {
|
||||
log.Error("自动保活请求失败", zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
77
internal/ckwk/types.go
Normal file
77
internal/ckwk/types.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package ckwk
|
||||
|
||||
import (
|
||||
"ckwk/pkg/log"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type CourseKind string
|
||||
type RecordType string
|
||||
type StudyStatus int
|
||||
|
||||
const (
|
||||
Run CourseKind = "run" // 我的课程
|
||||
Finish CourseKind = "finish" // 已结束
|
||||
Sign CourseKind = "sign" // 报名中
|
||||
|
||||
RecordStudy RecordType = "" // 学习记录
|
||||
RecordWork RecordType = "/work" // 作业记录
|
||||
RecordExam RecordType = "/exam" // 考试记录
|
||||
RecordDiscuss RecordType = "/discuss" // 讨论记录
|
||||
|
||||
StudyStart StudyStatus = 1 // 开始学习
|
||||
Study StudyStatus = 2 // 学习中
|
||||
StudyOver StudyStatus = 3 // 学习介绍
|
||||
)
|
||||
|
||||
// User: 用户
|
||||
type User struct {
|
||||
Name string `json:"name"`
|
||||
ID string `json:"id"`
|
||||
Dept string `json:"dept"`
|
||||
Class string `json:"class"`
|
||||
Gender string `json:"gender"`
|
||||
}
|
||||
|
||||
// Course: 课程
|
||||
type Course struct {
|
||||
Name string `json:"name"`
|
||||
ID int `json:"id"`
|
||||
Teacher string `json:"teacher"`
|
||||
Progress string `json:"progress"`
|
||||
StartTime string `json:"start_time"`
|
||||
StopTime string `json:"stop_time"`
|
||||
Credit float32 `json:"credit"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
func GetRecords[T any](wk *WK, rType RecordType, courseID, page string) (*AllRecordResp[T], error) {
|
||||
log.Debug("获取记录信息", zap.String("host", wk.Host), zap.String("type", string(rType)))
|
||||
resp, err := wk.Req.
|
||||
R().
|
||||
SetQueryParams(map[string]string{
|
||||
"courseId": courseID,
|
||||
"page": page,
|
||||
"_": fmt.Sprint(time.Now().UnixMilli()),
|
||||
}).
|
||||
Get(fmt.Sprintf("https://%s/user/study_record%s.json", wk.Host, rType))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result AllRecordResp[T]
|
||||
if err := json.Unmarshal(resp.Bytes(), &result); err != nil {
|
||||
log.Error("JSON解析失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !result.Status {
|
||||
return &result, fmt.Errorf("接口报错: %s", result.Msg)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
56
internal/dto/ckwk.go
Normal file
56
internal/dto/ckwk.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"ckwk/internal/ckwk"
|
||||
)
|
||||
|
||||
type Resp[T any] struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data T `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
func Success[T any](data T) Resp[T] {
|
||||
return Resp[T]{
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func Ok() Resp[any] {
|
||||
return Resp[any]{
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
Data: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func Error(code int, msg string) Resp[any] {
|
||||
return Resp[any]{
|
||||
Code: code,
|
||||
Message: msg,
|
||||
Data: nil,
|
||||
}
|
||||
}
|
||||
|
||||
type LoginReq struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Token string `json:"token"`
|
||||
Status ckwk.CourseKind `json:"status"`
|
||||
Host string `json:"host" binding:"required"`
|
||||
}
|
||||
|
||||
type StudyReq struct {
|
||||
NodeID string `json:"node_id"`
|
||||
StudyID string `json:"study_id"`
|
||||
StudyTime string `json:"study_time"`
|
||||
Status ckwk.StudyStatus `json:"status"`
|
||||
}
|
||||
|
||||
type AllRecordReq struct {
|
||||
CourseID string `json:"course_id"`
|
||||
Page int `json:"page"`
|
||||
RecordType ckwk.RecordType `json:"record_type"`
|
||||
}
|
||||
173
internal/handler/ckwk.go
Normal file
173
internal/handler/ckwk.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"ckwk/internal/ckwk"
|
||||
"ckwk/internal/dto"
|
||||
"ckwk/pkg/log"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type WKHandler struct {
|
||||
Session *ckwk.SessionManager
|
||||
}
|
||||
|
||||
func NewWKHandler() *WKHandler {
|
||||
return &WKHandler{
|
||||
Session: ckwk.NewSessionManager(),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *WKHandler) Login(ctx *gin.Context) {
|
||||
var req dto.LoginReq
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
ctx.JSON(200, dto.Error(-1, "请求参数错误"))
|
||||
return
|
||||
}
|
||||
log.Debug("请求数据", zap.Any("req", req))
|
||||
var cookies = []*http.Cookie{
|
||||
{
|
||||
Name: "token",
|
||||
Value: req.Token,
|
||||
Path: "/"},
|
||||
}
|
||||
wk := ckwk.NewWK(req.Username, req.Password, req.Host, cookies)
|
||||
if wk == nil {
|
||||
ctx.JSON(200, dto.Error(-1, "登录失败:请提供账号密码或有效的 Token,并确保 Host 正确"))
|
||||
return
|
||||
}
|
||||
|
||||
userinfo, err := wk.UserInfoGet()
|
||||
if err != nil {
|
||||
ctx.JSON(200, dto.Error(-1, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
courses, err := wk.CourseGet(req.Status)
|
||||
if err != nil {
|
||||
ctx.JSON(200, dto.Error(-1, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
sessionID := h.Session.Store(wk)
|
||||
|
||||
ctx.JSON(200, dto.Success(map[string]any{
|
||||
"session_id": sessionID,
|
||||
"user": userinfo,
|
||||
"courses": courses,
|
||||
}))
|
||||
}
|
||||
|
||||
func (h *WKHandler) Online(ctx *gin.Context) {
|
||||
val, ok := ctx.Get("wk_instance")
|
||||
if !ok {
|
||||
ctx.JSON(http.StatusOK, dto.Error(-1, "登录已过期"))
|
||||
return
|
||||
}
|
||||
wk := val.(*ckwk.WK)
|
||||
|
||||
flag, err := wk.Online()
|
||||
if err != nil {
|
||||
ctx.JSON(200, dto.Error(-1, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if !flag {
|
||||
ctx.JSON(200, dto.Error(-1, "保持账号状态失败"))
|
||||
return
|
||||
}
|
||||
ctx.JSON(200, dto.Ok())
|
||||
}
|
||||
|
||||
func (h *WKHandler) Logout(ctx *gin.Context) {
|
||||
sessionID := ctx.GetString("session_id")
|
||||
h.Session.Del(sessionID)
|
||||
ctx.JSON(200, dto.Ok())
|
||||
}
|
||||
|
||||
func (h *WKHandler) Study(ctx *gin.Context) {
|
||||
val, ok := ctx.Get("wk_instance")
|
||||
if !ok {
|
||||
ctx.JSON(200, dto.Error(-1, "登录已过期"))
|
||||
return
|
||||
}
|
||||
wk := val.(*ckwk.WK)
|
||||
|
||||
var req dto.StudyReq
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
ctx.JSON(200, dto.Error(-1, "请求参数错误"))
|
||||
return
|
||||
}
|
||||
|
||||
result, err := wk.Study(req.NodeID, req.StudyID, req.StudyTime, req.Status)
|
||||
if err != nil {
|
||||
ctx.JSON(200, dto.Error(-1, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(200, dto.Success(result))
|
||||
}
|
||||
|
||||
func (h *WKHandler) AllRecord(ctx *gin.Context) {
|
||||
val, ok := ctx.Get("wk_instance")
|
||||
if !ok {
|
||||
ctx.JSON(200, dto.Error(-1, "登录已过期"))
|
||||
return
|
||||
}
|
||||
wk := val.(*ckwk.WK)
|
||||
|
||||
var req dto.AllRecordReq
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
ctx.JSON(200, dto.Error(-1, "请求参数错误"))
|
||||
return
|
||||
}
|
||||
if req.Page < 1 {
|
||||
req.Page = 1
|
||||
}
|
||||
var list any
|
||||
var pageInfo any
|
||||
var err error
|
||||
|
||||
// 根据类型调用不同的泛型方法
|
||||
switch req.RecordType {
|
||||
case ckwk.RecordStudy:
|
||||
res, e := wk.GetStudyList(req.CourseID, fmt.Sprint(req.Page))
|
||||
err = e
|
||||
if e == nil {
|
||||
list, pageInfo = res.List, res.PageInfo
|
||||
}
|
||||
case ckwk.RecordWork:
|
||||
res, e := wk.GetWorkList(req.CourseID, fmt.Sprint(req.Page))
|
||||
err = e
|
||||
if e == nil {
|
||||
list, pageInfo = res.List, res.PageInfo
|
||||
}
|
||||
case ckwk.RecordExam:
|
||||
res, e := wk.GetExamList(req.CourseID, fmt.Sprint(req.Page))
|
||||
err = e
|
||||
if e == nil {
|
||||
list, pageInfo = res.List, res.PageInfo
|
||||
}
|
||||
case ckwk.RecordDiscuss:
|
||||
res, e := wk.GetDiscussList(req.CourseID, fmt.Sprint(req.Page))
|
||||
err = e
|
||||
if e == nil {
|
||||
list, pageInfo = res.List, res.PageInfo
|
||||
}
|
||||
default:
|
||||
ctx.JSON(200, dto.Error(-1, "不支持的记录类型"))
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
ctx.JSON(200, dto.Error(-1, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(200, dto.Success(map[string]any{
|
||||
"list": list,
|
||||
"page_info": pageInfo,
|
||||
}))
|
||||
}
|
||||
24
internal/middleware/session_middleware.go
Normal file
24
internal/middleware/session_middleware.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"ckwk/internal/ckwk"
|
||||
"ckwk/internal/dto"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SessionMiddleware(sm *ckwk.SessionManager) gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
sessionID := ctx.GetHeader("X-Session-Id")
|
||||
wk, ok := sm.Get(sessionID)
|
||||
if !ok {
|
||||
ctx.JSON(http.StatusUnauthorized, dto.Error(401, "登录过期"))
|
||||
ctx.Abort()
|
||||
return
|
||||
}
|
||||
ctx.Set("wk_instance", wk)
|
||||
ctx.Set("session_id", sessionID)
|
||||
ctx.Next()
|
||||
}
|
||||
}
|
||||
33
internal/router/router.go
Normal file
33
internal/router/router.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"ckwk/internal/handler"
|
||||
"ckwk/internal/middleware"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetupRouter() *gin.Engine {
|
||||
r := gin.Default()
|
||||
wkHandler := handler.NewWKHandler()
|
||||
sessionMiddleware := middleware.SessionMiddleware(wkHandler.Session)
|
||||
|
||||
api := r.Group("/api")
|
||||
{
|
||||
api.POST("/login", wkHandler.Login)
|
||||
v1 := api.Group("/v1", sessionMiddleware)
|
||||
{
|
||||
v1.POST("/online", wkHandler.Online)
|
||||
v1.POST("/logout", wkHandler.Logout)
|
||||
}
|
||||
|
||||
v2 := api.Group("/v2", sessionMiddleware)
|
||||
{
|
||||
v2.POST("/logout", wkHandler.Logout)
|
||||
v2.POST("/study", wkHandler.Study)
|
||||
v2.POST("/record", wkHandler.AllRecord)
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
22
pkg/common/rand.go
Normal file
22
pkg/common/rand.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Rand 生成指定长度的随机字符串,字符集为 [0-9a-zA-Z]
|
||||
func Rand(length int) string {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
seededRand := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
b := make([]byte, length)
|
||||
for i := range b {
|
||||
b[i] = charset[seededRand.Intn(len(charset))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func RandFloat64() float64 {
|
||||
return rand.Float64()
|
||||
}
|
||||
73
pkg/log/func.go
Normal file
73
pkg/log/func.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// GetLogger 获取原生 zap logger
|
||||
func GetLogger() *zap.Logger {
|
||||
return logger
|
||||
}
|
||||
|
||||
// Sync 刷新缓冲区
|
||||
func Sync() {
|
||||
_ = logger.Sync()
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 结构化日志 (Structured Logging) - 推荐高性能场景使用
|
||||
// 用法: log.Info("user login", zap.String("username", "admin"))
|
||||
// ============================================================================
|
||||
|
||||
func Debug(msg string, fields ...zap.Field) {
|
||||
logger.Debug(msg, fields...)
|
||||
}
|
||||
|
||||
func Info(msg string, fields ...zap.Field) {
|
||||
logger.Info(msg, fields...)
|
||||
}
|
||||
|
||||
func Warn(msg string, fields ...zap.Field) {
|
||||
logger.Warn(msg, fields...)
|
||||
}
|
||||
|
||||
func Error(msg string, fields ...zap.Field) {
|
||||
logger.Error(msg, fields...)
|
||||
}
|
||||
|
||||
func Fatal(msg string, fields ...zap.Field) {
|
||||
logger.Fatal(msg, fields...)
|
||||
}
|
||||
|
||||
func Panic(msg string, fields ...zap.Field) {
|
||||
logger.Panic(msg, fields...)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 格式化日志 (Sugared Logging) - 方便使用,类似 fmt.Printf
|
||||
// 用法: log.Infof("user %s login failed, count: %d", "admin", 3)
|
||||
// ============================================================================
|
||||
|
||||
func Debugf(template string, args ...interface{}) {
|
||||
sugar.Debugf(template, args...)
|
||||
}
|
||||
|
||||
func Infof(template string, args ...interface{}) {
|
||||
sugar.Infof(template, args...)
|
||||
}
|
||||
|
||||
func Warnf(template string, args ...interface{}) {
|
||||
sugar.Warnf(template, args...)
|
||||
}
|
||||
|
||||
func Errorf(template string, args ...interface{}) {
|
||||
sugar.Errorf(template, args...)
|
||||
}
|
||||
|
||||
func Fatalf(template string, args ...interface{}) {
|
||||
sugar.Fatalf(template, args...)
|
||||
}
|
||||
|
||||
func Panicf(template string, args ...interface{}) {
|
||||
sugar.Panicf(template, args...)
|
||||
}
|
||||
105
pkg/log/log.go
Normal file
105
pkg/log/log.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
logger *zap.Logger
|
||||
sugar *zap.SugaredLogger
|
||||
)
|
||||
|
||||
// 通用时间格式
|
||||
const (
|
||||
TimeFormatDate = "2006-01-02"
|
||||
TimeFormatDateTime = "2006-01-02 15:04:05"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Level string
|
||||
Filepath string
|
||||
MaxSizeMB int
|
||||
MaxAgeDay int
|
||||
Backups int
|
||||
Compress bool
|
||||
}
|
||||
|
||||
func init() {
|
||||
encoderConfig := zap.NewDevelopmentEncoderConfig()
|
||||
core := zapcore.NewCore(
|
||||
zapcore.NewConsoleEncoder(encoderConfig),
|
||||
zapcore.AddSync(os.Stdout),
|
||||
zap.DebugLevel,
|
||||
)
|
||||
|
||||
logger = zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1))
|
||||
sugar = logger.Sugar()
|
||||
}
|
||||
|
||||
func Init(cfg Config) {
|
||||
var zapLevel zapcore.Level
|
||||
|
||||
// 日志等级解析
|
||||
switch cfg.Level {
|
||||
case "debug":
|
||||
zapLevel = zap.DebugLevel
|
||||
case "info":
|
||||
zapLevel = zap.InfoLevel
|
||||
case "warning", "warn":
|
||||
zapLevel = zap.WarnLevel
|
||||
case "error":
|
||||
zapLevel = zap.ErrorLevel
|
||||
default:
|
||||
zapLevel = zap.InfoLevel
|
||||
}
|
||||
|
||||
// lumberjack 日志切割配置
|
||||
writeSyncer := zapcore.AddSync(&lumberjack.Logger{
|
||||
Filename: cfg.Filepath,
|
||||
MaxSize: cfg.MaxSizeMB,
|
||||
MaxBackups: cfg.Backups,
|
||||
MaxAge: cfg.MaxAgeDay,
|
||||
Compress: cfg.Compress,
|
||||
})
|
||||
|
||||
// 基础编码配置 (提取公共部分)
|
||||
baseEencoderConfig := zapcore.EncoderConfig{
|
||||
TimeKey: "time",
|
||||
LevelKey: "level",
|
||||
NameKey: "logger",
|
||||
CallerKey: "caller",
|
||||
MessageKey: "msg",
|
||||
StacktraceKey: "Stacktrace",
|
||||
LineEnding: zapcore.DefaultLineEnding,
|
||||
EncodeLevel: zapcore.CapitalLevelEncoder,
|
||||
EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
|
||||
enc.AppendString(t.Format(TimeFormatDateTime))
|
||||
},
|
||||
EncodeDuration: zapcore.StringDurationEncoder,
|
||||
EncodeCaller: zapcore.ShortCallerEncoder,
|
||||
}
|
||||
|
||||
// 1. 控制台编码器(开启颜色)
|
||||
consoleEncoderConfig := baseEencoderConfig
|
||||
consoleEncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
|
||||
encoderConsole := zapcore.NewConsoleEncoder(consoleEncoderConfig)
|
||||
|
||||
// 2.文件编码器(json 格式, 无色)
|
||||
fileEncoderConfig := baseEencoderConfig
|
||||
fileEncoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
|
||||
encoderJson := zapcore.NewJSONEncoder(fileEncoderConfig)
|
||||
|
||||
core := zapcore.NewTee(
|
||||
zapcore.NewCore(encoderJson, writeSyncer, zapLevel),
|
||||
zapcore.NewCore(encoderConsole, zapcore.AddSync(os.Stdout), zapLevel))
|
||||
|
||||
logger = zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1))
|
||||
sugar = logger.Sugar()
|
||||
|
||||
zap.ReplaceGlobals(logger)
|
||||
}
|
||||
71
pkg/request/client.go
Normal file
71
pkg/request/client.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package request
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
var (
|
||||
NoRedirectClient *resty.Client
|
||||
RestyClient *resty.Client
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0"
|
||||
DefaultTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Timeout time.Duration
|
||||
Proxy string
|
||||
Debug bool
|
||||
UserAgent string
|
||||
VerifySSL bool
|
||||
}
|
||||
|
||||
func DefaultConfg() *Config {
|
||||
return &Config{
|
||||
Timeout: DefaultTimeout,
|
||||
UserAgent: DefaultUserAgent,
|
||||
VerifySSL: true,
|
||||
Debug: false,
|
||||
}
|
||||
}
|
||||
|
||||
// NewClient 创建一个标准的 Resty 客户端
|
||||
func NewClient(cfg *Config) *resty.Client {
|
||||
if cfg == nil {
|
||||
cfg = DefaultConfg()
|
||||
}
|
||||
|
||||
client := resty.New()
|
||||
client.SetHeader("User-Agent", cfg.UserAgent)
|
||||
client.SetTimeout(cfg.Timeout)
|
||||
client.SetRetryCount(3)
|
||||
|
||||
client.SetTLSClientConfig(&tls.Config{
|
||||
InsecureSkipVerify: !cfg.VerifySSL,
|
||||
})
|
||||
|
||||
if cfg.Proxy != "" {
|
||||
client.SetProxy(cfg.Proxy)
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
// NewNoRedirectClient 创建一个禁止重定向的客户端
|
||||
func NewNoRedirectClient(cfg *Config) *resty.Client {
|
||||
client := NewClient(cfg)
|
||||
|
||||
client.SetRedirectPolicy(
|
||||
resty.RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}),
|
||||
)
|
||||
|
||||
return client
|
||||
}
|
||||
1
web/.gitignore
vendored
Normal file
1
web/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
frontend
|
||||
Reference in New Issue
Block a user