├── front ├── src │ ├── plugins │ │ └── element.js │ ├── assets │ │ ├── img │ │ │ ├── k.jpg │ │ │ └── background.svg │ │ ├── font │ │ │ ├── iconfont.eot │ │ │ ├── iconfont.ttf │ │ │ ├── iconfont.woff │ │ │ ├── iconfont.woff2 │ │ │ ├── iconfont.json │ │ │ ├── iconfont.css │ │ │ ├── iconfont.svg │ │ │ ├── iconfont.js │ │ │ ├── demo.css │ │ │ └── demo_index.html │ │ └── css │ │ │ └── global.css │ ├── App.vue │ ├── store │ │ └── index.js │ ├── main.js │ ├── router │ │ └── index.js │ └── components │ │ ├── FeedBack.vue │ │ ├── UpdatePass.vue │ │ ├── Home.vue │ │ ├── Login.vue │ │ ├── Register.vue │ │ └── Profile.vue ├── .browserslistrc ├── public │ ├── favicon.ico │ └── index.html ├── babel.config.js ├── README.md ├── .editorconfig ├── .eslintrc.js ├── package.json └── jest.config.js ├── doc ├── images │ ├── login.png │ ├── profile.png │ └── entry-task.drawio ├── entry │ ├── benchmark │ │ └── login.lua │ ├── deploy.md │ ├── bench.md │ └── webapi.md └── mysql │ └── db.sql ├── web ├── public │ ├── .DS_Store │ └── avatar │ │ ├── default.jpg │ │ ├── flowerk.jpeg │ │ └── khighness.jpg ├── logging │ └── log.go ├── util │ ├── file_type_test.go │ └── file_type.go ├── view │ ├── router_path.go │ └── rsp_handler.go ├── grpc │ ├── grpc_init.go │ ├── grpc_pool_test.go │ └── grpc_connector.go ├── common │ ├── constant.go │ └── model.go ├── config │ └── config.go ├── router │ └── router.go ├── middleware │ └── handler.go └── controller │ └── user_controller.go ├── bin ├── start.sh └── build.sh ├── application-web.yml ├── tcp ├── model │ ├── user_model.go │ └── mysql_init.go ├── logging │ └── log.go ├── common │ ├── common.go │ └── e │ │ └── code_msg.go ├── util │ ├── token_gen.go │ ├── check_user.go │ ├── password_test.go │ ├── password.go │ ├── check_user_test.go │ ├── fast_rand.go │ └── fast_rand_timing_test.go ├── cache │ ├── redis_init.go │ └── user_cache.go ├── config │ └── config.go ├── server │ └── server.go ├── mapper │ └── user_mapper.go └── service │ └── user_service.go ├── pkg ├── rpc │ ├── demo │ │ ├── public │ │ │ └── data.go │ │ ├── client │ │ │ └── client.go │ │ └── server │ │ │ └── server.go │ ├── codec.go │ ├── transport.go │ ├── client.go │ └── server.go └── logger │ └── log.go ├── .codeclimate.yml ├── cmd ├── web-server │ ├── main.go │ ├── Makefile │ └── Dockerfile ├── tcp-server │ ├── main.go │ ├── Dockerfile │ └── Makefile └── front-end │ ├── Dockerfile │ └── nginx.conf ├── application-tcp.yml ├── .gitignore ├── go.mod ├── LICENSE ├── pb └── user.proto └── README.md /front/src/plugins/element.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /front/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /doc/images/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khighness/entry-task/HEAD/doc/images/login.png -------------------------------------------------------------------------------- /web/public/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khighness/entry-task/HEAD/web/public/.DS_Store -------------------------------------------------------------------------------- /doc/images/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khighness/entry-task/HEAD/doc/images/profile.png -------------------------------------------------------------------------------- /front/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khighness/entry-task/HEAD/front/public/favicon.ico -------------------------------------------------------------------------------- /front/src/assets/img/k.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khighness/entry-task/HEAD/front/src/assets/img/k.jpg -------------------------------------------------------------------------------- /front/src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 7 | -------------------------------------------------------------------------------- /front/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /web/public/avatar/default.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khighness/entry-task/HEAD/web/public/avatar/default.jpg -------------------------------------------------------------------------------- /web/public/avatar/flowerk.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khighness/entry-task/HEAD/web/public/avatar/flowerk.jpeg -------------------------------------------------------------------------------- /web/public/avatar/khighness.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khighness/entry-task/HEAD/web/public/avatar/khighness.jpg -------------------------------------------------------------------------------- /front/src/assets/font/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khighness/entry-task/HEAD/front/src/assets/font/iconfont.eot -------------------------------------------------------------------------------- /front/src/assets/font/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khighness/entry-task/HEAD/front/src/assets/font/iconfont.ttf -------------------------------------------------------------------------------- /front/src/assets/font/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khighness/entry-task/HEAD/front/src/assets/font/iconfont.woff -------------------------------------------------------------------------------- /front/src/assets/font/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khighness/entry-task/HEAD/front/src/assets/font/iconfont.woff2 -------------------------------------------------------------------------------- /front/README.md: -------------------------------------------------------------------------------- 1 | ## entry-task vue frontend 2 | 3 | ### start 4 | 5 | ```shell 6 | $ npm install 7 | $ npm run serve 8 | ``` 9 | -------------------------------------------------------------------------------- /front/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /bin/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | docker run --name et-tcp-svc -d -p 20000:20000 entry/tcp-svc 4 | docker run --name et-web-svc -d -p 10000:10000 entry/web-svc 5 | docker run --name et-fe-svc -d -p 80:80 entry/fe-svc 6 | -------------------------------------------------------------------------------- /application-web.yml: -------------------------------------------------------------------------------- 1 | server: 2 | host: 0.0.0.0 3 | port: 10000 4 | 5 | rpc: 6 | addr: 0.0.0.0:20000 7 | max-open: 20000 8 | max-idle: 10000 9 | max-life-time: 3600 10 | max-idle-time: 3600 11 | 12 | -------------------------------------------------------------------------------- /tcp/model/user_model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // @Author Chen Zikang 4 | // @Email zikang.chen@shopee.com 5 | // @Since 2022-02-18 6 | 7 | // User 用户 8 | type User struct { 9 | Id int64 10 | Username string 11 | Password string 12 | ProfilePicture string 13 | } 14 | -------------------------------------------------------------------------------- /tcp/logging/log.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | 6 | "github.com/Khighness/entry-task/pkg/logger" 7 | ) 8 | 9 | // @Author Chen Zikang 10 | // @Email zikang.chen@shopee.com 11 | // @Since 2022-02-22 12 | 13 | var Log *logrus.Logger = logger.NewLogger(logrus.InfoLevel, "tcp", true) 14 | -------------------------------------------------------------------------------- /web/logging/log.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | 6 | "github.com/Khighness/entry-task/pkg/logger" 7 | ) 8 | 9 | // @Author Chen Zikang 10 | // @Email zikang.chen@shopee.com 11 | // @Since 2022-02-22 12 | 13 | var Log *logrus.Logger = logger.NewLogger(logrus.InfoLevel, "web", true) 14 | -------------------------------------------------------------------------------- /pkg/rpc/demo/public/data.go: -------------------------------------------------------------------------------- 1 | package public 2 | 3 | // @Author Chen Zikang 4 | // @Email zikang.chen@shopee.com 5 | // @Since 2022-02-21 6 | 7 | // User struct 8 | type User struct { 9 | Id int64 10 | Name string 11 | } 12 | 13 | // ResponseQueryUser struct 14 | type ResponseQueryUser struct { 15 | User 16 | Msg string 17 | } -------------------------------------------------------------------------------- /doc/entry/benchmark/login.lua: -------------------------------------------------------------------------------- 1 | function request() 2 | wrk.method = "POST" 3 | wrk.headers["content-type"] = "application/json" 4 | username = "user_" .. (2 + math.random(10000000)) 5 | password = "123456" 6 | wrk.body = string.format('{"username":"%s","password":"%s"}', username, password) 7 | return wrk.format() 8 | end 9 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | fixme: 3 | enabled: true 4 | gofmt: 5 | enabled: true 6 | golint: 7 | enabled: true 8 | govet: 9 | enabled: true 10 | exclude_paths: 11 | - grifts/**/* 12 | - "**/*_test.go" 13 | - "*_test.go" 14 | - "**_test.go" 15 | - logs/* 16 | - public/* 17 | - templates/* 18 | ratings: 19 | paths: 20 | - "**.go" 21 | -------------------------------------------------------------------------------- /cmd/web-server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/Khighness/entry-task/web/config" 5 | "github.com/Khighness/entry-task/web/grpc" 6 | "github.com/Khighness/entry-task/web/router" 7 | ) 8 | 9 | // @Author Chen Zikang 10 | // @Email zikang.chen@shopee.com 11 | // @Since 2022-02-16 12 | 13 | func main() { 14 | config.Load() 15 | grpc.InitPool() 16 | router.Start() 17 | } 18 | -------------------------------------------------------------------------------- /front/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: [ 7 | 'plugin:vue/essential', 8 | '@vue/standard' 9 | ], 10 | parserOptions: { 11 | parser: 'babel-eslint' 12 | }, 13 | rules: { 14 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 15 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /front/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | Vue.use(Vuex) 5 | 6 | export default new Vuex.Store({ 7 | state: { 8 | id: '', 9 | name: '' 10 | }, 11 | mutations: { 12 | getID (state, payload) { 13 | state.id = payload 14 | }, 15 | getName (state, payload) { 16 | state.name = payload 17 | } 18 | }, 19 | actions: { 20 | }, 21 | modules: { 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /web/util/file_type_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | // @Author Chen Zikang 10 | // @Email zikang.chen@shopee.com 11 | // @Since 2022-03-01 12 | 13 | func TestGetFileType(t *testing.T) { 14 | file, err := os.Open("/Users/zikang.chen/Pictures/khighness.jpg") 15 | if err != nil { 16 | t.Log("open file err:", err) 17 | } 18 | fSrc, _ := ioutil.ReadAll(file) 19 | t.Log(GetFileType(fSrc[:10])) 20 | } 21 | -------------------------------------------------------------------------------- /cmd/tcp-server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/Khighness/entry-task/tcp/cache" 5 | "github.com/Khighness/entry-task/tcp/config" 6 | "github.com/Khighness/entry-task/tcp/model" 7 | "github.com/Khighness/entry-task/tcp/server" 8 | ) 9 | 10 | // @Author Chen Zikang 11 | // @Email zikang.chen@shopee.com 12 | // @Since 2022-02-16 13 | 14 | func main() { 15 | config.Load() 16 | model.InitMySQL() 17 | cache.InitRedis() 18 | server.Start() 19 | } 20 | -------------------------------------------------------------------------------- /bin/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # tcp 4 | echo "$(TZ=UTC-8 date +%Y-%m-%d" "%H:%M:%S) building image tcp-svc ..." 5 | docker build -t entry/tcp-svc -f cmd/tcp-server/Dockerfile . 6 | 7 | # web 8 | echo "$(TZ=UTC-8 date +%Y-%m-%d" "%H:%M:%S) building image web-svc ..." 9 | docker build -t entry/web-svc -f cmd/web-server/Dockerfile . 10 | 11 | # vue 12 | echo "$(TZ=UTC-8 date +%Y-%m-%d" "%H:%M:%S) building image front-svc ..." 13 | docker build -t entry/front-svc -f cmd/front-end/Dockerfile . 14 | -------------------------------------------------------------------------------- /application-tcp.yml: -------------------------------------------------------------------------------- 1 | server: 2 | host: 0.0.0.0 3 | port: 20000 4 | 5 | mysql: 6 | host: 127.0.0.1 7 | port: 3306 8 | user: root 9 | pass: KANG1823 10 | name: entry_task 11 | max-open: 5000 12 | max-idle: 1000 13 | max-life-time: 3600 14 | max-idle-time: 3600 15 | 16 | redis: 17 | addr: 127.0.0.1:6379 18 | pass: KANG1823 19 | db: 0 20 | max-conn: 5000 21 | min-idle: 1000 22 | max-retries: 3 23 | dial-timeout: 5 24 | idle-timeout: 1800 25 | max-conn-age: 3600 26 | 27 | -------------------------------------------------------------------------------- /cmd/tcp-server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.17-buster as golang 2 | COPY ./tcp /src/tcp 3 | COPY ./pb /src/pb 4 | COPY ./go.mod /src/ 5 | COPY ./go.sum /src/ 6 | COPY ./cmd/tcp-server /src/cmd/tcp-server 7 | RUN cd /src/cmd/tcp-server && make linux 8 | 9 | FROM centos:7.9.2009 as linux 10 | RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime 11 | WORKDIR /var/tcp-svc/ 12 | ADD ./application-tcp.yml . 13 | COPY --from=golang /src/cmd/tcp-server/tcp-svc /bin/ 14 | EXPOSE 20000 15 | CMD ["/bin/tcp-svc"] 16 | 17 | -------------------------------------------------------------------------------- /cmd/tcp-server/Makefile: -------------------------------------------------------------------------------- 1 | GO = go 2 | PRODUCT = tcp-svc 3 | GOARCH := amd64 4 | GO111MODULE = on 5 | 6 | all: $(shell $(GO) env GOOS) 7 | 8 | build: 9 | env GO111MODULE=${GO111MODULE} GOOS=${GOOS} GOARCH=$(GOARCH) $(GO) build $(EXTFLAGS) -o $(PRODUCT)$(EXT) . 10 | 11 | linux: export GOOS=linux 12 | linux: build 13 | 14 | darwin: export GOOS=darwin 15 | darwin: EXT=.mach 16 | darwin: build 17 | 18 | run: 19 | make 20 | ./$(PRODUCT).mach 21 | 22 | .PHONY: clean 23 | clean: 24 | @rm -f $(PRODUCT) $(PRODUCT).elf $(PRODUCT).mach 25 | -------------------------------------------------------------------------------- /cmd/web-server/Makefile: -------------------------------------------------------------------------------- 1 | GO = go 2 | PRODUCT = web-svc 3 | GOARCH := amd64 4 | GO111MODULE = on 5 | 6 | all: $(shell $(GO) env GOOS) 7 | 8 | build: 9 | env GO111MODULE=${GO111MODULE} GOOS=${GOOS} GOARCH=$(GOARCH) $(GO) build $(EXTFLAGS) -o $(PRODUCT)$(EXT) . 10 | 11 | linux: export GOOS=linux 12 | linux: build 13 | 14 | darwin: export GOOS=darwin 15 | darwin: EXT=.mach 16 | darwin: build 17 | 18 | run: 19 | make 20 | ./$(PRODUCT).mach 21 | 22 | .PHONY: clean 23 | clean: 24 | @rm -f $(PRODUCT) $(PRODUCT).elf $(PRODUCT).mach 25 | -------------------------------------------------------------------------------- /web/view/router_path.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | // @Author Chen Zikang 4 | // @Email zikang.chen@shopee.com 5 | // @Since 2022-02-18 6 | 7 | const ( 8 | PingUrl = "/ping" // Ping 9 | RegisterUrl = "/register" // 注册 10 | LoginUrl = "/login" // 登录 11 | GetProfileUrl = "/user/profile" // 获取信息 12 | UpdateProfileUrl = "/user/update" // 更新信息 13 | ShowAvatarUrl = "/avatar/show/" // 展示头像 14 | UploadAvatarUrl = "/avatar/upload" // 上传头像 15 | LogoutUrl = "/logout" // 退出登录 16 | ) 17 | -------------------------------------------------------------------------------- /cmd/web-server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.17-buster as golang 2 | COPY ./web /src/web 3 | COPY ./pb /src/pb 4 | COPY ./go.mod /src/ 5 | COPY ./go.sum /src/ 6 | COPY ./cmd/web-server /src/cmd/web-server 7 | RUN cd /src/cmd/web-server && make linux 8 | 9 | FROM centos:7.9.2009 as linux 10 | RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime 11 | WORKDIR /var/web-svc/ 12 | ADD ./application-web.yml . 13 | ADD ./web/public/avatar ./web/public/avatar 14 | COPY --from=golang /src/cmd/web-server/web-svc /bin/ 15 | EXPOSE 10000 16 | CMD ["/bin/web-svc"] 17 | 18 | -------------------------------------------------------------------------------- /tcp/common/common.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | // @Author Chen Zikang 4 | // @Email zikang.chen@shopee.com 5 | // @Since 2022-02-15 6 | 7 | const ( 8 | // TokenBytes Token字节数组长度 9 | TokenBytes = 16 10 | // DefaultProfilePicture 注册账户的默认头像 11 | DefaultProfilePicture = "http://127.0.0.1:10000/avatar/show/default.jpg" 12 | ) 13 | 14 | // 注册要求 15 | const ( 16 | NameMinLen = 3 17 | NameMaxLen = 18 18 | PassMinLen = 6 19 | PassMaxLen = 20 20 | PassMinLevel = PassLevelB 21 | ) 22 | 23 | // 密码强度 24 | const ( 25 | PassLevelD = iota 26 | PassLevelC 27 | PassLevelB 28 | PassLevelA 29 | PassLevelS 30 | ) 31 | -------------------------------------------------------------------------------- /web/grpc/grpc_init.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/Khighness/entry-task/web/config" 7 | ) 8 | 9 | // @Author Chen Zikang 10 | // @Email zikang.chen@shopee.com 11 | // @Since 2022-03-09 12 | 13 | var GP *GrpcPool 14 | 15 | func InitPool() { 16 | rpcCfg := config.AppCfg.Rpc 17 | connector := GrpcConnector{GrpcServerAddr: rpcCfg.Addr} 18 | GP = NewGrpcPool(connector, &GrpcPoolConfig{ 19 | MaxOpenCount: rpcCfg.MaxOpen, 20 | MaxIdleCount: rpcCfg.MaxIdle, 21 | MaxLifeTime: time.Duration(rpcCfg.MaxLifeTime) * time.Second, 22 | MaxIdleTime: time.Duration(rpcCfg.MaxIdleTime) * time.Second, 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /cmd/front-end/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:17-buster as node 2 | COPY ./front/public /entry-task/public 3 | COPY ./front/src /entry-task/src 4 | COPY ./front/package.json /entry-task/ 5 | COPY ./front/package-lock.json /entry-task/ 6 | COPY ./front/.browserslistrc /entry-task/ 7 | COPY ./front/.editorconfig /entry-task/ 8 | COPY ./front/babel.config.js /entry-task/ 9 | COPY ./front/jest.config.js /entry-task/ 10 | RUN npm install -g npm@8.8.0 11 | RUN cd /entry-task && npm install && npm run build 12 | 13 | FROM nginx:1.21.6 as nginx 14 | WORKDIR /var/ 15 | COPY --from=node /entry-task/dist /entry-task 16 | COPY ./cmd/front-end/nginx.conf /etc/nginx/nginx.conf 17 | -------------------------------------------------------------------------------- /web/common/constant.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | // @Author Chen Zikang 4 | // @Email zikang.chen@shopee.com 5 | // @Since 2022-02-15 6 | 7 | const ( 8 | // HeaderTokenKey HTTP请求Header中的令牌Key 9 | HeaderTokenKey = "Authorization" 10 | // AvatarStoragePath 头像存储相对路径 11 | AvatarStoragePath = "./web/public/avatar/" 12 | ) 13 | 14 | const ( 15 | RpcSuccessCode = 10000 16 | HttpSuccessCode = 10000 17 | HttpSuccessMessage = "SUCCESS" 18 | HttpErrorCode = 20000 19 | HttpErrorMessage = "ERROR" 20 | HttpErrorServerBusyCode = 20001 21 | HttpErrorServerBusyMessage = "Server is busy, please try again later" 22 | HttpErrorRpcRequestCode = 20002 23 | HttpErrorRpcRequestMessage = "RPC failed or timeout" 24 | ) 25 | -------------------------------------------------------------------------------- /pkg/rpc/demo/client/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/gob" 5 | "log" 6 | "net" 7 | 8 | "github.com/Khighness/entry-task/pkg/rpc" 9 | "github.com/Khighness/entry-task/pkg/rpc/demo/public" 10 | ) 11 | 12 | // @Author KHighness 13 | // @Email zikang.chen@shopee.com 14 | // @Since 2022-02-20 15 | 16 | var QueryUser func(int64) (public.ResponseQueryUser, error) 17 | 18 | func main() { 19 | gob.Register(public.ResponseQueryUser{}) 20 | 21 | conn, err := net.Dial("tcp", "0.0.0.0:30000") 22 | if err != nil { 23 | log.Fatalln(err) 24 | } 25 | client := rpc.NewClient(conn) 26 | client.Call("queryUser", &QueryUser) 27 | response, err := QueryUser(1) 28 | if err != nil { 29 | log.Fatalln(err) 30 | } 31 | log.Printf("%+v", response) 32 | } 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Goland workspace 4 | .idea/ 5 | 6 | # log 7 | log/ 8 | .log 9 | 10 | # Binaries for programs and plugins 11 | *.exe 12 | *.exe~ 13 | *.dll 14 | *.so 15 | *.dylib 16 | 17 | # Test binary, built with `demo test -c` 18 | *.test 19 | 20 | # Output of the demo coverage tool, specifically when used with LiteIDE 21 | *.out 22 | 23 | # Dependency directories (remove the comment below to include it) 24 | # vendor/ 25 | 26 | # Libary 27 | node_modules 28 | 29 | # Build 30 | dist/ 31 | 32 | 33 | # local env files 34 | .env.local 35 | .env.*.local 36 | 37 | # Log files 38 | npm-debug.log* 39 | yarn-debug.log* 40 | yarn-error.log* 41 | pnpm-debug.log* 42 | 43 | # Editor directories and files 44 | .vscode 45 | *.suo 46 | *.ntvs* 47 | *.njsproj 48 | *.sln 49 | *.sw? 50 | -------------------------------------------------------------------------------- /front/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 12 | 13 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /pkg/rpc/codec.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | ) 7 | 8 | // @Author Chen Zikang 9 | // @Email zikang.chen@shopee.com 10 | // @Since 2022-02-21 11 | 12 | // Data presents the pb transported between server and client 13 | type Data struct { 14 | Name string // service name 15 | Args []interface{} // request's or response's body 16 | Err string // remote server error 17 | } 18 | 19 | // Encode data 20 | func Encode(data Data) ([]byte, error) { 21 | var buf bytes.Buffer 22 | encoder := gob.NewEncoder(&buf) 23 | if err := encoder.Encode(data); err != nil { 24 | return nil, err 25 | } 26 | return buf.Bytes(), nil 27 | } 28 | 29 | // Decode data 30 | func Decode(b []byte) (Data, error) { 31 | buf := bytes.NewBuffer(b) 32 | decoder := gob.NewDecoder(buf) 33 | var data Data 34 | if err := decoder.Decode(&data); err != nil { 35 | return Data{}, err 36 | } 37 | return data, nil 38 | } 39 | -------------------------------------------------------------------------------- /cmd/front-end/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes 6; 3 | error_log /var/log/nginx/error.log warn; 4 | pid /var/run/nginx.pid; 5 | events { 6 | worker_connections 1024; 7 | } 8 | http { 9 | include /etc/nginx/mime.types; 10 | default_type application/octet-stream; 11 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 12 | '$status $body_bytes_sent "$http_referer" ' 13 | '"$http_user_agent" "$http_x_forwarded_for"'; 14 | access_log /var/log/nginx/access.log main; 15 | sendfile on; 16 | keepalive_timeout 65; 17 | server { 18 | listen 80; 19 | server_name localhost; 20 | location / { 21 | root /entry-task; 22 | index index.html; 23 | try_files $uri $uri/ /index.html; 24 | } 25 | error_page 500 502 503 504 /50x.html; 26 | location = /50x.html { 27 | root /usr/share/nginx/html; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /front/src/assets/css/global.css: -------------------------------------------------------------------------------- 1 | /* 全局样式表 */ 2 | html, 3 | body, 4 | #app { 5 | height: 100%; 6 | margin: 0; 7 | padding: 0; 8 | } 9 | 10 | .el-breadcrumb{ 11 | margin-bottom: 15px; 12 | font-size: 12px; 13 | } 14 | 15 | .el-card{ 16 | box-shadow: 0 1px 1px raga(0 0 0 0.15) !important; 17 | } 18 | 19 | .el-table{ 20 | margin-top: 15px; 21 | font-size: 15px; 22 | } 23 | 24 | .el-pagination{ 25 | margin-top: 15px; 26 | } 27 | 28 | .avatar-uploader .el-upload { 29 | border: 1px dashed #d9d9d9; 30 | border-radius: 6px; 31 | cursor: pointer; 32 | position: relative; 33 | overflow: hidden; 34 | } 35 | .avatar-uploader .el-upload:hover { 36 | border-color: #409EFF; 37 | } 38 | .avatar-uploader-icon { 39 | font-size: 28px; 40 | color: #8c939d; 41 | width: 178px; 42 | height: 178px; 43 | line-height: 178px; 44 | text-align: center; 45 | } 46 | .avatar { 47 | width: 178px; 48 | height: 178px; 49 | display: block; 50 | } -------------------------------------------------------------------------------- /front/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import store from './store' 5 | import axios from 'axios' 6 | import elementUI from 'element-ui' 7 | import locale from 'element-ui/lib/locale/lang/en' 8 | import 'element-ui/lib/theme-chalk/index.css' 9 | import './plugins/element.js' 10 | import './assets/css/global.css' 11 | import './assets/font/iconfont.css' 12 | import VueCropper from 'vue-cropper' 13 | 14 | Vue.use(VueCropper) 15 | 16 | /* 阻止启动生产信息 */ 17 | Vue.config.productionTip = false 18 | 19 | /* 全局使用ElementUI */ 20 | Vue.use(elementUI, { locale }) 21 | 22 | /* axios */ 23 | axios.defaults.baseURL = 'http://127.0.0.1:10000/' 24 | 25 | /* 拦截器,在header中添加token */ 26 | axios.interceptors.request.use(config => { 27 | config.headers.Authorization = window.sessionStorage.getItem('entry-token') 28 | return config 29 | }) 30 | Vue.prototype.$http = axios 31 | 32 | new Vue({ 33 | router, 34 | store, 35 | render: h => h(App) 36 | }).$mount('#app') 37 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Khighness/entry-task 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/go-redis/redis v6.15.9+incompatible 7 | github.com/go-sql-driver/mysql v1.6.0 8 | github.com/sirupsen/logrus v1.8.1 9 | github.com/stretchr/testify v1.7.0 10 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 11 | google.golang.org/grpc v1.44.0 12 | google.golang.org/protobuf v1.27.1 13 | gopkg.in/ini.v1 v1.66.4 14 | ) 15 | 16 | require ( 17 | github.com/davecgh/go-spew v1.1.1 // indirect 18 | github.com/golang/protobuf v1.5.2 // indirect 19 | github.com/onsi/ginkgo v1.16.5 // indirect 20 | github.com/onsi/gomega v1.18.1 // indirect 21 | github.com/pmezard/go-difflib v1.0.0 // indirect 22 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 // indirect 23 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect 24 | golang.org/x/text v0.3.6 // indirect 25 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect 26 | gopkg.in/yaml.v2 v2.4.0 // indirect 27 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect 28 | ) 29 | -------------------------------------------------------------------------------- /web/common/model.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | // @Author Chen Zikang 4 | // @Email zikang.chen@shopee.com 5 | // @Since 2022-02-17 6 | 7 | // UserInfo 用户信息 8 | type UserInfo struct { 9 | Id int64 `json:"id"` 10 | Username string `json:"username"` 11 | ProfilePicture string `json:"profilePicture"` 12 | } 13 | 14 | // HttpResponse 接口返回信息 15 | type HttpResponse struct { 16 | Code int32 `json:"code"` 17 | Message string `json:"message"` 18 | Data interface{} `json:"data"` 19 | } 20 | 21 | // LoginRequest 登陆请求 22 | type LoginRequest struct { 23 | Username string `json:"username"` 24 | Password string `json:"password"` 25 | } 26 | 27 | // LoginResponse 登陆结果 28 | type LoginResponse struct { 29 | Token string `json:"token"` 30 | User UserInfo `json:"user"` 31 | } 32 | 33 | // RegisterRequest 注册请求 34 | type RegisterRequest struct { 35 | Username string `json:"username"` 36 | Password string `json:"password"` 37 | } 38 | 39 | // UpdateProfileRequest 更新账户请求 40 | type UpdateProfileRequest struct { 41 | Username string `json:"username"` 42 | } 43 | -------------------------------------------------------------------------------- /pkg/rpc/demo/server/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/gob" 5 | "errors" 6 | "log" 7 | 8 | "github.com/Khighness/entry-task/pkg/rpc" 9 | "github.com/Khighness/entry-task/pkg/rpc/demo/public" 10 | ) 11 | 12 | // @Author Chen Zikang 13 | // @Email zikang.chen@shopee.com 14 | // @Since 2022-02-21 15 | 16 | type userService struct{} 17 | 18 | func (u userService) queryUser(uid int64) (public.ResponseQueryUser, error) { 19 | db := make(map[int64]public.User) 20 | db[0] = public.User{Id: 0, Name: "KHighness"} 21 | db[1] = public.User{Id: 1, Name: "FlowerK"} 22 | if u, ok := db[uid]; ok { 23 | return public.ResponseQueryUser{User: u, Msg: "success"}, nil 24 | } 25 | return public.ResponseQueryUser{User: public.User{}, Msg: "fail"}, errors.New("uid is not in database") 26 | } 27 | 28 | func main() { 29 | gob.Register(public.ResponseQueryUser{}) 30 | 31 | addr := "127.0.0.1:30000" 32 | srv := rpc.NewServer(addr) 33 | service := userService{} 34 | srv.Register("queryUser", service.queryUser) 35 | log.Printf("Server is running at %v\n", addr) 36 | srv.Run() 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Chen Zikang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /web/grpc/grpc_pool_test.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "testing" 8 | "time" 9 | 10 | "github.com/Khighness/entry-task/pb" 11 | ) 12 | 13 | // @Author Chen Zikang 14 | // @Email zikang.chen@shopee.com 15 | // @Since 2022-03-08 16 | 17 | func Test(t *testing.T) { 18 | connector := GrpcConnector{GrpcServerAddr: "127.0.0.1:20000"} 19 | grpcPool := NewGrpcPool(connector, &GrpcPoolConfig{ 20 | MaxOpenCount: 10, 21 | MaxIdleCount: 5, 22 | MaxLifeTime: 10 * time.Second, 23 | MaxIdleTime: 5 * time.Second, 24 | }) 25 | 26 | go func() { 27 | req := &pb.LoginRequest{ 28 | Username: "Khighness", 29 | Password: "czk911", 30 | } 31 | f := func(cli pb.UserServiceClient) (interface{}, error) { 32 | return cli.Login(context.Background(), req) 33 | } 34 | rsp, err := grpcPool.Exec(f) 35 | if err != nil { 36 | fmt.Println(err) 37 | } else { 38 | valueOf := reflect.ValueOf(rsp) 39 | response := valueOf.Interface().(*pb.LoginResponse) 40 | fmt.Printf("%+v\n", response) 41 | } 42 | }() 43 | 44 | time.Sleep(3 * time.Second) 45 | fmt.Printf("%+v\n", grpcPool.Stat()) 46 | } 47 | -------------------------------------------------------------------------------- /tcp/util/token_gen.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | 7 | "github.com/Khighness/entry-task/tcp/common" 8 | ) 9 | 10 | // @Author Chen Zikang 11 | // @Email zikang.chen@shopee.com 12 | // @Since 2022-02-15 13 | 14 | // GenerateToken 生成token 15 | // '0' 48 16 | // 'A' 65 17 | func GenerateToken() string { 18 | //var sessionId bytes.Buffer 19 | 20 | // 生成随机16位byte 21 | buf := make([]byte, common.TokenBytes) 22 | for i := 0; i < common.TokenBytes; i++ { 23 | buf[i] = byte(Uint32()) 24 | } 25 | 26 | // md5计算消息摘要 27 | hash := md5.New() 28 | hash.Write(buf) 29 | buf = hash.Sum(nil) 30 | 31 | return hex.EncodeToString(buf) //test: 670ns 32 | 33 | // 转换为十六进制大写字符串 34 | //for i := 0; i < common.TokenBytes; i++ { 35 | // var b1 byte = (buf[i] & 0xf0) >> 4 36 | // var b2 byte = buf[i] & 0x0f 37 | // if b1 < 10 { 38 | // sessionId.WriteByte(48 + b1) 39 | // } else { 40 | // sessionId.WriteByte(55 + b1) 41 | // } 42 | // if b2 < 10 { 43 | // sessionId.WriteByte(48 + b2) 44 | // } else { 45 | // sessionId.WriteByte(55 + b2) 46 | // } 47 | //} 48 | // 49 | //return sessionId.String() // test: 1 us 50 | } 51 | -------------------------------------------------------------------------------- /tcp/util/check_user.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/Khighness/entry-task/tcp/common" 7 | "github.com/Khighness/entry-task/tcp/common/e" 8 | ) 9 | 10 | // @Author Chen Zikang 11 | // @Email zikang.chen@shopee.com 12 | // @Since 2022-02-16 13 | 14 | // CheckUsername 校验用户名 15 | func CheckUsername(username string) int { 16 | if len(username) < common.NameMinLen { 17 | return e.ErrorUsernameTooShort 18 | } 19 | if len(username) > common.NameMaxLen { 20 | return e.ErrorUsernameTooLong 21 | } 22 | return e.SUCCESS 23 | } 24 | 25 | // CheckPassword 校验密码 26 | func CheckPassword(password string) int { 27 | if len(password) < common.PassMinLen { 28 | return e.ErrorPasswordTooShort 29 | } 30 | if len(password) > common.PassMaxLen { 31 | return e.ErrorPasswordTooLong 32 | } 33 | 34 | var level int = common.PassLevelD 35 | patternList := []string{`[0-9]+`, `[a-z]+`, `[A-Z]+`, `[~!@#$%^&*_+]`} 36 | for _, pattern := range patternList { 37 | match, _ := regexp.MatchString(pattern, password) 38 | if match { 39 | level++ 40 | } 41 | } 42 | 43 | if level < common.PassMinLevel { 44 | return e.ErrorPasswordNotStrong 45 | } 46 | return e.SUCCESS 47 | } 48 | -------------------------------------------------------------------------------- /web/grpc/grpc_connector.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "time" 7 | 8 | "github.com/Khighness/entry-task/pb" 9 | "google.golang.org/grpc" 10 | "google.golang.org/grpc/credentials/insecure" 11 | "google.golang.org/grpc/keepalive" 12 | ) 13 | 14 | // @Author Chen Zikang 15 | // @Email zikang.chen@shopee.com 16 | // @Since 2022-03-08 17 | 18 | var errConnFailed error = errors.New("connect to server failed") 19 | 20 | // GrpcConnector grpc连接器 21 | type GrpcConnector struct { 22 | GrpcServerAddr string 23 | } 24 | 25 | // Connect 连接grpc服务器,返回客户端 26 | func (connector *GrpcConnector) Connect(ctx context.Context) (pb.UserServiceClient, error) { 27 | clientParameters := keepalive.ClientParameters{ 28 | Time: 30 * time.Second, // 客户端每空闲30s ping一下服务器 29 | Timeout: 1 * time.Second, // 假设连接已死之前等待1s,等待ping的ack确认 30 | PermitWithoutStream: true, // 即使没有活动流也允许ping 31 | } 32 | cc, err := grpc.Dial(connector.GrpcServerAddr, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithKeepaliveParams(clientParameters)) 33 | if err != nil { 34 | return nil, errConnFailed 35 | } 36 | client := pb.NewUserServiceClient(cc) 37 | return client, nil 38 | } 39 | -------------------------------------------------------------------------------- /web/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "io/ioutil" 5 | 6 | "gopkg.in/yaml.v2" 7 | 8 | "github.com/Khighness/entry-task/web/logging" 9 | ) 10 | 11 | // @Author Chen Zikang 12 | // @Email zikang.chen@shopee.com 13 | // @Since 2022-04-28 14 | 15 | type AppConfig struct { 16 | Server *ServerConfig `yaml:"server"` 17 | Rpc *RpcConfig `yaml:"rpc"` 18 | } 19 | 20 | type ServerConfig struct { 21 | Host string `yaml:"host"` 22 | Port int `yaml:"port"` 23 | } 24 | 25 | type RpcConfig struct { 26 | Addr string `yaml:"addr"` 27 | MaxOpen int `yaml:"max-open"` 28 | MaxIdle int `yaml:"max-idle"` 29 | MaxLifeTime int `yaml:"max-life-time"` 30 | MaxIdleTime int `yaml:"max-idle-time"` 31 | } 32 | 33 | var AppCfg *AppConfig 34 | 35 | // Load 导入配置 36 | func Load() { 37 | AppCfg = &AppConfig{} 38 | applicationFile, err := ioutil.ReadFile("application-web.yml") 39 | if err != nil { 40 | logging.Log.Fatal("Failed to load application configuration file") 41 | } 42 | err = yaml.Unmarshal(applicationFile, AppCfg) 43 | if err != nil { 44 | logging.Log.Fatal("Failed to read application configuration file") 45 | } 46 | logging.Log.Println("Succeed to load application configuration file") 47 | } 48 | -------------------------------------------------------------------------------- /tcp/util/password_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | // @Author Chen Zikang 12 | // @Email zikang.chen@shopee.com 13 | // @Since 2022-02-16 14 | 15 | var pass = "123456" 16 | 17 | // BCR 加密和解密时间,都大于200ms,太恐怖了 18 | func TestEncryptAndVerifyByBCR(t *testing.T) { 19 | encryptStartTime := time.Now() 20 | hash, err := EncryptPassByBCR(pass) 21 | fmt.Println("encrypt time:", time.Since(encryptStartTime)) 22 | fmt.Printf("encrypt:%s, len:%d\n", hash, len(hash)) 23 | assert.Nil(t, err) 24 | verifyStartTime := time.Now() 25 | result := VerifyPassByBCR(pass, hash) 26 | fmt.Println("verify time:", time.Since(verifyStartTime)) 27 | assert.Equal(t, true, result) 28 | } 29 | 30 | // MD5 加密5us,解密600ns 31 | func TestEncryptAndVerifyByMD5(t *testing.T) { 32 | encryptStartTime := time.Now() 33 | hash, err := EncryptPassByMd5(pass) 34 | fmt.Println("encrypt time:", time.Since(encryptStartTime)) 35 | fmt.Printf("encrypt:%s, len:%d\n", hash, len(hash)) 36 | assert.Nil(t, err) 37 | verifyStartTime := time.Now() 38 | result := VerifyPassByMD5(pass, hash) 39 | fmt.Println("verify time:", time.Since(verifyStartTime)) 40 | assert.Equal(t, true, result) 41 | 42 | fmt.Println(EncryptPassByMd5("czk911")) 43 | } 44 | -------------------------------------------------------------------------------- /front/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "entry_task", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "axios": "^0.21.1", 12 | "babel-runtime": "^6.26.0", 13 | "core-js": "^3.6.5", 14 | "element-ui": "^2.14.1", 15 | "jspdf": "^2.2.0", 16 | "vue": "^2.6.11", 17 | "vue-cropper": "^0.5.6", 18 | "vue-router": "^3.2.0", 19 | "vuex": "^3.4.0" 20 | }, 21 | "devDependencies": { 22 | "@vue/cli-plugin-babel": "~4.5.0", 23 | "@vue/cli-plugin-eslint": "~4.5.0", 24 | "@vue/cli-plugin-router": "~4.5.0", 25 | "@vue/cli-plugin-vuex": "~4.5.0", 26 | "@vue/cli-service": "~4.5.0", 27 | "@vue/eslint-config-standard": "^5.1.2", 28 | "babel-eslint": "^10.1.0", 29 | "eslint": "^6.7.2", 30 | "eslint-plugin-import": "^2.20.2", 31 | "eslint-plugin-node": "^11.1.0", 32 | "eslint-plugin-promise": "^4.2.1", 33 | "eslint-plugin-standard": "^4.0.0", 34 | "eslint-plugin-vue": "^6.2.2", 35 | "html2canvas": "^1.0.0-rc.7", 36 | "less": "^4.0.0", 37 | "less-loader": "^7.2.1", 38 | "vue-cli-plugin-element": "^1.0.1", 39 | "vue-template-compiler": "^2.6.11" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tcp/util/password.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | 7 | "golang.org/x/crypto/bcrypt" 8 | ) 9 | 10 | // @Author Chen Zikang 11 | // @Email zikang.chen@shopee.com 12 | // @Since 2022-02-15 13 | 14 | const ( 15 | PasswordCost = 12 16 | ) 17 | 18 | // EncryptPassByBCR BCR加密 19 | func EncryptPassByBCR(password string) (string, error) { 20 | bytes, err := bcrypt.GenerateFromPassword([]byte(password), PasswordCost) 21 | if err != nil { 22 | return "", err 23 | } 24 | return string(bytes), nil 25 | } 26 | 27 | // VerifyPassByBCR BCR校验 28 | // password 用户输入密码 29 | // hashedPassword 数据库存储密码 30 | func VerifyPassByBCR(password string, hashedPassword string) bool { 31 | err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) 32 | return err == nil 33 | } 34 | 35 | // EncryptPassByMd5 MD5加密 36 | func EncryptPassByMd5(password string) (string, error) { 37 | hash := md5.New() 38 | _, err := hash.Write([]byte(password)) 39 | if err != nil { 40 | return "", err 41 | } 42 | bytes := hash.Sum(nil) 43 | return hex.EncodeToString(bytes), nil 44 | } 45 | 46 | // VerifyPassByMD5 MD5校验 47 | // password 用户输入密码 48 | // hashedPassword 数据库存储密码 49 | func VerifyPassByMD5(password string, hashedPassword string) bool { 50 | inputPassword, _ := EncryptPassByMd5(password) 51 | return inputPassword == hashedPassword 52 | } 53 | -------------------------------------------------------------------------------- /pkg/rpc/transport.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | "net" 7 | ) 8 | 9 | // @Author Chen Zikang 10 | // @Email zikang.chen@shopee.com 11 | // @Since 2022-02-21 12 | 13 | // Transport struct 14 | type Transport struct { 15 | conn net.Conn 16 | } 17 | 18 | // NewTransport creates a transport 19 | func NewTransport(conn net.Conn) *Transport { 20 | return &Transport{conn} 21 | } 22 | 23 | // Send data 24 | func (t *Transport) Send(req Data) error { 25 | // Encode request into bytes 26 | b, err := Encode(req) 27 | if err != nil { 28 | return err 29 | } 30 | buf := make([]byte, 4+len(b)) 31 | 32 | // set header field 33 | binary.BigEndian.PutUint32(buf[:4], uint32(len(b))) 34 | // set content field 35 | copy(buf[4:], b) 36 | 37 | _, err = t.conn.Write(buf) 38 | return err 39 | } 40 | 41 | // Receive data 42 | func (t *Transport) Receive() (Data, error) { 43 | header := make([]byte, 4) 44 | _, err := io.ReadFull(t.conn, header) 45 | if err != nil { 46 | return Data{}, err 47 | } 48 | 49 | // read header field 50 | dataLen := binary.BigEndian.Uint32(header) 51 | // read content field 52 | content := make([]byte, dataLen) 53 | 54 | _, err = io.ReadFull(t.conn, content) 55 | if err != nil { 56 | return Data{}, err 57 | } 58 | // Decode response from bytes 59 | rsp, err := Decode(content) 60 | return rsp, err 61 | } 62 | -------------------------------------------------------------------------------- /tcp/cache/redis_init.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/go-redis/redis" 7 | 8 | "github.com/Khighness/entry-task/tcp/config" 9 | "github.com/Khighness/entry-task/tcp/logging" 10 | ) 11 | 12 | // @Author Chen Zikang 13 | // @Email zikang.chen@shopee.com 14 | // @Since 2022-02-15 15 | 16 | var ( 17 | RedisClient *redis.Client 18 | ) 19 | 20 | // InitRedis 初始化Redis连接池 21 | func InitRedis() { 22 | RedisClient = ConnectRedis(config.AppCfg.Redis) 23 | } 24 | 25 | // ConnectRedis 连接到Redis 26 | func ConnectRedis(redisCfg *config.RedisConfig) *redis.Client { 27 | options := &redis.Options{ 28 | Addr: redisCfg.Addr, 29 | Password: redisCfg.Pass, 30 | DB: redisCfg.Db, 31 | PoolSize: redisCfg.MaxConn, 32 | MinIdleConns: redisCfg.MinIdle, 33 | MaxRetries: redisCfg.MaxRetries, 34 | DialTimeout: time.Duration(redisCfg.DialTimeout) * time.Second, 35 | IdleTimeout: time.Duration(redisCfg.IdleTimeout) * time.Second, 36 | MaxConnAge: time.Duration(redisCfg.MaxConnAge) * time.Second, 37 | } 38 | redisClient := redis.NewClient(options) 39 | 40 | if _, err := redisClient.Ping().Result(); err != nil { 41 | logging.Log.Fatalf("Failed to connect to redis server [%s]: %s", redisCfg.Addr, err) 42 | } else { 43 | logging.Log.Printf("Succeed to connect to redis server [%s]", redisCfg.Addr) 44 | } 45 | return redisClient 46 | } 47 | -------------------------------------------------------------------------------- /front/src/assets/font/iconfont.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "2301690", 3 | "name": "排考系统", 4 | "font_family": "iconfont", 5 | "css_prefix_text": "icon", 6 | "description": "", 7 | "glyphs": [ 8 | { 9 | "icon_id": "2117103", 10 | "name": "邮箱", 11 | "font_class": "youxiang", 12 | "unicode": "e632", 13 | "unicode_decimal": 58930 14 | }, 15 | { 16 | "icon_id": "902049", 17 | "name": "密码", 18 | "font_class": "mima", 19 | "unicode": "e639", 20 | "unicode_decimal": 58937 21 | }, 22 | { 23 | "icon_id": "7685145", 24 | "name": "lock", 25 | "font_class": "lock", 26 | "unicode": "e708", 27 | "unicode_decimal": 59144 28 | }, 29 | { 30 | "icon_id": "7736478", 31 | "name": "users", 32 | "font_class": "users", 33 | "unicode": "e92e", 34 | "unicode_decimal": 59694 35 | }, 36 | { 37 | "icon_id": "9757345", 38 | "name": "折叠", 39 | "font_class": "zhedie", 40 | "unicode": "e601", 41 | "unicode_decimal": 58881 42 | }, 43 | { 44 | "icon_id": "16780733", 45 | "name": "登录-验证码", 46 | "font_class": "denglu-yanzhengma", 47 | "unicode": "e60c", 48 | "unicode_decimal": 58892 49 | }, 50 | { 51 | "icon_id": "16780734", 52 | "name": "登录-用户名", 53 | "font_class": "denglu-yonghuming", 54 | "unicode": "e60d", 55 | "unicode_decimal": 58893 56 | } 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /front/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | 4 | import Login from '@/components/Login.vue' 5 | import Register from '@/components/Register.vue' 6 | import Home from '@/components/Home' 7 | import Profile from '@/components/Profile' 8 | import UpdatePass from '@/components/UpdatePass' 9 | import FeedBack from '@/components/FeedBack.vue' 10 | 11 | Vue.use(VueRouter) 12 | 13 | const router = new VueRouter({ 14 | routes: [ 15 | { 16 | path: '/', 17 | redirect: '/login' 18 | }, { 19 | path: '/login', 20 | component: Login 21 | }, { 22 | path: '/register', 23 | component: Register 24 | }, { 25 | name: 'home', 26 | path: '/home', 27 | component: Home, 28 | redirect: '/profile', 29 | children: [ 30 | { path: '/profile', component: Profile }, 31 | { path: '/updatePass', component: UpdatePass }, 32 | { path: '/feedback', component: FeedBack } 33 | ] 34 | }] 35 | }) 36 | 37 | // 挂载路由导航守卫 38 | router.beforeEach((to, from, next) => { 39 | // to代表将要访问的路径 40 | // from代表从哪个路径跳转 41 | // next是一个函数,表示放行 42 | // next()放行 next('/login')强制跳转到/login页面 43 | if (to.path === '/login') { return next() } 44 | if (to.path === '/register') { return next() } 45 | // 获取token 46 | const token = window.sessionStorage.getItem('entry-token') 47 | if (!token) { return next('/login') } 48 | next() 49 | }) 50 | 51 | export default router 52 | -------------------------------------------------------------------------------- /tcp/model/mysql_init.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/Khighness/entry-task/tcp/config" 9 | "github.com/Khighness/entry-task/tcp/logging" 10 | _ "github.com/go-sql-driver/mysql" 11 | ) 12 | 13 | // @Author Chen Zikang 14 | // @Email zikang.chen@shopee.com 15 | // @Since 2022-02-15 16 | 17 | var ( 18 | DB *sql.DB 19 | ) 20 | 21 | // InitMySQL 初始化MySQL连接池 22 | func InitMySQL() { 23 | DB = ConnectMySQL(config.AppCfg.MySQL) 24 | } 25 | 26 | // ConnectMySQL 连接到MySQL 27 | func ConnectMySQL(mysqlCfg *config.MySQLConfig) *sql.DB { 28 | url := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?%s", mysqlCfg.User, mysqlCfg.Pass, mysqlCfg.Host, mysqlCfg.Port, mysqlCfg.Name, "charset=utf8mb4&parseTime=true") 29 | db, err := sql.Open("mysql", url) 30 | if err != nil { 31 | logging.Log.Fatalf("Wrong configuration of [MySQL] in config file: %s", err) 32 | } 33 | 34 | // 最大连接数 35 | db.SetMaxOpenConns(mysqlCfg.MaxOpen) 36 | // 闲置连接数 37 | db.SetMaxIdleConns(mysqlCfg.MaxIdle) 38 | // 最大存活时间 39 | db.SetConnMaxLifetime(time.Duration(mysqlCfg.MaxLifeTime) * time.Second) 40 | // 最大空闲时间 41 | db.SetConnMaxIdleTime(time.Duration(mysqlCfg.MaxIdleTime) * time.Second) 42 | 43 | if err = db.Ping(); err != nil { 44 | logging.Log.Fatalf("Failed to connect to mysql server [%s]: %s", fmt.Sprintf("%s:%d", mysqlCfg.Host, mysqlCfg.Port), err) 45 | } else { 46 | logging.Log.Infof("Succeed to connect to mysql server [%s]", fmt.Sprintf("%s:%d", mysqlCfg.Host, mysqlCfg.Port)) 47 | } 48 | return db 49 | } 50 | -------------------------------------------------------------------------------- /tcp/common/e/code_msg.go: -------------------------------------------------------------------------------- 1 | package e 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/Khighness/entry-task/tcp/common" 7 | ) 8 | 9 | // @Author Chen Zikang 10 | // @Email zikang.chen@shopee.com 11 | // @Since 2022-02-17 12 | 13 | // RPC 状态码 14 | const ( 15 | SUCCESS = 10000 16 | ERROR = 20000 17 | 18 | ErrorUsernameTooShort = 30001 19 | ErrorUsernameTooLong = 30002 20 | ErrorUsernameAlreadyExist = 30003 21 | ErrorPasswordTooShort = 30004 22 | ErrorPasswordTooLong = 30005 23 | ErrorPasswordNotStrong = 30006 24 | ErrorUsernameIncorrect = 30007 25 | ErrorPasswordIncorrect = 30008 26 | ErrorTokenIncorrect = 30009 27 | ErrorTokenExpired = 30010 28 | 29 | ErrorOperateDatabase = 40001 30 | ) 31 | 32 | // 状态码对应信息字典 33 | var codeMessageDic = map[int]string{ 34 | SUCCESS: "SUCCESS", 35 | ERROR: "ERROR", 36 | 37 | ErrorUsernameTooShort: fmt.Sprintf("用户名长度不得小于%d", common.NameMinLen), 38 | ErrorUsernameTooLong: fmt.Sprintf("用户名长度不得大于%d", common.NameMaxLen), 39 | ErrorUsernameAlreadyExist: "用户名已存在,请换个试试", 40 | ErrorPasswordTooShort: fmt.Sprintf("密码长度不得小于%d", common.PassMinLen), 41 | ErrorPasswordTooLong: fmt.Sprintf("密码长度不得大于%d", common.PassMaxLen), 42 | ErrorPasswordNotStrong: "密码强度较弱,最少需要包含数字/字母/特殊符号中的以上两种", 43 | ErrorUsernameIncorrect: "用户名错误", 44 | ErrorPasswordIncorrect: "密码错误", 45 | ErrorTokenIncorrect: "令牌非法", 46 | ErrorTokenExpired: "登陆状态已过期", 47 | 48 | ErrorOperateDatabase: "操作数据库失败", 49 | } 50 | 51 | // GetMsg 根据状态码获取信息 52 | func GetMsg(code int) string { 53 | msg, ok := codeMessageDic[code] 54 | if ok { 55 | return msg 56 | } 57 | return codeMessageDic[ERROR] 58 | } 59 | -------------------------------------------------------------------------------- /web/util/file_type.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "strconv" 7 | "strings" 8 | "sync" 9 | ) 10 | 11 | // @Author Chen Zikang 12 | // @Email zikang.chen@shopee.com 13 | // @Since 2022-03-01 14 | 15 | var fileTypeMap sync.Map 16 | 17 | func init() { 18 | fileTypeMap.Store("ffd8ff", "jpg") //JPEG (jpg) 19 | fileTypeMap.Store("89504e47", "png") //PNG (png) 20 | fileTypeMap.Store("47494638", "gif") //GIF (gif) 21 | fileTypeMap.Store("49492a00", "tif") //TIFF (tif) 22 | fileTypeMap.Store("424d", "bmp") //Windows Bitmap(bmp) 23 | fileTypeMap.Store("41433130", "dwg") //CAD (dwg) 24 | fileTypeMap.Store("255044462d312e", "pdf") //Adobe Acrobat (pdf) 25 | } 26 | 27 | // GetFileType 通过文件前几个字节判断文件内容类型 28 | func GetFileType(fSrc []byte) string { 29 | var fileType string 30 | fileCode := bytesToHexString(fSrc) 31 | 32 | fileTypeMap.Range(func(key, value interface{}) bool { 33 | k := key.(string) 34 | v := value.(string) 35 | if strings.HasPrefix(fileCode, strings.ToLower(k)) || 36 | strings.HasSuffix(k, strings.ToLower(fileCode)) { 37 | fileType = v 38 | return false 39 | } 40 | return true 41 | }) 42 | return fileType 43 | } 44 | 45 | // bytesToHexString 将字节数组转换为十六进制字符串 46 | func bytesToHexString(src []byte) string { 47 | res := bytes.Buffer{} 48 | if src == nil || len(src) <= 0 { 49 | return "" 50 | } 51 | temp := make([]byte, 0) 52 | for _, v := range src { 53 | sub := v & 0xFF 54 | hv := hex.EncodeToString(append(temp, sub)) 55 | if len(hv) < 2 { 56 | res.WriteString(strconv.FormatInt(int64(0), 10)) 57 | } 58 | res.WriteString(hv) 59 | } 60 | return res.String() 61 | } 62 | -------------------------------------------------------------------------------- /tcp/util/check_user_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/Khighness/entry-task/tcp/common/e" 9 | ) 10 | 11 | // @Author Chen Zikang 12 | // @Email zikang.chen@shopee.com 13 | // @Since 2022-02-16 14 | 15 | func TestCheckUsername(t *testing.T) { 16 | name1 := "k" 17 | name2 := "zzzzzzzzzzkkkkkkkkkk" 18 | name3 := "chen zikang" 19 | var status int 20 | 21 | status = CheckUsername(name1) 22 | assert.Equal(t, e.ErrorUsernameTooShort, status) 23 | status = CheckUsername(name2) 24 | assert.Equal(t, e.ErrorUsernameTooLong, status) 25 | status = CheckUsername(name3) 26 | assert.Equal(t, e.SUCCESS, status) 27 | } 28 | 29 | func TestCheckPassword(t *testing.T) { 30 | pass1 := "k" // error 31 | pass2 := "zzzzzzzzzzzkkkkkkkkkkk" // error 32 | pass3 := "123456" // level 1 33 | pass4 := "chen zikang" // level 1 34 | pass5 := "czk123" // level 2 35 | pass6 := "czk123CZK" // level 3 36 | pass7 := "czk123@CZK" // level 4 37 | var status int 38 | 39 | status = CheckPassword(pass1) 40 | assert.Equal(t, e.ErrorPasswordTooShort, status) 41 | status = CheckPassword(pass2) 42 | assert.Equal(t, e.ErrorPasswordTooLong, status) 43 | status = CheckPassword(pass3) 44 | assert.Equal(t, e.ErrorPasswordNotStrong, status) 45 | status = CheckPassword(pass4) 46 | assert.Equal(t, e.ErrorPasswordNotStrong, status) 47 | status = CheckPassword(pass5) 48 | assert.Equal(t, e.SUCCESS, status) 49 | status = CheckPassword(pass6) 50 | assert.Equal(t, e.SUCCESS, status) 51 | status = CheckPassword(pass7) 52 | assert.Equal(t, e.SUCCESS, status) 53 | } 54 | -------------------------------------------------------------------------------- /pb/user.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "./pb"; 4 | 5 | service UserService { 6 | rpc Register (RegisterRequest) returns (RegisterResponse) { } 7 | rpc Login (LoginRequest) returns (LoginResponse) { } 8 | rpc CheckToken (CheckTokenRequest) returns (CheckTokenResponse) {} 9 | rpc GetProfile (GetProfileRequest) returns (GetProfileResponse) {} 10 | rpc UpdateProfile (UpdateProfileRequest) returns (UpdateProfileResponse) {} 11 | rpc Logout (LogoutRequest) returns (LogoutResponse) {} 12 | } 13 | 14 | message User { 15 | int64 id = 1; 16 | string username = 2; 17 | string profile_picture = 4; 18 | } 19 | 20 | message RegisterRequest { 21 | string username = 1; 22 | string password = 2; 23 | } 24 | 25 | message RegisterResponse { 26 | int32 code = 1; 27 | string msg = 2; 28 | } 29 | 30 | message LoginRequest { 31 | string username = 1; 32 | string password = 2; 33 | } 34 | 35 | message LoginResponse { 36 | int32 code = 1; 37 | string msg = 2; 38 | string token = 3; 39 | User user = 5; 40 | } 41 | 42 | message CheckTokenRequest { 43 | string token = 1; 44 | } 45 | 46 | message CheckTokenResponse { 47 | int32 code = 1; 48 | string msg = 2; 49 | } 50 | 51 | message GetProfileRequest { 52 | string token = 1; 53 | } 54 | 55 | message GetProfileResponse { 56 | int32 code = 1; 57 | string msg = 2; 58 | User user = 3; 59 | } 60 | 61 | message UpdateProfileRequest { 62 | string token = 1; 63 | string username = 2; 64 | string profile_picture = 4; 65 | } 66 | 67 | message UpdateProfileResponse { 68 | int32 code = 1; 69 | string msg = 2; 70 | } 71 | 72 | message LogoutRequest { 73 | string token = 1; 74 | } 75 | 76 | message LogoutResponse { 77 | int32 code = 1; 78 | string msg = 2; 79 | } 80 | 81 | 82 | -------------------------------------------------------------------------------- /tcp/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "io/ioutil" 5 | 6 | "gopkg.in/yaml.v2" 7 | 8 | "github.com/Khighness/entry-task/tcp/logging" 9 | ) 10 | 11 | // @Author Chen Zikang 12 | // @Email zikang.chen@shopee.com 13 | // @Since 2022-04-28 14 | 15 | type AppConfig struct { 16 | Server *ServerConfig `yaml:"server"` 17 | MySQL *MySQLConfig `yaml:"mysql"` 18 | Redis *RedisConfig `yaml:"redis"` 19 | } 20 | 21 | type ServerConfig struct { 22 | Host string `yaml:"host"` 23 | Port int `yaml:"port"` 24 | } 25 | 26 | type MySQLConfig struct { 27 | Host string `yaml:"host"` 28 | Port int `yaml:"port"` 29 | User string `yaml:"user"` 30 | Pass string `yaml:"pass"` 31 | Name string `yaml:"name"` 32 | MaxOpen int `yaml:"max-open"` 33 | MaxIdle int `yaml:"max-idle"` 34 | MaxLifeTime int `yaml:"max-life-time"` 35 | MaxIdleTime int `yaml:"max-idle-time"` 36 | } 37 | 38 | type RedisConfig struct { 39 | Addr string `yaml:"addr"` 40 | Pass string `yaml:"pass"` 41 | Db int `yaml:"db"` 42 | MaxConn int `yaml:"max-conn"` 43 | MinIdle int `yaml:"min-idle"` 44 | DialTimeout int `yaml:"dial-timeout"` 45 | IdleTimeout int `yaml:"idle-timeout"` 46 | MaxRetries int `yaml:"max-retries"` 47 | MaxConnAge int `yaml:"max-conn-age"` 48 | } 49 | 50 | var AppCfg *AppConfig 51 | 52 | // Load 导入配置 53 | func Load() { 54 | AppCfg = &AppConfig{} 55 | applicationFile, err := ioutil.ReadFile("application-tcp.yml") 56 | if err != nil { 57 | logging.Log.Fatal("Failed to load application configuration file") 58 | } 59 | err = yaml.Unmarshal(applicationFile, AppCfg) 60 | if err != nil { 61 | logging.Log.Fatal("Failed to read application configuration file") 62 | } 63 | logging.Log.Println("Succeed to load application configuration file") 64 | } 65 | -------------------------------------------------------------------------------- /web/router/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/Khighness/entry-task/web/config" 8 | "github.com/Khighness/entry-task/web/controller" 9 | "github.com/Khighness/entry-task/web/logging" 10 | "github.com/Khighness/entry-task/web/middleware" 11 | "github.com/Khighness/entry-task/web/view" 12 | ) 13 | 14 | // @Author Chen Zikang 15 | // @Email zikang.chen@shopee.com 16 | // @Since 2022-02-15 17 | 18 | // Start 启动web server 19 | func Start() { 20 | userController := controller.UserController{} 21 | http.HandleFunc(view.PingUrl, middleware.CorsMiddleWare(middleware.TimeMiddleWare(userController.Ping))) 22 | http.HandleFunc(view.RegisterUrl, middleware.CorsMiddleWare(middleware.TimeMiddleWare(userController.Register))) 23 | http.HandleFunc(view.LoginUrl, middleware.CorsMiddleWare(middleware.TimeMiddleWare(userController.Login))) 24 | http.HandleFunc(view.GetProfileUrl, middleware.CorsMiddleWare(middleware.TimeMiddleWare(middleware.TokenMiddleWare(userController.GetProfile)))) 25 | http.HandleFunc(view.UpdateProfileUrl, middleware.CorsMiddleWare(middleware.TimeMiddleWare(middleware.TokenMiddleWare(userController.UpdateProfile)))) 26 | http.HandleFunc(view.ShowAvatarUrl, middleware.CorsMiddleWare(middleware.TimeMiddleWare(userController.ShowAvatar))) 27 | http.HandleFunc(view.UploadAvatarUrl, middleware.CorsMiddleWare(middleware.TimeMiddleWare(middleware.TokenMiddleWare(userController.UploadAvatar)))) 28 | http.HandleFunc(view.LogoutUrl, middleware.CorsMiddleWare(middleware.TimeMiddleWare(userController.Logout))) 29 | 30 | serverCfg := config.AppCfg.Server 31 | serverAddr := fmt.Sprintf("%s:%d", serverCfg.Host, serverCfg.Port) 32 | logging.Log.Infof("Web server is serving at [%s]", serverAddr) 33 | err := http.ListenAndServe(serverAddr, nil) 34 | if err != nil { 35 | logging.Log.Fatalf("Failed to start web server, error: %s", err) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tcp/util/fast_rand.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | // @Author Chen Zikang 9 | // @Email zikang.chen@shopee.com 10 | // @Since 2022-02-15 11 | 12 | // Uint32 returns pseudorandom uint32. 13 | // It is safe calling this function from concurrent goroutines. 14 | func Uint32() uint32 { 15 | v := rngPool.Get() 16 | if v == nil { 17 | v = &RNG{} 18 | } 19 | r := v.(*RNG) 20 | x := r.Uint32() 21 | rngPool.Put(r) 22 | return x 23 | } 24 | 25 | var rngPool sync.Pool 26 | 27 | // Uint32n returns pseudorandom uint32 in the range [0..maxN). 28 | // It is safe calling this function from concurrent goroutines. 29 | func Uint32n(maxN uint32) uint32 { 30 | x := Uint32() 31 | // See http://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/ 32 | return uint32((uint64(x) * uint64(maxN)) >> 32) 33 | } 34 | 35 | // RNG is a pseudorandom number generator. 36 | // It is unsafe to call RNG methods from concurrent goroutines. 37 | type RNG struct { 38 | x uint32 39 | } 40 | 41 | // Uint32 returns pseudorandom uint32. 42 | // It is unsafe to call this method from concurrent goroutines. 43 | func (r *RNG) Uint32() uint32 { 44 | for r.x == 0 { 45 | r.x = getRandomUint32() 46 | } 47 | 48 | // See https://en.wikipedia.org/wiki/Xorshift 49 | x := r.x 50 | x ^= x << 13 51 | x ^= x >> 17 52 | x ^= x << 5 53 | r.x = x 54 | return x 55 | } 56 | 57 | // Uint32n returns pseudorandom uint32 in the range [0..maxN). 58 | // It is unsafe to call this method from concurrent goroutines. 59 | func (r *RNG) Uint32n(maxN uint32) uint32 { 60 | x := r.Uint32() 61 | // See http://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/ 62 | return uint32((uint64(x) * uint64(maxN)) >> 32) 63 | } 64 | 65 | // Seed sets the r state to n. 66 | func (r *RNG) Seed(n uint32) { 67 | r.x = n 68 | } 69 | 70 | func getRandomUint32() uint32 { 71 | x := time.Now().UnixNano() 72 | return uint32((x >> 32) ^ x) 73 | } 74 | -------------------------------------------------------------------------------- /tcp/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net" 7 | "time" 8 | 9 | "google.golang.org/grpc" 10 | "google.golang.org/grpc/keepalive" 11 | "google.golang.org/grpc/reflection" 12 | 13 | "github.com/Khighness/entry-task/pb" 14 | "github.com/Khighness/entry-task/tcp/cache" 15 | "github.com/Khighness/entry-task/tcp/config" 16 | "github.com/Khighness/entry-task/tcp/logging" 17 | "github.com/Khighness/entry-task/tcp/mapper" 18 | "github.com/Khighness/entry-task/tcp/model" 19 | "github.com/Khighness/entry-task/tcp/service" 20 | ) 21 | 22 | // @Author Chen Zikang 23 | // @Email zikang.chen@shopee.com 24 | // @Since 2022-02-17 25 | 26 | // Start 启动tcp server 27 | func Start() { 28 | serverCfg := config.AppCfg.Server 29 | serverAddr := fmt.Sprintf("%s:%d", serverCfg.Host, serverCfg.Port) 30 | listener, err := net.Listen("tcp", serverAddr) 31 | if err != nil { 32 | log.Fatalln("Failed to start tcp server :", err) 33 | } 34 | 35 | enforcementPolicy := keepalive.EnforcementPolicy{ 36 | MinTime: 5 * time.Minute, // 客户端两次ping的等待 37 | PermitWithoutStream: true, // 即使没有活动流也允许ping 38 | } 39 | serverParameters := keepalive.ServerParameters{ 40 | MaxConnectionIdle: 30 * time.Minute, // 如果客户端空闲30m,断连 41 | MaxConnectionAge: time.Hour, // 任何客户端存活1h,断连 42 | MaxConnectionAgeGrace: 5 * time.Second, // 在强制关闭连接之前,等待5s,让rpc完成 43 | Time: 1 * time.Minute, // 如果客户端空闲1分钟,ping客户端以确保连接正常 44 | Timeout: 1 * time.Second, // 如果ping请求1s内未恢复,则认为连接断开 45 | } 46 | s := grpc.NewServer(grpc.KeepaliveEnforcementPolicy(enforcementPolicy), grpc.KeepaliveParams(serverParameters)) 47 | 48 | userMapper := mapper.NewUserMapper(model.DB) 49 | userCache := cache.NewUserCache(cache.RedisClient) 50 | userService := service.NewUserService(userMapper, userCache) 51 | pb.RegisterUserServiceServer(s, userService) 52 | reflection.Register(s) 53 | logging.Log.Printf("GRPC tcp server is serving at [%s]", serverAddr) 54 | 55 | if err = s.Serve(listener); err != nil { 56 | logging.Log.Fatalf("GRPC tcp server failed to serve at [%s]: %s", serverAddr, err) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /web/view/rsp_handler.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/Khighness/entry-task/web/common" 8 | ) 9 | 10 | // @Author Chen Zikang 11 | // @Email zikang.chen@shopee.com 12 | // @Since 2022-02-23 13 | 14 | // HandleBizSuccess 处理业务成功结果 15 | func HandleBizSuccess(w http.ResponseWriter, data interface{}) { 16 | response, _ := json.Marshal(common.HttpResponse{ 17 | Code: common.HttpSuccessCode, 18 | Message: common.HttpSuccessMessage, 19 | Data: data, 20 | }) 21 | _, _ = w.Write(response) 22 | } 23 | 24 | // HandleBizError 处理业务错误结果 25 | func HandleBizError(w http.ResponseWriter, data interface{}) { 26 | response, _ := json.Marshal(common.HttpResponse{ 27 | Code: common.HttpErrorCode, 28 | Message: common.HttpErrorMessage, 29 | Data: data, 30 | }) 31 | _, _ = w.Write(response) 32 | } 33 | 34 | // HandleErrorServerBusy 处理服务繁忙 35 | func HandleErrorServerBusy(w http.ResponseWriter) { 36 | response, _ := json.Marshal(common.HttpResponse{ 37 | Code: common.HttpErrorServerBusyCode, 38 | Message: common.HttpErrorServerBusyMessage, 39 | }) 40 | _, _ = w.Write(response) 41 | } 42 | 43 | // HandleErrorRpcRequest 处理RPC请求错误 44 | func HandleErrorRpcRequest(w http.ResponseWriter) { 45 | response, _ := json.Marshal(common.HttpResponse{ 46 | Code: common.HttpErrorRpcRequestCode, 47 | Message: common.HttpErrorRpcRequestMessage, 48 | }) 49 | _, _ = w.Write(response) 50 | } 51 | 52 | // HandleErrorRpcResponse 处理RPC结果错误 53 | func HandleErrorRpcResponse(w http.ResponseWriter, code int32, msg string) { 54 | response, _ := json.Marshal(common.HttpResponse{ 55 | Code: code, 56 | Message: msg, 57 | }) 58 | _, _ = w.Write(response) 59 | } 60 | 61 | // HandleMethodError 处理方法错误 62 | func HandleMethodError(w http.ResponseWriter, data interface{}) { 63 | response, _ := json.Marshal(common.HttpResponse{ 64 | Code: http.StatusMethodNotAllowed, 65 | Message: "Method Not Allowed", 66 | Data: data, 67 | }) 68 | _, _ = w.Write(response) 69 | } 70 | 71 | // HandleRequestError 处理请求错误 72 | func HandleRequestError(w http.ResponseWriter, data interface{}) { 73 | response, _ := json.Marshal(common.HttpResponse{ 74 | Code: http.StatusBadRequest, 75 | Message: "Bad Request", 76 | Data: data, 77 | }) 78 | _, _ = w.Write(response) 79 | } 80 | -------------------------------------------------------------------------------- /doc/mysql/db.sql: -------------------------------------------------------------------------------- 1 | -- 建表 2 | CREATE TABLE `user` ( 3 | `id` int NOT NULL AUTO_INCREMENT COMMENT 'ID', 4 | `username` varchar(20) CHARACTER SET utf8mb4 DEFAULT '' COMMENT '用户名', 5 | `password` varchar(60) CHARACTER SET utf8mb4 DEFAULT '' COMMENT '密码', 6 | `profile_picture` varchar(100) CHARACTER SET utf8mb4 DEFAULT '' COMMENT '头像', 7 | PRIMARY KEY (`id`), 8 | UNIQUE INDEX index_username(`username`) 9 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 10 | 11 | -- 初始化 12 | INSERT INTO entry_task.user (id, username, password, profile_picture) VALUES (1, 'KHighness', 'cea13832a6a48e6b83c472a50ca55934', 'http://127.0.0.1:10000/avatar/show/khighness.jpg'); 13 | INSERT INTO entry_task.user (id, username, password, profile_picture) VALUES (2, 'FlowerK', 'cea13832a6a48e6b83c472a50ca55934', 'http://127.0.0.1:10000/avatar/show/flowerk.jpeg'); 14 | 15 | -- 存储过程:从起始start_id开始,插入max_num条数据 16 | DELIMITER $$ 17 | CREATE PROCEDURE insert_user(IN start_id INT(10),IN max_num INT(10)) 18 | BEGIN 19 | DECLARE i INT DEFAULT 0; 20 | SET @user_prefix = 'user_'; 21 | SET autocommit = 0; 22 | REPEAT 23 | INSERT INTO entry_task.user(id,username,password,profile_picture) VALUES((start_id+i),CONCAT(@user_prefix,start_id+i),'e10adc3949ba59abbe56e057f20f883e','http://127.0.0.1:10000/avatar/default.jpg'); 24 | SET i = i + 1; 25 | UNTIL i = max_num 26 | END REPEAT; 27 | COMMIT; 28 | END $$ 29 | 30 | -- 插入1000,0000条数据 31 | call insert_user(3, 10000000) 32 | 33 | -- 函数:产生n位随机名字 34 | -- DELIMITER $$ 35 | -- CREATE FUNCTION rand_name(n INT) RETURNS VARCHAR(255) 36 | -- BEGIN 37 | -- DECLARE chars_str VARCHAR(100) DEFAULT '@0123456789abcdefghijklmnopqrstuvwsyzABCDEFGHIJKLMNOPQRSTUVWXYZ='; 38 | -- DECLARE return_str VARCHAR(255) DEFAULT ''; 39 | -- DECLARE i INT DEFAULT 0; 40 | -- WHILE i < n DO 41 | -- SET return_str = CONCAT(return_str,SUBSTRING(chars_str,FLOOR(1+RAND()*64),1)); 42 | -- SET i = i + 1; 43 | -- END WHILE; 44 | -- RETURN return_str; 45 | -- END $$ 46 | 47 | -- 最大连接数 48 | -- show variables like '%max_connections%'; 49 | -- 服务器响应的最大连接数 50 | -- show global status like 'Max_used_connections'; 51 | -- 设置最大连接数 52 | -- set global max_connections = 10000; 53 | -- 客户端连接 54 | -- show processlist; 55 | -- 客户端连接ip数 56 | -- select SUBSTRING_INDEX(host,':',1) as ip , count(*) from information_schema.processlist group by ip 57 | -------------------------------------------------------------------------------- /pkg/rpc/client.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "reflect" 7 | ) 8 | 9 | // @Author Chen Zikang 10 | // @Email zikang.chen@shopee.com 11 | // @Since 2022-02-21 12 | 13 | // Client struct 14 | type Client struct { 15 | conn net.Conn 16 | } 17 | 18 | // NewClient create a new client 19 | func NewClient(conn net.Conn) *Client { 20 | return &Client{conn} 21 | } 22 | 23 | // GetConn returns the connection to server 24 | func (c *Client) GetConn() net.Conn { 25 | return c.conn 26 | } 27 | 28 | // Call transforms a function prototype into a function 29 | func (c *Client) Call(name string, fptr interface{}) { 30 | container := reflect.ValueOf(fptr).Elem() 31 | 32 | f := func(req []reflect.Value) []reflect.Value { 33 | cliTransport := NewTransport(c.conn) 34 | 35 | errorHandler := func(err error) []reflect.Value { 36 | outArgs := make([]reflect.Value, container.Type().NumOut()) 37 | for i := 0; i < len(outArgs)-1; i++ { 38 | outArgs[i] = reflect.Zero(container.Type().Out(i)) 39 | } 40 | outArgs[len(outArgs)-1] = reflect.ValueOf(&err).Elem() 41 | return outArgs 42 | } 43 | 44 | // package request arguments 45 | inArgs := make([]interface{}, 0, len(req)) 46 | for i := range req { 47 | inArgs = append(inArgs, req[i].Interface()) 48 | } 49 | // send request to server 50 | err := cliTransport.Send(Data{Name: name, Args: inArgs}) 51 | if err != nil { // local network error or Decode error 52 | return errorHandler(err) 53 | } 54 | // receive response from server 55 | rsp, err := cliTransport.Receive() 56 | if err != nil { // local network error or Decode error 57 | return errorHandler(errors.New(rsp.Err)) 58 | } 59 | if len(rsp.Args) == 0 { 60 | rsp.Args = make([]interface{}, container.Type().NumOut()) 61 | } 62 | // un package response arguments 63 | numOut := container.Type().NumOut() 64 | outArgs := make([]reflect.Value, numOut) 65 | for i := 0; i < numOut; i++ { 66 | if i != numOut-1 { // un package arguments 67 | if rsp.Args[i] == nil { 68 | outArgs[i] = reflect.Zero(container.Type().Out(i)) 69 | } else { 70 | outArgs[i] = reflect.ValueOf(rsp.Args[i]) 71 | } 72 | } else { // un package error 73 | outArgs[i] = reflect.Zero(container.Type().Out(i)) 74 | } 75 | } 76 | 77 | return outArgs 78 | } 79 | 80 | container.Set(reflect.MakeFunc(container.Type(), f)) 81 | } 82 | -------------------------------------------------------------------------------- /doc/entry/deploy.md: -------------------------------------------------------------------------------- 1 | ## entry task deploy document 2 | 3 | ### Run in local development 4 | 5 | 6 | #### 🐬 MySQL 7 | 8 | 9 | ```shell 10 | $ mkdir -p ~/Docker/mysql/data ~/Docker/mysql/conf 11 | $ docker run --name mysql -d -p 3306:3306 \ 12 | -e MYSQL_ROOT_PASSWORD=KANG1823 mysql:8.0.20 13 | $ docker cp mysql:/etc/mysql/my.cnf ~/Docker/mysql/conf 14 | $ cat << EOF >>~/Docker/mysql/conf/my.cnf 15 | [mysqld] 16 | character-set-server=utf8 17 | max_connections=30000 18 | [client] 19 | default-character-set=utf8 20 | [mysql] 21 | default-character-set=utf8 22 | EOF 23 | 24 | $ docker stop mysql && docker rm mysql 25 | $ docker run --name mysql \ 26 | -d -p 3306:3306 \ 27 | -e MYSQL_ROOT_PASSWORD=KANG1823 \ 28 | -v ~/Docker/mysql/conf/my.cnf:/etc/mysql/my.cnf \ 29 | -v ~/Docker/mysql/data:/var/lib/mysql \ 30 | --restart=on-failure:3 \ 31 | mysql:8.0.20 32 | $ docker exec -it mysql bash 33 | $ mysql -u root -p KANG1823 34 | $ ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'KANG1823'; 35 | ``` 36 | 37 | 38 | 39 | #### 💠 Redis 40 | 41 | 42 | ```shell 43 | $ mkdir -p ~/Docker/redis/data ~/Docker/redis/conf 44 | $ cd ~/Docker/redis/conf 45 | $ touch redis.conf 46 | $ cat << EOF >>~/Docker/redis/conf/redis.conf 47 | port 6379 48 | daemonize no 49 | protected-mode no 50 | requirepass KANG1823 51 | loglevel notice 52 | 53 | maxmemory-policy volatile-ttl 54 | slowlog-log-slower-than 2000 55 | maxclients 30000 56 | timeout 3600 57 | 58 | dir /usr/local/redis/data/ 59 | appendonly yes 60 | appendfilename "appendonly.aof" 61 | appendfsync no 62 | auto-aof-rewrite-min-size 128mb 63 | dbfilename dump.rdb 64 | save 900 1 65 | EOF 66 | 67 | $ docker run -d -p 6379:6379 --name redis \ 68 | -v ~/Docker/redis/data:/data \ 69 | -v ~/Docker/redis/conf/redis.conf:/etc/redis/redis.conf \ 70 | redis:6.2.6 \ 71 | --requirepass "KANG1823" 72 | ``` 73 | 74 | 75 | 76 | #### 🚀 Start 77 | 78 | 1. 导入脚本 79 | 80 | ``` 81 | ./doc/mysql/db.sql 82 | ``` 83 | 84 | 2. 下载依赖 85 | 86 | ```shell 87 | $ go mod tidy 88 | ``` 89 | 90 | 3. 启动tcp server 91 | 92 | ```shell 93 | $ go run cmd/tcp-server/main.go 94 | ``` 95 | 96 | 4. 启动web server 97 | 98 | ```shell 99 | $ go run cmd/web-server/main.go 100 | ``` 101 | 102 | 5. 启动vue 103 | 104 | ```shell 105 | $ cd front 106 | $ npm install 107 | $ npm run serve 108 | ``` 109 | 110 | 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## entry-task 2 | 3 |

