├── 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 |
2 |
3 |
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 |
5 |
6 |
7 |
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 |
45 |
46 |
47 |
48 | ### Preview
49 |
50 |
51 |
52 | | login |
53 | profile |
54 |
55 |
56 |  |
57 |  |
58 |
59 |
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 |
6 |
7 |
8 |
9 | 首页
12 | 问题反馈
13 |
14 |
15 |
16 |
17 |
23 |
24 |
25 | 问题反馈
26 |
27 |
28 |
29 |
37 |
38 |
39 |
40 |
41 | 提交反馈
42 | 取消
43 |
44 |
45 |
46 |
47 |
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 |
6 |
7 |
8 |
9 | 首页
10 | 密码修改
11 |
12 |
13 |
14 |
15 |
21 |
22 |
23 |
30 |
31 |
32 |
33 |
40 |
41 |
42 |
43 |
50 |
51 |
52 |
53 | 修改密码
54 | 取消
55 |
56 |
57 |
58 |
59 |
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 |
6 |
7 |
8 |
9 |
10 | entry-task
11 |
12 |
13 |
![]()
14 |
15 |
16 | {{ username }}
17 |
18 |
19 |
20 | 账号设置
23 | 密码设置问题反馈
28 | 退出登录
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
50 |
51 |
56 |
57 |
58 |
59 |
60 |
61 | {{ item.data }}
62 |
63 |
64 |
65 |
71 |
72 |
73 |
74 |
75 | {{ subitem.data }}
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
162 |
163 |
230 |
--------------------------------------------------------------------------------
/front/src/components/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
14 |
15 |
16 |
17 |

18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | 登录
29 | 重置
30 |
31 |
32 |
33 | 没有账号?点此注册
34 |
35 |
36 |
37 |
40 |
41 |
42 |
43 |
121 |
122 |
223 |
--------------------------------------------------------------------------------
/front/src/components/Register.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | 注册
28 | 重置
29 |
30 |
31 |
32 | 已有账号? 点此登录
33 |
34 |
35 |
36 |
39 |
40 |
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 |
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 |
--------------------------------------------------------------------------------
/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 |
6 |
7 |
8 |
9 | 首页
10 | 账号设置
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | 更新
42 | 取消
43 |
44 |
45 |
46 |
47 |
48 |
66 |
70 |
71 |
72 |
73 |
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 |
20 |
21 | - Unicode
22 | - Font class
23 | - Symbol
24 |
25 |
26 |
查看项目
27 |
28 |
29 |
30 |
31 |
32 |
33 | -
34 |
35 |
邮箱
36 | 
37 |
38 |
39 | -
40 |
41 |
密码
42 | 
43 |
44 |
45 | -
46 |
47 |
lock
48 | 
49 |
50 |
51 | -
52 |
53 |
users
54 | 
55 |
56 |
57 | -
58 |
59 |
折叠
60 | 
61 |
62 |
63 | -
64 |
65 |
登录-验证码
66 | 
67 |
68 |
69 | -
70 |
71 |
登录-用户名
72 | 
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">3</span>
116 |
117 |
118 | "iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。
119 |
120 |
121 |
122 |
123 |
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 |
--------------------------------------------------------------------------------