4 | go 5 | vue 6 | docker 7 | kubernetes 8 |

9 | 10 |

11 | 📑 WEBAPI | 🚀 DEPLOY | 🛳 BENCH 12 |

13 | 14 | 15 | 16 | ### Feature 17 | 18 | - Native http API based on Go lib 19 | - Prefect connection pool for RPC 20 | - Beautiful front page built from VUE 21 | - Elegant code style and exhaustive comments 22 | 23 | 24 | 25 | ### Structure 26 | 27 | ``` 28 | entry-task 29 | ├─bin scripts 30 | ├─cmd startup 31 | ├─doc document 32 | ├─front frontend 33 | ├─pb grpc proto 34 | ├─pkg rpc and log 35 | ├─tcp tcp-server 36 | └─web web-server 37 | ``` 38 | 39 | 40 | 41 | ### Architecture 42 | 43 |

44 | Architecture 45 |

46 | 47 | 48 | ### Preview 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 |
loginprofile
60 | 61 | 62 | ### Build 63 | 64 | First, you should modify the configuration files `application-tcp.yml` and `application-web.yml`. 65 | 66 | Next, you can build the docker images by the following command: 67 | ```shell 68 | ./bin/build.sh 69 | ``` 70 | 71 | Then, you can start the services by the following command: 72 | ```shell 73 | ./bin/start.sh 74 | ``` 75 | 76 | 77 | 78 | ### Extension 79 | 80 | The branch [master](https://github.com/Khighness/entry-task/tree/master) use the grpc. If you need the custom rpc, please switch to branch [develop](https://github.com/Khighness/entry-task/tree/develop). 81 | 82 | 83 | 84 | ### License 85 | 86 | Khighness's entry-task is open-sourced software licensed under the [MIT license](https://github.com/Khighness/entry-task/blob/master/LICENSE). 87 | -------------------------------------------------------------------------------- /tcp/cache/user_cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/go-redis/redis" 7 | 8 | "github.com/Khighness/entry-task/tcp/model" 9 | ) 10 | 11 | // @Author Chen Zikang 12 | // @Email zikang.chen@shopee.com 13 | // @Since 2022-02-15 14 | 15 | const ( 16 | // UserTokenKeyPrefix 用户token存储的key 17 | UserTokenKeyPrefix = "entry:user:token:" 18 | // UserTokenTimeout 用户token过期时间 19 | UserTokenTimeout = time.Hour * 24 20 | ) 21 | 22 | // UserCache 用户缓存操作 23 | type UserCache struct { 24 | client *redis.Client 25 | } 26 | 27 | // NewUserCache 创建 UserCache 28 | func NewUserCache(client *redis.Client) *UserCache { 29 | return &UserCache{client: client} 30 | } 31 | 32 | // GetUserId 获取用户id 33 | func (userCache *UserCache) GetUserId(token string) (int64, error) { 34 | return userCache.client.HGet(userCache.generateUserTokenKey(token), "id").Int64() 35 | } 36 | 37 | // GetUserInfo 获取用户信息 38 | func (userCache *UserCache) GetUserInfo(token string) (*model.User, error) { 39 | userTokenKey := userCache.generateUserTokenKey(token) 40 | id, err := userCache.client.HGet(userTokenKey, "id").Int64() 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | username := userCache.client.HGet(userTokenKey, "username").Val() 46 | profilePicture := userCache.client.HGet(userTokenKey, "profile_picture").Val() 47 | return &model.User{ 48 | Id: id, 49 | Username: username, 50 | Password: "", 51 | ProfilePicture: profilePicture, 52 | }, nil 53 | } 54 | 55 | // SetUserInfo 缓存用户信息 56 | func (userCache *UserCache) SetUserInfo(token string, user *model.User) { 57 | userTokenKey := userCache.generateUserTokenKey(token) 58 | userCache.client.HSet(userTokenKey, "id", user.Id) 59 | userCache.client.HSet(userTokenKey, "username", user.Username) 60 | userCache.client.HSet(userTokenKey, "profile_picture", user.ProfilePicture) 61 | userCache.client.Expire(userTokenKey, UserTokenTimeout) 62 | } 63 | 64 | // DelUserInfo 删除用户信息 65 | func (userCache *UserCache) DelUserInfo(token string) { 66 | userCache.client.Del(userCache.generateUserTokenKey(token)) 67 | } 68 | 69 | // SetUserField 缓存用户字段信息 70 | func (userCache *UserCache) SetUserField(token, key, val string) { 71 | userCache.client.HSet(userCache.generateUserTokenKey(token), key, val) 72 | } 73 | 74 | // DelUserField 删除用户字段信息 75 | func (userCache *UserCache) DelUserField(token, key string) { 76 | userCache.client.HDel(userCache.generateUserTokenKey(token), key) 77 | } 78 | 79 | // generateUserTokenKey 生成用户token的key 80 | func (userCache *UserCache) generateUserTokenKey(token string) string { 81 | return UserTokenKeyPrefix + token 82 | } 83 | -------------------------------------------------------------------------------- /tcp/mapper/user_mapper.go: -------------------------------------------------------------------------------- 1 | package mapper 2 | 3 | import ( 4 | "database/sql" 5 | 6 | "github.com/Khighness/entry-task/tcp/model" 7 | ) 8 | 9 | // @Author Chen Zikang 10 | // @Email zikang.chen@shopee.com 11 | // @Since 2022-02-16 12 | 13 | // UserMapper 用户数据库操作 14 | type UserMapper struct { 15 | db *sql.DB 16 | } 17 | 18 | // NewUserMapper 创建 UserMapper 19 | func NewUserMapper(db *sql.DB) *UserMapper { 20 | return &UserMapper{db: db} 21 | } 22 | 23 | // SaveUser 保存用户信息 24 | func (userMapper *UserMapper) SaveUser(user *model.User) (int64, error) { 25 | result, err := userMapper.db.Exec("INSERT INTO user(`username`, `password`, `profile_picture`) values(?, ?, ?)", user.Username, user.Password, user.ProfilePicture) 26 | if err != nil { 27 | return 0, err 28 | } 29 | id, _ := result.LastInsertId() 30 | return id, nil 31 | } 32 | 33 | // UpdateUserUsernameById 根据id更新用户名 34 | func (userMapper *UserMapper) UpdateUserUsernameById(id int64, username string) error { 35 | _, err := userMapper.db.Exec("UPDATE `user` SET `username` = ? WHERE `id` = ?", username, id) 36 | return err 37 | } 38 | 39 | // UpdateUserProfilePictureById 根据id更新用户头像 40 | func (userMapper *UserMapper) UpdateUserProfilePictureById(id int64, profilePicture string) error { 41 | _, err := userMapper.db.Exec("UPDATE `user` SET `profile_picture` = ? WHERE `id` = ?", profilePicture, id) 42 | return err 43 | } 44 | 45 | // CheckUserUsernameExist 检查用户名是否已存在 46 | // MySQL比较字符串在大小写敏感的情况下,必须转binary 47 | // 在binary下,查询username无法走index_username 48 | // 为了保证性能,用户名的大小写不敏感 49 | func (userMapper *UserMapper) CheckUserUsernameExist(username string) (bool, error) { 50 | var count int64 51 | err := userMapper.db.QueryRow("SELECT COUNT(`username`) FROM `user` WHERE `username` = ?", username).Scan(&count) 52 | if err != nil { 53 | return false, err 54 | } 55 | return count > 0, nil 56 | } 57 | 58 | // QueryUserById 根据id查询用户信息 59 | func (userMapper *UserMapper) QueryUserById(id int64) (user *model.User, err error) { 60 | user = new(model.User) 61 | err = userMapper.db.QueryRow("SELECT `username`, `password`, `profile_picture` FROM `user` WHERE id = ?", id).Scan(&user.Username, &user.Password, &user.ProfilePicture) 62 | if err != nil { 63 | return nil, err 64 | } 65 | return user, nil 66 | } 67 | 68 | // QueryUserByUsername 根据username查询用户信息 69 | func (userMapper *UserMapper) QueryUserByUsername(username string) (user *model.User, err error) { 70 | user = new(model.User) 71 | err = userMapper.db.QueryRow("SELECT `id`, `password`, `profile_picture` FROM `user` WHERE `username` = ?", username).Scan(&user.Id, &user.Password, &user.ProfilePicture) 72 | if err != nil { 73 | return nil, err 74 | } 75 | return user, nil 76 | } 77 | -------------------------------------------------------------------------------- /web/middleware/handler.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/Khighness/entry-task/pb" 9 | "github.com/Khighness/entry-task/web/common" 10 | "github.com/Khighness/entry-task/web/grpc" 11 | "github.com/Khighness/entry-task/web/logging" 12 | "github.com/Khighness/entry-task/web/view" 13 | ) 14 | 15 | // @Author Chen Zikang 16 | // @Email zikang.chen@shopee.com 17 | // @Since 2022-02-15 18 | 19 | // MiddleHandler 封装接口 20 | type MiddleHandler func(next http.HandlerFunc) http.HandlerFunc 21 | 22 | // CorsMiddleWare 处理跨域 23 | func CorsMiddleWare(next http.HandlerFunc) http.HandlerFunc { 24 | return func(w http.ResponseWriter, r *http.Request) { 25 | // 允许访问所有域 26 | w.Header().Set("Access-Control-Allow-Origin", "*") 27 | // 允许header值 28 | w.Header().Add("Access-Control-Allow-Headers", "Content-Type, AccessToken, X-CSRF-Token, Authorization, Token") 29 | // 允许携带cookie 30 | w.Header().Add("Access-Control-Allow-Credentials", "true") 31 | // 允许请求方法 32 | w.Header().Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") 33 | // 允许返回任意格式数据 34 | w.Header().Set("content-type", "*") 35 | 36 | // 跨域第一次OPTIONS请求,直接放行 37 | if r.Method == "OPTIONS" { 38 | w.WriteHeader(http.StatusNoContent) 39 | return 40 | } 41 | next.ServeHTTP(w, r) 42 | } 43 | 44 | } 45 | 46 | // TimeMiddleWare 日志打印 47 | func TimeMiddleWare(next http.HandlerFunc) http.HandlerFunc { 48 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 49 | // 处理业务耗时 50 | startTime := time.Now() 51 | next.ServeHTTP(w, r) 52 | timeElapsed := time.Since(startTime) 53 | 54 | logging.Log.Infof("[IP:%v] url:%v, method:%v, time:%v", r.RemoteAddr, r.URL.Path, r.Method, timeElapsed) 55 | }) 56 | } 57 | 58 | // TokenMiddleWare Token认证 59 | func TokenMiddleWare(next http.HandlerFunc) http.HandlerFunc { 60 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 61 | // 从header中取出token 62 | token := r.Header.Get(common.HeaderTokenKey) 63 | if token == "" { 64 | view.HandleBizError(w, "Authorization failed") 65 | return 66 | } 67 | logging.Log.Debugf("[verify token] token: %s", token) 68 | 69 | // 校验token是否合法 70 | checkToken := func(cli pb.UserServiceClient) (interface{}, error) { 71 | return cli.CheckToken(context.Background(), &pb.CheckTokenRequest{Token: r.Header.Get(common.HeaderTokenKey)}) 72 | } 73 | rpcRsp, err := grpc.GP.Exec(checkToken) 74 | if err != nil { 75 | view.HandleErrorRpcRequest(w) 76 | return 77 | } 78 | rsp, _ := rpcRsp.(*pb.CheckTokenResponse) 79 | 80 | // 认证失败 81 | if rsp.Code != common.RpcSuccessCode { 82 | view.HandleErrorRpcResponse(w, rsp.Code, rsp.Msg) 83 | return 84 | } 85 | 86 | // 认证成功,继续处理业务 87 | next.ServeHTTP(w, r) 88 | }) 89 | } 90 | -------------------------------------------------------------------------------- /doc/entry/bench.md: -------------------------------------------------------------------------------- 1 | ## entry-task benchmark test document 2 | 3 | 4 | 5 | ### login test 6 | 7 | | client | QPS | 8 | | ------ | ------- | 9 | | 200 | 5623.24 | 10 | | 1000 | 5326.15 | 11 | | 1500 | 4152.13 | 12 | | 2000 | 3622.45 | 13 | 14 | 15 | 16 | #### client: 200 17 | 18 | ```shell 19 | $ wrk -t6 -c200 -d10s --latency -s login.lua "http://127.0.0.1:10000/login" 20 | Running 10s test @ http://127.0.0.1:10000/login 21 | 6 threads and 200 connections 22 | Thread Stats Avg Stdev Max +/- Stdev 23 | Latency 35.10ms 5.80ms 91.40ms 79.81% 24 | Req/Sec 0.94k 111.79 1.20k 69.33% 25 | Latency Distribution 26 | 50% 34.78ms 27 | 75% 37.46ms 28 | 90% 41.31ms 29 | 99% 52.17ms 30 | 56357 requests in 10.02s, 27.94MB read 31 | Socket errors: connect 0, read 56, write 0, timeout 0 32 | Requests/sec: 5623.24 33 | Transfer/sec: 2.79MB 34 | 35 | ``` 36 | 37 | #### client: 1000 38 | 39 | ```shell 40 | $ wrk -t6 -c1000 -d10s --latency -s login.lua "http://127.0.0.1:10000/login" 41 | Running 10s test @ http://127.0.0.1:10000/login 42 | 6 threads and 1000 connections 43 | Thread Stats Avg Stdev Max +/- Stdev 44 | Latency 180.29ms 99.42ms 1.13s 64.97% 45 | Req/Sec 0.92k 526.45 2.43k 64.08% 46 | Latency Distribution 47 | 50% 172.72ms 48 | 75% 228.45ms 49 | 90% 321.70ms 50 | 99% 487.72ms 51 | 53804 requests in 10.10s, 25.28MB read 52 | Socket errors: connect 0, read 3251, write 0, timeout 0 53 | Requests/sec: 5326.15 54 | Transfer/sec: 2.50MB 55 | ``` 56 | 57 | #### client: 1500 58 | 59 | ```shell 60 | $ wrk -t6 -c1500 -d10s --latency -s login.lua "http://127.0.0.1:10000/login" 61 | Running 10s test @ http://127.0.0.1:10000/login 62 | 6 threads and 1500 connections 63 | Thread Stats Avg Stdev Max +/- Stdev 64 | Latency 328.51ms 115.68ms 1.10s 84.69% 65 | Req/Sec 718.34 358.47 1.58k 64.59% 66 | Latency Distribution 67 | 50% 302.30ms 68 | 75% 342.56ms 69 | 90% 450.89ms 70 | 99% 770.59ms 71 | 41790 requests in 10.06s, 20.46MB read 72 | Socket errors: connect 0, read 7925, write 0, timeout 0 73 | Requests/sec: 4152.13 74 | Transfer/sec: 2.03MB 75 | ``` 76 | 77 | #### client: 2000 78 | 79 | ```shell 80 | $ wrk -t6 -c2000 -d10s --latency -s login.lua "http://127.0.0.1:10000/login" 81 | Running 10s test @ http://127.0.0.1:10000/login 82 | 6 threads and 2000 connections 83 | Thread Stats Avg Stdev Max +/- Stdev 84 | Latency 432.74ms 108.63ms 1.05s 84.48% 85 | Req/Sec 666.14 393.20 1.52k 62.03% 86 | Latency Distribution 87 | 50% 396.85ms 88 | 75% 486.51ms 89 | 90% 568.72ms 90 | 99% 807.69ms 91 | 36546 requests in 10.09s, 17.98MB read 92 | Socket errors: connect 0, read 16909, write 0, timeout 0 93 | Requests/sec: 3622.45 94 | Transfer/sec: 1.78MB 95 | ``` 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /front/src/components/FeedBack.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 48 | 49 | 88 | 89 | 122 | -------------------------------------------------------------------------------- /pkg/logger/log.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | "path" 10 | "strings" 11 | "time" 12 | 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | // @Author Chen Zikang 17 | // @Email zikang.chen@shopee.com 18 | // @Since 2022-02-22 19 | 20 | const ( 21 | dateFormat = "2006-01-02" 22 | timeFormat = "2006-01-02 15:04:05.999" 23 | maxFunctionLength = 30 24 | fileSuffix = ".log" 25 | ) 26 | 27 | // NewLogger 创建Logger 28 | func NewLogger(logLevel logrus.Level, logFile string, enableConsole bool) *logrus.Logger { 29 | logger := logrus.New() 30 | logger.SetLevel(logLevel) 31 | logger.SetFormatter(&LogFormatter{}) 32 | if enableConsole && logFile != "" { 33 | logger.SetOutput(io.MultiWriter(LogFile(logFile), os.Stdout)) 34 | } else if enableConsole { 35 | logger.SetOutput(os.Stdout) 36 | } else if logFile != "" { 37 | logger.SetOutput(LogFile(logFile)) 38 | } else { 39 | logger.SetOutput(nil) 40 | } 41 | logger.SetReportCaller(true) 42 | return logger 43 | } 44 | 45 | // LogFormatter 自定义格式 46 | type LogFormatter struct{} 47 | 48 | // Format 日志输出格式 49 | func (f *LogFormatter) Format(entry *logrus.Entry) ([]byte, error) { 50 | var buf *bytes.Buffer 51 | if entry.Buffer != nil { 52 | buf = entry.Buffer 53 | } else { 54 | buf = &bytes.Buffer{} 55 | } 56 | 57 | datetime := entry.Time.Format(timeFormat) 58 | if len(datetime) < len(timeFormat) { 59 | for i := 0; i < len(timeFormat)-len(datetime); i++ { 60 | datetime = datetime + "0" 61 | } 62 | } 63 | logLevel := strings.ToUpper(entry.Level.String()) 64 | if len(logLevel) > 5 { 65 | logLevel = logLevel[:5] 66 | } else if len(logLevel) < 5 { 67 | logLevel = logLevel + " " 68 | } 69 | function := entry.Caller.Function[strings.LastIndex(entry.Caller.Function, "/")+1:] 70 | funcLen := len(function) 71 | if funcLen < maxFunctionLength { 72 | for i := 0; i < maxFunctionLength-funcLen; i++ { 73 | function = function + " " 74 | } 75 | } else if funcLen > maxFunctionLength { 76 | function = function[len(function)-maxFunctionLength:] 77 | } 78 | logStr := fmt.Sprintf("%s %s [%s] - %s\n", datetime, logLevel, function, entry.Message) 79 | 80 | buf.WriteString(logStr) 81 | return buf.Bytes(), nil 82 | } 83 | 84 | // LogFile 日志输出文件 85 | func LogFile(file string) *os.File { 86 | dir, _ := os.Getwd() 87 | logDirPath := dir + "/log/" + file 88 | _, err := os.Stat(logDirPath) 89 | if os.IsNotExist(err) { 90 | if err := os.MkdirAll(logDirPath, 0777); err != nil { 91 | log.Fatalln("Create logger dir failed") 92 | return nil 93 | } 94 | } 95 | logFileName := time.Now().Format(dateFormat) + fileSuffix 96 | fileName := path.Join(logDirPath, logFileName) 97 | if _, err = os.Stat(fileName); err != nil { 98 | if _, err = os.Create(fileName); err != nil { 99 | log.Println("Create logger file failed") 100 | return nil 101 | } 102 | } 103 | logFile, err := os.OpenFile(fileName, os.O_APPEND|os.O_WRONLY, os.ModeAppend) 104 | if err != nil { 105 | log.Println("Open logger file failed") 106 | return nil 107 | } 108 | return logFile 109 | } 110 | -------------------------------------------------------------------------------- /pkg/rpc/server.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net" 7 | "reflect" 8 | 9 | "github.com/Khighness/entry-task/pkg/logger" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | // @Author Chen Zikang 14 | // @Email zikang.chen@shopee.com 15 | // @Since 2022-02-21 16 | 17 | // Server struct 18 | type Server struct { 19 | logger *logrus.Logger 20 | addr string 21 | functions map[string]reflect.Value 22 | } 23 | 24 | // NewServer create a new server 25 | func NewServer(addr string) *Server { 26 | return &Server{ 27 | logger: logger.NewLogger(logrus.WarnLevel, "", true), 28 | addr: addr, // the net address of server 29 | functions: make(map[string]reflect.Value), // key: the name of func , value: reflect Value of function 30 | } 31 | } 32 | 33 | // Run start server 34 | func (s *Server) Run() { 35 | listener, err := net.Listen("tcp", s.addr) 36 | if err != nil { 37 | s.logger.Infof("Listen at %s err: %v ", s.addr, err) 38 | return 39 | } 40 | 41 | for { 42 | conn, err := listener.Accept() 43 | if err != nil { 44 | s.logger.Errorf("Accept client err: %v", err) 45 | continue 46 | } else { 47 | s.logger.Infof("Accept client: %s", conn.RemoteAddr()) 48 | } 49 | 50 | go func() { 51 | srvTransport := NewTransport(conn) 52 | for { 53 | // read request from client 54 | req, err := srvTransport.Receive() 55 | if err != nil { 56 | if err != io.EOF { 57 | s.logger.Infof("Read request err: %v", err) 58 | } 59 | return 60 | } 61 | // get function by name 62 | f, ok := s.functions[req.Name] 63 | // if function requested does not exist 64 | if !ok { 65 | e := fmt.Sprintf("Func %s does not exist", req.Name) 66 | s.logger.Errorf(e) 67 | if err = srvTransport.Send(Data{Name: req.Name, Err: e}); err != nil { 68 | s.logger.Printf("Transport write err: %v", err) 69 | } 70 | continue 71 | } 72 | s.logger.Debugf("Call func: %s", req.Name) 73 | 74 | // un package function arguments 75 | inArgs := make([]reflect.Value, len(req.Args)) 76 | for i := range req.Args { 77 | inArgs[i] = reflect.ValueOf(req.Args[i]) 78 | } 79 | // invoke requested function 80 | out := f.Call(inArgs) 81 | // package response arguments 82 | outArgs := make([]interface{}, len(out)-1) 83 | for i := range req.Args { 84 | outArgs[i] = out[i].Interface() 85 | } 86 | // package error argument 87 | var e string 88 | if _, ok := out[len(out)-1].Interface().(error); !ok { 89 | e = "" 90 | } else { 91 | e = out[len(out)-1].Interface().(error).Error() 92 | } 93 | // send response to client 94 | err = srvTransport.Send(Data{Name: req.Name, Args: outArgs, Err: e}) 95 | if err != nil { 96 | s.logger.Errorf("Transport write err: %v", err) 97 | } 98 | } 99 | }() 100 | } 101 | } 102 | 103 | // Register register a function via name 104 | func (s *Server) Register(name string, f interface{}) { 105 | if _, ok := s.functions[name]; ok { 106 | return 107 | } 108 | s.functions[name] = reflect.ValueOf(f) 109 | s.logger.Debugf("Register function: %v", name) 110 | } 111 | -------------------------------------------------------------------------------- /front/src/assets/font/iconfont.css: -------------------------------------------------------------------------------- 1 | @font-face {font-family: "iconfont"; 2 | src: url('iconfont.eot?t=1609321267885'); /* IE9 */ 3 | src: url('iconfont.eot?t=1609321267885#iefix') format('embedded-opentype'), /* IE6-IE8 */ 4 | url('data:application/x-font-woff2;charset=utf-8;base64,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') format('woff2'), 5 | url('iconfont.woff?t=1609321267885') format('woff'), 6 | url('iconfont.ttf?t=1609321267885') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */ 7 | url('iconfont.svg?t=1609321267885#iconfont') format('svg'); /* iOS 4.1- */ 8 | } 9 | 10 | .iconfont { 11 | font-family: "iconfont" !important; 12 | font-size: 16px; 13 | font-style: normal; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-osx-font-smoothing: grayscale; 16 | } 17 | 18 | .iconyouxiang:before { 19 | content: "\e632"; 20 | } 21 | 22 | .iconmima:before { 23 | content: "\e639"; 24 | } 25 | 26 | .iconlock:before { 27 | content: "\e708"; 28 | } 29 | 30 | .iconusers:before { 31 | content: "\e92e"; 32 | } 33 | 34 | .iconzhedie:before { 35 | content: "\e601"; 36 | } 37 | 38 | .icondenglu-yanzhengma:before { 39 | content: "\e60c"; 40 | } 41 | 42 | .icondenglu-yonghuming:before { 43 | content: "\e60d"; 44 | } 45 | 46 | -------------------------------------------------------------------------------- /tcp/util/fast_rand_timing_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "math/rand" 5 | "sync" 6 | "sync/atomic" 7 | "testing" 8 | "unsafe" 9 | ) 10 | 11 | // @Author Chen Zikang 12 | // @Email zikang.chen@shopee.com 13 | // @Since 2022-02-16 14 | 15 | // BenchSink prevents the compiler from optimizing away benchmark loops. 16 | var BenchSink uint32 17 | 18 | func BenchmarkUint32n(b *testing.B) { 19 | b.RunParallel(func(pb *testing.PB) { 20 | s := uint32(0) 21 | for pb.Next() { 22 | s += Uint32n(1e6) 23 | } 24 | atomic.AddUint32(&BenchSink, s) 25 | }) 26 | } 27 | 28 | func BenchmarkRNGUint32n(b *testing.B) { 29 | b.RunParallel(func(pb *testing.PB) { 30 | var r RNG 31 | s := uint32(0) 32 | for pb.Next() { 33 | s += r.Uint32n(1e6) 34 | } 35 | atomic.AddUint32(&BenchSink, s) 36 | }) 37 | } 38 | 39 | func BenchmarkRNGUint32nWithLock(b *testing.B) { 40 | var r RNG 41 | var rMu sync.Mutex 42 | b.RunParallel(func(pb *testing.PB) { 43 | s := uint32(0) 44 | for pb.Next() { 45 | rMu.Lock() 46 | s += r.Uint32n(1e6) 47 | rMu.Unlock() 48 | } 49 | atomic.AddUint32(&BenchSink, s) 50 | }) 51 | } 52 | 53 | func BenchmarkRNGUint32nArray(b *testing.B) { 54 | var rr [64]struct { 55 | r RNG 56 | mu sync.Mutex 57 | 58 | // pad prevents from false sharing 59 | pad [64 - (unsafe.Sizeof(RNG{})+unsafe.Sizeof(sync.Mutex{}))%64]byte 60 | } 61 | var n uint32 62 | b.RunParallel(func(pb *testing.PB) { 63 | s := uint32(0) 64 | for pb.Next() { 65 | idx := atomic.AddUint32(&n, 1) 66 | r := &rr[idx%uint32(len(rr))] 67 | r.mu.Lock() 68 | s += r.r.Uint32n(1e6) 69 | r.mu.Unlock() 70 | } 71 | atomic.AddUint32(&BenchSink, s) 72 | }) 73 | } 74 | 75 | func BenchmarkMathRandInt31n(b *testing.B) { 76 | b.RunParallel(func(pb *testing.PB) { 77 | s := uint32(0) 78 | for pb.Next() { 79 | s += uint32(rand.Int31n(1e6)) 80 | } 81 | atomic.AddUint32(&BenchSink, s) 82 | }) 83 | } 84 | 85 | func BenchmarkMathRandRNGInt31n(b *testing.B) { 86 | b.RunParallel(func(pb *testing.PB) { 87 | r := rand.New(rand.NewSource(42)) 88 | s := uint32(0) 89 | for pb.Next() { 90 | s += uint32(r.Int31n(1e6)) 91 | } 92 | atomic.AddUint32(&BenchSink, s) 93 | }) 94 | } 95 | 96 | func BenchmarkMathRandRNGInt31nWithLock(b *testing.B) { 97 | r := rand.New(rand.NewSource(42)) 98 | var rMu sync.Mutex 99 | b.RunParallel(func(pb *testing.PB) { 100 | s := uint32(0) 101 | for pb.Next() { 102 | rMu.Lock() 103 | s += uint32(r.Int31n(1e6)) 104 | rMu.Unlock() 105 | } 106 | atomic.AddUint32(&BenchSink, s) 107 | }) 108 | } 109 | 110 | func BenchmarkMathRandRNGInt31nArray(b *testing.B) { 111 | var rr [64]struct { 112 | r *rand.Rand 113 | mu sync.Mutex 114 | 115 | // pad prevents from false sharing 116 | pad [64 - (unsafe.Sizeof(RNG{})+unsafe.Sizeof(sync.Mutex{}))%64]byte 117 | } 118 | for i := range rr { 119 | rr[i].r = rand.New(rand.NewSource(int64(i))) 120 | } 121 | 122 | var n uint32 123 | b.RunParallel(func(pb *testing.PB) { 124 | s := uint32(0) 125 | for pb.Next() { 126 | idx := atomic.AddUint32(&n, 1) 127 | r := &rr[idx%uint32(len(rr))] 128 | r.mu.Lock() 129 | s += uint32(r.r.Int31n(1e6)) 130 | r.mu.Unlock() 131 | } 132 | atomic.AddUint32(&BenchSink, s) 133 | }) 134 | } 135 | -------------------------------------------------------------------------------- /front/src/components/UpdatePass.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 60 | 61 | 130 | 131 | 160 | -------------------------------------------------------------------------------- /doc/entry/webapi.md: -------------------------------------------------------------------------------- 1 | ## entry-task web api document 2 | 3 | 4 | 5 | 🚀 状态码:10000为SUCCESS,其他均为ERROR。 6 | 7 | | code | message | 8 | | ----- | -------------------------------------------------------- | 9 | | 10000 | SUCCESS | 10 | | 20000 | ERROR | 11 | | 20001 | Server is busy, please try again later | 12 | | 20002 | RPC failed or timeout | 13 | | 30001 | 用户名长度不得小于3 | 14 | | 30002 | 用户名长度不得大于18 | 15 | | 30003 | 用户名已存在,请换个试试 | 16 | | 30004 | 密码长度不得小于6 | 17 | | 30005 | 密码长度不得大于20 | 18 | | 30006 | 密码长度较弱,最少需要包含数字/字母/特殊符号中的以上两种 | 19 | | 30007 | 用户名错误 | 20 | | 30008 | 密码错误 | 21 | | 30009 | 令牌非法 | 22 | | 30010 | 登录状态已过期 | 23 | | 40001 | 操作数据库失败 | 24 | 25 | 26 | 27 | ### 1. 用户注册 28 | 29 | 请求方式:POST 30 | 31 | 请求路径:/register 32 | 33 | 请求数据:`application/json` 34 | 35 | | 字段 | 类型 | 描述 | 36 | | -------- | ------ | ------ | 37 | | username | string | 用户名 | 38 | | password | string | 密码 | 39 | 40 | 返回数据:`application/json` 41 | 42 | | 字段 | 类型 | 描述 | 43 | | ------- | ------ | -------------------------- | 44 | | code | int | 状态码 | 45 | | message | string | 信息,注册失败时为提示信息 | 46 | | data | string | null | 47 | 48 | 请求示例: 49 | 50 | ```shell 51 | $ curl -X POST \ 52 | -d '{"username":"Khighness","password":"czk911"}' \ 53 | 'http://127.0.0.1:10000/register' 54 | { 55 | "code": 10000, 56 | "message": "SUCCESS", 57 | "data": null 58 | } 59 | ``` 60 | 61 | 62 | 63 | ### 2. 用户登录 64 | 65 | 请求方式:POST 66 | 67 | 请求路径:/login 68 | 69 | 请求数据:`application/json` 70 | 71 | | 字段 | 类型 | 描述 | 72 | | -------- | ------ | ------ | 73 | | username | string | 用户名 | 74 | | password | string | 密码 | 75 | 76 | 返回数据:`application/json` 77 | 78 | | 字段 | 类型 | 描述 | 79 | | ------- | ------ | -------------- | 80 | | code | int | 状态码 | 81 | | message | string | 信息 | 82 | | data | json | 令牌和用户信息 | 83 | 84 | 请求示例: 85 | 86 | ```shell 87 | $ curl -X POST \ 88 | -d '{"username":"Khighness","password":"czk911"}' \ 89 | 'http://127.0.0.1:10000/login' 90 | { 91 | "code": 10000, 92 | "message": "SUCCESS", 93 | "data": { 94 | "token": "1b17da6fb96ee7bf9bf1ca11a1d68703", 95 | "user": { 96 | "id": 1, 97 | "username": "Khighness", 98 | "profilePicture": "http://127.0.0.1:10000/avatar/show/khighness.jpg" 99 | } 100 | } 101 | } 102 | ``` 103 | 104 | 105 | 106 | ### 3. 个人信息 107 | 108 | 请求方式:GET 109 | 110 | 请求路径:/user/profile 111 | 112 | 请求头部:`Authorization` 113 | 114 | 返回数据:`application/json` 115 | 116 | | 字段 | 类型 | 描述 | 117 | | ------- | ------ | -------- | 118 | | code | int | 状态码 | 119 | | message | string | 信息 | 120 | | data | json | 用户信息 | 121 | 122 | 请求示例: 123 | ```shell 124 | $ curl -X GET \ 125 | -H 'Authorization:720df5d0c0e9649597f54b531a1e348d' \ 126 | 'http://127.0.0.1:10000/user/profile' 127 | { 128 | "code": 10000, 129 | "message": "SUCCESS", 130 | "data": { 131 | "id": 1, 132 | "username": "KHighness", 133 | "profilePicture": "http://127.0.0.1:10000/avatar/show/khighness.jpg" 134 | } 135 | } 136 | ``` 137 | 138 | 139 | 140 | ### 4. 更新信息 141 | 142 | 请求方式:PUT 143 | 144 | 请求路径:/user/update 145 | 146 | 请求头部:`Authorization` 147 | 148 | 请求数据:`application/json` 149 | 150 | | 字段 | 类型 | 描述 | 151 | | -------- | ------ | -------------------------------- | 152 | | username | string | 用户名 | 153 | 154 | 返回数据:`application/json` 155 | 156 | | 字段 | 类型 | 描述 | 157 | | ------- | ------ | ------ | 158 | | code | int | 状态码 | 159 | | message | string | 信息 | 160 | | data | string | null | 161 | 162 | 请求示例: 163 | 164 | ```shell 165 | $ curl -X PUT \ 166 | -H 'Authorization:1b17da6fb96ee7bf9bf1ca11a1d68703' \ 167 | -d '{"username":"Khighness1"}' \ 168 | 'http://127.0.0.1:10000/user/update' 169 | { 170 | "code": 10000, 171 | "message": "SUCCESS", 172 | "data": null 173 | } 174 | ``` 175 | 176 | 177 | 178 | ### 5. 展示图片 179 | 180 | 请求方式:GET 181 | 182 | 请求路径:/avatar/show/${picture} 183 | 184 | 返回数据:`image/jpg` / `image/png` / `image/jpeg` 185 | 186 | 请求示例: 187 | 188 | ```shell 189 | $ curl -X GET \ 190 | -o '/Users/zikang.chen/Pictures/output.jpg' \ 191 | 'http://127.0.0.1:10000/avatar/show/khighness.jpg' 192 | % Total % Received % Xferd Average Speed Time Time Time Current 193 | Dload Upload Total Spent Left Speed 194 | 100 30100 0 30100 0 0 14.3M 0 --:--:-- --:--:-- --:--:-- 14.3M 195 | ``` 196 | 197 | 198 | 199 | ### 6. 更新头像 200 | 201 | 请求方式:POST 202 | 203 | 请求路径:/avatar/upload 204 | 205 | 请求头部:`Authorization` 206 | 207 | 请求数据:`multi/form-data` 208 | 209 | | 字段 | 类型 | 描述 | 210 | | --------------- | ---- | -------- | 211 | | profile_picture | file | 头像文件 | 212 | 213 | 返回数据: 214 | 215 | 请求示例: 216 | 217 | ```shell 218 | $ curl -X POST \ 219 | -H 'Authorization:1b17da6fb96ee7bf9bf1ca11a1d68703' \ 220 | -F 'profile_picture=@/Users/zikang.chen/Pictures/Khighness.jpg' \ 221 | 'http://127.0.0.1:10000/avatar/upload' 222 | { 223 | "code": 10000, 224 | "message": "SUCCESS", 225 | "data": null 226 | } 227 | ``` 228 | 229 | 230 | 231 | ### 7. 退出登录 232 | 233 | 请求方式:get 234 | 235 | 请求路径:/logout 236 | 237 | 请求头部:`Authorization` 238 | 239 | 返回数据:`application/json` 240 | 241 | | 字段 | 类型 | 描述 | 242 | | ------- | ------ | ------ | 243 | | code | int | 状态码 | 244 | | message | string | 信息 | 245 | | data | string | null | 246 | 247 | 请求示例: 248 | 249 | ```shell 250 | $ curl -X GET \ 251 | -H 'Authorization:1b17da6fb96ee7bf9bf1ca11a1d68703' \ 252 | 'http://127.0.0.1:10000/logout' 253 | { 254 | "code": 10000, 255 | "message": "SUCCESS", 256 | "data": null 257 | } 258 | ``` 259 | -------------------------------------------------------------------------------- /front/src/components/Home.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 89 | 90 | 162 | 163 | 230 | -------------------------------------------------------------------------------- /front/src/components/Login.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 42 | 43 | 121 | 122 | 223 | -------------------------------------------------------------------------------- /front/src/components/Register.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 41 | 42 | 138 | 139 | 218 | -------------------------------------------------------------------------------- /front/jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "C:\\Users\\ahfyq\\AppData\\Local\\Temp\\jest", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | // clearMocks: false, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | // collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: null, 25 | 26 | // The directory where Jest should output its coverage files 27 | coverageDirectory: 'coverage', 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "\\\\node_modules\\\\" 32 | // ], 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: null, 44 | 45 | // A path to a custom dependency extractor 46 | // dependencyExtractor: null, 47 | 48 | // Make calling deprecated APIs throw helpful error messages 49 | // errorOnDeprecated: false, 50 | 51 | // Force coverage collection from ignored files using an array of glob patterns 52 | // forceCoverageMatch: [], 53 | 54 | // A path to a module which exports an async function that is triggered once before all test suites 55 | // globalSetup: null, 56 | 57 | // A path to a module which exports an async function that is triggered once after all test suites 58 | // globalTeardown: null, 59 | 60 | // A set of global variables that need to be available in all test environments 61 | // globals: {}, 62 | 63 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 64 | // maxWorkers: "50%", 65 | 66 | // An array of directory names to be searched recursively up from the requiring module's location 67 | // moduleDirectories: [ 68 | // "node_modules" 69 | // ], 70 | 71 | // An array of file extensions your modules use 72 | moduleFileExtensions: [ 73 | 'js', 74 | 'json', 75 | 'jsx', 76 | 'ts', 77 | 'tsx', 78 | 'node' 79 | ], 80 | 81 | // A map from regular expressions to module names that allow to stub out resources with a single module 82 | // moduleNameMapper: {}, 83 | 84 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 85 | // modulePathIgnorePatterns: [], 86 | 87 | // Activates notifications for test results 88 | // notify: false, 89 | 90 | // An enum that specifies notification mode. Requires { notify: true } 91 | // notifyMode: "failure-change", 92 | 93 | // A preset that is used as a base for Jest's configuration 94 | // preset: null, 95 | 96 | // Run tests from one or more projects 97 | // projects: null, 98 | 99 | // Use this configuration option to add custom reporters to Jest 100 | // reporters: undefined, 101 | 102 | // Automatically reset mock state between every test 103 | // resetMocks: false, 104 | 105 | // Reset the module registry before running each individual test 106 | // resetModules: false, 107 | 108 | // A path to a custom resolver 109 | // resolver: null, 110 | 111 | // Automatically restore mock state between every test 112 | // restoreMocks: false, 113 | 114 | // The root directory that Jest should scan for tests and modules within 115 | // rootDir: null, 116 | 117 | // A list of paths to directories that Jest should use to search for files in 118 | // roots: [ 119 | // "" 120 | // ], 121 | 122 | // Allows you to use a custom runner instead of Jest's default test runner 123 | // runner: "jest-runner", 124 | 125 | // The paths to modules that run some code to configure or set up the testing environment before each test 126 | // setupFiles: [], 127 | 128 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 129 | // setupFilesAfterEnv: [], 130 | 131 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 132 | // snapshotSerializers: [], 133 | 134 | // The test environment that will be used for testing 135 | testEnvironment: 'node' 136 | 137 | // Options that will be passed to the testEnvironment 138 | // testEnvironmentOptions: {}, 139 | 140 | // Adds a location field to test results 141 | // testLocationInResults: false, 142 | 143 | // The glob patterns Jest uses to detect test files 144 | // testMatch: [ 145 | // "**/__tests__/**/*.[jt]s?(x)", 146 | // "**/?(*.)+(spec|test).[tj]s?(x)" 147 | // ], 148 | 149 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 150 | // testPathIgnorePatterns: [ 151 | // "\\\\node_modules\\\\" 152 | // ], 153 | 154 | // The regexp pattern or array of patterns that Jest uses to detect test files 155 | // testRegex: [], 156 | 157 | // This option allows the use of a custom results processor 158 | // testResultsProcessor: null, 159 | 160 | // This option allows use of a custom test runner 161 | // testRunner: "jasmine2", 162 | 163 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 164 | // testURL: "http://localhost", 165 | 166 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 167 | // timers: "real", 168 | 169 | // A map from regular expressions to paths to transformers 170 | // transform: null, 171 | 172 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 173 | // transformIgnorePatterns: [ 174 | // "\\\\node_modules\\\\" 175 | // ], 176 | 177 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 178 | // unmockedModulePathPatterns: undefined, 179 | 180 | // Indicates whether each individual test should be reported during the run 181 | // verbose: null, 182 | 183 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 184 | // watchPathIgnorePatterns: [], 185 | 186 | // Whether to use watchman for file crawling 187 | // watchman: true, 188 | } 189 | -------------------------------------------------------------------------------- /front/src/assets/font/iconfont.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Created by iconfont 9 | 10 | 11 | 12 | 13 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /front/src/assets/font/iconfont.js: -------------------------------------------------------------------------------- 1 | !(function (t) { var e; var c; var n; var o; var i; var l; var s = ''; var d = (d = document.getElementsByTagName('script'))[d.length - 1].getAttribute('data-injectcss'); if (d && !t.__iconfont__svg__cssinject__) { t.__iconfont__svg__cssinject__ = !0; try { document.write('') } catch (t) { console && console.log(t) } } function a () { i || (i = !0, n()) }e = function () { var t, e, c, n; (n = document.createElement('div')).innerHTML = s, s = null, (c = n.getElementsByTagName('svg')[0]) && (c.setAttribute('aria-hidden', 'true'), c.style.position = 'absolute', c.style.width = 0, c.style.height = 0, c.style.overflow = 'hidden', t = c, (e = document.body).firstChild ? (n = t, (c = e.firstChild).parentNode.insertBefore(n, c)) : e.appendChild(t)) }, document.addEventListener ? ~['complete', 'loaded', 'interactive'].indexOf(document.readyState) ? setTimeout(e, 0) : (c = function () { document.removeEventListener('DOMContentLoaded', c, !1), e() }, document.addEventListener('DOMContentLoaded', c, !1)) : document.attachEvent && (n = e, o = t.document, i = !1, (l = function () { try { o.documentElement.doScroll('left') } catch (t) { return void setTimeout(l, 50) }a() })(), o.onreadystatechange = function () { o.readyState == 'complete' && (o.onreadystatechange = null, a()) }) }(window)) 2 | -------------------------------------------------------------------------------- /web/controller/user_controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "net/http" 10 | "os" 11 | "strings" 12 | "time" 13 | 14 | "github.com/Khighness/entry-task/pb" 15 | "github.com/Khighness/entry-task/web/common" 16 | "github.com/Khighness/entry-task/web/config" 17 | "github.com/Khighness/entry-task/web/grpc" 18 | "github.com/Khighness/entry-task/web/logging" 19 | "github.com/Khighness/entry-task/web/util" 20 | "github.com/Khighness/entry-task/web/view" 21 | ) 22 | 23 | // @Author Chen Zikang 24 | // @Email zikang.chen@shopee.com 25 | // @Since 2022-02-15 26 | 27 | // UserController 用户控制器 28 | type UserController struct{} 29 | 30 | // Ping Return Pong 31 | func (userController *UserController) Ping(w http.ResponseWriter, r *http.Request) { 32 | view.HandleBizSuccess(w, "Pong") 33 | } 34 | 35 | // Register 用户注册 36 | func (userController *UserController) Register(w http.ResponseWriter, r *http.Request) { 37 | if r.Method != http.MethodPost { 38 | view.HandleMethodError(w, "Allowed Method: [POST]") 39 | return 40 | } 41 | var registerReq common.RegisterRequest 42 | err := json.NewDecoder(r.Body).Decode(®isterReq) 43 | if err != nil { 44 | view.HandleRequestError(w, "Body should be json for registering data") 45 | return 46 | } 47 | 48 | register := func(cli pb.UserServiceClient) (interface{}, error) { 49 | return cli.Register(context.Background(), &pb.RegisterRequest{Username: registerReq.Username, Password: registerReq.Password}) 50 | } 51 | rpcRsp, err := grpc.GP.Exec(register) 52 | if err != nil { 53 | view.HandleErrorRpcRequest(w) 54 | return 55 | } 56 | rsp, _ := rpcRsp.(*pb.RegisterResponse) 57 | if rsp.Code != common.RpcSuccessCode { 58 | view.HandleErrorRpcResponse(w, rsp.Code, rsp.Msg) 59 | return 60 | } 61 | view.HandleBizSuccess(w, nil) 62 | } 63 | 64 | // Login 用户登录 65 | func (userController *UserController) Login(w http.ResponseWriter, r *http.Request) { 66 | if r.Method != http.MethodPost { 67 | view.HandleMethodError(w, "Allowed Method: [POST]") 68 | return 69 | } 70 | var loginReq common.LoginRequest 71 | err := json.NewDecoder(r.Body).Decode(&loginReq) 72 | if err != nil { 73 | view.HandleRequestError(w, "Body should be json for logining data") 74 | return 75 | } 76 | 77 | login := func(cli pb.UserServiceClient) (interface{}, error) { 78 | return cli.Login(context.Background(), &pb.LoginRequest{Username: loginReq.Username, Password: loginReq.Password}) 79 | } 80 | rpcRsp, err := grpc.GP.Exec(login) 81 | if err != nil { 82 | view.HandleErrorRpcRequest(w) 83 | return 84 | } 85 | rsp, _ := rpcRsp.(*pb.LoginResponse) 86 | if rsp.Code != common.RpcSuccessCode { 87 | view.HandleErrorRpcResponse(w, rsp.Code, rsp.Msg) 88 | return 89 | } 90 | view.HandleBizSuccess(w, common.LoginResponse{ 91 | Token: rsp.Token, 92 | User: common.UserInfo{ 93 | Id: rsp.User.Id, 94 | Username: rsp.User.Username, 95 | ProfilePicture: rsp.User.ProfilePicture, 96 | }, 97 | }) 98 | } 99 | 100 | // GetProfile 获取信息 101 | func (userController *UserController) GetProfile(w http.ResponseWriter, r *http.Request) { 102 | if r.Method != http.MethodGet { 103 | view.HandleMethodError(w, "Allowed Method: [GET]") 104 | return 105 | } 106 | 107 | getProfile := func(cli pb.UserServiceClient) (interface{}, error) { 108 | return cli.GetProfile(context.Background(), &pb.GetProfileRequest{Token: r.Header.Get(common.HeaderTokenKey)}) 109 | } 110 | rpcRsp, err := grpc.GP.Exec(getProfile) 111 | if err != nil { 112 | view.HandleErrorRpcRequest(w) 113 | return 114 | } 115 | rsp, _ := rpcRsp.(*pb.GetProfileResponse) 116 | if rsp.Code != common.RpcSuccessCode { 117 | view.HandleErrorRpcResponse(w, rsp.Code, rsp.Msg) 118 | return 119 | } 120 | view.HandleBizSuccess(w, common.UserInfo{ 121 | Id: rsp.User.Id, 122 | Username: rsp.User.Username, 123 | ProfilePicture: rsp.User.ProfilePicture, 124 | }) 125 | } 126 | 127 | // UpdateProfile 更新信息 128 | func (userController *UserController) UpdateProfile(w http.ResponseWriter, r *http.Request) { 129 | if r.Method != http.MethodPut { 130 | view.HandleMethodError(w, "Allowed Method: [PUT]") 131 | return 132 | } 133 | var updateProfileRequest common.UpdateProfileRequest 134 | err := json.NewDecoder(r.Body).Decode(&updateProfileRequest) 135 | if err != nil { 136 | view.HandleRequestError(w, "Body should be json for registering data") 137 | return 138 | } 139 | 140 | updateProfile := func(cli pb.UserServiceClient) (interface{}, error) { 141 | return cli.UpdateProfile(context.Background(), &pb.UpdateProfileRequest{ 142 | Token: r.Header.Get(common.HeaderTokenKey), 143 | Username: updateProfileRequest.Username, 144 | }) 145 | } 146 | rpcRsp, err := grpc.GP.Exec(updateProfile) 147 | if err != nil { 148 | view.HandleErrorRpcRequest(w) 149 | return 150 | } 151 | rsp, _ := rpcRsp.(*pb.UpdateProfileResponse) 152 | if rsp.Code != common.RpcSuccessCode { 153 | view.HandleErrorRpcResponse(w, rsp.Code, rsp.Msg) 154 | return 155 | } 156 | view.HandleBizSuccess(w, nil) 157 | } 158 | 159 | // ShowAvatar 显示头像 160 | func (userController *UserController) ShowAvatar(w http.ResponseWriter, r *http.Request) { 161 | if r.Method != http.MethodGet { 162 | view.HandleMethodError(w, "Allowed Method: [GET]") 163 | return 164 | } 165 | 166 | profilePicture := strings.TrimLeft(r.URL.Path, view.ShowAvatarUrl) 167 | profilePicturePath := common.AvatarStoragePath + profilePicture 168 | _, err := os.Stat(profilePicturePath) 169 | if os.IsNotExist(err) { 170 | view.HandleBizError(w, profilePicture+" does not exist") 171 | logging.Log.Warn(profilePicturePath + " does not exist") 172 | return 173 | } 174 | file, _ := os.OpenFile(profilePicturePath, os.O_RDONLY, 0444) 175 | defer file.Close() 176 | buf, _ := ioutil.ReadAll(file) 177 | _, _ = w.Write(buf) 178 | } 179 | 180 | // UploadAvatar 上传头像 181 | func (userController *UserController) UploadAvatar(w http.ResponseWriter, r *http.Request) { 182 | if r.Method != http.MethodPost { 183 | view.HandleMethodError(w, "Allowed Method: [POST]") 184 | return 185 | } 186 | 187 | uploadFile, header, _ := r.FormFile("profile_picture") 188 | if uploadFile == nil { 189 | view.HandleRequestError(w, "Picture file cannot be null") 190 | return 191 | } 192 | defer uploadFile.Close() 193 | if !(strings.HasSuffix(header.Filename, "jpg") || strings.HasSuffix(header.Filename, "png") || strings.HasSuffix(header.Filename, "jpeg")) { 194 | view.HandleRequestError(w, "Please upload jpg/png/jpeg file as profile picture") 195 | return 196 | } 197 | fileStream, _ := io.ReadAll(uploadFile) 198 | fileType := util.GetFileType(fileStream[:10]) 199 | if !(fileType == "jpg" || fileType == "png" || fileType == "jpeg") { 200 | view.HandleRequestError(w, "The suffix of the uploaded file does not match the content of the file") 201 | return 202 | } 203 | avatarName := fmt.Sprintf("%d-%s", time.Now().Unix(), header.Filename) 204 | 205 | serverCfg := config.AppCfg.Server 206 | serverAddr := fmt.Sprintf("%s:%d", serverCfg.Host, serverCfg.Port) 207 | profilePicture := fmt.Sprintf("http://%s%s%s", serverAddr, view.ShowAvatarUrl, avatarName) 208 | createFile, err := os.OpenFile(common.AvatarStoragePath+avatarName, os.O_WRONLY|os.O_CREATE, 0766) 209 | defer createFile.Close() 210 | _, err = createFile.Write(fileStream) 211 | if err != nil { 212 | view.HandleBizError(w, "Upload profile picture failed") 213 | return 214 | } 215 | 216 | updateProfile := func(cli pb.UserServiceClient) (interface{}, error) { 217 | return cli.UpdateProfile(context.Background(), &pb.UpdateProfileRequest{ 218 | Token: r.Header.Get(common.HeaderTokenKey), 219 | ProfilePicture: profilePicture, 220 | }) 221 | } 222 | rpcRsp, err := grpc.GP.Exec(updateProfile) 223 | if err != nil { 224 | view.HandleErrorRpcRequest(w) 225 | return 226 | } 227 | rsp, _ := rpcRsp.(*pb.UpdateProfileResponse) 228 | if rsp.Code != common.RpcSuccessCode { 229 | view.HandleErrorRpcResponse(w, rsp.Code, rsp.Msg) 230 | return 231 | } 232 | view.HandleBizSuccess(w, nil) 233 | } 234 | 235 | // Logout 退出登录 236 | func (userController *UserController) Logout(w http.ResponseWriter, r *http.Request) { 237 | if r.Method != http.MethodGet { 238 | view.HandleMethodError(w, "Allowed Method: [GET]") 239 | return 240 | } 241 | 242 | logout := func(cli pb.UserServiceClient) (interface{}, error) { 243 | return cli.Logout(context.Background(), &pb.LogoutRequest{Token: r.Header.Get(common.HeaderTokenKey)}) 244 | } 245 | rpcRsp, err := grpc.GP.Exec(logout) 246 | if err != nil { 247 | view.HandleErrorRpcRequest(w) 248 | return 249 | } 250 | rsp, _ := rpcRsp.(*pb.LogoutResponse) 251 | if rsp.Code != common.RpcSuccessCode { 252 | view.HandleErrorRpcResponse(w, rsp.Code, rsp.Msg) 253 | return 254 | } 255 | view.HandleBizSuccess(w, nil) 256 | } 257 | -------------------------------------------------------------------------------- /front/src/assets/img/background.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 21 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /tcp/service/user_service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/Khighness/entry-task/pb" 8 | "github.com/Khighness/entry-task/tcp/cache" 9 | "github.com/Khighness/entry-task/tcp/common" 10 | "github.com/Khighness/entry-task/tcp/common/e" 11 | "github.com/Khighness/entry-task/tcp/logging" 12 | "github.com/Khighness/entry-task/tcp/mapper" 13 | "github.com/Khighness/entry-task/tcp/model" 14 | "github.com/Khighness/entry-task/tcp/util" 15 | ) 16 | 17 | // @Author Chen Zikang 18 | // @Email zikang.chen@shopee.com 19 | // @Since 2022-02-15 20 | 21 | // TODO 对表单输入过滤 22 | // TODO 防止XSRF攻击 23 | 24 | // UserService 用户业务 25 | type UserService struct { 26 | userMapper *mapper.UserMapper 27 | userCache *cache.UserCache 28 | } 29 | 30 | // NewUserService 创建用户业务操作 31 | func NewUserService(userMapper *mapper.UserMapper, userCache *cache.UserCache) *UserService { 32 | return &UserService{ 33 | userMapper: userMapper, 34 | userCache: userCache, 35 | } 36 | } 37 | 38 | // Register 用户注册 39 | func (s *UserService) Register(ctx context.Context, in *pb.RegisterRequest) (*pb.RegisterResponse, error) { 40 | var status = e.SUCCESS 41 | var err error 42 | 43 | // 校验用户名和密码 44 | if status = util.CheckUsername(in.Username); status != e.SUCCESS { 45 | return &pb.RegisterResponse{ 46 | Code: int32(status), 47 | Msg: e.GetMsg(status), 48 | }, nil 49 | } 50 | if status = util.CheckPassword(in.Password); status != e.SUCCESS { 51 | return &pb.RegisterResponse{ 52 | Code: int32(status), 53 | Msg: e.GetMsg(status), 54 | }, nil 55 | } 56 | 57 | // 检查用户名的唯一性 58 | exist, err := s.userMapper.CheckUserUsernameExist(in.Username) 59 | if err != nil { 60 | status = e.ErrorOperateDatabase 61 | return &pb.RegisterResponse{ 62 | Code: int32(status), 63 | Msg: e.GetMsg(status), 64 | }, nil 65 | } 66 | if exist { 67 | status = e.ErrorUsernameAlreadyExist 68 | return &pb.RegisterResponse{ 69 | Code: int32(status), 70 | Msg: e.GetMsg(status), 71 | }, nil 72 | } 73 | 74 | // 加密,计算hash 75 | var hashedPassword string 76 | hashedPassword, _ = util.EncryptPassByMd5(in.Password) 77 | 78 | // 保存到数据库 79 | id, err := s.userMapper.SaveUser(&model.User{ 80 | Username: in.Username, 81 | Password: hashedPassword, 82 | ProfilePicture: common.DefaultProfilePicture, 83 | }) 84 | if err != nil { 85 | status = e.ErrorOperateDatabase 86 | return &pb.RegisterResponse{ 87 | Code: int32(status), 88 | Msg: e.GetMsg(status), 89 | }, nil 90 | } 91 | 92 | logging.Log.Infof("[user register] userId: %v, username:%s", id, in.Username) 93 | return &pb.RegisterResponse{ 94 | Code: int32(status), 95 | Msg: e.GetMsg(status), 96 | }, nil 97 | } 98 | 99 | // Login 用户登录 100 | func (s *UserService) Login(ctx context.Context, in *pb.LoginRequest) (*pb.LoginResponse, error) { 101 | var status = e.SUCCESS 102 | 103 | // 校验用户名是否存在 104 | dbUser, err := s.userMapper.QueryUserByUsername(in.Username) 105 | if err != nil { 106 | status = e.ErrorUsernameIncorrect 107 | return &pb.LoginResponse{ 108 | Code: int32(status), 109 | Msg: e.GetMsg(status), 110 | }, nil 111 | } 112 | 113 | // 验证密码 114 | if !util.VerifyPassByMD5(in.Password, dbUser.Password) { 115 | status = e.ErrorPasswordIncorrect 116 | return &pb.LoginResponse{ 117 | Code: int32(status), 118 | Msg: e.GetMsg(status), 119 | }, nil 120 | } 121 | 122 | // 生成token,并在redis中缓存用户信息 123 | token := util.GenerateToken() 124 | go s.userCache.SetUserInfo(token, &model.User{ 125 | Id: dbUser.Id, 126 | Username: in.Username, 127 | Password: dbUser.Password, 128 | ProfilePicture: dbUser.ProfilePicture, 129 | }) 130 | 131 | logging.Log.Infof("[user login] userId: %d, username: %s", dbUser.Id, in.Username) 132 | return &pb.LoginResponse{ 133 | Code: int32(status), 134 | Msg: e.GetMsg(status), 135 | Token: token, 136 | User: &pb.User{ 137 | Id: dbUser.Id, 138 | Username: in.Username, 139 | ProfilePicture: dbUser.ProfilePicture, 140 | }, 141 | }, nil 142 | } 143 | 144 | // CheckToken 检查token 145 | func (s *UserService) CheckToken(ctx context.Context, in *pb.CheckTokenRequest) (*pb.CheckTokenResponse, error) { 146 | var status = e.SUCCESS 147 | id, err := s.userCache.GetUserId(in.Token) 148 | if err != nil { 149 | status = e.ErrorTokenExpired 150 | return &pb.CheckTokenResponse{ 151 | Code: int32(status), 152 | Msg: e.GetMsg(status), 153 | }, nil 154 | } 155 | logging.Log.Infof("[check token] token:%s,id:%d", in.Token, id) 156 | return &pb.CheckTokenResponse{ 157 | Code: int32(status), 158 | Msg: e.GetMsg(status), 159 | }, nil 160 | } 161 | 162 | // GetProfile 获取信息 163 | // TODO 多节点,分布式锁 164 | func (s *UserService) GetProfile(ctx context.Context, in *pb.GetProfileRequest) (*pb.GetProfileResponse, error) { 165 | var status = e.SUCCESS 166 | 167 | // 从缓存中获取用户信息 168 | caUser, err := s.userCache.GetUserInfo(in.Token) 169 | if err != nil { 170 | status = e.ErrorTokenExpired 171 | return &pb.GetProfileResponse{ 172 | Code: int32(status), 173 | Msg: e.GetMsg(status), 174 | User: nil, 175 | }, nil 176 | } 177 | 178 | // 用户信息失效,说明已更新 179 | // 从数据库获取,添加到缓存 180 | if caUser.Username == "" || caUser.ProfilePicture == "" { 181 | dbUser, err := s.userMapper.QueryUserById(caUser.Id) 182 | if err != nil { 183 | status = e.ErrorOperateDatabase 184 | return &pb.GetProfileResponse{ 185 | Code: int32(status), 186 | Msg: e.GetMsg(status), 187 | User: nil, 188 | }, nil 189 | } 190 | caUser.Username = dbUser.Username 191 | caUser.ProfilePicture = dbUser.ProfilePicture 192 | s.userCache.SetUserField(in.Token, "username", caUser.Username) 193 | s.userCache.SetUserField(in.Token, "profile_picture", caUser.ProfilePicture) 194 | } 195 | 196 | return &pb.GetProfileResponse{ 197 | Code: int32(status), 198 | Msg: e.GetMsg(status), 199 | User: &pb.User{ 200 | Id: caUser.Id, 201 | Username: caUser.Username, 202 | ProfilePicture: caUser.ProfilePicture, 203 | }, 204 | }, nil 205 | } 206 | 207 | // UpdateProfile 更新信息 208 | // 更新字段,延时2S双删 209 | func (s *UserService) UpdateProfile(ctx context.Context, in *pb.UpdateProfileRequest) (*pb.UpdateProfileResponse, error) { 210 | var status = e.SUCCESS 211 | 212 | // 从缓存中获取用户id 213 | caUser, err := s.userCache.GetUserInfo(in.Token) 214 | if err != nil { 215 | status = e.ErrorTokenExpired 216 | return &pb.UpdateProfileResponse{ 217 | Code: int32(status), 218 | Msg: e.GetMsg(status), 219 | }, nil 220 | } 221 | // 从数据库查询最新信息 222 | dbUser, err := s.userMapper.QueryUserById(caUser.Id) 223 | if err != nil { 224 | status = e.ErrorOperateDatabase 225 | return &pb.UpdateProfileResponse{ 226 | Code: int32(status), 227 | Msg: e.GetMsg(status), 228 | }, nil 229 | } 230 | caUser.Username = dbUser.Username 231 | caUser.ProfilePicture = dbUser.ProfilePicture 232 | 233 | // 用户名不为空 234 | if in.Username != "" { 235 | // 检查用户名合法性 236 | if status = util.CheckUsername(in.Username); status != e.SUCCESS { 237 | return &pb.UpdateProfileResponse{ 238 | Code: int32(status), 239 | Msg: e.GetMsg(status), 240 | }, nil 241 | } 242 | // 检查用户名是否变动 243 | if in.Username != caUser.Username { 244 | // 检查是否有其他人已使用 245 | exist, err := s.userMapper.CheckUserUsernameExist(in.Username) 246 | if err != nil { 247 | status = e.ErrorOperateDatabase 248 | return &pb.UpdateProfileResponse{ 249 | Code: int32(status), 250 | Msg: e.GetMsg(status), 251 | }, nil 252 | } 253 | if exist { 254 | status = e.ErrorUsernameAlreadyExist 255 | return &pb.UpdateProfileResponse{ 256 | Code: int32(status), 257 | Msg: e.GetMsg(status), 258 | }, nil 259 | } 260 | 261 | s.userCache.DelUserField(in.Token, "username") 262 | defer func() { 263 | go func() { 264 | time.Sleep(2 * time.Second) 265 | s.userCache.DelUserField(in.Token, "username") 266 | }() 267 | }() 268 | err = s.userMapper.UpdateUserUsernameById(caUser.Id, in.Username) 269 | if err != nil { 270 | status = e.ErrorOperateDatabase 271 | return &pb.UpdateProfileResponse{ 272 | Code: int32(status), 273 | Msg: e.GetMsg(status), 274 | }, nil 275 | } 276 | logging.Log.Infof("[user update] userId:%d,username:%s", caUser.Id, in.Username) 277 | } 278 | } 279 | 280 | // 用户头像不为空,说明已更新 281 | if in.ProfilePicture != "" { 282 | s.userCache.DelUserField(in.Token, "username") 283 | defer func() { 284 | go func() { 285 | time.Sleep(2 * time.Second) 286 | s.userCache.DelUserField(in.Token, "profile_picture") 287 | }() 288 | }() 289 | err = s.userMapper.UpdateUserProfilePictureById(caUser.Id, in.ProfilePicture) 290 | if err != nil { 291 | status = e.ErrorOperateDatabase 292 | return &pb.UpdateProfileResponse{ 293 | Code: int32(status), 294 | Msg: e.GetMsg(status), 295 | }, nil 296 | } 297 | logging.Log.Infof("[user update] userId:%d,avatar:%s", caUser.Id, in.ProfilePicture) 298 | } 299 | 300 | return &pb.UpdateProfileResponse{ 301 | Code: int32(status), 302 | Msg: e.GetMsg(status), 303 | }, nil 304 | } 305 | 306 | // Logout 退出登录 307 | func (s *UserService) Logout(ctx context.Context, in *pb.LogoutRequest) (*pb.LogoutResponse, error) { 308 | logging.Log.Infof("[user logout] token:%s", in.Token) 309 | s.userCache.DelUserInfo(in.Token) 310 | return &pb.LogoutResponse{ 311 | Code: e.SUCCESS, 312 | Msg: e.GetMsg(e.SUCCESS), 313 | }, nil 314 | } 315 | -------------------------------------------------------------------------------- /front/src/components/Profile.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 74 | 75 | 257 | 258 | 289 | -------------------------------------------------------------------------------- /front/src/assets/font/demo.css: -------------------------------------------------------------------------------- 1 | /* Logo 字体 */ 2 | @font-face { 3 | font-family: "iconfont logo"; 4 | src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834'); 5 | src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'), 6 | url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'), 7 | url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'), 8 | url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg'); 9 | } 10 | 11 | .logo { 12 | font-family: "iconfont logo"; 13 | font-size: 160px; 14 | font-style: normal; 15 | -webkit-font-smoothing: antialiased; 16 | -moz-osx-font-smoothing: grayscale; 17 | } 18 | 19 | /* tabs */ 20 | .nav-tabs { 21 | position: relative; 22 | } 23 | 24 | .nav-tabs .nav-more { 25 | position: absolute; 26 | right: 0; 27 | bottom: 0; 28 | height: 42px; 29 | line-height: 42px; 30 | color: #666; 31 | } 32 | 33 | #tabs { 34 | border-bottom: 1px solid #eee; 35 | } 36 | 37 | #tabs li { 38 | cursor: pointer; 39 | width: 100px; 40 | height: 40px; 41 | line-height: 40px; 42 | text-align: center; 43 | font-size: 16px; 44 | border-bottom: 2px solid transparent; 45 | position: relative; 46 | z-index: 1; 47 | margin-bottom: -1px; 48 | color: #666; 49 | } 50 | 51 | 52 | #tabs .active { 53 | border-bottom-color: #f00; 54 | color: #222; 55 | } 56 | 57 | .tab-container .content { 58 | display: none; 59 | } 60 | 61 | /* 页面布局 */ 62 | .main { 63 | padding: 30px 100px; 64 | width: 960px; 65 | margin: 0 auto; 66 | } 67 | 68 | .main .logo { 69 | color: #333; 70 | text-align: left; 71 | margin-bottom: 30px; 72 | line-height: 1; 73 | height: 110px; 74 | margin-top: -50px; 75 | overflow: hidden; 76 | *zoom: 1; 77 | } 78 | 79 | .main .logo a { 80 | font-size: 160px; 81 | color: #333; 82 | } 83 | 84 | .helps { 85 | margin-top: 40px; 86 | } 87 | 88 | .helps pre { 89 | padding: 20px; 90 | margin: 10px 0; 91 | border: solid 1px #e7e1cd; 92 | background-color: #fffdef; 93 | overflow: auto; 94 | } 95 | 96 | .icon_lists { 97 | width: 100% !important; 98 | overflow: hidden; 99 | *zoom: 1; 100 | } 101 | 102 | .icon_lists li { 103 | width: 100px; 104 | margin-bottom: 10px; 105 | margin-right: 20px; 106 | text-align: center; 107 | list-style: none !important; 108 | cursor: default; 109 | } 110 | 111 | .icon_lists li .code-name { 112 | line-height: 1.2; 113 | } 114 | 115 | .icon_lists .icon { 116 | display: block; 117 | height: 100px; 118 | line-height: 100px; 119 | font-size: 42px; 120 | margin: 10px auto; 121 | color: #333; 122 | -webkit-transition: font-size 0.25s linear, width 0.25s linear; 123 | -moz-transition: font-size 0.25s linear, width 0.25s linear; 124 | transition: font-size 0.25s linear, width 0.25s linear; 125 | } 126 | 127 | .icon_lists .icon:hover { 128 | font-size: 100px; 129 | } 130 | 131 | .icon_lists .svg-icon { 132 | /* 通过设置 font-size 来改变图标大小 */ 133 | width: 1em; 134 | /* 图标和文字相邻时,垂直对齐 */ 135 | vertical-align: -0.15em; 136 | /* 通过设置 color 来改变 SVG 的颜色/fill */ 137 | fill: currentColor; 138 | /* path 和 stroke 溢出 viewBox 部分在 IE 下会显示 139 | normalize.css 中也包含这行 */ 140 | overflow: hidden; 141 | } 142 | 143 | .icon_lists li .name, 144 | .icon_lists li .code-name { 145 | color: #666; 146 | } 147 | 148 | /* markdown 样式 */ 149 | .markdown { 150 | color: #666; 151 | font-size: 14px; 152 | line-height: 1.8; 153 | } 154 | 155 | .highlight { 156 | line-height: 1.5; 157 | } 158 | 159 | .markdown img { 160 | vertical-align: middle; 161 | max-width: 100%; 162 | } 163 | 164 | .markdown h1 { 165 | color: #404040; 166 | font-weight: 500; 167 | line-height: 40px; 168 | margin-bottom: 24px; 169 | } 170 | 171 | .markdown h2, 172 | .markdown h3, 173 | .markdown h4, 174 | .markdown h5, 175 | .markdown h6 { 176 | color: #404040; 177 | margin: 1.6em 0 0.6em 0; 178 | font-weight: 500; 179 | clear: both; 180 | } 181 | 182 | .markdown h1 { 183 | font-size: 28px; 184 | } 185 | 186 | .markdown h2 { 187 | font-size: 22px; 188 | } 189 | 190 | .markdown h3 { 191 | font-size: 16px; 192 | } 193 | 194 | .markdown h4 { 195 | font-size: 14px; 196 | } 197 | 198 | .markdown h5 { 199 | font-size: 12px; 200 | } 201 | 202 | .markdown h6 { 203 | font-size: 12px; 204 | } 205 | 206 | .markdown hr { 207 | height: 1px; 208 | border: 0; 209 | background: #e9e9e9; 210 | margin: 16px 0; 211 | clear: both; 212 | } 213 | 214 | .markdown p { 215 | margin: 1em 0; 216 | } 217 | 218 | .markdown>p, 219 | .markdown>blockquote, 220 | .markdown>.highlight, 221 | .markdown>ol, 222 | .markdown>ul { 223 | width: 80%; 224 | } 225 | 226 | .markdown ul>li { 227 | list-style: circle; 228 | } 229 | 230 | .markdown>ul li, 231 | .markdown blockquote ul>li { 232 | margin-left: 20px; 233 | padding-left: 4px; 234 | } 235 | 236 | .markdown>ul li p, 237 | .markdown>ol li p { 238 | margin: 0.6em 0; 239 | } 240 | 241 | .markdown ol>li { 242 | list-style: decimal; 243 | } 244 | 245 | .markdown>ol li, 246 | .markdown blockquote ol>li { 247 | margin-left: 20px; 248 | padding-left: 4px; 249 | } 250 | 251 | .markdown code { 252 | margin: 0 3px; 253 | padding: 0 5px; 254 | background: #eee; 255 | border-radius: 3px; 256 | } 257 | 258 | .markdown strong, 259 | .markdown b { 260 | font-weight: 600; 261 | } 262 | 263 | .markdown>table { 264 | border-collapse: collapse; 265 | border-spacing: 0px; 266 | empty-cells: show; 267 | border: 1px solid #e9e9e9; 268 | width: 95%; 269 | margin-bottom: 24px; 270 | } 271 | 272 | .markdown>table th { 273 | white-space: nowrap; 274 | color: #333; 275 | font-weight: 600; 276 | } 277 | 278 | .markdown>table th, 279 | .markdown>table td { 280 | border: 1px solid #e9e9e9; 281 | padding: 8px 16px; 282 | text-align: left; 283 | } 284 | 285 | .markdown>table th { 286 | background: #F7F7F7; 287 | } 288 | 289 | .markdown blockquote { 290 | font-size: 90%; 291 | color: #999; 292 | border-left: 4px solid #e9e9e9; 293 | padding-left: 0.8em; 294 | margin: 1em 0; 295 | } 296 | 297 | .markdown blockquote p { 298 | margin: 0; 299 | } 300 | 301 | .markdown .anchor { 302 | opacity: 0; 303 | transition: opacity 0.3s ease; 304 | margin-left: 8px; 305 | } 306 | 307 | .markdown .waiting { 308 | color: #ccc; 309 | } 310 | 311 | .markdown h1:hover .anchor, 312 | .markdown h2:hover .anchor, 313 | .markdown h3:hover .anchor, 314 | .markdown h4:hover .anchor, 315 | .markdown h5:hover .anchor, 316 | .markdown h6:hover .anchor { 317 | opacity: 1; 318 | display: inline-block; 319 | } 320 | 321 | .markdown>br, 322 | .markdown>p>br { 323 | clear: both; 324 | } 325 | 326 | 327 | .hljs { 328 | display: block; 329 | background: white; 330 | padding: 0.5em; 331 | color: #333333; 332 | overflow-x: auto; 333 | } 334 | 335 | .hljs-comment, 336 | .hljs-meta { 337 | color: #969896; 338 | } 339 | 340 | .hljs-string, 341 | .hljs-variable, 342 | .hljs-template-variable, 343 | .hljs-strong, 344 | .hljs-emphasis, 345 | .hljs-quote { 346 | color: #df5000; 347 | } 348 | 349 | .hljs-keyword, 350 | .hljs-selector-tag, 351 | .hljs-type { 352 | color: #a71d5d; 353 | } 354 | 355 | .hljs-literal, 356 | .hljs-symbol, 357 | .hljs-bullet, 358 | .hljs-attribute { 359 | color: #0086b3; 360 | } 361 | 362 | .hljs-section, 363 | .hljs-name { 364 | color: #63a35c; 365 | } 366 | 367 | .hljs-tag { 368 | color: #333333; 369 | } 370 | 371 | .hljs-title, 372 | .hljs-attr, 373 | .hljs-selector-id, 374 | .hljs-selector-class, 375 | .hljs-selector-attr, 376 | .hljs-selector-pseudo { 377 | color: #795da3; 378 | } 379 | 380 | .hljs-addition { 381 | color: #55a532; 382 | background-color: #eaffea; 383 | } 384 | 385 | .hljs-deletion { 386 | color: #bd2c00; 387 | background-color: #ffecec; 388 | } 389 | 390 | .hljs-link { 391 | text-decoration: underline; 392 | } 393 | 394 | /* 代码高亮 */ 395 | /* PrismJS 1.15.0 396 | https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */ 397 | /** 398 | * prism.js default theme for JavaScript, CSS and HTML 399 | * Based on dabblet (http://dabblet.com) 400 | * @author Lea Verou 401 | */ 402 | code[class*="language-"], 403 | pre[class*="language-"] { 404 | color: black; 405 | background: none; 406 | text-shadow: 0 1px white; 407 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 408 | text-align: left; 409 | white-space: pre; 410 | word-spacing: normal; 411 | word-break: normal; 412 | word-wrap: normal; 413 | line-height: 1.5; 414 | 415 | -moz-tab-size: 4; 416 | -o-tab-size: 4; 417 | tab-size: 4; 418 | 419 | -webkit-hyphens: none; 420 | -moz-hyphens: none; 421 | -ms-hyphens: none; 422 | hyphens: none; 423 | } 424 | 425 | pre[class*="language-"]::-moz-selection, 426 | pre[class*="language-"] ::-moz-selection, 427 | code[class*="language-"]::-moz-selection, 428 | code[class*="language-"] ::-moz-selection { 429 | text-shadow: none; 430 | background: #b3d4fc; 431 | } 432 | 433 | pre[class*="language-"]::selection, 434 | pre[class*="language-"] ::selection, 435 | code[class*="language-"]::selection, 436 | code[class*="language-"] ::selection { 437 | text-shadow: none; 438 | background: #b3d4fc; 439 | } 440 | 441 | @media print { 442 | 443 | code[class*="language-"], 444 | pre[class*="language-"] { 445 | text-shadow: none; 446 | } 447 | } 448 | 449 | /* Code blocks */ 450 | pre[class*="language-"] { 451 | padding: 1em; 452 | margin: .5em 0; 453 | overflow: auto; 454 | } 455 | 456 | :not(pre)>code[class*="language-"], 457 | pre[class*="language-"] { 458 | background: #f5f2f0; 459 | } 460 | 461 | /* Inline code */ 462 | :not(pre)>code[class*="language-"] { 463 | padding: .1em; 464 | border-radius: .3em; 465 | white-space: normal; 466 | } 467 | 468 | .token.comment, 469 | .token.prolog, 470 | .token.doctype, 471 | .token.cdata { 472 | color: slategray; 473 | } 474 | 475 | .token.punctuation { 476 | color: #999; 477 | } 478 | 479 | .namespace { 480 | opacity: .7; 481 | } 482 | 483 | .token.property, 484 | .token.tag, 485 | .token.boolean, 486 | .token.number, 487 | .token.constant, 488 | .token.symbol, 489 | .token.deleted { 490 | color: #905; 491 | } 492 | 493 | .token.selector, 494 | .token.attr-name, 495 | .token.string, 496 | .token.char, 497 | .token.builtin, 498 | .token.inserted { 499 | color: #690; 500 | } 501 | 502 | .token.operator, 503 | .token.entity, 504 | .token.url, 505 | .language-css .token.string, 506 | .style .token.string { 507 | color: #9a6e3a; 508 | background: hsla(0, 0%, 100%, .5); 509 | } 510 | 511 | .token.atrule, 512 | .token.attr-value, 513 | .token.keyword { 514 | color: #07a; 515 | } 516 | 517 | .token.function, 518 | .token.class-name { 519 | color: #DD4A68; 520 | } 521 | 522 | .token.regex, 523 | .token.important, 524 | .token.variable { 525 | color: #e90; 526 | } 527 | 528 | .token.important, 529 | .token.bold { 530 | font-weight: bold; 531 | } 532 | 533 | .token.italic { 534 | font-style: italic; 535 | } 536 | 537 | .token.entity { 538 | cursor: help; 539 | } 540 | -------------------------------------------------------------------------------- /doc/images/entry-task.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /front/src/assets/font/demo_index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IconFont Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |

19 | 29 |
30 |
31 |
    32 | 33 |
  • 34 | 35 |
    邮箱
    36 |
    &#xe632;
    37 |
  • 38 | 39 |
  • 40 | 41 |
    密码
    42 |
    &#xe639;
    43 |
  • 44 | 45 |
  • 46 | 47 |
    lock
    48 |
    &#xe708;
    49 |
  • 50 | 51 |
  • 52 | 53 |
    users
    54 |
    &#xe92e;
    55 |
  • 56 | 57 |
  • 58 | 59 |
    折叠
    60 |
    &#xe601;
    61 |
  • 62 | 63 |
  • 64 | 65 |
    登录-验证码
    66 |
    &#xe60c;
    67 |
  • 68 | 69 |
  • 70 | 71 |
    登录-用户名
    72 |
    &#xe60d;
    73 |
  • 74 | 75 |
76 |
77 |

Unicode 引用

78 |
79 | 80 |

Unicode 是字体在网页端最原始的应用方式,特点是:

81 |
    82 |
  • 兼容性最好,支持 IE6+,及所有现代浏览器。
  • 83 |
  • 支持按字体的方式去动态调整图标大小,颜色等等。
  • 84 |
  • 但是因为是字体,所以不支持多色。只能使用平台里单色的图标,就算项目里有多色图标也会自动去色。
  • 85 |
86 |
87 |

注意:新版 iconfont 支持多色图标,这些多色图标在 Unicode 模式下将不能使用,如果有需求建议使用symbol 的引用方式

88 |
89 |

Unicode 使用步骤如下:

90 |

第一步:拷贝项目下面生成的 @font-face

91 |
@font-face {
 93 |   font-family: 'iconfont';
 94 |   src: url('iconfont.eot');
 95 |   src: url('iconfont.eot?#iefix') format('embedded-opentype'),
 96 |       url('iconfont.woff2') format('woff2'),
 97 |       url('iconfont.woff') format('woff'),
 98 |       url('iconfont.ttf') format('truetype'),
 99 |       url('iconfont.svg#iconfont') format('svg');
100 | }
101 | 
102 |

第二步:定义使用 iconfont 的样式

103 |
.iconfont {
105 |   font-family: "iconfont" !important;
106 |   font-size: 16px;
107 |   font-style: normal;
108 |   -webkit-font-smoothing: antialiased;
109 |   -moz-osx-font-smoothing: grayscale;
110 | }
111 | 
112 |

第三步:挑选相应图标并获取字体编码,应用于页面

113 |
114 | <span class="iconfont">&#x33;</span>
116 | 
117 |
118 |

"iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。

119 |
120 |
121 |
122 |
123 |
    124 | 125 |
  • 126 | 127 |
    128 | 邮箱 129 |
    130 |
    .iconyouxiang 131 |
    132 |
  • 133 | 134 |
  • 135 | 136 |
    137 | 密码 138 |
    139 |
    .iconmima 140 |
    141 |
  • 142 | 143 |
  • 144 | 145 |
    146 | lock 147 |
    148 |
    .iconlock 149 |
    150 |
  • 151 | 152 |
  • 153 | 154 |
    155 | users 156 |
    157 |
    .iconusers 158 |
    159 |
  • 160 | 161 |
  • 162 | 163 |
    164 | 折叠 165 |
    166 |
    .iconzhedie 167 |
    168 |
  • 169 | 170 |
  • 171 | 172 |
    173 | 登录-验证码 174 |
    175 |
    .icondenglu-yanzhengma 176 |
    177 |
  • 178 | 179 |
  • 180 | 181 |
    182 | 登录-用户名 183 |
    184 |
    .icondenglu-yonghuming 185 |
    186 |
  • 187 | 188 |
189 |
190 |

font-class 引用

191 |
192 | 193 |

font-class 是 Unicode 使用方式的一种变种,主要是解决 Unicode 书写不直观,语意不明确的问题。

194 |

与 Unicode 使用方式相比,具有如下特点:

195 |
    196 |
  • 兼容性良好,支持 IE8+,及所有现代浏览器。
  • 197 |
  • 相比于 Unicode 语意明确,书写更直观。可以很容易分辨这个 icon 是什么。
  • 198 |
  • 因为使用 class 来定义图标,所以当要替换图标时,只需要修改 class 里面的 Unicode 引用。
  • 199 |
  • 不过因为本质上还是使用的字体,所以多色图标还是不支持的。
  • 200 |
201 |

使用步骤如下:

202 |

第一步:引入项目下面生成的 fontclass 代码:

203 |
<link rel="stylesheet" href="./iconfont.css">
204 | 
205 |

第二步:挑选相应图标并获取类名,应用于页面:

206 |
<span class="iconfont iconxxx"></span>
207 | 
208 |
209 |

" 210 | iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。

211 |
212 |
213 |
214 |
215 |
    216 | 217 |
  • 218 | 221 |
    邮箱
    222 |
    #iconyouxiang
    223 |
  • 224 | 225 |
  • 226 | 229 |
    密码
    230 |
    #iconmima
    231 |
  • 232 | 233 |
  • 234 | 237 |
    lock
    238 |
    #iconlock
    239 |
  • 240 | 241 |
  • 242 | 245 |
    users
    246 |
    #iconusers
    247 |
  • 248 | 249 |
  • 250 | 253 |
    折叠
    254 |
    #iconzhedie
    255 |
  • 256 | 257 |
  • 258 | 261 |
    登录-验证码
    262 |
    #icondenglu-yanzhengma
    263 |
  • 264 | 265 |
  • 266 | 269 |
    登录-用户名
    270 |
    #icondenglu-yonghuming
    271 |
  • 272 | 273 |
274 |
275 |

Symbol 引用

276 |
277 | 278 |

这是一种全新的使用方式,应该说这才是未来的主流,也是平台目前推荐的用法。相关介绍可以参考这篇文章 279 | 这种用法其实是做了一个 SVG 的集合,与另外两种相比具有如下特点:

280 |
    281 |
  • 支持多色图标了,不再受单色限制。
  • 282 |
  • 通过一些技巧,支持像字体那样,通过 font-size, color 来调整样式。
  • 283 |
  • 兼容性较差,支持 IE9+,及现代浏览器。
  • 284 |
  • 浏览器渲染 SVG 的性能一般,还不如 png。
  • 285 |
286 |

使用步骤如下:

287 |

第一步:引入项目下面生成的 symbol 代码:

288 |
<script src="./iconfont.js"></script>
289 | 
290 |

第二步:加入通用 CSS 代码(引入一次就行):

291 |
<style>
292 | .icon {
293 |   width: 1em;
294 |   height: 1em;
295 |   vertical-align: -0.15em;
296 |   fill: currentColor;
297 |   overflow: hidden;
298 | }
299 | </style>
300 | 
301 |

第三步:挑选相应图标并获取类名,应用于页面:

302 |
<svg class="icon" aria-hidden="true">
303 |   <use xlink:href="#icon-xxx"></use>
304 | </svg>
305 | 
306 |
307 |
308 | 309 |
310 |
311 | 330 | 331 | 332 | --------------------------------------------------------------------------------