├── .github └── ISSUE_TEMPLATE │ └── bug_report.md ├── .gitignore ├── .hz ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── biz ├── dal │ ├── alog │ │ └── log.go │ ├── bind │ │ └── init.go │ ├── casbin │ │ ├── builtin_operators.go │ │ ├── builtin_operators_test.go │ │ ├── casbin.go │ │ ├── casbin_test.go │ │ ├── rbac_with_domains_model.conf │ │ └── transport.go │ ├── db │ │ ├── custom_type.go │ │ ├── db.go │ │ ├── model.go │ │ └── option.go │ ├── discover │ │ ├── discover.go │ │ └── discover_test.go │ ├── init.go │ ├── ldap │ │ ├── ldap.go │ │ └── ldap_test.go │ ├── migrate │ │ ├── install.go │ │ └── migrate.go │ ├── mysql │ │ └── init.go │ ├── notify │ │ └── notify.go │ ├── redis │ │ ├── demo.go │ │ └── init.go │ └── redisStore │ │ └── redis_store.go ├── handler │ ├── audit │ │ └── audit_log.go │ ├── dashboard │ │ └── dashboard_service.go │ ├── panic.go │ ├── ping.go │ ├── role │ │ └── role.go │ ├── sys_api │ │ └── api.go │ ├── task │ │ └── task_service.go │ ├── task_log │ │ └── log_service.go │ ├── tool │ │ └── tool.go │ ├── user │ │ └── user_service.go │ └── worker │ │ └── worker_service.go ├── model │ ├── dashboard.go │ ├── db_method.go │ ├── role.go │ ├── sys_api.go │ ├── task.go │ ├── task_log.go │ ├── user.go │ └── worker.go ├── mw │ ├── access_log.go │ ├── audit_log.go │ ├── auth.go │ ├── casbin.go │ ├── cors.go │ ├── jwt.go │ ├── jwt_test.go │ ├── mw.go │ ├── recovery.go │ ├── req_aes.go │ ├── requestid.go │ └── with_role.go ├── pack │ ├── dispatcher │ │ ├── cronExpr_help.txt │ │ ├── des.go │ │ ├── dispatcher.go │ │ ├── lang.go │ │ ├── q.go │ │ ├── route.go │ │ ├── scheduler.go │ │ └── task_worker.go │ ├── oidc_callback │ │ ├── oidc_callback.go │ │ └── oidc_test.go │ └── task_ssh │ │ └── index.go ├── response │ ├── common.go │ ├── errno.go │ ├── resp.go │ └── stack.go ├── router │ ├── audit │ │ ├── audit.go │ │ └── middleware.go │ ├── dashboard │ │ ├── dashboard.go │ │ └── middleware.go │ ├── register.go │ ├── role │ │ ├── middleware.go │ │ └── role.go │ ├── sys_api │ │ ├── middleware.go │ │ └── sys_api.go │ ├── task │ │ ├── middleware.go │ │ └── task.go │ ├── task_log │ │ ├── middleware.go │ │ └── task_log.go │ ├── user │ │ ├── middleware.go │ │ └── user.go │ └── worker │ │ ├── middleware.go │ │ └── worker.go └── utils │ ├── getSysInfo.go │ ├── list_to_tree.go │ ├── struct.go │ └── validate.go ├── build.sh ├── cmd ├── index.go ├── srv_http │ ├── main │ │ └── main.go │ ├── router.go │ ├── router_gen.go │ └── start.go ├── srv_rpc │ ├── rpc_test.go │ └── start.go ├── var.go └── worker │ └── main.go ├── conf ├── config.go ├── config.yaml ├── consts.go ├── worker.go └── worker.yaml ├── doc ├── data.sql ├── doc.txt ├── readme.md ├── sql.md └── struct.pptx ├── docker-compose.yml ├── dockers ├── docker-compose.yaml ├── jobor.Dockerfile ├── jobor_worker.Dockerfile ├── jobor_worker_go.Dockerfile └── jobor_worker_py.Dockerfile ├── docs ├── docs.go ├── swagger.json └── swagger.yaml ├── fsembed ├── dist │ ├── assets │ │ ├── ApiPage-65504097.js │ │ ├── AuditLogPage-09a50e77.css │ │ ├── AuditLogPage-6333878d.js │ │ ├── AuditLogPage-6333878d.js.gz │ │ ├── AutoOmissionTag.vue_vue_type_script_setup_true_lang-b334684b.js │ │ ├── Bage.vue_vue_type_script_setup_true_lang-f47800c7.js │ │ ├── BlankLayout-3a8d7bc7.css │ │ ├── BlankLayout-f95ca45c.js │ │ ├── DashboardPage-0979a718.js │ │ ├── DashboardPage-0979a718.js.gz │ │ ├── DashboardPage-f6d511d2.css │ │ ├── Dropdown-cc67fbc4.js │ │ ├── Dropdown-cc67fbc4.js.gz │ │ ├── EditButton.vue_vue_type_script_setup_true_lang-6838874c.js │ │ ├── FiraCode-Regular-f13d1ece.woff2 │ │ ├── FocusDetector-fac0e688.js │ │ ├── FocusDetector-fac0e688.js.gz │ │ ├── FormItem-9f1c1532.js │ │ ├── FormItem-9f1c1532.js.gz │ │ ├── Forward-2b3a5ea4.js │ │ ├── Grid-25c745f0.js │ │ ├── Icon-01f3be5d.js │ │ ├── Icon-01f3be5d.js.gz │ │ ├── InputNumber-3ef1b11c.js │ │ ├── LatoLatin-Regular-ddd4ef7f.woff2 │ │ ├── LatoLatin-Semibold-267eef30.woff2 │ │ ├── LayoutContent-2c0a0216.js │ │ ├── LayoutSider-8ddd0d78.js │ │ ├── LoginMain-10c5d795.css │ │ ├── LoginMain-d5f6707d.js │ │ ├── LoginMain-d5f6707d.js.gz │ │ ├── PageLayout-32092343.css │ │ ├── PageLayout-8b0bff69.js │ │ ├── PageLayout-8b0bff69.js.gz │ │ ├── RestPassword.vue_vue_type_script_setup_true_lang-12be81f1.js │ │ ├── RolePageTree-ba345166.js │ │ ├── RolePageTree-ba345166.js.gz │ │ ├── RoleSelect.vue_vue_type_script_setup_true_lang-ab8d76b3.js │ │ ├── Space-b1fb6b4a.js │ │ ├── Spin-6545735b.js │ │ ├── Switch-eb6978c1.js │ │ ├── TaskLang.vue_vue_type_script_setup_true_lang-adecfa66.js │ │ ├── TaskLogPage-879ad244.js │ │ ├── TaskLogPage-879ad244.js.gz │ │ ├── TaskLogPage-a592a9a2.css │ │ ├── TaskPage-5639496f.js │ │ ├── TaskPage-5639496f.js.gz │ │ ├── TooltipText.vue_vue_type_script_setup_true_lang-7f7f941d.js │ │ ├── UserPage-848618db.js │ │ ├── UserSelect.vue_vue_type_script_setup_true_lang-7bf54d99.js │ │ ├── WorkerPage-26a66a82.js │ │ ├── WorkerPage-26a66a82.js.gz │ │ ├── WorkerPage-55e137a1.css │ │ ├── _plugin-vue_export-helper-c27b6911.js │ │ ├── get-e68d4109.js │ │ ├── index-bbb52c5f.js │ │ ├── index-bbb52c5f.js.gz │ │ ├── index-dee022e3.css │ │ ├── jobor_ico-0674759c.png │ │ ├── jobor_logo-6a5466fd.png │ │ ├── sysApi-cbbdeda6.js │ │ ├── useRequest-1c3afba9.js │ │ ├── useTableData-66353f3c.js │ │ ├── useTableData-66353f3c.js.gz │ │ ├── validate-1d95410a.js │ │ └── worker-2ff11057.js │ ├── iconfont.js │ ├── iconfont.js.gz │ ├── index.html │ └── vite.svg ├── fs.go └── jobor3.sql ├── go.mod ├── handler.go ├── idl ├── README.md ├── api.proto ├── audit.proto ├── base.proto ├── dashboard.proto ├── kv.proto ├── role.proto ├── rpc.proto ├── srv_rpc.proto ├── sys_api.proto ├── task.proto ├── task_log.proto ├── user.proto ├── worker.proto └── worker_rpc.proto ├── img ├── Wechatid.jpeg ├── dash3.png ├── jobor-dash.jpeg ├── log-detail3.png ├── log3.png ├── notify-email.png ├── struct.png ├── structv3.png ├── task-list3.png ├── wechat.jpeg └── worker3.png ├── kitex_gen ├── api │ └── api.pb.go ├── audit │ ├── audit.go │ └── audit.pb.go ├── base │ └── base.pb.go ├── dashboard │ └── dashboard.pb.go ├── env │ └── env.pb.go ├── pbapi │ ├── heartbeat │ │ ├── client.go │ │ ├── heartbeat.go │ │ ├── invoker.go │ │ └── server.go │ ├── srv_rpc.pb.go │ ├── taskservice │ │ ├── client.go │ │ ├── invoker.go │ │ ├── server.go │ │ └── taskservice.go │ └── worker_rpc.pb.go ├── role │ ├── index.go │ └── role.pb.go ├── sys_api │ └── sys_api.pb.go ├── task │ ├── index.go │ └── task.pb.go ├── task_log │ └── task_log.pb.go ├── user │ ├── index.go │ └── user.pb.go └── worker │ └── worker.pb.go ├── main.go ├── pkg ├── convert │ ├── JsonMapStruct.go │ ├── cast.go │ ├── cast_test.go │ ├── caste.go │ ├── convert.go │ ├── structToJson.go │ └── time.go ├── file_folder │ ├── file.go │ ├── folder.go │ ├── tar.go │ ├── utils.go │ └── utils_test.go ├── jwt │ └── jwt.go ├── log │ ├── log.go │ └── logrus.go ├── logger │ ├── formatter.go │ ├── logger.go │ └── rotate.go ├── notify │ ├── README.md │ ├── dingding │ │ ├── README.md │ │ ├── ding_test.go │ │ └── dingding.go │ ├── email │ │ ├── README.md │ │ ├── email.go │ │ ├── email_test.go │ │ └── email_v2.go │ ├── lark │ │ ├── lark.go │ │ └── lark_test.go │ ├── notify.go │ ├── notify_test.go │ ├── slack │ │ ├── README.md │ │ └── slack.go │ ├── telegram │ │ ├── README.md │ │ └── telegram.go │ └── wechat │ │ ├── README.md │ │ └── wechat.go └── utils │ ├── In.go │ ├── In_test.go │ ├── base64.go │ ├── base64_test.go │ ├── cert.go │ ├── checkmail.go │ ├── cmd.go │ ├── cmd_test.go │ ├── colorString.go │ ├── convert.go │ ├── cron.go │ ├── crypto.go │ ├── crypto_test.go │ ├── data.go │ ├── hash.go │ ├── hash_test.go │ ├── homedir.go │ ├── jsonMarshal.go │ ├── random.go │ ├── randstr.go │ ├── randstr_test.go │ ├── request.go │ ├── routine.go │ ├── setList.go │ ├── slice.go │ ├── string.go │ ├── string_test.go │ ├── struct.go │ ├── struct_test.go │ ├── utils.go │ ├── uuid.go │ ├── uuid_test.go │ └── zip.go ├── profile ├── rpc_biz ├── mw │ └── access_log.go ├── registry │ ├── index.go │ ├── registry_mysql.go │ └── registry_redis.go ├── rpc_consts.go └── svc │ ├── server_rpc.go │ └── worker_rpc.go ├── script └── bootstrap.sh ├── scripts └── build.sh └── worker └── worker.go /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Go template 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.exe~ 6 | *.dll 7 | *.so 8 | *.dylib 9 | 10 | # Test binary, built with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Dependency directories (remove the comment below to include it) 17 | # vendor/ 18 | 19 | *.iml 20 | .idea/** 21 | 22 | # For Mac OS X 23 | *~ 24 | .DS_Store 25 | 26 | # For GoLand IDE 27 | .idea/ 28 | .vscode/tasks.json 29 | 30 | # For Go vendor 31 | .cache/ 32 | 33 | # For Project 34 | #/pkg 35 | /bin 36 | /build 37 | 38 | 39 | *.o 40 | *.a 41 | 42 | _obj 43 | _test 44 | *.cgo1.go 45 | *.cgo2.c 46 | _cgo_defun.c 47 | _cgo_gotypes.go 48 | _cgo_export.* 49 | _testmain.go 50 | *.prof 51 | *.log 52 | .DS_Store? 53 | .Trashes 54 | .svn 55 | *.pid 56 | *.bak 57 | *.[568vq] 58 | [568vq].out 59 | #cmd/* 60 | cmd/apibox 61 | *.toml 62 | *.yaml 63 | config.yaml 64 | 65 | internal/tmp 66 | logs 67 | test/ 68 | go.sum 69 | jobor-frontend/mock 70 | jobor-frontend/node_modules 71 | /jobor-frontend/ 72 | jobor_frontend/ 73 | -------------------------------------------------------------------------------- /.hz: -------------------------------------------------------------------------------- 1 | // Code generated by hz. DO NOT EDIT. 2 | 3 | hz version: v0.6.5 4 | handlerDir: biz/handler 5 | modelDir: kitex_gen 6 | routerDir: "" 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | CONTRIBUTING 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: start build 2 | 3 | # Go parameters 4 | # NOW = $(shell date -u '+%Y%m%d%I%M%S') 5 | # GO_PATH=$(shell which go) 6 | MAIN_PATH=./main.go 7 | BUILD_PATH=./build/package/ 8 | CONFIG_FILE=./configs/config.toml 9 | GO_CMD=go 10 | GO_BUILD=$(GO_CMD) build 11 | GO_RUN=$(GO_CMD) run 12 | GO_CLEAN=$(GO_CMD) clean 13 | GO_TEST=$(GO_CMD) test 14 | GO_GET=$(GO_CMD) get 15 | # BINARY_NAME=jobob-amd64 16 | BINARY_NAME=app 17 | BINARY_LINUX=linux-amd64 18 | BINARY_WINDOWS=windows-amd64.exe 19 | BINARY_DARWIN=darwin-amd64 20 | BINARY_ARM=app-arm 21 | BINARY_UNIX=$(BINARY_NAME)_unix 22 | 23 | 24 | all: clean build build-linux build-windows 25 | 26 | build-linux-drawin: build-linux build-mac 27 | 28 | # -ldflags "-w -s" 29 | build: 30 | go build -o $(BUILD_PATH)$(BINARY_NAME) $(MAIN_PATH) 31 | 32 | #swagger: 33 | # bash swag init --generalInfo cmd/main/main.go --exclude cmd/main/test 34 | # swag init -g ./internal/app/router/swagger.go -o ./docs/swagger 35 | # 36 | 37 | test: 38 | $(GO_TEST) -cover -race ./... 39 | 40 | run: 41 | $(GO_RUN) $(MAIN_PATH) -c $(CONFIG_FILE) 42 | 43 | start: 44 | $(GO_BUILD) -o $(BINARY_NAME) -v $(MAIN_PATH) 45 | ./$(BINARY_NAME) 46 | 47 | clean: 48 | rm -f $(BINARY_LINUX) 49 | rm -f $(BINARY_WINDOWS) 50 | 51 | # Cross compilation 52 | build-linux: 53 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO_BUILD) -o $(BUILD_PATH)$(BINARY_LINUX) $(MAIN_PATH) 54 | build-windows: 55 | CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GO_BUILD) -o $(BUILD_PATH)$(BINARY_WINDOWS) $(MAIN_PATH) 56 | build-mac: 57 | CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(GO_BUILD) -o $(BUILD_PATH)$(BINARY_DARWIN) $(MAIN_PATH) 58 | build-android: 59 | CGO_ENABLED=0 GOOS=linux GOARCH=arm $(GO_BUILD) -o $(BUILD_PATH)$(BINARY_ARM) $(MAIN_PATH) 60 | 61 | build-docker: 62 | docker run --rm -it -v "$(GO_PATH)":/go -w /t/a/b/c golang:latest go build -o $(BINARY_NAME) $(MAIN_PATH) 63 | 64 | help: 65 | @echo "make - 格式化 Go 代码, 并编译生成二进制文件" 66 | @echo "make build - 编译 Go 代码, 生成二进制文件" 67 | @echo "make run - 直接运行 Go 代码" 68 | @echo "make clean - 移除二进制文件和 vim swap files" 69 | @echo "make gotool - 运行 Go 工具 'fmt' and 'vet'" -------------------------------------------------------------------------------- /biz/dal/bind/init.go: -------------------------------------------------------------------------------- 1 | package bind 2 | 3 | import ( 4 | "github.com/bytedance/go-tagexpr/v2/binding" 5 | binding2 "github.com/cloudwego/hertz/pkg/app/server/binding" 6 | ) 7 | 8 | type BindError struct { 9 | ErrType, FailField, Msg string 10 | } 11 | 12 | // Error implements error interface. 13 | func (e *BindError) Error() string { 14 | if e.Msg != "" { 15 | return e.ErrType + ": 表达式路径=" + e.FailField + ", 参数错误原因=" + e.Msg 16 | } 17 | return e.ErrType + ": 表达式路径=" + e.FailField + ", 参数错误原因=无效的" 18 | } 19 | 20 | type ValidateError struct { 21 | ErrType, FailField, Msg string 22 | } 23 | 24 | // Error implements error interface. 25 | func (e *ValidateError) Error() string { 26 | if e.Msg != "" { 27 | return e.ErrType + ": 表达式路径=" + e.FailField + ", 验证错误原因=" + e.Msg 28 | } 29 | return e.ErrType + ": 表达式路径=" + e.FailField + ", 验证错误原因=无效的" // cause=invalid 30 | } 31 | 32 | func Init() { 33 | CustomBindErrFunc := func(failField, msg string) error { 34 | err := BindError{ 35 | ErrType: "参数绑定错误", 36 | FailField: "[绑定失败字段]: " + failField, 37 | Msg: msg, 38 | //Msg: "[绑定错误信息]: " + msg, 39 | } 40 | 41 | return &err 42 | } 43 | 44 | CustomValidateErrFunc := func(failField, msg string) error { 45 | err := ValidateError{ 46 | ErrType: "参数验证错误", 47 | FailField: "[验证失败字段]: " + failField, 48 | Msg: msg, 49 | //Msg: "[验证错误信息]: " + msg, 50 | } 51 | 52 | return &err 53 | } 54 | 55 | binding.SetErrorFactory(CustomBindErrFunc, CustomValidateErrFunc) 56 | 57 | // 默认false,全局生效 58 | binding.SetLooseZeroMode(true) 59 | 60 | // 使用标准库 61 | bindConfig := binding2.NewBindConfig() 62 | bindConfig.UseStdJSONUnmarshaler() 63 | 64 | // 使用gjson 65 | //binding.UseGJSONUnmarshaler() 66 | 67 | // 使用第三方json unmarshal方法 68 | //binding.UseThirdPartyJSONUnmarshaler() 69 | } 70 | -------------------------------------------------------------------------------- /biz/dal/casbin/casbin_test.go: -------------------------------------------------------------------------------- 1 | package casbin 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestNewCasbin(t *testing.T) { 8 | //t.Helper() 9 | ////t.Logf("%s < %s: %t", key1, key2, myRes) 10 | //e, err := NewCasbin(fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", "ocean", "123456", "127.0.0.1", "3306", "ops")) 11 | //fmt.Println("err:", err) 12 | // 13 | //roles, err := e.GetRolesForUser("a11", "jobor") 14 | //if err != nil { 15 | // fmt.Println("GetRolesForUser:", err) 16 | // return 17 | //} 18 | //fmt.Println("roles:", roles) 19 | // 20 | //user, err := e.GetUsersForRole("ops", "jobor") 21 | //if err != nil { 22 | // return 23 | //} 24 | //fmt.Println("user:", user) 25 | // 26 | ////filedIndex 0 -- v0 27 | ////filedIndex 1 -- v1 28 | //policy, err := e.RemoveFilteredNamedGroupingPolicy("g", 0, "a11", "", "sys") 29 | //if err != nil { 30 | // fmt.Println("RemoveFilteredNamedGroupingPolicy:", err) 31 | // return 32 | //} 33 | //fmt.Println("policy:", policy) 34 | } 35 | -------------------------------------------------------------------------------- /biz/dal/casbin/rbac_with_domains_model.conf: -------------------------------------------------------------------------------- 1 | [request_definition] 2 | r = sub, dom, obj, act 3 | 4 | [policy_definition] 5 | p = sub, dom, obj, act 6 | 7 | [role_definition] 8 | g = _, _, _ 9 | 10 | [policy_effect] 11 | e = some(where (p.eft == allow)) 12 | 13 | [matchers] 14 | m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act -------------------------------------------------------------------------------- /biz/dal/casbin/transport.go: -------------------------------------------------------------------------------- 1 | package casbin 2 | -------------------------------------------------------------------------------- /biz/dal/discover/discover.go: -------------------------------------------------------------------------------- 1 | package discover 2 | 3 | import ( 4 | "fmt" 5 | "jobor/conf" 6 | utils2 "jobor/pkg/utils" 7 | "log" 8 | "strings" 9 | 10 | "github.com/cloudwego/hertz/pkg/app/server" 11 | 12 | "github.com/cloudwego/hertz/pkg/app/server/registry" 13 | hc "github.com/cloudwego/hertz/pkg/common/config" 14 | "github.com/cloudwego/hertz/pkg/common/utils" 15 | consulapi "github.com/hashicorp/consul/api" 16 | "github.com/hertz-contrib/registry/consul" 17 | "github.com/hertz-contrib/registry/redis" 18 | ) 19 | 20 | func RegistryWeb(options *[]hc.Option) { 21 | tags := map[string]string{"app": strings.ToLower(conf.GetConf().Server.Service), 22 | "group": conf.GetEnv(), "dyeing": conf.GetDyeing()} 23 | if conf.GetConf().Server.EnableRegistry { 24 | localIP := utils2.GetOutBoundIP("tcp", conf.GetConf().Redis.Address) 25 | var r registry.Registry 26 | //var err error 27 | switch conf.GetConf().Server.RegistryCenter { 28 | case "consul": 29 | localIP = utils2.GetOutBoundIP("tcp", conf.GetConf().Consul.Address) 30 | cc := consulapi.DefaultConfig() 31 | cc.Address = conf.GetConf().Consul.Address 32 | cc.Token = conf.GetConf().Consul.Token 33 | consulClient, err := consulapi.NewClient(cc) 34 | if err != nil { 35 | log.Fatal(err) 36 | return 37 | } 38 | // build a consul register with the consul client 39 | r = consul.NewConsulRegister(consulClient) 40 | case "redis": 41 | localIP = utils2.GetOutBoundIP("tcp", conf.GetConf().Redis.Address) 42 | r = redis.NewRedisRegistry(conf.GetConf().Redis.Address) 43 | //case "etcd": 44 | // localIP = utils2.GetOutBoundIP("tcp", conf.GetConf().Etcd.HttpAddress) 45 | // r, err = etcd.NewEtcdRegistry([]string{conf.GetConf().Etcd.HttpAddress}) 46 | // if err != nil { 47 | // panic(err) 48 | // } 49 | //case "zookeeper", "zk": 50 | // localIP = utils2.GetOutBoundIP("tcp", conf.GetConf().Zk.HttpAddress) 51 | // r, err = zookeeper.NewZookeeperRegistry([]string{conf.GetConf().Zk.HttpAddress}, 40*time.Second) 52 | // if err != nil { 53 | // panic(err) 54 | // } 55 | case "default": 56 | log.Fatalf("尚未支持该注册中心[%s]类型\n", conf.GetConf().Server.RegistryCenter) 57 | } 58 | addr := fmt.Sprintf("%s%s", localIP, conf.GetConf().Server.HttpAddress) 59 | *options = append(*options, server.WithRegistry(r, ®istry.Info{ 60 | ServiceName: conf.GetConf().Server.Service, 61 | Addr: utils.NewNetAddr("tcp", addr), 62 | Weight: 10, 63 | Tags: tags, 64 | })) 65 | } 66 | } 67 | 68 | func RegistryRPC(options *[]hc.Option) { 69 | 70 | } 71 | -------------------------------------------------------------------------------- /biz/dal/discover/discover_test.go: -------------------------------------------------------------------------------- 1 | package discover 2 | 3 | import ( 4 | "fmt" 5 | utils2 "jobor/pkg/utils" 6 | "testing" 7 | ) 8 | 9 | func TestRegistry(t *testing.T) { 10 | fmt.Println(utils2.GetLocalIPv4Address()) 11 | fmt.Println(utils2.GetOutBoundIP("tcp", "172.20.106.5:8500")) 12 | } 13 | -------------------------------------------------------------------------------- /biz/dal/init.go: -------------------------------------------------------------------------------- 1 | package dal 2 | 3 | import ( 4 | "context" 5 | "github.com/cloudwego/hertz/pkg/app/server" 6 | "github.com/cloudwego/hertz/pkg/common/hlog" 7 | "jobor/biz/dal/alog" 8 | "jobor/biz/dal/bind" 9 | "jobor/biz/dal/casbin" 10 | "jobor/biz/dal/db" 11 | "jobor/biz/dal/migrate" 12 | "jobor/biz/dal/mysql" 13 | "jobor/biz/dal/redis" 14 | "jobor/biz/dal/redisStore" 15 | "jobor/biz/model" 16 | "jobor/biz/mw" 17 | "jobor/biz/pack/dispatcher" 18 | "jobor/biz/pack/oidc_callback" 19 | "jobor/biz/response" 20 | "jobor/conf" 21 | ) 22 | 23 | var ( 24 | H *server.Hertz 25 | 26 | // NoLogin 登录验证 27 | NoLogin = []string{ 28 | "/login", "/404", "/static", "/favicon.ico", "/air", "/dashboard", "/hostid", "/license", 29 | "/jobor", "/iconfont", "/index.html", "/vite.svg", "/assets", 30 | "/favicon.ico", "/ping", "/swagger/", "/api/v1/swagger/*", "/api/v1/jobor/debug/pprof", 31 | "/metrics", "/routes", "/reverse", "/air", "/api/v1/login", "/fs/", "/api/v1/login-init ", 32 | oidc_callback.CallbackPath, 33 | oidc_callback.GotoRedirect, 34 | } 35 | 36 | // NoAuthorized 权限验证 37 | NoAuthorized = []string{ 38 | "/api/v1/mfa/", "/api/v1/jobor/enum", "/api/v1/jobor/task", "/api/v1/jobor/task-log", "/api/v1/jobor/dashboard", 39 | "/api/v1/sys/user-self", "/api/v1/user-info", 40 | "/api/v1/jobor/user/profile", "/api/v1/sys/user/password", 41 | "/api/v1/logout", "/api/v1/sys/login-history", 42 | "/api/v1/sys/user-profile/", "/api/v1/otp"} 43 | ) 44 | 45 | func init() { 46 | NoAuthorized = append(NoAuthorized, NoLogin...) 47 | } 48 | 49 | func Init() { 50 | //startTime := time.Now() 51 | //InitZeroLog(config.GetConf().Server.LogFileName, config.LogLevel()) 52 | alog.InitHzLog(conf.GetConf().Server.LogFileName, conf.LogLevel()) 53 | db.DefaultAesKey = conf.GetConf().Ext.DataAesKey 54 | response.DefaultAesKey = conf.GetConf().Ext.WebAesKey 55 | // Casbin连接初始化 56 | if _, err := casbin.NewCasbin(conf.GetConf()); err != nil { 57 | hlog.Fatal(err) 58 | } 59 | //hlog.Debug(time.Since(startTime).String()) 60 | // 参数绑定报错自定义初始化 61 | bind.Init() 62 | // Redis连接初始化 63 | redis.Init(conf.GetConf()) 64 | 65 | // MySQL连接初始化 66 | mysql.Init() 67 | 68 | // init redis store 69 | redisStore.Init() 70 | 71 | // DB连接初始化 72 | //migrate.Migrate() 73 | 74 | // DB 数据初始化 75 | install, err := migrate.QueryIsInstall(context.TODO(), db.DB) 76 | if err != nil { 77 | hlog.Fatal(err) 78 | return 79 | } 80 | if !install { 81 | hlog.Debugf("jobor db init is start ") 82 | err = migrate.StartInstall(context.TODO(), db.DB, "jobor3.sql") 83 | if err != nil { 84 | hlog.Fatal(err) 85 | return 86 | } 87 | } 88 | 89 | // JWT连接初始化 90 | mw.InitJwt() 91 | //hlog.Debug(time.Since(startTime).String()) 92 | 93 | go func() { 94 | hlog.Fatal(redis.Subscribe(context.TODO(), dispatcher.Fn, model.PubSubChannel)) 95 | }() 96 | 97 | if _, err := dispatcher.InitQSrv(&conf.GetConf().Redis, dispatcher.Queue); err != nil { 98 | hlog.Fatal(err) 99 | return 100 | } 101 | 102 | dispatcher.InitCron() 103 | 104 | } 105 | -------------------------------------------------------------------------------- /biz/dal/ldap/ldap_test.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/go-ldap/ldap/v3" 9 | ) 10 | 11 | func TestLDAP_Authentication(t *testing.T) { 12 | // cn=ab,cn=g1,ou=coo,dc=example,dc=org 13 | var ldapApi = NewLDAP() 14 | ldapApi.Option.Addr = fmt.Sprintf("%s:%s", "127.0.0.1", "389") 15 | //ldapApi.Option.Addr = fmt.Sprintf("ldap://%s:%s", "127.0.0.1", "389") 16 | //ldapApi.Option.BaseDN = "dc=example,dc=org" 17 | ldapApi.Option.BaseDN = "dc=example,dc=org" 18 | ldapApi.Option.AuthFilter = "(cn=%s)" 19 | //ldapApi.Option.Attributes = []string{"uid","cn"} 20 | ldapApi.Option.Attributes = []string{"cn", "mail", "displayName", "dn"} 21 | ldapApi.Option.Username = "cn=admin,dc=example,dc=org" 22 | ldapApi.Option.Password = "admin" 23 | user, ldapAuthErr := ldapApi.Authentication("ab", "123") 24 | fmt.Println("ldapAuthErr:", ldapAuthErr) 25 | fmt.Println("user:", user) 26 | } 27 | 28 | func TestLDAP_Authentication1(t *testing.T) { 29 | aa() 30 | } 31 | 32 | func aa() { 33 | ldap.DefaultTimeout = time.Second * 3 34 | //conn, err := ldap.DialTLS("tcp",fmt.Sprintf("%s:%s", "127.0.0.1", "636"), &tls.Config{InsecureSkipVerify: true}) 35 | //conn, err := ldap.DialURL(fmt.Sprintf("ldap://%s:%s", "127.0.0.1", "389")) 36 | conn, err := ldap.Dial("tcp", fmt.Sprintf("%s:%s", "127.0.0.1", "389")) 37 | if err != nil { 38 | panic(err) 39 | } 40 | //err = conn.Bind("ab", "123") 41 | err = conn.Bind("cn=admin,dc=example,dc=org", "admin") 42 | if err != nil { 43 | fmt.Print("bind err:", err) 44 | return 45 | } 46 | 47 | //Filter:=fmt.Sprintf("(cn=*)") 48 | Filter := fmt.Sprintf("(cn=%s)", "ab") 49 | Attributes := []string{"cn"} 50 | searchRequest := ldap.NewSearchRequest( 51 | "ou=coo,dc=example,dc=org", 52 | ldap.ScopeWholeSubtree, 53 | ldap.NeverDerefAliases, // ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, ldap.DerefAlways 54 | 0, // SizeLimit: 大小设置,一般设置为0 55 | 0, // TimeLimit: 时间设置,一般设置为0 56 | false, //TypesOnly: 设置false(返回的值要多一点) 57 | Filter, // "(objectClass=*)", 58 | Attributes, //Attributes 需要返回的属性值 59 | nil) 60 | cur, err := conn.Search(searchRequest) 61 | fmt.Println("cur.searchRequest:", searchRequest) 62 | if err != nil { 63 | fmt.Println("cur.Search err:", err) 64 | return 65 | } 66 | fmt.Println("entry:", cur.Entries[0].DN) 67 | } 68 | -------------------------------------------------------------------------------- /biz/dal/migrate/migrate.go: -------------------------------------------------------------------------------- 1 | package migrate 2 | 3 | import ( 4 | "github.com/cloudwego/hertz/pkg/common/hlog" 5 | "jobor/biz/dal/db" 6 | "jobor/biz/model" 7 | "jobor/kitex_gen/audit" 8 | ) 9 | 10 | func Migrate() { 11 | 12 | err := db.DB.AutoMigrate( 13 | &model.JoborTask{}, 14 | &model.JoborWorker{}, 15 | &model.JoborLog{}, 16 | &audit.AuditLog{}, 17 | &model.Api{}, 18 | &model.Role{}, 19 | &model.User{}, 20 | ) 21 | if err != nil { 22 | hlog.Fatal("AutoMigrate err:", err) 23 | } 24 | //fmt.Println(db.DB.AutoMigrate(new(tbs.Key)).Error) 25 | //MigSys() 26 | } 27 | 28 | func MigSys() { 29 | err := db.DB.AutoMigrate( 30 | //&api.Api{}, 31 | ) 32 | if err != nil { 33 | hlog.Fatal("auto migrate sys err:", err) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /biz/dal/mysql/init.go: -------------------------------------------------------------------------------- 1 | package mysql 2 | 3 | import ( 4 | "jobor/biz/dal/db" 5 | "jobor/conf" 6 | "log" 7 | "os" 8 | "time" 9 | 10 | "gorm.io/driver/mysql" 11 | "gorm.io/gorm" 12 | "gorm.io/gorm/logger" 13 | "gorm.io/gorm/schema" 14 | ) 15 | 16 | //var DB *gorm.DB 17 | 18 | func Init() { 19 | db.DB = ConnectDB(conf.GetConf().MySQL.DSN) 20 | //db.DB = DB 21 | } 22 | 23 | func ConnectDB(dsn string) *gorm.DB { 24 | newLogger := logger.New( 25 | log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer 26 | logger.Config{ 27 | SlowThreshold: time.Second, // 慢 SQL 阈值 28 | LogLevel: logger.Error, // TaskLog level 29 | Colorful: false, // 禁用彩色打印 30 | }, 31 | ) 32 | var gconf = gorm.Config{ 33 | //DryRun: false, 34 | Logger: newLogger, 35 | SkipDefaultTransaction: true, 36 | PrepareStmt: true, 37 | QueryFields: true, 38 | DisableForeignKeyConstraintWhenMigrating: true, 39 | NamingStrategy: schema.NamingStrategy{ 40 | //TablePrefix: "t_", // 表名前缀,`User` 的表名应该是 `t_users` 41 | SingularTable: true, // 使用单数表名,启用该选项,此时,`User` 的表名应该是 `t_user` 42 | }, 43 | } 44 | 45 | tdb, err := gorm.Open(mysql.Open(dsn), &gconf) 46 | if err != nil { 47 | panic(err) 48 | } 49 | return tdb 50 | } 51 | -------------------------------------------------------------------------------- /biz/dal/notify/notify.go: -------------------------------------------------------------------------------- 1 | package notify 2 | -------------------------------------------------------------------------------- /biz/dal/redisStore/redis_store.go: -------------------------------------------------------------------------------- 1 | package redisStore 2 | 3 | import ( 4 | "fmt" 5 | "jobor/conf" 6 | 7 | "github.com/cloudwego/hertz/pkg/common/hlog" 8 | "github.com/hertz-contrib/sessions" 9 | "github.com/hertz-contrib/sessions/redis" 10 | ) 11 | 12 | var Store redis.Store 13 | 14 | func Init() { 15 | Store = Connect() 16 | } 17 | 18 | func Connect() redis.Store { 19 | store, err := redis.NewStoreWithDB(10, "tcp", 20 | conf.GetConf().Redis.Address, 21 | conf.GetConf().Redis.Password, 22 | fmt.Sprintf("%d", conf.GetConf().Redis.Db), 23 | []byte(conf.GetConf().Authentication.AuthSecret)) 24 | if err != nil { 25 | hlog.Fatalf("session init redis err, %s", err) 26 | } 27 | store.Options(GetSessionOption()) 28 | return store 29 | } 30 | 31 | func GetSessionOption() sessions.Options { 32 | return sessions.Options{ 33 | Path: "/", 34 | MaxAge: conf.GetConf().Authentication.MaxAge, 35 | } 36 | } 37 | 38 | func GetRedisStore() (rediStore *redis.RediStore, err error) { 39 | rediStore, err = redis.GetRedisStore(Store) 40 | if err != nil { 41 | return nil, err 42 | } 43 | return rediStore, err 44 | } 45 | -------------------------------------------------------------------------------- /biz/handler/audit/audit_log.go: -------------------------------------------------------------------------------- 1 | // Code generated by hertz generator. 2 | 3 | package audit 4 | 5 | import ( 6 | "context" 7 | "jobor/biz/dal/db" 8 | "jobor/biz/model" 9 | "jobor/biz/response" 10 | 11 | "github.com/cloudwego/hertz/pkg/app" 12 | audit "jobor/kitex_gen/audit" 13 | ) 14 | 15 | // GetAuditLog . 16 | // 17 | // @Summary audit log get summary 18 | // @Description audit log get 19 | // @Tags audit 20 | // @Param status query string false "status success|failed" 21 | // @Param username query string false "username" 22 | // @router /api/v1/jobor/audit-log [GET] 23 | func GetAuditLog(ctx context.Context, c *app.RequestContext) { 24 | var err error 25 | var req audit.AuditReq 26 | if err = c.BindAndValidate(&req); err != nil { 27 | response.ParamFailed(ctx, c, err) 28 | return 29 | } 30 | 31 | var objs []audit.AuditLog 32 | 33 | resp := response.InitPageData(ctx, c, &objs, false) 34 | 35 | if err = model.PageDataWithScopes(db.DB.Model(&audit.AuditLog{}), audit.NameAuditLog, model.Find, &resp, 36 | model.GetScopesList(audit.SelectScopes()), audit.WhereScopes(&req), audit.OrderScopes()); err != nil { 37 | response.SendBaseResp(ctx, c, err) 38 | return 39 | } 40 | response.SendDataResp(ctx, c, response.Succeed, resp) 41 | } 42 | -------------------------------------------------------------------------------- /biz/handler/dashboard/dashboard_service.go: -------------------------------------------------------------------------------- 1 | // Code generated by hertz generator. 2 | 3 | package dashboard 4 | 5 | import ( 6 | "context" 7 | "jobor/biz/dal/db" 8 | "jobor/biz/model" 9 | "jobor/biz/response" 10 | 11 | "github.com/cloudwego/hertz/pkg/app" 12 | ) 13 | 14 | // GetDashboard . 15 | // 16 | // @Summary dashboard summary 17 | // @Description dashboard 18 | // @Tags dashboard 19 | // 20 | // @router /api/v1/jobor/dashboard [GET] 21 | func GetDashboard(ctx context.Context, c *app.RequestContext) { 22 | var err error 23 | var req model.DashParam 24 | err = c.BindAndValidate(&req) 25 | if err != nil { 26 | response.ParamFailed(ctx, c, err) 27 | //response.SendBaseResp(ctx, c, err) 28 | return 29 | } 30 | 31 | resp := model.Dash{} 32 | resp, err = model.GetDash(ctx, db.DB, req) 33 | if err != nil { 34 | response.SendBaseResp(ctx, c, err) 35 | return 36 | } 37 | response.SendDataResp(ctx, c, response.Succeed, resp) 38 | } 39 | -------------------------------------------------------------------------------- /biz/handler/panic.go: -------------------------------------------------------------------------------- 1 | // Code generated by hertz generator. 2 | 3 | package handler 4 | 5 | import ( 6 | "context" 7 | 8 | "github.com/cloudwego/hertz/pkg/app" 9 | ) 10 | 11 | // Panic . 12 | func Panic(ctx context.Context, c *app.RequestContext) { 13 | panic("Panic Test") 14 | } 15 | -------------------------------------------------------------------------------- /biz/handler/ping.go: -------------------------------------------------------------------------------- 1 | // Code generated by hertz generator. 2 | 3 | package handler 4 | 5 | import ( 6 | "context" 7 | 8 | "github.com/cloudwego/hertz/pkg/app" 9 | "github.com/cloudwego/hertz/pkg/common/utils" 10 | "github.com/cloudwego/hertz/pkg/protocol/consts" 11 | ) 12 | 13 | // Ping . 14 | func Ping(ctx context.Context, c *app.RequestContext) { 15 | c.JSON(consts.StatusOK, utils.H{ 16 | "message": "pong", 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /biz/handler/task_log/log_service.go: -------------------------------------------------------------------------------- 1 | // Code generated by hertz generator. 2 | 3 | package task_log 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "jobor/biz/model" 9 | "jobor/biz/pack/dispatcher" 10 | "jobor/biz/response" 11 | "jobor/kitex_gen/task_log" 12 | "jobor/pkg/convert" 13 | 14 | "github.com/cloudwego/hertz/pkg/app" 15 | ) 16 | 17 | // GetLog . 18 | // 19 | // @Summary jobor log get summary 20 | // @Description jobor log get 21 | // @Tags jobor log 22 | // @Param name query string false "name" 23 | // @Param page query int false "page" 24 | // @Param pageSize query int false "pageSize" 25 | // 26 | // @router /api/v1/jobor/log [GET] 27 | func GetLog(ctx context.Context, c *app.RequestContext) { 28 | var err error 29 | userinfo, err := model.GetUserSession(c, false) 30 | if err != nil { 31 | response.SendBaseResp(ctx, c, err) 32 | return 33 | } 34 | var req task_log.LogQuery 35 | err = c.BindAndValidate(&req) 36 | if err = c.BindAndValidate(&req); err != nil { 37 | response.ParamFailed(ctx, c, err) 38 | return 39 | } 40 | 41 | var objs model.Logs 42 | 43 | resp := response.InitPageData(ctx, c, &objs, false) 44 | 45 | if _, err = objs.List(&req, &userinfo, &resp); err != nil { 46 | response.SendBaseResp(ctx, c, err) 47 | return 48 | } 49 | response.SendDataResp(ctx, c, response.Succeed, resp) 50 | } 51 | 52 | // AbortTask . 53 | // 54 | // @Summary jobor task abort summary 55 | // @Description jobor task abort 56 | // @Tags jobor log 57 | // 58 | // @router /api/v1/jobor/log/:id/abort [POST] 59 | func AbortTask(ctx context.Context, c *app.RequestContext) { 60 | var err error 61 | var req task_log.PostLogReq 62 | err = c.BindAndValidate(&req) 63 | if err = c.BindAndValidate(&req); err != nil { 64 | response.ParamFailed(ctx, c, err) 65 | return 66 | } 67 | _id := c.Params.ByName("id") 68 | _, ok := dispatcher.CacheTask.TaskLogs[convert.ToInt(_id)] 69 | if !ok { 70 | response.SendBaseResp(ctx, c, fmt.Errorf("任务[%s]已经完成或不存在", _id)) 71 | return 72 | } 73 | dispatcher.CacheTask.TaskLogs[convert.ToInt(_id)].TaskCancel() 74 | 75 | response.SendDataResp(ctx, c, response.Succeed, "") 76 | } 77 | 78 | // GetLogById . 79 | // 80 | // @Summary jobor task log get summary 81 | // @Description jobor task log get 82 | // @Tags jobor log 83 | // @Param id path int true "int valid" 84 | // 85 | // @router /api/v1/jobor/log/{id} [GET] 86 | func GetLogById(ctx context.Context, c *app.RequestContext) { 87 | var err error 88 | _id := c.Params.ByName("id") 89 | 90 | Resp, err := model.GetLogInfoById(_id, false) 91 | if err != nil { 92 | response.SendBaseResp(ctx, c, err) 93 | return 94 | } 95 | 96 | response.SendDataResp(ctx, c, response.Succeed, Resp) 97 | } 98 | -------------------------------------------------------------------------------- /biz/mw/access_log.go: -------------------------------------------------------------------------------- 1 | package mw 2 | 3 | import ( 4 | "context" 5 | "jobor/conf" 6 | "time" 7 | 8 | "github.com/cloudwego/hertz/pkg/app" 9 | "github.com/cloudwego/hertz/pkg/common/hlog" 10 | ) 11 | 12 | func AccessLog() app.HandlerFunc { 13 | return func(ctx context.Context, c *app.RequestContext) { 14 | start := time.Now() 15 | reqId, _ := ctx.Value(conf.RequestIDHeaderValue).(string) 16 | //spanid1, _ := c.Value("Spanid").(string) 17 | //spanid2, _ := c.Value("SpanId").(string) 18 | //spanid3, _ := c.Value("spanid").(string) 19 | //fmt.Println("spanid:", spanid1, spanid3, spanid2) 20 | c.Set(conf.RequestIDHeaderValue, reqId) 21 | //reqId := "" 22 | defer func() { 23 | un := c.GetString("username") 24 | if un == "" { 25 | un = "Anonymous" 26 | } 27 | end := time.Now() 28 | latency := end.Sub(start).String() 29 | hlog.CtxDebugf(ctx, "username=%s status=%d cost=%s method=%s full_path=%s client_ip=%s host=%s user_agent=%s request_id=%s", 30 | un, c.Response.StatusCode(), latency, 31 | c.Request.Header.Method(), c.Request.URI().RequestURI(), c.ClientIP(), c.Request.Host(), 32 | c.UserAgent(), reqId) 33 | }() 34 | c.Next(ctx) 35 | if c.Response.Header.Get("Server") == "hertz" { 36 | c.Response.Header.Del("Server") 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /biz/mw/jwt_test.go: -------------------------------------------------------------------------------- 1 | package mw 2 | -------------------------------------------------------------------------------- /biz/mw/mw.go: -------------------------------------------------------------------------------- 1 | package mw 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "jobor/biz/response" 7 | "strings" 8 | 9 | "github.com/cloudwego/hertz/pkg/app" 10 | ) 11 | 12 | // NoMethodHandler 未找到请求方法的处理函数 13 | func NoMethodHandler() app.HandlerFunc { 14 | return func(c context.Context, ctx *app.RequestContext) { 15 | var data = response.Response{Code: 405, Message: "方法不被允许", Status: "error"} 16 | ctx.JSON(405, data) 17 | } 18 | } 19 | 20 | // NoRouteHandler 未找到请求路由的处理函数 21 | func NoRouteHandler() app.HandlerFunc { 22 | return func(c context.Context, ctx *app.RequestContext) { 23 | var data = response.Response{Code: 500, Message: "未找到请求路由的处理函数", Status: "error"} 24 | ctx.JSON(500, data) 25 | } 26 | } 27 | 28 | // SkipperFunc 定义中间件跳过函数 29 | type SkipperFunc func(c context.Context, ctx *app.RequestContext) bool 30 | 31 | // AllowPathPrefixSkipper 检查请求路径是否包含指定的前缀,如果包含则跳过 32 | func AllowPathPrefixSkipper(prefixes ...string) SkipperFunc { 33 | return func(c context.Context, ctx *app.RequestContext) bool { 34 | path := string(ctx.Request.URI().RequestURI()) 35 | //fmt.Println("path:", path) 36 | pathLen := len(path) 37 | 38 | if path == "/" { 39 | return true 40 | } 41 | 42 | for _, p := range prefixes { 43 | if pl := len(p); pathLen >= pl && path[:pl] == p { 44 | return true 45 | } 46 | } 47 | return false 48 | } 49 | } 50 | 51 | // AllowPathPrefixNoSkipper 检查请求路径是否包含指定的前缀,如果包含则不跳过 52 | func AllowPathPrefixNoSkipper(prefixes ...string) SkipperFunc { 53 | return func(c context.Context, ctx *app.RequestContext) bool { 54 | path := string(ctx.Request.URI().RequestURI()) 55 | pathLen := len(path) 56 | 57 | for _, p := range prefixes { 58 | if pl := len(p); pathLen >= pl && path[:pl] == p { 59 | return false 60 | } 61 | } 62 | return true 63 | } 64 | } 65 | 66 | // AllowMethodAndPathPrefixSkipper 检查请求方法和路径是否包含指定的前缀,如果包含则跳过 67 | func AllowMethodAndPathPrefixSkipper(prefixes ...string) SkipperFunc { 68 | return func(c context.Context, ctx *app.RequestContext) bool { 69 | path := JoinRouter(string(ctx.Request.Method()), string(ctx.Request.URI().RequestURI())) 70 | pathLen := len(path) 71 | 72 | for _, p := range prefixes { 73 | if pl := len(p); pathLen >= pl && path[:pl] == p { 74 | return true 75 | } 76 | } 77 | return false 78 | } 79 | } 80 | 81 | // JoinRouter 拼接路由 82 | func JoinRouter(method, path string) string { 83 | if len(path) > 0 && path[0] != '/' { 84 | path = "/" + path 85 | } 86 | return fmt.Sprintf("%s%s", strings.ToUpper(method), path) 87 | } 88 | -------------------------------------------------------------------------------- /biz/mw/recovery.go: -------------------------------------------------------------------------------- 1 | package mw 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "jobor/biz/response" 7 | 8 | "github.com/cloudwego/hertz/pkg/app" 9 | "github.com/cloudwego/hertz/pkg/app/middlewares/server/recovery" 10 | "github.com/cloudwego/hertz/pkg/common/hlog" 11 | ) 12 | 13 | func Recovery() app.HandlerFunc { 14 | return recovery.Recovery( 15 | recovery.WithRecoveryHandler(func(c context.Context, ctx *app.RequestContext, err interface{}, stack []byte) { 16 | hlog.SystemLogger().CtxErrorf(c, "[Recovery] err=%v\nstack=%s", err, stack) 17 | response.FailedCodeRecovery(c, ctx, int(response.Err_ServerInternalErr), fmt.Errorf("[Recovery]: %v", err), fmt.Errorf("%s", string(stack))) 18 | ctx.Next(c) 19 | })) 20 | } 21 | -------------------------------------------------------------------------------- /biz/mw/req_aes.go: -------------------------------------------------------------------------------- 1 | package mw 2 | 3 | import ( 4 | "context" 5 | "jobor/biz/response" 6 | "jobor/pkg/utils" 7 | 8 | "github.com/cloudwego/hertz/pkg/app" 9 | ) 10 | 11 | // ReqAesDec header: X-Enc-Data = yes 12 | func ReqAesDec() app.HandlerFunc { 13 | return func(c context.Context, ctx *app.RequestContext) { 14 | encryptTag := ctx.Request.Header.Get("X-Enc-Data") 15 | if encryptTag == "yes" { 16 | rawData := ctx.GetRawData() // body 只能读一次,读出来之后需要重置下 Body 17 | data := utils.DeTxtByAes(string(rawData), response.DefaultAesKey) 18 | ctx.Request.ResetBody() 19 | ctx.Request.SetBodyRaw([]byte(data)) // 重置body 20 | ctx.Request.Header.Set("Content-Type", "application/json; charset=utf-8") 21 | } 22 | // 处理请求 23 | ctx.Next(c) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /biz/mw/requestid.go: -------------------------------------------------------------------------------- 1 | package mw 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | ) 6 | 7 | type RequestIdHook struct{} 8 | 9 | func (h *RequestIdHook) Levels() []logrus.Level { 10 | return logrus.AllLevels 11 | } 12 | 13 | func (h *RequestIdHook) Fire(e *logrus.Entry) error { 14 | ctx := e.Context 15 | if ctx == nil { 16 | return nil 17 | } 18 | value := ctx.Value("X-Request-Id") 19 | if value != nil { 20 | e.Data["log_id"] = value 21 | } 22 | return nil 23 | } 24 | 25 | func InitRequestid() { 26 | //logger := hertzlogrus.NewLogger(hertzlogrus.WithHook(&RequestIdHook{})) 27 | //hlog.SetLogger(logger) 28 | 29 | } 30 | -------------------------------------------------------------------------------- /biz/mw/with_role.go: -------------------------------------------------------------------------------- 1 | package mw 2 | 3 | import ( 4 | "context" 5 | "github.com/cloudwego/hertz/pkg/app" 6 | "jobor/biz/model" 7 | "jobor/biz/response" 8 | "jobor/pkg/utils" 9 | ) 10 | 11 | func HandlerFuncWithRole(allowRoles []string) app.HandlerFunc { 12 | return func(ctx context.Context, c *app.RequestContext) { 13 | userValue, err := model.GetUserSession(c, false) 14 | if err != nil { 15 | response.SendBaseResp(ctx, c, err) 16 | return 17 | } 18 | 19 | if len(utils.Intersect(utils.Union([]string{admin, root, JoborAdmin, JoborRW}, allowRoles), userValue.Roles)) > 0 { 20 | c.Next(ctx) 21 | return 22 | } else { 23 | response.SendBaseResp(ctx, c, response.UnauthorizedErr) 24 | c.Abort() 25 | return 26 | } 27 | } 28 | } 29 | 30 | var ( 31 | //AdminRoles = []string{"jobor_admin", "jobor_rw"} 32 | JoborAdmin = "jobor_admin" 33 | JoborRW = "jobor_rw" 34 | JoborR = "jobor_r" 35 | AmsRw = "jobor_ams_rw" 36 | AmsR = "jobor_ams_r" 37 | AssetRW = "jobor_asset_rw" 38 | AssetR = "jobor_asset_r" 39 | JoborInfoRw = "jobor_info_rw" 40 | JoborInfoR = "jobor_info_r" 41 | SysRw = "jobor_sys_rw" 42 | SysR = "jobor_sys_r" 43 | AmsRolesRW = []string{AmsRw} 44 | AmsRolesR = append([]string{AmsR, JoborR}, AmsRolesRW...) 45 | AssetRolesRW = []string{AssetRW} 46 | AssetRolesR = append([]string{AssetR, JoborR}, AssetRolesRW...) 47 | JoborInfoRolesRW = []string{JoborInfoRw} 48 | JoborInfoRolesR = append([]string{JoborInfoR, JoborR}, JoborInfoRolesRW...) 49 | SysRolesRW = []string{SysRw} 50 | SysRolesR = append([]string{SysR, JoborR}, SysRolesRW...) 51 | ) 52 | -------------------------------------------------------------------------------- /biz/pack/dispatcher/cronExpr_help.txt: -------------------------------------------------------------------------------- 1 | 1、定时任务格式: 2 | Second Minute Hour Day Month Week 3 | 秒 分钟 小时 天 月 星期 4 | 0-59 0-59 0-23 1-31 1-12 0-6 5 | Second 每分钟第几秒执行 6 | Minute 每个小时的第几分钟执行该任务 7 | Hour 每天的第几个小时执行该任务 8 | Day 每月的第几天执行该任务 9 | Month 每年的第几个月执行该任务 10 | DayOfWeek 每周的第几天执行该任务,0表示周日 11 | 12 | 2、特殊符号含义 13 | "*"代表取值范围内的所有数字, 14 | "/"代表 "每", 15 | "L"代表每月的最后一天仅可在月中使用 16 | “-”代表从某个数字到某个数字, 17 | “,”分开几个离散的数字 18 | 19 | 3、一些示例 20 | 5 * * * * * 每分钟的第5分钟执行 21 | 30 5 * * * * 每小时的第5:30执行 22 | 30 7 8 * * * 每天的08:07:30执行 23 | 30 5 8 6 * * 每月的6日8时5分30秒执行 24 | 30 6 12 * * 0 每星期日的12:06:30执行[注:0表示星期天,1表示星期1,以此类推] 25 | 30 3 10,20 * * * 每天10点及20点的03:30执行[注:“,”用来连接多个不连续的时段] 26 | 25 8-11 * * * * 每小时的8-11分的第25秒执行[注:“-”用来连接连续的时段] 27 | */15 * * * * * 每15秒执行一次 [即每个分钟的第0 15 30 45 60秒执行 ] 28 | 1 30 6 */10 * * 每月中,每隔10天的第6:30:1执行一次[即每月的1、11、21、31日是的6:30:1执行一次 ] 29 | 10,20,30 * * * * * 每分钟的第10、20、30秒执行 -------------------------------------------------------------------------------- /biz/pack/dispatcher/des.go: -------------------------------------------------------------------------------- 1 | package dispatcher 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | var ( 10 | xin = "*" 11 | wenhao = "?" 12 | dao = "-" 13 | mei = "/" 14 | huo = "," 15 | ) 16 | 17 | func main() { 18 | cron := "* 5-8 1,5,7 * * ?" 19 | s, _ := descCorn(cron) 20 | fmt.Println(s) 21 | } 22 | 23 | func descCorn(cronExp string) (str string, err error) { 24 | 25 | tmpCorns := strings.Split(cronExp, " ") 26 | if len(tmpCorns) != 6 { 27 | err = errors.New("长度必须为6") 28 | return 29 | } 30 | var sBuffer strings.Builder 31 | // 解析月 32 | descMonth(tmpCorns[4], &sBuffer) 33 | // 解析周 34 | descWeek(tmpCorns[5], &sBuffer) 35 | // 解析日 36 | descDay(tmpCorns[3], &sBuffer) 37 | // 解析时 38 | descHour(tmpCorns[2], &sBuffer) 39 | // 解析分 40 | descMintue(tmpCorns[1], &sBuffer) 41 | // 解析秒 42 | descSecond(tmpCorns[0], &sBuffer) 43 | sBuffer.WriteString("执行") 44 | str = sBuffer.String() 45 | return 46 | } 47 | 48 | func descSecond(s string, sb *strings.Builder) { 49 | _ = desc(s, sb, "秒") 50 | } 51 | 52 | func descMintue(s string, sb *strings.Builder) { 53 | _ = desc(s, sb, "分") 54 | } 55 | 56 | func descHour(s string, sb *strings.Builder) { 57 | _ = desc(s, sb, "时") 58 | } 59 | 60 | func descDay(s string, sb *strings.Builder) { 61 | _ = desc(s, sb, "天") 62 | } 63 | 64 | func descWeek(s string, sb *strings.Builder) { 65 | 66 | } 67 | 68 | func descMonth(s string, sb *strings.Builder) { 69 | _ = desc(s, sb, "月") 70 | } 71 | 72 | func desc(s string, sb *strings.Builder, danwei string) (e error) { 73 | if s == "1/1" { 74 | s = "*" 75 | } 76 | if s == "0/0" { 77 | s = "0" 78 | } 79 | if xin == s { 80 | sb.WriteString("每" + danwei) 81 | return 82 | } 83 | if wenhao == s { 84 | return 85 | } 86 | if strings.Contains(s, huo) { 87 | arr := strings.Split(s, huo) 88 | for _, x := range arr { 89 | if len(x) != 0 { 90 | sb.WriteString("第" + x + danwei + "和") 91 | } 92 | } 93 | sb.WriteString("的") 94 | return 95 | } 96 | if strings.Contains(s, dao) { 97 | arr := strings.Split(s, huo) 98 | if len(arr) != 2 { 99 | e = errors.New("长度不能小于2") 100 | return 101 | } 102 | sb.WriteString("从第" + arr[0] + danwei + "到第" + arr[1] + danwei + "每" + danwei) 103 | sb.WriteString("的") 104 | return 105 | } 106 | if strings.Contains(s, mei) { 107 | arr := strings.Split(s, huo) 108 | if len(arr) != 2 { 109 | e = errors.New("长度不能小于2") 110 | return 111 | } 112 | if arr[0] == arr[1] || arr[0] == "0" { 113 | sb.WriteString("每" + arr[1] + danwei) 114 | } else { 115 | sb.WriteString("每" + arr[1] + danwei + "的第" + arr[0] + danwei) 116 | } 117 | return 118 | } 119 | sb.WriteString("第" + s + danwei) 120 | return 121 | } 122 | -------------------------------------------------------------------------------- /biz/pack/dispatcher/q.go: -------------------------------------------------------------------------------- 1 | package dispatcher 2 | 3 | import ( 4 | "fmt" 5 | "github.com/RichardKnop/machinery/v1" 6 | "github.com/RichardKnop/machinery/v1/config" 7 | "github.com/cloudwego/hertz/pkg/common/hlog" 8 | "jobor/conf" 9 | ) 10 | 11 | const ( 12 | exchange = "jobor_exchange" 13 | ExchangeType = "direct" 14 | BindingKey = "jobor_tasks123" 15 | Queue = "jobor_queue" 16 | RegisterTaskName = "TaskWorker" 17 | ) 18 | 19 | var QSrv *machinery.Server 20 | 21 | func InitQSrv(rds *conf.Redis, queue string) (*machinery.Server, error) { 22 | var qbr = fmt.Sprintf("redis://%s@%s/1", rds.Password, rds.Address) 23 | var cnf = &config.Config{ 24 | //Broker: "amqp://guest:guest@localhost:5672/", 25 | // redis://password@localhost:6379/1 "amqp://guest:guest@localhost:5672/", 26 | DefaultQueue: queue, 27 | Broker: qbr, 28 | ResultBackend: qbr, 29 | //AMQP: &config.AMQPConfig{ 30 | // Exchange: exchange, 31 | // ExchangeType: ExchangeType, 32 | // BindingKey: BindingKey, 33 | //}, 34 | ResultsExpireIn: 120, 35 | Redis: &config.RedisConfig{ 36 | MaxIdle: 3, 37 | IdleTimeout: 240, 38 | ReadTimeout: 15, 39 | WriteTimeout: 15, 40 | ConnectTimeout: 15, 41 | NormalTasksPollPeriod: 1000, 42 | DelayedTasksPollPeriod: 500, 43 | }, 44 | } 45 | 46 | server, err := machinery.NewServer(cnf) 47 | if err != nil { 48 | // do something with the error 49 | return nil, err 50 | } 51 | QSrv = server 52 | //server.SendTask() 53 | err = QSrv.RegisterTask(RegisterTaskName, TaskWorker) 54 | if err != nil { 55 | hlog.Errorf("register Task err: %s", err) 56 | return nil, err 57 | } 58 | return server, nil 59 | } 60 | -------------------------------------------------------------------------------- /biz/pack/dispatcher/route.go: -------------------------------------------------------------------------------- 1 | package dispatcher 2 | 3 | import ( 4 | "fmt" 5 | "github.com/cloudwego/hertz/pkg/common/hlog" 6 | "jobor/biz/model" 7 | "math/rand" 8 | "time" 9 | ) 10 | 11 | const ( 12 | // Random 1:Random 2:RoundRobin 3:Weight 4:LeastTask 13 | Random int = iota + 1 14 | RoundRobin 15 | Weight 16 | LeastTask 17 | ) 18 | 19 | // Select a next run worker 20 | 21 | // JoborWorker will return next run worker 22 | // if JoborWorker is nil,because not find valid worker 23 | type JoborWorker func() *model.JoborWorker 24 | 25 | func init() { 26 | rand.Seed(time.Now().UnixNano()) 27 | } 28 | 29 | // GetWorkerByRoutePolicy return a type JoborWorker, it will return a worker 30 | func GetWorkerByRoutePolicy(routingKeys []string, routePolicy int, lang string) JoborWorker { 31 | switch routePolicy { 32 | case Random: 33 | return random(routingKeys, lang) 34 | case RoundRobin: 35 | return roundRobin(routingKeys, lang) 36 | case Weight: 37 | return weight(routingKeys, lang) 38 | case LeastTask: 39 | return leastTask(routingKeys, lang) 40 | default: 41 | return random(routingKeys, lang) 42 | } 43 | } 44 | 45 | // 随机调度 46 | func random(routingKeys []string, lang string) JoborWorker { 47 | return func() *model.JoborWorker { 48 | workers, err := model.GetWorkers(routingKeys, lang) 49 | if err != nil { 50 | err = fmt.Errorf("get online worker failed: %s", err) 51 | hlog.Errorf(err.Error()) 52 | return nil 53 | } 54 | return &workers[rand.Int()%len(workers)] 55 | } 56 | } 57 | 58 | // 轮询调度 59 | func roundRobin(routingKeys []string, lang string) JoborWorker { 60 | var i = rand.Int() 61 | return func() *model.JoborWorker { 62 | workers, err := model.GetWorkers(routingKeys, lang) 63 | if err != nil { 64 | err = fmt.Errorf("get online worker failed: %s", err) 65 | hlog.Errorf(err.Error()) 66 | return nil 67 | } 68 | worker := workers[i%len(workers)] 69 | i++ 70 | return &worker 71 | } 72 | } 73 | 74 | // 权重调度 75 | func weight(routingKeys []string, lang string) JoborWorker { 76 | return func() *model.JoborWorker { 77 | workers, err := model.GetWorkers(routingKeys, lang) 78 | if err != nil { 79 | err = fmt.Errorf("get online worker failed: %s", err) 80 | hlog.Errorf(err.Error()) 81 | return nil 82 | } 83 | allWeight := 0 84 | 85 | for _, w := range workers { 86 | allWeight += int(w.Weight) 87 | } 88 | get := rand.Int() % allWeight 89 | pre := 0 90 | 91 | for _, w := range workers { 92 | if pre <= get && get < pre+int(w.Weight) { 93 | return &w 94 | } 95 | pre += int(w.Weight) 96 | } 97 | 98 | return nil 99 | } 100 | } 101 | 102 | // 最少调度 103 | func leastTask(routingKeys []string, lang string) JoborWorker { 104 | return func() *model.JoborWorker { 105 | workers, err := model.GetWorkers(routingKeys, lang) 106 | if err != nil { 107 | err = fmt.Errorf("get online worker failed: %s", err) 108 | hlog.Errorf(err.Error()) 109 | return nil 110 | } 111 | return &workers[rand.Int()%len(workers)] 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /biz/pack/dispatcher/task_worker.go: -------------------------------------------------------------------------------- 1 | package dispatcher 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/cloudwego/hertz/pkg/common/hlog" 7 | "github.com/tidwall/gjson" 8 | "io" 9 | "jobor/kitex_gen/pbapi" 10 | task2 "jobor/kitex_gen/task" 11 | "time" 12 | ) 13 | 14 | func TaskWorker(ctx context.Context, data string, taskId int64, lang string, stream pbapi.TaskService_RunTaskServer) error { 15 | startTime := time.Now() 16 | defer func() { 17 | hlog.CtxDebugf(ctx, "Task=[%d] lang=%s worker run Task finish, cost time: %s.", 18 | taskId, lang, time.Since(startTime).String()) 19 | }() 20 | request := task2.TaskRequest{TaskId: taskId, TaskLang: lang, TaskData: []byte(data)} 21 | runner, err := GetDataRun(&request) 22 | if err != nil { 23 | hlog.CtxErrorf(ctx, "dispatcher getDataRun err: %s", err.Error()) 24 | return err 25 | } 26 | gd := gjson.Parse(data) 27 | if gd.Get("pre_cmd").String() != "" { 28 | preRes, exitCode, err := runner.RunPreCmd(ctx) 29 | if err != nil { 30 | err = fmt.Errorf("\n%s, Return exitCode:%5d", err, exitCode) // write exitCode,total 5 byte 31 | resp := pbapi.StreamResponse{Resp: []byte(fmt.Sprintf("预执行结果:\n%s\n预执行错误:%s", preRes, 32 | err.Error()))} 33 | err = stream.Send(&resp) 34 | return err 35 | } 36 | preResp := pbapi.StreamResponse{Resp: []byte(fmt.Sprintf("预执行结果:\n%s\n预执行成功\n\n", preRes))} 37 | err = stream.Send(&preResp) 38 | if err != nil { 39 | return err 40 | } 41 | } 42 | _ = stream.Send(&pbapi.StreamResponse{Resp: []byte(fmt.Sprintf("任务执行返回:\n"))}) 43 | hlog.CtxDebugf(ctx, "Task=[%d] lang=%s runner run pre cmd is success", taskId, lang) 44 | out := runner.Run(ctx) 45 | hlog.CtxDebugf(ctx, "Task=[%d] lang=%s runner run start", taskId, lang) 46 | defer func(out io.ReadCloser) { 47 | err := out.Close() 48 | if err != nil { 49 | hlog.CtxErrorf(ctx, "runner run close failed: %s", err) 50 | } 51 | }(out) 52 | var buf = make([]byte, 1024) 53 | 54 | resp := "" 55 | for { 56 | n, err := out.Read(buf) 57 | 58 | if err != nil { 59 | if err == io.EOF { 60 | hlog.CtxDebugf(ctx, "Task=[%d] lang=%s runner run is finish", taskId, lang) 61 | return nil 62 | } 63 | // if read failed please send default err code -1 64 | hlog.CtxErrorf(ctx, "read failed from %s", err) 65 | err = stream.Send(&pbapi.StreamResponse{Resp: []byte(err.Error() + fmt.Sprintf("%3d", DefaultExitCode))}) 66 | if err != nil { 67 | hlog.Errorf("send failed: %s", err) 68 | } 69 | return nil 70 | } 71 | if n > 0 { 72 | hlog.Debugf("worker out data: %s", string(buf[:n])) 73 | //fmt.Println("worker out:", string(buf[:n])) 74 | resp += string(buf[:n]) 75 | resp := pbapi.StreamResponse{Resp: buf[:n]} 76 | err = stream.Send(&resp) 77 | if err != nil { 78 | hlog.Errorf("stream send failed: %s", err) 79 | return nil 80 | } 81 | } 82 | } 83 | //hlog.CtxInfof(taskCtx, "TaskWorker: %s", data) 84 | //return nil 85 | } 86 | -------------------------------------------------------------------------------- /biz/pack/oidc_callback/oidc_test.go: -------------------------------------------------------------------------------- 1 | package oidc_callback 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | func TestCallbackPasswd(t *testing.T) { 10 | //o := NewOIDC(context.Background(), "", "", "", "", "") 11 | //info, err := o.OidcCallbackWithPassword(context.Background(), "", "xxxx", "adfsasdf") 12 | //if err != nil { 13 | // fmt.Println("err1", err) 14 | // return 15 | //} 16 | //var claims json.RawMessage 17 | //_ = info.Claims(&claims) 18 | ////marshal, _ := json.Marshal(info) 19 | //fmt.Println("userInfo:", string(claims)) 20 | 21 | _, err := InitProvider(context.Background(), "https://sso.xxx.local") 22 | if err != nil { 23 | fmt.Println("err2", err) 24 | return 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /biz/pack/task_ssh/index.go: -------------------------------------------------------------------------------- 1 | package task_ssh 2 | 3 | import ( 4 | "fmt" 5 | "golang.org/x/crypto/ssh" 6 | "os/exec" 7 | "time" 8 | ) 9 | 10 | type SshServer struct { 11 | Username string `json:"username"` 12 | Password string `json:"password"` 13 | Host string `json:"host"` 14 | Port int32 15 | AuthMode string 16 | PrivateKey string 17 | PrivateSecret string 18 | Cmd string 19 | ClientConfig *ssh.ClientConfig 20 | Client *ssh.Client 21 | Session *ssh.Session 22 | Err error 23 | } 24 | 25 | func (s *SshServer) ExecRemoteSshInit() error { 26 | //privateKey := "\/++\nuoaG/" 27 | var err error 28 | var authMethods []ssh.AuthMethod 29 | if s.Password != "" { 30 | authMethods = append(authMethods, ssh.Password(s.Password)) 31 | } 32 | if s.PrivateKey != "" { 33 | var hostSigner ssh.Signer 34 | if s.PrivateSecret != "" { 35 | hostSigner, err = ssh.ParsePrivateKeyWithPassphrase([]byte(s.PrivateKey), []byte(s.PrivateSecret)) 36 | } else { 37 | hostSigner, err = ssh.ParsePrivateKey([]byte(s.PrivateKey)) 38 | } 39 | if err != nil { 40 | return err 41 | } 42 | //_ = hostSigner 43 | authMethods = append(authMethods, ssh.PublicKeys(hostSigner)) 44 | } 45 | config := &ssh.ClientConfig{ 46 | User: s.Username, 47 | Auth: authMethods, 48 | HostKeyCallback: ssh.InsecureIgnoreHostKey(), 49 | } 50 | s.Client, err = ssh.Dial("tcp", fmt.Sprintf("%s:%d", s.Host, s.Port), config) 51 | if err != nil { 52 | err = fmt.Errorf("Failed to dial: " + err.Error()) 53 | return err 54 | } 55 | //defer s.Client.Close() 56 | //s.Session, err = s.Client.NewSession() 57 | //if err != nil { 58 | // err = fmt.Errorf("Failed to new session: " + err.Error()) 59 | // return err 60 | //} 61 | ////defer s.Session.Close() 62 | return nil 63 | } 64 | 65 | func (s *SshServer) ExecRemoteSshCmd(Cmd string) (string, error) { 66 | var exitCode = 0 67 | session, err := s.Client.NewSession() 68 | if err != nil { 69 | err = fmt.Errorf("Failed to new session: " + err.Error()) 70 | return "", err 71 | } 72 | defer session.Close() 73 | 74 | output, err := session.Output(Cmd) 75 | if err != nil { 76 | if exitError, ok := err.(*exec.ExitError); ok { 77 | exitCode = exitError.ExitCode() 78 | } 79 | err = fmt.Errorf("Failed to session output: " + err.Error()) 80 | return "", err 81 | } 82 | now := time.Now().Local().Format("2006-01-02 15:04:05") 83 | resp := string(output) + fmt.Sprintf("\n%s\n%s Task Run Finished, Return exitCode:%5d", "", now, exitCode) 84 | return resp, nil 85 | } 86 | 87 | func (s *SshServer) Close() { 88 | _ = s.Client.Close() 89 | } 90 | -------------------------------------------------------------------------------- /biz/response/stack.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "runtime" 8 | "strings" 9 | ) 10 | 11 | var ( 12 | dunno = []byte("???") 13 | centerDot = []byte("·") 14 | dot = []byte(".") 15 | slash = []byte("/") 16 | ) 17 | 18 | // Stack returns a nicely formatted Stack frame, skipping skip frames. 19 | func Stack(skip int) []byte { 20 | buf := new(bytes.Buffer) 21 | var lines [][]byte 22 | var lastFile string 23 | for i := skip; ; i++ { 24 | pc, file, line, ok := runtime.Caller(i) 25 | if !ok { 26 | break 27 | } 28 | _, _ = fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc) 29 | if file != lastFile { 30 | data, err := ioutil.ReadFile(file) 31 | if err != nil { 32 | continue 33 | } 34 | lines = bytes.Split(data, []byte{'\n'}) 35 | lastFile = file 36 | } 37 | _, _ = fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line)) 38 | } 39 | return buf.Bytes() 40 | } 41 | 42 | // source returns a space-trimmed slice of the n'th line. 43 | func source(lines [][]byte, n int) []byte { 44 | n-- // in Stack trace, lines are 1-indexed but our array is 0-indexed 45 | if n < 0 || n >= len(lines) { 46 | return dunno 47 | } 48 | return bytes.TrimSpace(lines[n]) 49 | } 50 | 51 | // function returns, if possible, the name of the function containing the PC. 52 | func function(pc uintptr) []byte { 53 | fn := runtime.FuncForPC(pc) 54 | if fn == nil { 55 | return dunno 56 | } 57 | name := []byte(fn.Name()) 58 | if lastslash := bytes.LastIndex(name, slash); lastslash >= 0 { 59 | name = name[lastslash+1:] 60 | } 61 | if period := bytes.Index(name, dot); period >= 0 { 62 | name = name[period+1:] 63 | } 64 | name = bytes.Replace(name, centerDot, dot, -1) 65 | return name 66 | } 67 | 68 | // err stack 69 | func caller() (file string, line int) { 70 | skipCaller, skipPath := 4, 4 71 | _, file, line, ok := runtime.Caller(skipCaller) 72 | if !ok { 73 | return "", 0 74 | } 75 | separator := "/" 76 | subFile := strings.Split(file, separator) 77 | if len(subFile) > skipPath { 78 | subFile = subFile[len(subFile)-skipPath:] 79 | } 80 | return strings.Join(subFile, separator), line 81 | } 82 | -------------------------------------------------------------------------------- /biz/router/audit/audit.go: -------------------------------------------------------------------------------- 1 | // Code generated by hertz generator. DO NOT EDIT. 2 | 3 | package audit 4 | 5 | import ( 6 | "github.com/cloudwego/hertz/pkg/app/server" 7 | audit "jobor/biz/handler/audit" 8 | ) 9 | 10 | /* 11 | This file will register all the routes of the services in the master idl. 12 | And it will update automatically when you use the "update" command for the idl. 13 | So don't modify the contents of the file, or your code will be deleted when it is updated. 14 | */ 15 | 16 | // Register register routes based on the IDL 'api.${HTTP Method}' annotation. 17 | func Register(r *server.Hertz) { 18 | 19 | root := r.Group("/", rootMw()...) 20 | { 21 | _api := root.Group("/api", _apiMw()...) 22 | { 23 | _v1 := _api.Group("/v1", _v1Mw()...) 24 | { 25 | _jobor := _v1.Group("/jobor", _joborMw()...) 26 | _jobor.GET("/audit-log", append(_getauditlogMw(), audit.GetAuditLog)...) 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /biz/router/audit/middleware.go: -------------------------------------------------------------------------------- 1 | // Code generated by hertz generator. 2 | 3 | package audit 4 | 5 | import ( 6 | "github.com/cloudwego/hertz/pkg/app" 7 | ) 8 | 9 | func rootMw() []app.HandlerFunc { 10 | // your code... 11 | return nil 12 | } 13 | 14 | func _apiMw() []app.HandlerFunc { 15 | // your code... 16 | return nil 17 | } 18 | 19 | func _v1Mw() []app.HandlerFunc { 20 | // your code... 21 | return nil 22 | } 23 | 24 | func _joborMw() []app.HandlerFunc { 25 | // your code... 26 | return nil 27 | } 28 | 29 | func _getloginhistoryMw() []app.HandlerFunc { 30 | // your code... 31 | return nil 32 | } 33 | 34 | func _getauditloginMw() []app.HandlerFunc { 35 | // your code... 36 | return nil 37 | } 38 | 39 | func _getauditlogMw() []app.HandlerFunc { 40 | // your code... 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /biz/router/dashboard/dashboard.go: -------------------------------------------------------------------------------- 1 | // Code generated by hertz generator. DO NOT EDIT. 2 | 3 | package dashboard 4 | 5 | import ( 6 | "github.com/cloudwego/hertz/pkg/app/server" 7 | dashboard "jobor/biz/handler/dashboard" 8 | ) 9 | 10 | /* 11 | This file will register all the routes of the services in the master idl. 12 | And it will update automatically when you use the "update" command for the idl. 13 | So don't modify the contents of the file, or your code will be deleted when it is updated. 14 | */ 15 | 16 | // Register register routes based on the IDL 'api.${HTTP Method}' annotation. 17 | func Register(r *server.Hertz) { 18 | 19 | root := r.Group("/", rootMw()...) 20 | { 21 | _api := root.Group("/api", _apiMw()...) 22 | { 23 | _v1 := _api.Group("/v1", _v1Mw()...) 24 | { 25 | _jobor := _v1.Group("/jobor", _joborMw()...) 26 | _jobor.GET("/dashboard", append(_getdashboardMw(), dashboard.GetDashboard)...) 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /biz/router/dashboard/middleware.go: -------------------------------------------------------------------------------- 1 | // Code generated by hertz generator. 2 | 3 | package dashboard 4 | 5 | import ( 6 | "github.com/cloudwego/hertz/pkg/app" 7 | ) 8 | 9 | func rootMw() []app.HandlerFunc { 10 | // your code... 11 | return nil 12 | } 13 | 14 | func _apiMw() []app.HandlerFunc { 15 | // your code... 16 | return nil 17 | } 18 | 19 | func _v1Mw() []app.HandlerFunc { 20 | // your code... 21 | return nil 22 | } 23 | 24 | func _joborMw() []app.HandlerFunc { 25 | // your code... 26 | return nil 27 | } 28 | 29 | func _getdashboardMw() []app.HandlerFunc { 30 | // your code... 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /biz/router/register.go: -------------------------------------------------------------------------------- 1 | // Code generated by hertz generator. DO NOT EDIT. 2 | 3 | package router 4 | 5 | import ( 6 | "github.com/cloudwego/hertz/pkg/app/server" 7 | audit "jobor/biz/router/audit" 8 | dashboard "jobor/biz/router/dashboard" 9 | role "jobor/biz/router/role" 10 | sys_api "jobor/biz/router/sys_api" 11 | task "jobor/biz/router/task" 12 | task_log "jobor/biz/router/task_log" 13 | user "jobor/biz/router/user" 14 | worker "jobor/biz/router/worker" 15 | ) 16 | 17 | // GeneratedRegister registers routers generated by IDL. 18 | func GeneratedRegister(r *server.Hertz) { 19 | //INSERT_POINT: DO NOT DELETE THIS LINE! 20 | dashboard.Register(r) 21 | 22 | role.Register(r) 23 | 24 | sys_api.Register(r) 25 | 26 | task_log.Register(r) 27 | 28 | worker.Register(r) 29 | 30 | task.Register(r) 31 | 32 | user.Register(r) 33 | 34 | audit.Register(r) 35 | } 36 | -------------------------------------------------------------------------------- /biz/router/role/middleware.go: -------------------------------------------------------------------------------- 1 | // Code generated by hertz generator. 2 | 3 | package role 4 | 5 | import ( 6 | "github.com/cloudwego/hertz/pkg/app" 7 | ) 8 | 9 | func rootMw() []app.HandlerFunc { 10 | // your code... 11 | return nil 12 | } 13 | 14 | func _apiMw() []app.HandlerFunc { 15 | // your code... 16 | return nil 17 | } 18 | 19 | func _v1Mw() []app.HandlerFunc { 20 | // your code... 21 | return nil 22 | } 23 | 24 | func _sysMw() []app.HandlerFunc { 25 | // your code... 26 | return nil 27 | } 28 | 29 | func _roleMw() []app.HandlerFunc { 30 | // your code... 31 | return nil 32 | } 33 | 34 | func _getroleMw() []app.HandlerFunc { 35 | // your code... 36 | return nil 37 | } 38 | 39 | func _putroleMw() []app.HandlerFunc { 40 | // your code... 41 | return nil 42 | } 43 | 44 | func _deleteroleMw() []app.HandlerFunc { 45 | // your code... 46 | return nil 47 | } 48 | 49 | func _postroleMw() []app.HandlerFunc { 50 | // your code... 51 | return nil 52 | } 53 | 54 | func _getroletreeMw() []app.HandlerFunc { 55 | // your code... 56 | return nil 57 | } 58 | 59 | func _getroleallMw() []app.HandlerFunc { 60 | // your code... 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /biz/router/role/role.go: -------------------------------------------------------------------------------- 1 | // Code generated by hertz generator. DO NOT EDIT. 2 | 3 | package role 4 | 5 | import ( 6 | "github.com/cloudwego/hertz/pkg/app/server" 7 | role "jobor/biz/handler/role" 8 | ) 9 | 10 | /* 11 | This file will register all the routes of the services in the master idl. 12 | And it will update automatically when you use the "update" command for the idl. 13 | So don't modify the contents of the file, or your code will be deleted when it is updated. 14 | */ 15 | 16 | // Register register routes based on the IDL 'api.${HTTP Method}' annotation. 17 | func Register(r *server.Hertz) { 18 | 19 | root := r.Group("/", rootMw()...) 20 | { 21 | _api := root.Group("/api", _apiMw()...) 22 | { 23 | _v1 := _api.Group("/v1", _v1Mw()...) 24 | { 25 | _sys := _v1.Group("/sys", _sysMw()...) 26 | _sys.GET("/role", append(_getroleMw(), role.GetRole)...) 27 | _role := _sys.Group("/role", _roleMw()...) 28 | _role.PUT("/:id", append(_putroleMw(), role.PutRole)...) 29 | _role.DELETE("/:id", append(_deleteroleMw(), role.DeleteRole)...) 30 | _sys.POST("/role", append(_postroleMw(), role.PostRole)...) 31 | _sys.GET("/role-tree", append(_getroletreeMw(), role.GetRoleTree)...) 32 | _sys.GET("/roles", append(_getroleallMw(), role.GetRoleAll)...) 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /biz/router/sys_api/middleware.go: -------------------------------------------------------------------------------- 1 | // Code generated by hertz generator. 2 | 3 | package sys_api 4 | 5 | import ( 6 | "github.com/cloudwego/hertz/pkg/app" 7 | ) 8 | 9 | func rootMw() []app.HandlerFunc { 10 | // your code... 11 | return nil 12 | } 13 | 14 | func _apiMw() []app.HandlerFunc { 15 | // your code... 16 | return nil 17 | } 18 | 19 | func _v1Mw() []app.HandlerFunc { 20 | // your code... 21 | return nil 22 | } 23 | 24 | func _joborMw() []app.HandlerFunc { 25 | // your code... 26 | return nil 27 | } 28 | 29 | func _api0Mw() []app.HandlerFunc { 30 | // your code... 31 | return nil 32 | } 33 | 34 | func _getapiMw() []app.HandlerFunc { 35 | // your code... 36 | return nil 37 | } 38 | 39 | func _putapiMw() []app.HandlerFunc { 40 | // your code... 41 | return nil 42 | } 43 | 44 | func _deleteapiMw() []app.HandlerFunc { 45 | // your code... 46 | return nil 47 | } 48 | 49 | func _postapiMw() []app.HandlerFunc { 50 | // your code... 51 | return nil 52 | } 53 | 54 | func _triggerupdateapiMw() []app.HandlerFunc { 55 | // your code... 56 | return nil 57 | } 58 | 59 | func _getapiallMw() []app.HandlerFunc { 60 | // your code... 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /biz/router/sys_api/sys_api.go: -------------------------------------------------------------------------------- 1 | // Code generated by hertz generator. DO NOT EDIT. 2 | 3 | package sys_api 4 | 5 | import ( 6 | "github.com/cloudwego/hertz/pkg/app/server" 7 | sys_api "jobor/biz/handler/sys_api" 8 | ) 9 | 10 | /* 11 | This file will register all the routes of the services in the master idl. 12 | And it will update automatically when you use the "update" command for the idl. 13 | So don't modify the contents of the file, or your code will be deleted when it is updated. 14 | */ 15 | 16 | // Register register routes based on the IDL 'api.${HTTP Method}' annotation. 17 | func Register(r *server.Hertz) { 18 | 19 | root := r.Group("/", rootMw()...) 20 | { 21 | _api := root.Group("/api", _apiMw()...) 22 | { 23 | _v1 := _api.Group("/v1", _v1Mw()...) 24 | { 25 | _jobor := _v1.Group("/jobor", _joborMw()...) 26 | _jobor.GET("/api", append(_getapiMw(), sys_api.GetApi)...) 27 | _api0 := _jobor.Group("/api", _api0Mw()...) 28 | _api0.PUT("/:id", append(_putapiMw(), sys_api.PutApi)...) 29 | _api0.DELETE("/:id", append(_deleteapiMw(), sys_api.DeleteApi)...) 30 | _jobor.POST("/api", append(_postapiMw(), sys_api.PostApi)...) 31 | _jobor.GET("/api-auto-update", append(_triggerupdateapiMw(), sys_api.TriggerUpdateApi)...) 32 | _jobor.GET("/apis", append(_getapiallMw(), sys_api.GetApiAll)...) 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /biz/router/task/middleware.go: -------------------------------------------------------------------------------- 1 | // Code generated by hertz generator. 2 | 3 | package task 4 | 5 | import ( 6 | "github.com/cloudwego/hertz/pkg/app" 7 | ) 8 | 9 | func rootMw() []app.HandlerFunc { 10 | // your code... 11 | return nil 12 | } 13 | 14 | func _apiMw() []app.HandlerFunc { 15 | // your code... 16 | return nil 17 | } 18 | 19 | func _v1Mw() []app.HandlerFunc { 20 | // your code... 21 | return nil 22 | } 23 | 24 | func _joborMw() []app.HandlerFunc { 25 | // your code... 26 | return nil 27 | } 28 | 29 | func _taskMw() []app.HandlerFunc { 30 | // your code... 31 | return nil 32 | } 33 | 34 | func _gettaskMw() []app.HandlerFunc { 35 | // your code... 36 | return nil 37 | } 38 | 39 | func _puttaskMw() []app.HandlerFunc { 40 | // your code... 41 | return nil 42 | } 43 | 44 | func _deletetaskMw() []app.HandlerFunc { 45 | // your code... 46 | return nil 47 | } 48 | 49 | func _posttaskMw() []app.HandlerFunc { 50 | // your code... 51 | return nil 52 | } 53 | 54 | func _gettaskallMw() []app.HandlerFunc { 55 | // your code... 56 | return nil 57 | } 58 | 59 | func _task0Mw() []app.HandlerFunc { 60 | // your code... 61 | return nil 62 | } 63 | 64 | func _gettaskbyidMw() []app.HandlerFunc { 65 | // your code... 66 | return nil 67 | } 68 | 69 | func _runtaskMw() []app.HandlerFunc { 70 | // your code... 71 | return nil 72 | } 73 | 74 | func _task1Mw() []app.HandlerFunc { 75 | // your code... 76 | return nil 77 | } 78 | 79 | func _idMw() []app.HandlerFunc { 80 | // your code... 81 | return nil 82 | } 83 | -------------------------------------------------------------------------------- /biz/router/task/task.go: -------------------------------------------------------------------------------- 1 | // Code generated by hertz generator. DO NOT EDIT. 2 | 3 | package task 4 | 5 | import ( 6 | "github.com/cloudwego/hertz/pkg/app/server" 7 | task "jobor/biz/handler/task" 8 | ) 9 | 10 | /* 11 | This file will register all the routes of the services in the master idl. 12 | And it will update automatically when you use the "update" command for the idl. 13 | So don't modify the contents of the file, or your code will be deleted when it is updated. 14 | */ 15 | 16 | // Register register routes based on the IDL 'api.${HTTP Method}' annotation. 17 | func Register(r *server.Hertz) { 18 | 19 | root := r.Group("/", rootMw()...) 20 | { 21 | _api := root.Group("/api", _apiMw()...) 22 | { 23 | _v1 := _api.Group("/v1", _v1Mw()...) 24 | { 25 | _jobor := _v1.Group("/jobor", _joborMw()...) 26 | _jobor.POST("/task", append(_posttaskMw(), task.PostTask)...) 27 | _task := _jobor.Group("/task", _taskMw()...) 28 | _task.PUT("/:id", append(_puttaskMw(), task.PutTask)...) 29 | _task.DELETE("/:id", append(_deletetaskMw(), task.DeleteTask)...) 30 | _jobor.GET("/tasks", append(_gettaskallMw(), task.GetTaskAll)...) 31 | _jobor.GET("/task", append(_gettaskMw(), task.GetTask)...) 32 | _task0 := _jobor.Group("/task", _task0Mw()...) 33 | { 34 | _id := _task0.Group("/:id", _idMw()...) 35 | _id.POST("/run", append(_runtaskMw(), task.RunTask)...) 36 | } 37 | { 38 | _task1 := _jobor.Group("/task", _task1Mw()...) 39 | _task1.GET("/:id", append(_gettaskbyidMw(), task.GetTaskById)...) 40 | } 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /biz/router/task_log/middleware.go: -------------------------------------------------------------------------------- 1 | // Code generated by hertz generator. 2 | 3 | package task_log 4 | 5 | import ( 6 | "github.com/cloudwego/hertz/pkg/app" 7 | ) 8 | 9 | func rootMw() []app.HandlerFunc { 10 | // your code... 11 | return nil 12 | } 13 | 14 | func _apiMw() []app.HandlerFunc { 15 | // your code... 16 | return nil 17 | } 18 | 19 | func _v1Mw() []app.HandlerFunc { 20 | // your code... 21 | return nil 22 | } 23 | 24 | func _joborMw() []app.HandlerFunc { 25 | // your code... 26 | return nil 27 | } 28 | 29 | func _getlogMw() []app.HandlerFunc { 30 | // your code... 31 | return nil 32 | } 33 | 34 | func _logMw() []app.HandlerFunc { 35 | // your code... 36 | return nil 37 | } 38 | 39 | func _idMw() []app.HandlerFunc { 40 | // your code... 41 | return nil 42 | } 43 | 44 | func _aborttaskMw() []app.HandlerFunc { 45 | // your code... 46 | return nil 47 | } 48 | 49 | func _log0Mw() []app.HandlerFunc { 50 | // your code... 51 | return nil 52 | } 53 | 54 | func _getlogbyidMw() []app.HandlerFunc { 55 | // your code... 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /biz/router/task_log/task_log.go: -------------------------------------------------------------------------------- 1 | // Code generated by hertz generator. DO NOT EDIT. 2 | 3 | package task_log 4 | 5 | import ( 6 | "github.com/cloudwego/hertz/pkg/app/server" 7 | task_log "jobor/biz/handler/task_log" 8 | ) 9 | 10 | /* 11 | This file will register all the routes of the services in the master idl. 12 | And it will update automatically when you use the "update" command for the idl. 13 | So don't modify the contents of the file, or your code will be deleted when it is updated. 14 | */ 15 | 16 | // Register register routes based on the IDL 'api.${HTTP Method}' annotation. 17 | func Register(r *server.Hertz) { 18 | 19 | root := r.Group("/", rootMw()...) 20 | { 21 | _api := root.Group("/api", _apiMw()...) 22 | { 23 | _v1 := _api.Group("/v1", _v1Mw()...) 24 | { 25 | _jobor := _v1.Group("/jobor", _joborMw()...) 26 | _jobor.GET("/log", append(_getlogMw(), task_log.GetLog)...) 27 | _log := _jobor.Group("/log", _logMw()...) 28 | { 29 | _id := _log.Group("/:id", _idMw()...) 30 | _id.POST("/abort", append(_aborttaskMw(), task_log.AbortTask)...) 31 | } 32 | { 33 | _log0 := _jobor.Group("/log", _log0Mw()...) 34 | _log0.GET("/:id", append(_getlogbyidMw(), task_log.GetLogById)...) 35 | } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /biz/router/user/middleware.go: -------------------------------------------------------------------------------- 1 | // Code generated by hertz generator. 2 | 3 | package user 4 | 5 | import ( 6 | "github.com/cloudwego/hertz/pkg/app" 7 | ) 8 | 9 | func rootMw() []app.HandlerFunc { 10 | // your code... 11 | return nil 12 | } 13 | 14 | func _apiMw() []app.HandlerFunc { 15 | // your code... 16 | return nil 17 | } 18 | 19 | func _v1Mw() []app.HandlerFunc { 20 | // your code... 21 | return nil 22 | } 23 | 24 | func _joborMw() []app.HandlerFunc { 25 | // your code... 26 | return nil 27 | } 28 | 29 | func _getuserselfMw() []app.HandlerFunc { 30 | // your code... 31 | return nil 32 | } 33 | 34 | func _user_switchMw() []app.HandlerFunc { 35 | // your code... 36 | return nil 37 | } 38 | 39 | func _switchuserMw() []app.HandlerFunc { 40 | // your code... 41 | return nil 42 | } 43 | 44 | func _sysMw() []app.HandlerFunc { 45 | // your code... 46 | return nil 47 | } 48 | 49 | func _userMw() []app.HandlerFunc { 50 | // your code... 51 | return nil 52 | } 53 | 54 | func _postuserMw() []app.HandlerFunc { 55 | // your code... 56 | return nil 57 | } 58 | 59 | func _putuserMw() []app.HandlerFunc { 60 | // your code... 61 | return nil 62 | } 63 | 64 | func _deleteuserMw() []app.HandlerFunc { 65 | // your code... 66 | return nil 67 | } 68 | 69 | func _syncuserMw() []app.HandlerFunc { 70 | // your code... 71 | return nil 72 | } 73 | 74 | func _getuserallMw() []app.HandlerFunc { 75 | // your code... 76 | return nil 77 | } 78 | 79 | func _user0Mw() []app.HandlerFunc { 80 | // your code... 81 | return nil 82 | } 83 | 84 | func _getuserMw() []app.HandlerFunc { 85 | // your code... 86 | return nil 87 | } 88 | 89 | func _getuserbyidMw() []app.HandlerFunc { 90 | // your code... 91 | return nil 92 | } 93 | 94 | func _user1Mw() []app.HandlerFunc { 95 | // your code... 96 | return nil 97 | } 98 | 99 | func _putuserpassrestMw() []app.HandlerFunc { 100 | // your code... 101 | return nil 102 | } 103 | 104 | func _putuserpasswordMw() []app.HandlerFunc { 105 | // your code... 106 | return nil 107 | } 108 | 109 | func _putuserprofileMw() []app.HandlerFunc { 110 | // your code... 111 | return nil 112 | } 113 | -------------------------------------------------------------------------------- /biz/router/user/user.go: -------------------------------------------------------------------------------- 1 | // Code generated by hertz generator. DO NOT EDIT. 2 | 3 | package user 4 | 5 | import ( 6 | "github.com/cloudwego/hertz/pkg/app/server" 7 | user "jobor/biz/handler/user" 8 | ) 9 | 10 | /* 11 | This file will register all the routes of the services in the master idl. 12 | And it will update automatically when you use the "update" command for the idl. 13 | So don't modify the contents of the file, or your code will be deleted when it is updated. 14 | */ 15 | 16 | // Register register routes based on the IDL 'api.${HTTP Method}' annotation. 17 | func Register(r *server.Hertz) { 18 | 19 | root := r.Group("/", rootMw()...) 20 | { 21 | _api := root.Group("/api", _apiMw()...) 22 | { 23 | _v1 := _api.Group("/v1", _v1Mw()...) 24 | { 25 | _jobor := _v1.Group("/jobor", _joborMw()...) 26 | _jobor.POST("/user", append(_postuserMw(), user.PostUser)...) 27 | _user := _jobor.Group("/user", _userMw()...) 28 | _user.PUT("/:id", append(_putuserMw(), user.PutUser)...) 29 | _user.DELETE("/:id", append(_deleteuserMw(), user.DeleteUser)...) 30 | _user.PUT("/pass-reset", append(_putuserpassrestMw(), user.PutUserPassRest)...) 31 | _user.PUT("/password", append(_putuserpasswordMw(), user.PutUserPassword)...) 32 | _user.PUT("/profile", append(_putuserprofileMw(), user.PutUserProfile)...) 33 | _jobor.GET("/user-self", append(_getuserselfMw(), user.GetUserSelf)...) 34 | _jobor.GET("/user-sync", append(_syncuserMw(), user.SyncUser)...) 35 | _jobor.GET("/users", append(_getuserallMw(), user.GetUserAll)...) 36 | _jobor.GET("/user", append(_getuserMw(), user.GetUser)...) 37 | _user0 := _jobor.Group("/user", _user0Mw()...) 38 | _user0.GET("/:id", append(_getuserbyidMw(), user.GetUserById)...) 39 | { 40 | _user_switch := _jobor.Group("/user-switch", _user_switchMw()...) 41 | _user_switch.GET("/:user_id", append(_switchuserMw(), user.SwitchUser)...) 42 | } 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /biz/router/worker/middleware.go: -------------------------------------------------------------------------------- 1 | // Code generated by hertz generator. 2 | 3 | package worker 4 | 5 | import ( 6 | "github.com/cloudwego/hertz/pkg/app" 7 | ) 8 | 9 | func rootMw() []app.HandlerFunc { 10 | // your code... 11 | return nil 12 | } 13 | 14 | func _apiMw() []app.HandlerFunc { 15 | // your code... 16 | return nil 17 | } 18 | 19 | func _v1Mw() []app.HandlerFunc { 20 | // your code... 21 | return nil 22 | } 23 | 24 | func _joborMw() []app.HandlerFunc { 25 | // your code... 26 | return nil 27 | } 28 | 29 | func _workerMw() []app.HandlerFunc { 30 | // your code... 31 | return nil 32 | } 33 | 34 | func _getworkerMw() []app.HandlerFunc { 35 | // your code... 36 | return nil 37 | } 38 | 39 | func _putworkerMw() []app.HandlerFunc { 40 | // your code... 41 | return nil 42 | } 43 | 44 | func _deleteworkerMw() []app.HandlerFunc { 45 | // your code... 46 | return nil 47 | } 48 | 49 | func _postworkerMw() []app.HandlerFunc { 50 | // your code... 51 | return nil 52 | } 53 | 54 | func _getworkerallMw() []app.HandlerFunc { 55 | // your code... 56 | return nil 57 | } 58 | 59 | func _worker0Mw() []app.HandlerFunc { 60 | // your code... 61 | return nil 62 | } 63 | 64 | func _getworkerbyidMw() []app.HandlerFunc { 65 | // your code... 66 | return nil 67 | } 68 | 69 | func _getworkerroutingkeyMw() []app.HandlerFunc { 70 | // your code... 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /biz/router/worker/worker.go: -------------------------------------------------------------------------------- 1 | // Code generated by hertz generator. DO NOT EDIT. 2 | 3 | package worker 4 | 5 | import ( 6 | "github.com/cloudwego/hertz/pkg/app/server" 7 | worker "jobor/biz/handler/worker" 8 | ) 9 | 10 | /* 11 | This file will register all the routes of the services in the master idl. 12 | And it will update automatically when you use the "update" command for the idl. 13 | So don't modify the contents of the file, or your code will be deleted when it is updated. 14 | */ 15 | 16 | // Register register routes based on the IDL 'api.${HTTP Method}' annotation. 17 | func Register(r *server.Hertz) { 18 | 19 | root := r.Group("/", rootMw()...) 20 | { 21 | _api := root.Group("/api", _apiMw()...) 22 | { 23 | _v1 := _api.Group("/v1", _v1Mw()...) 24 | { 25 | _jobor := _v1.Group("/jobor", _joborMw()...) 26 | _jobor.GET("/worker", append(_getworkerMw(), worker.GetWorker)...) 27 | _worker := _jobor.Group("/worker", _workerMw()...) 28 | _worker.PUT("/:id", append(_putworkerMw(), worker.PutWorker)...) 29 | _worker.DELETE("/:id", append(_deleteworkerMw(), worker.DeleteWorker)...) 30 | _jobor.POST("/worker", append(_postworkerMw(), worker.PostWorker)...) 31 | _jobor.GET("/worker-routing-key", append(_getworkerroutingkeyMw(), worker.GetWorkerRoutingKey)...) 32 | _jobor.GET("/workers", append(_getworkerallMw(), worker.GetWorkerAll)...) 33 | { 34 | _worker0 := _jobor.Group("/worker", _worker0Mw()...) 35 | _worker0.GET("/:id", append(_getworkerbyidMw(), worker.GetWorkerById)...) 36 | } 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /biz/utils/struct.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | ) 8 | 9 | // 读取结构体的指定字段 10 | func GetStructField(input interface{}, key string) (value interface{}, err error) { 11 | rv := reflect.ValueOf(input) 12 | rt := reflect.TypeOf(input) 13 | if rt.Kind() != reflect.Struct { 14 | return value, errors.New("输入参数必须为struct") 15 | } 16 | 17 | keyExist := false 18 | for i := 0; i < rt.NumField(); i++ { 19 | curField := rv.Field(i) 20 | if rt.Field(i).Name == key { 21 | switch curField.Kind() { 22 | case reflect.String, reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8, reflect.Int, reflect.Float64, reflect.Float32: 23 | keyExist = true 24 | value = curField.Interface() 25 | default: 26 | return value, errors.New("key must be int float or string") 27 | } 28 | } 29 | } 30 | 31 | if !keyExist { 32 | return value, errors.New(fmt.Sprintf("key %s not found in %s's field", key, rt)) 33 | } 34 | return 35 | } 36 | 37 | // 读取slice的字段,并返回slice 38 | func GetSliceStructField(input interface{}, key string) (value []interface{}, err error) { 39 | rv := reflect.ValueOf(input) 40 | rt := reflect.TypeOf(input) 41 | if rt.Kind() != reflect.Slice { 42 | return value, errors.New("输入参数必须为slice") 43 | } 44 | var val []interface{} 45 | 46 | for i := 0; i < rv.Len(); i++ { 47 | sv := rv.Index(i) 48 | st := sv.Type() 49 | if st.Kind() != reflect.Struct { 50 | return value, errors.New("输入参数必须为struct") 51 | } 52 | 53 | for j := 0; j < sv.NumField(); j++ { 54 | if st.Field(j).Name == key { 55 | val = append(val, sv.Field(j).Interface()) 56 | } 57 | } 58 | } 59 | return val, nil 60 | } 61 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | RUN_NAME="jobor_rpc" 3 | 4 | mkdir -p output/bin 5 | cp script/* output/ 6 | chmod +x output/bootstrap.sh 7 | 8 | if [ "$IS_SYSTEM_TEST_ENV" != "1" ]; then 9 | go build -o output/bin/${RUN_NAME} 10 | else 11 | go test -c -covermode=set -o output/bin/${RUN_NAME} -coverpkg=./... 12 | fi 13 | 14 | -------------------------------------------------------------------------------- /cmd/srv_http/main/main.go: -------------------------------------------------------------------------------- 1 | // Code generated by hertz generator. 2 | 3 | package main 4 | 5 | import ( 6 | "jobor/cmd/srv_http" 7 | _ "jobor/docs" 8 | ) 9 | 10 | // @title 数据库管理平台[DBS] API 11 | // @version 1.0 12 | // @description 支持Mysql、Redis、OceanBase 13 | // @description Authorization Bearer token 14 | // @description header: x-enc-data = yes 15 | 16 | // @securityDefinitions.apikey ApiKeyAuth 17 | // @in header 18 | // @name Authorization 19 | // @schema Bearer 20 | 21 | // @contact.name UAC 22 | // @contact.url 23 | // 24 | // //@host localhost:5616 25 | // 26 | // // @BasePath / 27 | // 28 | // //@schemes http 29 | func main() { 30 | srv_http.Start() 31 | } 32 | -------------------------------------------------------------------------------- /cmd/srv_http/router_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by hertz generator. DO NOT EDIT. 2 | 3 | package srv_http 4 | 5 | import ( 6 | router "jobor/biz/router" 7 | 8 | "github.com/cloudwego/hertz/pkg/app/server" 9 | ) 10 | 11 | // register registers all routers. 12 | func register(r *server.Hertz) { 13 | 14 | router.GeneratedRegister(r) 15 | 16 | customizedRegister(r) 17 | } 18 | -------------------------------------------------------------------------------- /cmd/srv_rpc/rpc_test.go: -------------------------------------------------------------------------------- 1 | package srv_rpc 2 | 3 | import "testing" 4 | 5 | func TestStartSrvRpc(t *testing.T) { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /cmd/srv_rpc/start.go: -------------------------------------------------------------------------------- 1 | package srv_rpc 2 | 3 | import ( 4 | "github.com/cloudwego/kitex/pkg/klog" 5 | "github.com/cloudwego/kitex/pkg/rpcinfo" 6 | "github.com/cloudwego/kitex/server" 7 | "jobor/conf" 8 | "jobor/kitex_gen/pbapi/heartbeat" 9 | "jobor/rpc_biz/mw" 10 | "jobor/rpc_biz/svc" 11 | "net" 12 | ) 13 | 14 | func StartSrvRpc() error { 15 | klog.Infof("start srv grpc server service") 16 | ebi := &rpcinfo.EndpointBasicInfo{ 17 | ServiceName: conf.AppWorkerName, 18 | Tags: make(map[string]string), 19 | } 20 | ebi.Tags["idc"] = "cn-sh" 21 | ebi.Tags["name"] = conf.AppName 22 | //ebi.Tags["dyeing"] = conf.GetDyeing() 23 | //consulConfig := consulapi.Config{ 24 | // Address: conf.GetConf().Consul.Address, 25 | // //Scheme: "https", 26 | // //Token: "TEST-MY-TOKEN", 27 | //} 28 | //r, err := consul.NewConsulRegisterWithConfig(&consulConfig, 29 | // consul.WithCheck(&consulapi.AgentServiceCheck{ 30 | // Interval: "7s", 31 | // Timeout: "5s", 32 | // DeregisterCriticalServiceAfter: "30s", 33 | // }, 34 | // )) 35 | //if err != nil { 36 | // klog.Fatal(err) 37 | //} 38 | addr, _ := net.ResolveTCPAddr("tcp", conf.GetConf().Server.GrpcAddress) 39 | svr := heartbeat.NewServer(new(svc.HeartbeatImpl), server.WithServerBasicInfo(ebi), 40 | server.WithServiceAddr(addr), 41 | server.WithMiddleware(mw.AccessLogMW), 42 | //server.WithLogger(), 43 | //server.WithACLRules(), 44 | //server.WithErrorHandler(), 45 | server.WithMuxTransport(), // IO多路复用 46 | ) 47 | if err := svr.Run(); err != nil { 48 | klog.Info("srv grpc stopped with error:", err) 49 | return err 50 | } else { 51 | klog.Info("srv grpc stopped") 52 | return nil 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /cmd/var.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/cobra" 6 | "time" 7 | ) 8 | 9 | var ( 10 | Ver = "v3.0.5" 11 | BuildDate = time.Now().Format("2006.01.02") 12 | ) 13 | 14 | func Version() *cobra.Command { 15 | versionCmd := &cobra.Command{ 16 | Use: "version", 17 | Short: "Print the version of Jobor", 18 | Long: `This is mars jobor`, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | fmt.Printf("Version : %s\n", Ver) 21 | fmt.Printf("Commit : %s\n", "") 22 | fmt.Printf("BuildDate : %s\n", BuildDate) 23 | }, 24 | } 25 | return versionCmd 26 | } 27 | -------------------------------------------------------------------------------- /cmd/worker/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/cloudwego/hertz/pkg/common/hlog" 6 | "github.com/spf13/cobra" 7 | Cmd "jobor/cmd" 8 | "jobor/conf" 9 | "jobor/worker" 10 | "os" 11 | ) 12 | 13 | var ( 14 | //cfg string 15 | 16 | rootCmd = &cobra.Command{ 17 | Use: "", 18 | Short: "Start Run Jobor Worker", 19 | Long: `This is dolphin jobor worker`, 20 | Example: `## 启动命令 ./app -c ./conf/worker.yaml`, 21 | Run: func(cmd *cobra.Command, args []string) { 22 | if len(conf.FlagWorker) == 0 { 23 | _ = cmd.Help() 24 | os.Exit(0) 25 | } 26 | // 加载配置 27 | conf.GetWorkerConf() 28 | 29 | //go func() { 30 | // hlog.Fatal(worker.StartWorkerRpc()) 31 | //}() 32 | //worker.MQWorker() 33 | hlog.Fatal(worker.StartWorkerRpc()) 34 | }, 35 | } 36 | ) 37 | 38 | func init() { 39 | //var c = &conf.WorkerConfig{} 40 | //DefaultIP := "0.0.0.0" 41 | //DefaultPort := int32(20052) 42 | //DefaultMode := "release" // release, debug, test 43 | //DefaultLevel := "info" 44 | //DefaultLog := "./logs" 45 | rootCmd.Flags().StringVarP(&conf.FlagWorker, "conf", "c", "", "config file, example: ./conf/worker.yaml") 46 | //rootCmd.Flags().StringVarP(&c.IP, "ip", "i", DefaultIP, "服务IP") 47 | //rootCmd.Flags().Int32VarP(&c.Port, "port", "p", DefaultPort, "服务启动的端口: 20052 e.g") 48 | //rootCmd.Flags().StringVarP(&c.Mode, "mode", "m", DefaultMode, "启动模式(release, debug, test e.g)") 49 | //rootCmd.Flags().StringVarP(&c.LogPath, "log", "f", DefaultLog, "日志目录(/data/logs e.g)") 50 | //rootCmd.Flags().StringVarP(&c.LogLevel, "level", "l", DefaultLevel, "日志级别(DEBUG, INFO, WARNING e.g)") 51 | if conf.FlagWorker == "" { 52 | conf.FlagWorker = "./conf/worker.yaml" 53 | //fmt.Println("请使用\"-c\"指定配置文件") 54 | //os.Exit(-1) 55 | } 56 | return 57 | } 58 | 59 | func main() { 60 | rootCmd.AddCommand(Cmd.Version()) 61 | if err := rootCmd.Execute(); err != nil { 62 | _ = fmt.Errorf("rootCmd.Execute failed %s", err.Error()) 63 | os.Exit(-1) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /conf/config.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | service: "xxx" 3 | http_address: ":5002" 4 | grpc_address: ":2002" 5 | enable_pprof: true 6 | enable_gzip: true 7 | enable_auth: true 8 | enable_access_log: true 9 | log_level: trace 10 | log_file_name: "log/std.log" 11 | log_max_size: 20 # A file can be up to 20M. 12 | log_max_age: 10 #A file can exist for a maximum of 10 days. 13 | log_max_backups: 5 # Save up to 5 files at the same time. 14 | enable_registry: false 15 | registry_center: "redis" 16 | 17 | authentication: 18 | local_auth: true 19 | max_age : 86400 20 | enable_session : true 21 | auth_secret : "jobor_auth_key" 22 | enable_code : false 23 | enable_mfa : false 24 | enable_email : false 25 | enable_sms : false 26 | login_fail_forbid: 10 27 | login_fail_captcha: 3 28 | 29 | jwt: 30 | type: "Bearer" # Bearer 31 | key: "xx" 32 | refresh_key: "xxx" 33 | age: 360000 34 | 35 | mysql: 36 | dsn: "xxx:xxx@tcp(xxx:3306)/jobor?charset=utf8mb4&parseTime=True&loc=Local&timeout=30s" 37 | 38 | redis: 39 | address: "xxx:6379" 40 | password: "" 41 | db: 1 42 | 43 | ldap: 44 | enabled: false 45 | addr: "xxxx:389" 46 | username: "CN=xxx,OU=xxx,OU=xxx,DC=example,DC=com" 47 | password: "xxx" 48 | base_dn: "dc=example,dc=com" 49 | bind_dn: "CN=xxx,OU=xx,OU=xxx,DC=example,DC=com" 50 | bind_pass: "xxxx" 51 | auth_filter: "(&(sAMAccountName=%s))" 52 | attributes: ["sAMAccountName", "displayName", "mail", "phone"] 53 | timeout: 180 #单位 秒 54 | tls: false 55 | 56 | sso: 57 | enable: true 58 | issuer_url: "http://sso.xxx.local" 59 | client_id: "jobor" 60 | client_secret: "xx" 61 | scope: "openid email role" 62 | 63 | consul: 64 | address: "xxxx:8500" 65 | token: "" 66 | 67 | ext: 68 | telemetry_ep: "" 69 | 70 | 71 | email: 72 | username: "xx@xx.cn" 73 | password: "xx" 74 | smtpServer: "smtp.xxx.com" 75 | smtpPort: 587 76 | tls: false 77 | 78 | ent_wechat: 79 | url: "https://qyapi.weixin.qq.com/cgi-bin" 80 | corp_id: "xxx" 81 | agent_id: "xxx" 82 | secret: "xxxx" -------------------------------------------------------------------------------- /conf/consts.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import "time" 4 | 5 | const ( 6 | // MinGoVersion 最小 Go 版本 7 | MinGoVersion = 1.20 8 | 9 | // AppVersion 项目版本 10 | AppVersion = "v1.20" 11 | 12 | // AppName 项目名称 13 | AppName = "dolphin.jobor" 14 | AppWorkerName = "dolphin.jobor_worker" 15 | 16 | // Dom 租户/域 17 | Dom = "jobor" 18 | 19 | // AppDomain 应用域名 20 | AppDomain = "http://127.0.0.1" 21 | 22 | // AppPort 项目端口 23 | AppPort = 5668 24 | 25 | RequestIDHeaderValue = "X-Request-Id" 26 | SchemeHeaderValue = "X-jobor-Scheme" 27 | 28 | // AppAccessLogFile 项目访问日志存放文件 29 | AppAccessLogFile = "./log/" + AppName + "-access.log" 30 | 31 | // AppCronLogFile 项目后台任务日志存放文件 32 | AppCronLogFile = "./log/" + AppName + "-cron.log" 33 | 34 | // AppInstallMark 项目安装完成标识 35 | AppInstallMark = "INSTALL.lock" 36 | 37 | // HeaderLoginToken 登录验证 Token,Header 中传递的参数 38 | HeaderLoginToken = "Token" 39 | 40 | // HeaderSignToken 签名验证 Authorization,Header 中传递的参数 41 | HeaderSignToken = "Authorization" 42 | 43 | // HeaderSignTokenDate 签名验证 Date,Header 中传递的参数 44 | HeaderSignTokenDate = "Authorization-Date" 45 | 46 | // HeaderSignTokenTimeout 签名有效期为 2 分钟 47 | HeaderSignTokenTimeout = time.Minute * 2 48 | 49 | // RedisKeyPrefixLoginUser Redis Key 前缀 - 登录用户信息 50 | RedisKeyPrefixLoginUser = AppName + ":login-user:" 51 | 52 | // RedisKeyPrefixSignature Redis Key 前缀 - 签名验证信息 53 | RedisKeyPrefixSignature = AppName + ":signature:" 54 | 55 | // ZhCN 简体中文 - 中国 56 | ZhCN = "zh-cn" 57 | 58 | // EnUS 英文 - 美国 59 | EnUS = "en-us" 60 | 61 | // MaxRequestsPerSecond 每秒最大请求量 62 | MaxRequestsPerSecond = 10000 63 | 64 | // LoginSessionTTL 登录有效期为 24 小时 65 | LoginSessionTTL = time.Hour * 24 66 | ) 67 | -------------------------------------------------------------------------------- /conf/worker.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "github.com/bytedance/go-tagexpr/v2/validator" 5 | "github.com/cloudwego/hertz/pkg/common/hlog" 6 | "gopkg.in/yaml.v3" 7 | "os" 8 | ) 9 | 10 | var ( 11 | wconf *WorkerConfig 12 | ) 13 | 14 | // GetWorkerConf gets configuration instance 15 | func GetWorkerConf() *WorkerConfig { 16 | once.Do(initWorkerConf) 17 | return wconf 18 | } 19 | 20 | func GetHostname() string { 21 | e := os.Getenv("hostname") 22 | if len(e) == 0 { 23 | return "default" 24 | } 25 | return e 26 | } 27 | 28 | type WorkerConfig struct { 29 | WorkerName string `yaml:"worker_name"` 30 | HttpAddress string `yaml:"http_address"` 31 | GRpcAddr string `yaml:"grpc_addr"` 32 | RoutingKey string `yaml:"routing_key"` 33 | Weight int `yaml:"weight"` 34 | Servers []string `yaml:"servers"` 35 | LogLevel string `yaml:"log_level"` 36 | LogFileName string `yaml:"log_file_name"` 37 | LogMaxSize int `yaml:"log_max_size"` 38 | LogMaxBackups int `yaml:"log_max_backups"` 39 | LogMaxAge int `yaml:"log_max_age"` 40 | Concurrency int `yaml:"concurrency"` 41 | Redis Redis `yaml:"redis"` 42 | } 43 | 44 | func initWorkerConf() { 45 | confFileRelPath := FlagWorker 46 | content, err := os.ReadFile(confFileRelPath) 47 | if err != nil { 48 | panic(err) 49 | } 50 | 51 | wconf = new(WorkerConfig) 52 | //wconf.HttpAddress = ":2002" 53 | wconf.GRpcAddr = ":20021" 54 | wconf.LogLevel = "./log/std.log" 55 | wconf.LogLevel = "debug" 56 | wconf.Concurrency = 20 57 | wconf.Weight = 100 58 | err = yaml.Unmarshal(content, wconf) 59 | if err != nil { 60 | hlog.Error("parse yaml error - %v", err) 61 | panic(err) 62 | } 63 | if err = validator.Validate(wconf); err != nil { 64 | hlog.Error("validate config error - %v", err) 65 | panic(err) 66 | } 67 | if wconf.WorkerName == "" { 68 | wconf.WorkerName, _ = os.Hostname() 69 | } 70 | 71 | //pretty.Printf("%+v\n", conf) 72 | } 73 | 74 | var FlagWorker = "conf/worker.yaml" 75 | -------------------------------------------------------------------------------- /conf/worker.yaml: -------------------------------------------------------------------------------- 1 | worker_name: "hostname01" 2 | routing_key: default 3 | weight: 100 4 | concurrency: 100 5 | servers: ["127.0.0.1:2002"] # jobor server rpc addrs 6 | -------------------------------------------------------------------------------- /doc/data.sql: -------------------------------------------------------------------------------- 1 | create table api -------------------------------------------------------------------------------- /doc/doc.txt: -------------------------------------------------------------------------------- 1 | tar zcvf jobor-linux-amd64-1.0.1.tar.gz jobor-linux-amd64-1.0.1/ 2 | tar zcvf jobor-drawin-amd64-1.0.1.tar.gz jobor-drawin-amd64-1.0.1/ 3 | 4 | tar -zxvf jobor-1.0.1.tar.gz 5 | cd jobor-1.0.1 6 | server: 7 | ./bin/jobor server -c configs/config.toml 8 | ./bin/jobor worker -c configs/worker.toml -------------------------------------------------------------------------------- /doc/readme.md: -------------------------------------------------------------------------------- 1 | ## 为什么选择golang 2 | 3 | 调度任务系统一般负责触发任务,而执行任务的一般是业务系统,这个时候,很多任务对于调度系统来说都是阻塞的。 4 | 所以调度系统是IO密集型而非CPU密集型。Java开发调度系统,需要非常多的线程,一般会达到几百,甚至几千几万个线程。 5 | CPU大量的工作用来做系统线程切换。golang的goroutine非常适和任务多,IO密集型的任务调度系统。 6 | 7 | ## 功能设计 8 | * 支持cron表达式创建任务 9 | * 支持简单语句创建任务,比如"每天1点执行","每次10分钟执行一次"等 10 | * 支持接入golang,java等sdk方式调度任务 11 | * 支持常见的如grpc,dubbo协议等 12 | * 任务分组,一般情况下,调度系统会创建很多任务,我们需要根据任务进行分组,分组后可以设置权限。但是不能和appid绑定,因为实际业务调度并非和任务是一堆的。 13 | * 立即触发任务 14 | * 执行日志记录 15 | * 任务告警 16 | * 失败任务重试。 17 | 18 | ## 控制台 19 | * 多租户(暂时不考虑) 20 | * 分组管理,分组会有组名称等信息。 21 | * 任务管理,任务包括,http任务(存储),dubbo任务(存储),rpc任务(存储),sdk任务(自动和预存储),任务名称,任务分组等。 22 | * 调度管理,包括调度的任务是什么,分组是哪个,多负责人。 23 | * 人员管理,增删改查人员信息。 24 | 25 | ## 结构设计 26 | * doc文档类 27 | * common 公共代码 28 | * ops 控制台 29 | * server 实际运行项目的节点 30 | 31 | ## 技术选型 32 | * 通讯层 fasthttp 33 | * 数据库层 gorm 34 | * 界面 vue-element-ui 35 | 36 | ## 数据设计 37 | * 存储mysql 38 | * 服务端缓存数据 39 | * ops支持在无mysql的情况下修改节点信息 40 | * 支持mysql的动态停机和下线。 41 | * 节点之间全量元数据管理 42 | 43 | ## 节点设计 44 | * 无主设计,数据节点直接,基于数据块的1主2备。低于3个节点的时候全备。 45 | * 所有节点,全量mysql数据库中的数据。采用ops通知和mysql时间重试机制,实现数据修改更新。 46 | 47 | ## 高可用设计 48 | * 多节点部署防止单点故障 49 | * 每个节点保存全量任务,通过redis抢锁实现分片 50 | * 支持数据库宕机 51 | * 基于"数据块"的主从,从节点和主节点不断心跳。一旦发现其中有一个块宕机了,立马寻找下一个备份。 52 | 53 | ## 容错相关 -------------------------------------------------------------------------------- /doc/sql.md: -------------------------------------------------------------------------------- 1 | 2 | ## 任务表 3 | * 任务id(自增int) 4 | * 任务名称 5 | * 任务配置信息,json 6 | * 任务类型,http,grpc等 7 | 8 | ## trigger表 9 | * 调度id 10 | * 调度名称 11 | 12 | ## 分组表 13 | * 分组id 14 | * 分组名称 15 | * 分组负责人 -------------------------------------------------------------------------------- /doc/struct.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v-mars/jobor/dca6625b3560ddeebff83a14c90f0cac7b8688df/doc/struct.pptx -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | services: 3 | server: 4 | image: iocean/z-mars-jobor-server:v1.0.1 5 | container_name: jobor_server 6 | restart: always 7 | ports: 8 | - 8000:8000 9 | volumes: 10 | - ./configs/config.toml:/data/config.toml 11 | depends_on: 12 | - mysql 13 | - redis 14 | worker: 15 | image: iocean/z-mars-jobor-worker:v1.0.1 16 | container_name: jobor_worker 17 | volumes: 18 | - ./configs/worker.toml:/data/worker.toml 19 | depends_on: 20 | - server 21 | mysql: 22 | image: mysql:8.0 23 | command: [mysqld, --character-set-server=utf8, --collation-server=utf8_bin] 24 | environment: 25 | MYSQL_ROOT_PASSWORD: "Jobor123" 26 | MYSQL_DATABASE: "jobor" 27 | MYSQL_USER: "jobor_rw" 28 | MYSQL_PASSWORD: "Jobor_rw" 29 | volumes: 30 | - "./mysql_data/var/lib/mysql:/var/lib/mysql:rw" 31 | - "./mysql_data/etc/mysql/my.cnf:/etc/mysql/my.cnf" 32 | - "/etc/localtime:/etc/localtime:ro" 33 | ports: 34 | - 3306:3306 35 | 36 | redis: 37 | image: redis:5 38 | restart: always 39 | ports: 40 | - 6379:6379 -------------------------------------------------------------------------------- /dockers/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | go_infra: 4 | image: xxxx:8.6.2 # 镜像 5 | container_name: go_infra # 定义容器名称 6 | restart: always # 开机启动,失败也会一直重启 7 | environment: 8 | - "RUN_ENV=dev" # 设置集群名称为elasticsearch 9 | volumes: 10 | - /etc/localtime=/etc/localtime 11 | - ./go_infra/log:/data/log 12 | ports: 13 | - "5620:5620" -------------------------------------------------------------------------------- /dockers/jobor.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM iocean/golang:1.20 as builder 2 | WORKDIR /data 3 | COPY ./ ./ 4 | RUN go env -w GO111MODULE="on" && go env -w GOPROXY="https://goproxy.cn" && go env -w GOSUMDB="off" && go mod tidy && \ 5 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./app ./main.go 6 | 7 | 8 | FROM iocean/ubuntu:22.10 9 | WORKDIR /data 10 | COPY --from=builder /data/app ./ 11 | COPY ./fs ./fs 12 | #COPY --from=builder /data/godemo/docs/ ./docs/ 13 | #RUN apt update && apt install tzdata inetutils-ping telnet traceroute -y 14 | #RUN yum install inetutils-ping vim telnet traceroute -y && mkdir -pv /data/log 15 | ENV INST_NO=0 16 | ENV BEGIN_PORT=5002 17 | ENV SERVICE=jobor-server 18 | ENV LOCATION=CN 19 | ENV LANG=C.UTF-8 20 | CMD exec ./app -c conf/config.yaml 21 | 22 | # docker build -f dockers/jobor.Dockerfile -t iocean/jobor-server:20230822_144731-dev-jobor . 23 | # docker push iocean/jobor-server:20230822_144731-dev-jobor 24 | -------------------------------------------------------------------------------- /dockers/jobor_worker.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM iocean/golang:1.20 as builder 2 | WORKDIR /data 3 | COPY ./ ./ 4 | RUN go env -w GO111MODULE="on" && go env -w GOPROXY="https://goproxy.cn" && go env -w GOSUMDB="off" && go mod tidy && \ 5 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./app ./cmd/worker/main.go 6 | 7 | 8 | FROM iocean/ubuntu:22.10 9 | WORKDIR /data 10 | COPY --from=builder /data/app ./ 11 | #COPY --from=builder /data/godemo/docs/ ./docs/ 12 | #RUN apt update && apt install tzdata inetutils-ping telnet traceroute -y 13 | #RUN yum install inetutils-ping vim telnet traceroute -y && mkdir -pv /data/log 14 | ENV INST_NO=0 15 | ENV BEGIN_PORT=20021 16 | ENV SERVICE=jobor-worker 17 | ENV LOCATION=CN 18 | ENV LANG=C.UTF-8 19 | CMD exec ./app -c conf/worker.yaml 20 | #ENTRYPOINT exec ./app -f conf/worker.yaml 21 | 22 | # docker build -f dockers/jobor_worker.Dockerfile -t iocean/jobor:worker-v1.0.1 . 23 | # docker push iocean/jobor:worker-v1.0.1 24 | -------------------------------------------------------------------------------- /dockers/jobor_worker_go.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM iocean/golang:1.20 as builder 2 | WORKDIR /data 3 | COPY ./ ./ 4 | RUN go env -w GO111MODULE="on" && go env -w GOPROXY="https://goproxy.cn" && go env -w GOSUMDB="off" && go mod tidy && \ 5 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./app ./cmd/worker/main.go 6 | 7 | FROM golang:1.20 8 | WORKDIR /data 9 | COPY --from=builder /data/app ./ 10 | RUN go env -w GO111MODULE="on" && go env -w GOPROXY="https://goproxy.cn" 11 | #COPY --from=builder /data/godemo/docs/ ./docs/ 12 | #RUN apt update && apt install tzdata inetutils-ping telnet traceroute -y 13 | #RUN yum install inetutils-ping vim telnet traceroute -y && mkdir -pv /data/log 14 | ENV INST_NO=0 15 | ENV BEGIN_PORT=20021 16 | ENV SERVICE=jobor-worker 17 | ENV LOCATION=CN 18 | ENV LANG=C.UTF-8 19 | CMD exec ./app -c conf/worker.yaml 20 | #ENTRYPOINT exec ./app -f conf/worker.yaml 21 | 22 | # docker build -f dockers/jobor_worker.Dockerfile -t iocean/jobor:worker-v1.0.1 . 23 | # docker push iocean/jobor:worker-v1.0.1 24 | # 阿里云:https://mirrors.aliyun.com/goproxy 25 | # 微软:https://goproxy.io 七牛云:https://goproxy.cn GoCenter:https://gocenter.io -------------------------------------------------------------------------------- /dockers/jobor_worker_py.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM iocean/golang:1.20 as builder 2 | WORKDIR /data 3 | COPY ./ ./ 4 | RUN go env -w GO111MODULE="on" && go env -w GOPROXY="https://goproxy.cn" && go env -w GOSUMDB="off" && go mod tidy && \ 5 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./app ./cmd/worker/main.go 6 | 7 | 8 | FROM python:3.7.16 9 | WORKDIR /data 10 | COPY --from=builder /data/app ./ 11 | #COPY --from=builder /data/godemo/docs/ ./docs/ 12 | #RUN apt update && apt install tzdata inetutils-ping telnet traceroute -y 13 | #RUN yum install inetutils-ping vim telnet traceroute -y && mkdir -pv /data/log 14 | ENV INST_NO=0 15 | ENV BEGIN_PORT=20021 16 | ENV SERVICE=jobor-worker 17 | ENV LOCATION=CN 18 | ENV LANG=C.UTF-8 19 | CMD exec ./app -c conf/worker.yaml 20 | #ENTRYPOINT exec ./app -f conf/worker.yaml 21 | 22 | # docker build -f dockers/jobor_worker.Dockerfile -t iocean/jobor:worker-v1.0.1 . 23 | # docker push iocean/jobor:worker-v1.0.1 24 | -------------------------------------------------------------------------------- /fsembed/dist/assets/AuditLogPage-09a50e77.css: -------------------------------------------------------------------------------- 1 | .auto-color[data-v-d43398d7]{filter:grayscale(1) contrast(9999) invert(1)}.space[data-v-d43398d7]{display:flex;align-items:center}.space>*[data-v-d43398d7]:not(:last-child){margin-right:6px}.pagination[data-v-d43398d7]{margin-top:8px;display:flex;justify-content:flex-end}.box-shadow[data-v-d43398d7]{transition:box-shadow .3s;box-shadow:0 1px 2px -2px #0000001a,0 3px 6px #00000003,0 5px 12px 4px #0000000d}.box-shadow[data-v-d43398d7]:hover{box-shadow:#aeaeae33 0 0 10px}.main[data-v-d43398d7]{display:flex}.main .code-box[data-v-d43398d7]{padding-left:24px;position:relative;flex:1;overflow-x:hidden}.main .code-box>pre[data-v-d43398d7]{height:60vh;overflow-y:auto} 2 | -------------------------------------------------------------------------------- /fsembed/dist/assets/AuditLogPage-6333878d.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v-mars/jobor/dca6625b3560ddeebff83a14c90f0cac7b8688df/fsembed/dist/assets/AuditLogPage-6333878d.js.gz -------------------------------------------------------------------------------- /fsembed/dist/assets/AutoOmissionTag.vue_vue_type_script_setup_true_lang-b334684b.js: -------------------------------------------------------------------------------- 1 | import{d as v,T as C,m as b,i as $,B as a,D as f,E as t,S as m,X as n,U as i,a9 as N,J as e,C as c,V as B,aW as T,F as g,aZ as k,a2 as P}from "./index-bbb52c5f.js";import{N as w}from "./Switch-eb6978c1.js";import{N as F}from "./TooltipText.vue_vue_type_script_setup_true_lang-7f7f941d.js";import{N as h}from "./Space-b1fb6b4a.js";import{b as p,j as V}from "./Dropdown-cc67fbc4.js";const O=v({__name:"PopconfirmSwitch",props:{status:{type:[Boolean,String]},checkFunction:null,onText:{default:"开启"},offText:{default:"关闭"},onColor:{default:"#409eff"},offColor:{default:"#d03050"}},setup(y){const o=y,{checkFunction:s,status:l}=C(o),d=b(!1);async function r(){d.value=!0,await s.value(),d.value=!1}const x=$(()=>l.value?`是否${o.offText}`:`是否${o.onText}`);function S({checked:_}){const u={};return _?u.background=o.onColor:u.background=o.offColor,u}return(_, u)=>(a(),f(e(F),{onPositiveClick:u[0]||(u[0]=()=>r())},{trigger:t(()=>[m(e(w),N(_.$attrs,{size:"small",value:e(l),loading:d.value,"rail-style":S}),{checked:t(()=>[n(i(o.onText),1)]),unchecked:t(()=>[n(i(o.offText),1)]),_:1},16,["value","loading"])]),default:t(()=>[n(" "+i(e(x)),1)]),_:1}))}}),E={flex:"","items-center":""},z={key:0,class:"tag-list"},R=v({__name:"AutoOmissionTag",props:{names:null},setup(y){const o=y,{names:s}=C(o);return(l, d)=>(a(),c("span",E,[B(l.$slots,"default"),!e(T)(e(s))&&e(s).length<=2?(a(),c("span",z,[m(e(h),null,{default:t(()=>[(a(!0),c(g,null,k(e(s), r=>(a(),f(e(p),N({key:r,bordered:!1,type:"primary"},l.$attrs),{default:t(()=>[n(i(r),1)]),_:2},1040))),128))]),_:1})])):(a(),f(e(V),N({key:1,trigger:"hover",placement:"top"},l.$attrs),{trigger:t(()=>[m(e(h),null,{default:t(()=>[(a(!0),c(g,null,k(e(s).slice(0,1), r=>(a(),f(e(p),{key:r,bordered:!1,type:"primary"},{default:t(()=>[n(i(r),1)]),_:2},1024))),128)),e(T)(e(s))?P("",!0):(a(),f(e(p),{key:0,type:"primary",bordered:!1},{default:t(()=>[n("…")]),_:1}))]),_:1})]),default:t(()=>[m(e(h),null,{default:t(()=>[(a(!0),c(g,null,k(e(s), r=>(a(),f(e(p),{key:r,bordered:!1,type:"primary",size:"small"},{default:t(()=>[n(i(r),1)]),_:2},1024))),128))]),_:1})]),_:1},16))]))}});export{R as _,O as a}; 2 | -------------------------------------------------------------------------------- /fsembed/dist/assets/Bage.vue_vue_type_script_setup_true_lang-f47800c7.js: -------------------------------------------------------------------------------- 1 | import{d as p,B as d,C as f,G as t,aV as g,U as u}from "./index-bbb52c5f.js";const b={style:{"line-height":"inherit","vertical-align":"baseline",position:"relative",display:"inline-block"}},y={style:{display:"inline-block","margin-left":"6px"}},M=p({__name:"Bage",props:{text:{default:""},color:{default:"#f90"},type:{default:"processing"}},setup(s){const e=s,a="#e6ebf1",c="#f90",l="#19be6b",i="#ed4014",r="#e6ebf1",n="#2d8cf0",o={processing:n,primary:n,success:l,error:i,info:r,warning:c,blue:"#1890ff",green:"#52c41a",red:"#f5222d",yellow:"#fadb14",pink:"#eb2f96",magenta:"#eb2f96",volcano:"#fa541c",orange:"#fa8c16",gold:"#faad14",lime:"#a0d911",cyan:"#13c2c2",geekblue:"#2f54eb",purple:"#722ed1",default:a};return(z, G)=>(d(),f("span",b,[t("span",{style:g({"background-color":o[e.type]?o[e.type]:e.color,position:"relative",width:"6px",height:"6px",display:"inline-block","border-radius":"50%","vertical-align":"middle",top:"-1px"})},null,4),t("span",y,u(e.text),1)]))}});export{M as _}; 2 | -------------------------------------------------------------------------------- /fsembed/dist/assets/BlankLayout-3a8d7bc7.css: -------------------------------------------------------------------------------- 1 | .fade-enter-active[data-v-8b25f00d],.fade-leave-active[data-v-8b25f00d]{transition:opacity .3s ease}.fade-enter-from[data-v-8b25f00d],.fade-leave-to[data-v-8b25f00d]{opacity:0} 2 | -------------------------------------------------------------------------------- /fsembed/dist/assets/BlankLayout-f95ca45c.js: -------------------------------------------------------------------------------- 1 | import{_ as o}from "./_plugin-vue_export-helper-c27b6911.js";import{B as t,D as _,$ as n}from "./index-bbb52c5f.js";const c={};function a(r, s){const e=n("RouterView");return t(),_(e)}const f=o(c,[["render",a],["__scopeId","data-v-8b25f00d"]]);export{f as default}; 2 | -------------------------------------------------------------------------------- /fsembed/dist/assets/DashboardPage-0979a718.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v-mars/jobor/dca6625b3560ddeebff83a14c90f0cac7b8688df/fsembed/dist/assets/DashboardPage-0979a718.js.gz -------------------------------------------------------------------------------- /fsembed/dist/assets/DashboardPage-f6d511d2.css: -------------------------------------------------------------------------------- 1 | .box[data-v-4b4ef9ae]{border:2px solid #fff;border-radius:8px;padding:12px}.card-panel-icon-wrapper[data-v-4b4ef9ae]{float:left;padding:16px;transition:all .38s ease-out;border-radius:6px}.box[data-v-9d90c189]{border:2px solid #fff;border-radius:8px;padding:12px}.v-space[data-v-7e6e3074]>*[data-v-7e6e3074]:not(:last-child){margin-bottom:10px} 2 | -------------------------------------------------------------------------------- /fsembed/dist/assets/Dropdown-cc67fbc4.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v-mars/jobor/dca6625b3560ddeebff83a14c90f0cac7b8688df/fsembed/dist/assets/Dropdown-cc67fbc4.js.gz -------------------------------------------------------------------------------- /fsembed/dist/assets/EditButton.vue_vue_type_script_setup_true_lang-6838874c.js: -------------------------------------------------------------------------------- 1 | import{d as a,B as c,C as m,G as h,T as _,m as f,D as w,E as e,S as i,J as t,X as u,a9 as v,W as x,U as g}from "./index-bbb52c5f.js";import{N as k}from "./TooltipText.vue_vue_type_script_setup_true_lang-7f7f941d.js";import{N as B}from "./Icon-01f3be5d.js";const z={xmlns:"http://www.w3.org/2000/svg","xmlns:xlink":"http://www.w3.org/1999/xlink",viewBox:"0 0 1024 1024"},C=h("path",{d:"M360 184h-8c4.4 0 8-3.6 8-8v8h304v-8c0 4.4 3.6 8 8 8h-8v72h72v-80c0-35.3-28.7-64-64-64H352c-35.3 0-64 28.7-64 64v80h72v-72zm504 72H160c-17.7 0-32 14.3-32 32v32c0 4.4 3.6 8 8 8h60.4l24.7 523c1.6 34.1 29.8 61 63.9 61h454c34.2 0 62.3-26.8 63.9-61l24.7-523H888c4.4 0 8-3.6 8-8v-32c0-17.7-14.3-32-32-32zM731.3 840H292.7l-24.2-512h487l-24.2 512z",fill:"currentColor"},null,-1),N=[C],T=a({name:"DeleteOutlined",render:function(o, l){return c(),m("svg",z,N)}}),$={xmlns:"http://www.w3.org/2000/svg","xmlns:xlink":"http://www.w3.org/1999/xlink",viewBox:"0 0 1024 1024"},E=h("path",{d:"M257.7 752c2 0 4-.2 6-.5L431.9 722c2-.4 3.9-1.3 5.3-2.8l423.9-423.9a9.96 9.96 0 0 0 0-14.1L694.9 114.9c-1.9-1.9-4.4-2.9-7.1-2.9s-5.2 1-7.1 2.9L256.8 538.8c-1.5 1.5-2.4 3.3-2.8 5.3l-29.5 168.2a33.5 33.5 0 0 0 9.4 29.8c6.6 6.4 14.9 9.9 23.8 9.9zm67.4-174.4L687.8 215l73.3 73.3l-362.7 362.6l-88.9 15.7l15.6-89zM880 836H144c-17.7 0-32 14.3-32 32v36c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-36c0-17.7-14.3-32-32-32z",fill:"currentColor"},null,-1),H=[E],D=a({name:"EditOutlined",render:function(o, l){return c(),m("svg",$,H)}}),I=a({__name:"RemoveConfirmButton",props:{confirmText:{default:"是否删除该条信息"},showIco:{type:Boolean},showTxt:{type:Boolean},delFunction:null},setup(n){const o=n,{confirmText:l,delFunction:r}=_(o),s=f(!1);async function d(){s.value=!0,await r.value(),s.value=!1}return(y, p)=>(c(),w(t(k),{onPositiveClick:p[0]||(p[0]=()=>d())},{trigger:e(()=>[i(t(x),v({size:"small",loading:s.value,type:"error",quaternary:""},y.$attrs),{icon:e(()=>[i(t(B),{component:t(T)},null,8,["component"])]),default:e(()=>[u(" 删除 ")]),_:1},16,["loading"])]),default:e(()=>[u(" "+g(t(l)),1)]),_:1}))}}),P=a({__name:"EditButton",props:{btnText:{default:"编辑"},showIco:{type:Boolean},showTxt:{type:Boolean}},setup(n){const o=n,{btnText:l}=_(o),r=f(!1);return(s, d)=>(c(),w(t(x),v({size:"small",loading:r.value,type:"primary",quaternary:""},s.$attrs),{icon:e(()=>[i(t(B),{component:t(D)},null,8,["component"])]),default:e(()=>[u(" "+g(t(l)),1)]),_:1},16,["loading"]))}});export{P as _,I as a}; 2 | -------------------------------------------------------------------------------- /fsembed/dist/assets/FiraCode-Regular-f13d1ece.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v-mars/jobor/dca6625b3560ddeebff83a14c90f0cac7b8688df/fsembed/dist/assets/FiraCode-Regular-f13d1ece.woff2 -------------------------------------------------------------------------------- /fsembed/dist/assets/FocusDetector-fac0e688.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v-mars/jobor/dca6625b3560ddeebff83a14c90f0cac7b8688df/fsembed/dist/assets/FocusDetector-fac0e688.js.gz -------------------------------------------------------------------------------- /fsembed/dist/assets/FormItem-9f1c1532.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v-mars/jobor/dca6625b3560ddeebff83a14c90f0cac7b8688df/fsembed/dist/assets/FormItem-9f1c1532.js.gz -------------------------------------------------------------------------------- /fsembed/dist/assets/Forward-2b3a5ea4.js: -------------------------------------------------------------------------------- 1 | import{d as n,h as r}from "./index-bbb52c5f.js";const e=n({name:"Backward",render(){return r("svg",{viewBox:"0 0 20 20",fill:"none",xmlns:"http://www.w3.org/2000/svg"},r("path",{d:"M12.2674 15.793C11.9675 16.0787 11.4927 16.0672 11.2071 15.7673L6.20572 10.5168C5.9298 10.2271 5.9298 9.7719 6.20572 9.48223L11.2071 4.23177C11.4927 3.93184 11.9675 3.92031 12.2674 4.206C12.5673 4.49169 12.5789 4.96642 12.2932 5.26634L7.78458 9.99952L12.2932 14.7327C12.5789 15.0326 12.5673 15.5074 12.2674 15.793Z",fill:"currentColor"}))}}),t=n({name:"FastBackward",render(){return r("svg",{viewBox:"0 0 20 20",version:"1.1",xmlns:"http://www.w3.org/2000/svg"},r("g",{stroke:"none","stroke-width":"1",fill:"none","fill-rule":"evenodd"},r("g",{fill:"currentColor","fill-rule":"nonzero"},r("path",{d:"M8.73171,16.7949 C9.03264,17.0795 9.50733,17.0663 9.79196,16.7654 C10.0766,16.4644 10.0634,15.9897 9.76243,15.7051 L4.52339,10.75 L17.2471,10.75 C17.6613,10.75 17.9971,10.4142 17.9971,10 C17.9971,9.58579 17.6613,9.25 17.2471,9.25 L4.52112,9.25 L9.76243,4.29275 C10.0634,4.00812 10.0766,3.53343 9.79196,3.2325 C9.50733,2.93156 9.03264,2.91834 8.73171,3.20297 L2.31449,9.27241 C2.14819,9.4297 2.04819,9.62981 2.01448,9.8386 C2.00308,9.89058 1.99707,9.94459 1.99707,10 C1.99707,10.0576 2.00356,10.1137 2.01585,10.1675 C2.05084,10.3733 2.15039,10.5702 2.31449,10.7254 L8.73171,16.7949 Z"}))))}}),l=n({name:"FastForward",render(){return r("svg",{viewBox:"0 0 20 20",version:"1.1",xmlns:"http://www.w3.org/2000/svg"},r("g",{stroke:"none","stroke-width":"1",fill:"none","fill-rule":"evenodd"},r("g",{fill:"currentColor","fill-rule":"nonzero"},r("path",{d:"M11.2654,3.20511 C10.9644,2.92049 10.4897,2.93371 10.2051,3.23464 C9.92049,3.53558 9.93371,4.01027 10.2346,4.29489 L15.4737,9.25 L2.75,9.25 C2.33579,9.25 2,9.58579 2,10.0000012 C2,10.4142 2.33579,10.75 2.75,10.75 L15.476,10.75 L10.2346,15.7073 C9.93371,15.9919 9.92049,16.4666 10.2051,16.7675 C10.4897,17.0684 10.9644,17.0817 11.2654,16.797 L17.6826,10.7276 C17.8489,10.5703 17.9489,10.3702 17.9826,10.1614 C17.994,10.1094 18,10.0554 18,10.0000012 C18,9.94241 17.9935,9.88633 17.9812,9.83246 C17.9462,9.62667 17.8467,9.42976 17.6826,9.27455 L11.2654,3.20511 Z"}))))}}),C=n({name:"Forward",render(){return r("svg",{viewBox:"0 0 20 20",fill:"none",xmlns:"http://www.w3.org/2000/svg"},r("path",{d:"M7.73271 4.20694C8.03263 3.92125 8.50737 3.93279 8.79306 4.23271L13.7944 9.48318C14.0703 9.77285 14.0703 10.2281 13.7944 10.5178L8.79306 15.7682C8.50737 16.0681 8.03263 16.0797 7.73271 15.794C7.43279 15.5083 7.42125 15.0336 7.70694 14.7336L12.2155 10.0005L7.70694 5.26729C7.42125 4.96737 7.43279 4.49264 7.73271 4.20694Z",fill:"currentColor"}))}});export{e as B,C as F,l as a,t as b}; 2 | -------------------------------------------------------------------------------- /fsembed/dist/assets/Icon-01f3be5d.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v-mars/jobor/dca6625b3560ddeebff83a14c90f0cac7b8688df/fsembed/dist/assets/Icon-01f3be5d.js.gz -------------------------------------------------------------------------------- /fsembed/dist/assets/LatoLatin-Regular-ddd4ef7f.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v-mars/jobor/dca6625b3560ddeebff83a14c90f0cac7b8688df/fsembed/dist/assets/LatoLatin-Regular-ddd4ef7f.woff2 -------------------------------------------------------------------------------- /fsembed/dist/assets/LatoLatin-Semibold-267eef30.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v-mars/jobor/dca6625b3560ddeebff83a14c90f0cac7b8688df/fsembed/dist/assets/LatoLatin-Semibold-267eef30.woff2 -------------------------------------------------------------------------------- /fsembed/dist/assets/LoginMain-10c5d795.css: -------------------------------------------------------------------------------- 1 | .auto-color[data-v-eb7b51ee]{filter:grayscale(1) contrast(9999) invert(1)}.space[data-v-eb7b51ee]{display:flex;align-items:center}.space>*[data-v-eb7b51ee]:not(:last-child){margin-right:6px}.pagination[data-v-eb7b51ee]{margin-top:8px;display:flex;justify-content:flex-end}.box-shadow[data-v-eb7b51ee]{transition:box-shadow .3s;box-shadow:0 1px 2px -2px #0000001a,0 3px 6px #00000003,0 5px 12px 4px #0000000d}.box-shadow[data-v-eb7b51ee]:hover{box-shadow:#aeaeae33 0 0 10px}.img[data-v-eb7b51ee]{width:32px;margin:0 10px;border:1px solid #eee;border-radius:100%;cursor:pointer;padding:6px}.active-login[data-v-eb7b51ee]{background-color:#c2daf1}.auto-color[data-v-bc6866bd]{filter:grayscale(1) contrast(9999) invert(1)}.space[data-v-bc6866bd]{display:flex;align-items:center}.space>*[data-v-bc6866bd]:not(:last-child){margin-right:6px}.pagination[data-v-bc6866bd]{margin-top:8px;display:flex;justify-content:flex-end}.box-shadow[data-v-bc6866bd]{transition:box-shadow .3s;box-shadow:0 1px 2px -2px #0000001a,0 3px 6px #00000003,0 5px 12px 4px #0000000d}.box-shadow[data-v-bc6866bd]:hover{box-shadow:#aeaeae33 0 0 10px}.form-container[data-v-bc6866bd]{width:100%;max-width:450px;padding:20px}.form-header[data-v-bc6866bd]{margin-bottom:24px}.form-header .title[data-v-bc6866bd]{font-size:24px;color:#1d2129}.form-header .desc[data-v-bc6866bd]{color:#80909c;font-size:16px}.auto-color[data-v-107a1a2f]{filter:grayscale(1) contrast(9999) invert(1)}.space[data-v-107a1a2f]{display:flex;align-items:center}.space>*[data-v-107a1a2f]:not(:last-child){margin-right:6px}.pagination[data-v-107a1a2f]{margin-top:8px;display:flex;justify-content:flex-end}.box-shadow[data-v-107a1a2f]{transition:box-shadow .3s;box-shadow:0 1px 2px -2px #0000001a,0 3px 6px #00000003,0 5px 12px 4px #0000000d}.box-shadow[data-v-107a1a2f]:hover{box-shadow:#aeaeae33 0 0 10px}.container-box .banner[data-v-107a1a2f]{width:35%;background:linear-gradient(163.85deg,#1d2129 0%,#00308f 100%)}.logo-img[data-v-107a1a2f]{position:absolute;left:25px;top:20px;height:40px}.banner-inner[data-v-107a1a2f]{top:calc(50% - 170px);left:40px;right:40px;z-index:10}.banner-inner .banner-desc[data-v-107a1a2f]{font-size:20px;margin-top:18px;color:#fff}@media screen and (max-width: 1000px){.banner[data-v-107a1a2f]{font-size:20px;display:none}}@media screen and (min-width: 1000px){.banner[data-v-107a1a2f]{font-size:32px;display:flex}.content .logo-img[data-v-107a1a2f]{display:none}} 2 | -------------------------------------------------------------------------------- /fsembed/dist/assets/LoginMain-d5f6707d.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v-mars/jobor/dca6625b3560ddeebff83a14c90f0cac7b8688df/fsembed/dist/assets/LoginMain-d5f6707d.js.gz -------------------------------------------------------------------------------- /fsembed/dist/assets/PageLayout-32092343.css: -------------------------------------------------------------------------------- 1 | .auto-color[data-v-e59811c3]{filter:grayscale(1) contrast(9999) invert(1)}.space[data-v-e59811c3]{display:flex;align-items:center}.space>*[data-v-e59811c3]:not(:last-child){margin-right:6px}.pagination[data-v-e59811c3]{margin-top:8px;display:flex;justify-content:flex-end}.box-shadow[data-v-e59811c3]{transition:box-shadow .3s;box-shadow:0 1px 2px -2px #0000001a,0 3px 6px #00000003,0 5px 12px 4px #0000000d}.box-shadow[data-v-e59811c3]:hover{box-shadow:#aeaeae33 0 0 10px}.avator[data-v-e59811c3]{display:flex;cursor:pointer;align-items:center;justify-content:center;width:40px;height:40px;border-radius:8px;background:bisque;color:#fa541c;white-space:nowrap}.avator .avator-inner[data-v-e59811c3]{position:relative}.avator .avator-inner[data-v-e59811c3]:before{content:attr(alt);visibility:hidden;font-size:40px}.avator .avator-inner .avator-container[data-v-e59811c3]{position:absolute;inset:0;container-type:inline-size;text-align:center;display:flex;justify-content:center;align-items:center}.avator .avator-inner .avator-container span[data-v-e59811c3]{font-size:calc(24px - 10cqw);overflow:hidden;max-width:40px;text-overflow:ellipsis}.fade-enter-active[data-v-91d1c4f9],.fade-leave-active[data-v-91d1c4f9]{transition:opacity .3s ease}.fade-enter-from[data-v-91d1c4f9],.fade-leave-to[data-v-91d1c4f9]{opacity:0} 2 | -------------------------------------------------------------------------------- /fsembed/dist/assets/PageLayout-8b0bff69.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v-mars/jobor/dca6625b3560ddeebff83a14c90f0cac7b8688df/fsembed/dist/assets/PageLayout-8b0bff69.js.gz -------------------------------------------------------------------------------- /fsembed/dist/assets/RestPassword.vue_vue_type_script_setup_true_lang-12be81f1.js: -------------------------------------------------------------------------------- 1 | import{u as f}from "./useRequest-1c3afba9.js";import{d as q,T as V,m as n,B as v,C as x,G as m,V as F,S as o,E as l,J as r,D as I,a2 as S,W as U,X as O,Y as L,F as M,Z as P}from "./index-bbb52c5f.js";import{v as T}from "./validate-1d95410a.js";import{N as $,a as w}from "./FormItem-9f1c1532.js";import{b as i}from "./Icon-01f3be5d.js";const j={flex:"","justify-end":""},Y=q({__name:"RestPassword",props:{id:null,showOldPassword:{type:Boolean,default:!0}},emits:["refresh"],setup(c, {emit:g}){const N=c,{id:d,showOldPassword:y}=V(N),u=n(!1),p=n(null),{run:b,loading:k}=f(P.passRestById),{run:h,loading:_}=f(P.resetOneselfPassword),a=n({oldPassword:"",newPassword:"",validateNewPassword:""}),B={oldPassword:{required:!0,message:"请输入旧密码",trigger:"blur"},newPassword:{required:!0,validator(t, e){if(e){if(e===a.value.oldPassword)return new Error("不能与旧密码一样")}else return new Error("请输入新密码");return!0},trigger:"blur"},validateNewPassword:{required:!0,validator(t, e){if(e){if(e!==a.value.newPassword)return new Error("与新密码不一致")}else return new Error("请输入新密码");return!0},trigger:"blur"},email:{required:!1,validator(t, e){if(e){if(!T(e))return new Error("请输入正确的邮箱地址")}else return!0;return!0},trigger:"blur"}},E=()=>{u.value=!0},R=async()=>{var t;(t=p.value)==null||t.validate(async e=>{if(e)return;const s={old_password:a.value.oldPassword,password:a.value.newPassword},{result:C}=d!=null&&d.value?await b({...s,id:d.value}):await h(s);C&&(u.value=!1,g("refresh"))})};return(t, e)=>(v(),x(M,null,[m("span",{onClick:E},[F(t.$slots,"default")]),o(r(L),{show:u.value,"onUpdate:show":e[4]||(e[4]= s=>u.value=s),title:"重置密码",preset:"card","auto-focus":!1,"w-100":""},{default:l(()=>[o(r($),{ref_key:"formRef",ref:p,model:a.value,rules:B},{default:l(()=>[r(y)?(v(),I(r(w),{key:0,path:"oldPassword",label:"旧密码"},{default:l(()=>[o(r(i),{value:a.value.oldPassword,"onUpdate:value":e[0]||(e[0]= s=>a.value.oldPassword=s),type:"password","show-password-on":"click",placeholder:"输入旧密码"},null,8,["value"])]),_:1})):S("",!0),o(r(w),{path:"newPassword",label:"新密码"},{default:l(()=>[o(r(i),{value:a.value.newPassword,"onUpdate:value":e[1]||(e[1]= s=>a.value.newPassword=s),placeholder:"输入新密码",type:"password","show-password-on":"click"},null,8,["value"])]),_:1}),o(r(w),{path:"validateNewPassword",label:"二次确认密码"},{default:l(()=>[o(r(i),{value:a.value.validateNewPassword,"onUpdate:value":e[2]||(e[2]= s=>a.value.validateNewPassword=s),placeholder:"输入二次确认密码",type:"password","show-password-on":"click"},null,8,["value"])]),_:1}),m("div",j,[o(r(U),{type:"primary",loading:r(k)||r(_),onClick:e[3]||(e[3]= s=>R())},{default:l(()=>[O(" 确定 ")]),_:1},8,["loading"])])]),_:1},8,["model"])]),_:1},8,["show"])],64))}});export{Y as _}; 2 | -------------------------------------------------------------------------------- /fsembed/dist/assets/RolePageTree-ba345166.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v-mars/jobor/dca6625b3560ddeebff83a14c90f0cac7b8688df/fsembed/dist/assets/RolePageTree-ba345166.js.gz -------------------------------------------------------------------------------- /fsembed/dist/assets/RoleSelect.vue_vue_type_script_setup_true_lang-ab8d76b3.js: -------------------------------------------------------------------------------- 1 | import{aj as r,d as u,i as p,B as c,D as d,a9 as m,J as s}from "./index-bbb52c5f.js";import{u as y}from "./useRequest-1c3afba9.js";import{h as R}from "./Dropdown-cc67fbc4.js";const f={getRoleListAll(){return r("GET /sys/roles",{params:{}})},getRoleById(e){return r(["GET /sys/role/:id",e],{})},getRoleList(e){return r("GET /sys/role",{params:e})},getRoleTreeList(e){return r("GET /sys/role-tree",{params:e})},createRole(e){return r("POST /sys/role",{data:e})},editRole(e, o){return r(["PUT /sys/role/:id",e],{data:o})},delRole(e){return r(["DELETE /sys/role/:id",e])}},_=u({__name:"RoleSelect",props:{keyName:{default:"id"}},setup(e){const o=e,{data:l,loading:n}=y(f.getRoleListAll,{auto:!0}),i=p(()=>l.value?l.value.list.map(t=>({label:`${t.title} | ${t.name}`,value:t[o.keyName]})):[]);return(a, t)=>(c(),d(s(R),m({loading:s(n),options:s(i),filterable:"",placeholder:"请选择部门"},a.$attrs),null,16,["loading","options"]))}});export{_,f as r}; 2 | -------------------------------------------------------------------------------- /fsembed/dist/assets/Space-b1fb6b4a.js: -------------------------------------------------------------------------------- 1 | import{a0 as B,d as C,g as G,u as S,ax as R,i as j,aK as E,h as y,a1 as L,c3 as M,ai as v}from "./index-bbb52c5f.js";import{g as I}from "./FocusDetector-fac0e688.js";const P={gapSmall:"4px 8px",gapMedium:"8px 12px",gapLarge:"12px 16px"},T=()=>P,A={name:"Space",self:T},N=A;let h;const O=()=>{if(!B)return!0;if(h===void 0){const e=document.createElement("div");e.style.display="flex",e.style.flexDirection="column",e.style.rowGap="1px",e.appendChild(document.createElement("div")),e.appendChild(document.createElement("div")),document.body.appendChild(e);const n=e.scrollHeight===1;return document.body.removeChild(e),h=n}return h},W=Object.assign(Object.assign({},S.props),{align:String,justify:{type:String,default:"start"},inline:Boolean,vertical:Boolean,size:{type:[String,Number,Array],default:"medium"},wrapItem:{type:Boolean,default:!0},itemStyle:[String,Object],wrap:{type:Boolean,default:!0},internalUseGap:{type:Boolean,default:void 0}}),H=C({name:"Space",props:W,setup(e){const{mergedClsPrefixRef:n,mergedRtlRef:u}=G(e),a=S("Space","-space",void 0,N,e,n),g=R("Space",u,n);return{useGap:O(),rtlEnabled:g,mergedClsPrefix:n,margin:j(()=>{const{size:t}=e;if(Array.isArray(t))return{horizontal:t[0],vertical:t[1]};if(typeof t=="number")return{horizontal:t,vertical:t};const{self:{[L("gap",t)]:f}}=a.value,{row:l,col:p}=M(f);return{horizontal:v(p),vertical:v(l)}})}},render(){const{vertical:e,align:n,inline:u,justify:a,itemStyle:g,margin:t,wrap:f,mergedClsPrefix:l,rtlEnabled:p,useGap:o,wrapItem:b,internalUseGap:w}=this,c=E(I(this));if(!c.length)return null;const x=`${t.horizontal}px`,m=`${t.horizontal/2}px`,$=`${t.vertical}px`,i=`${t.vertical/2}px`,s=c.length-1,d=a.startsWith("space-");return y("div",{role:"none",class:[`${l}-space`,p&&`${l}-space--rtl`],style:{display:u?"inline-flex":"flex",flexDirection:e?"column":"row",justifyContent:["start","end"].includes(a)?"flex-"+a:a,flexWrap:!f||e?"nowrap":"wrap",marginTop:o||e?"":`-${i}`,marginBottom:o||e?"":`-${i}`,alignItems:n,gap:o?`${t.vertical}px ${t.horizontal}px`:""}},!b&&(o||w)?c:c.map((z, r)=>y("div",{role:"none",style:[g,{maxWidth:"100%"},o?"":e?{marginBottom:r!==s?$:""}:p?{marginLeft:d?a==="space-between"&&r===s?"":m:r!==s?x:"",marginRight:d?a==="space-between"&&r===0?"":m:"",paddingTop:i,paddingBottom:i}:{marginRight:d?a==="space-between"&&r===s?"":m:r!==s?x:"",marginLeft:d?a==="space-between"&&r===0?"":m:"",paddingTop:i,paddingBottom:i}]},z)))}});export{H as N}; 2 | -------------------------------------------------------------------------------- /fsembed/dist/assets/TaskLang.vue_vue_type_script_setup_true_lang-adecfa66.js: -------------------------------------------------------------------------------- 1 | import{d as u,aR as d,B as a,D as l,a9 as _,J as t,E as r,X as o,U as i}from "./index-bbb52c5f.js";import{h as y,b as s}from "./Dropdown-cc67fbc4.js";const m=u({__name:"LangSelect",setup(n){const e=d([{value:"shell",label:"Shell"},{value:"api",label:"API"},{value:"python3",label:"Python3"},{value:"golang",label:"Golang"}]);return(p, c)=>(a(),l(t(y),_({options:e,placeholder:"请选择任务类型"},p.$attrs),null,16,["options"]))}}),g=u({__name:"TaskLang",props:{value:null},setup(n){const e=n;return(p, c)=>e.value==="api"?(a(),l(t(s),{key:0,type:"warning",bordered:!1},{default:r(()=>[o(" API ")]),_:1})):e.value==="shell"?(a(),l(t(s),{key:1,type:"primary",bordered:!1},{default:r(()=>[o(" Shell ")]),_:1})):e.value==="python"?(a(),l(t(s),{key:2,type:"primary",bordered:!1},{default:r(()=>[o(" Python ")]),_:1})):e.value==="golang"?(a(),l(t(s),{key:3,type:"primary",bordered:!1},{default:r(()=>[o(" Golang ")]),_:1})):(a(),l(t(s),{key:4,type:"info",bordered:!1},{default:r(()=>[o(i(e.value),1)]),_:1}))}});export{m as _,g as a}; 2 | -------------------------------------------------------------------------------- /fsembed/dist/assets/TaskLogPage-879ad244.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v-mars/jobor/dca6625b3560ddeebff83a14c90f0cac7b8688df/fsembed/dist/assets/TaskLogPage-879ad244.js.gz -------------------------------------------------------------------------------- /fsembed/dist/assets/TaskLogPage-a592a9a2.css: -------------------------------------------------------------------------------- 1 | .tb_task[data-v-c7df17e5]{width:98%;text-align:left;border-collapse:collapse;font-size:14px;color:#000000bf;line-height:1.1;word-break:break-all;white-space:nowrap;border-left:1px solid #e8e8e8;border-right:1px solid #e8e8e8;font-family:Microsoft YaHei UI,Microsoft YaHei,微软雅黑,SimSun,宋体,sans-serif,serif;margin:5px;border-top:1px solid #e8e8e8}.tb_task_caption[data-v-c7df17e5]{text-align:left;font-size:16px;font-weight:700;margin-bottom:10px}.tb_task_pre[data-v-c7df17e5]{width:95%;text-align:left;padding:8px 12px;margin:0 0 8px;font-size:.875rem;word-break:break-all;word-wrap:break-word;color:#333238;background-color:#fbfafd;border:1px solid #dcdcde;border-radius:2px}.gl-font-weight-bold[data-v-c7df17e5]{font-weight:600;text-align:left}.tb_task_caption[data-v-320ed8b2]{text-align:left;font-size:16px;font-weight:700;margin-bottom:10px} 2 | -------------------------------------------------------------------------------- /fsembed/dist/assets/TaskPage-5639496f.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v-mars/jobor/dca6625b3560ddeebff83a14c90f0cac7b8688df/fsembed/dist/assets/TaskPage-5639496f.js.gz -------------------------------------------------------------------------------- /fsembed/dist/assets/UserSelect.vue_vue_type_script_setup_true_lang-7bf54d99.js: -------------------------------------------------------------------------------- 1 | import{d as i,T as m,i as f,B as d,D as v,a9 as _,J as a,Z as g}from "./index-bbb52c5f.js";import{u as h}from "./useRequest-1c3afba9.js";import{h as k}from "./Dropdown-cc67fbc4.js";const b=i({__name:"UserSelect",props:{valueKey:{default:"id"}},setup(n){const l=n,{valueKey:u}=m(l),{data:o,loading:p}=h(g.getDolphinAllUser,{auto:!0}),c=f(()=>{var s,r;return o.value?(r=(s=o.value)==null?void 0:s.list)==null?void 0:r.filter(e=>e.status).map(e=>({label:`${e.nickname} | ${e.username} `,value:e[u.value]})):[]});return(t, s)=>(d(),v(a(k),_({loading:a(p),options:a(c),placeholder:"请选择用户",filterable:""},t.$attrs),null,16,["loading","options"]))}});export{b as _}; 2 | -------------------------------------------------------------------------------- /fsembed/dist/assets/WorkerPage-26a66a82.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v-mars/jobor/dca6625b3560ddeebff83a14c90f0cac7b8688df/fsembed/dist/assets/WorkerPage-26a66a82.js.gz -------------------------------------------------------------------------------- /fsembed/dist/assets/WorkerPage-55e137a1.css: -------------------------------------------------------------------------------- 1 | .auto-color[data-v-7010b788]{filter:grayscale(1) contrast(9999) invert(1)}.space[data-v-7010b788]{display:flex;align-items:center}.space>*[data-v-7010b788]:not(:last-child){margin-right:6px}.pagination[data-v-7010b788]{margin-top:8px;display:flex;justify-content:flex-end}.box-shadow[data-v-7010b788]{transition:box-shadow .3s;box-shadow:0 1px 2px -2px #0000001a,0 3px 6px #00000003,0 5px 12px 4px #0000000d}.box-shadow[data-v-7010b788]:hover{box-shadow:#aeaeae33 0 0 10px} 2 | -------------------------------------------------------------------------------- /fsembed/dist/assets/_plugin-vue_export-helper-c27b6911.js: -------------------------------------------------------------------------------- 1 | const s=(t,r)=>{const o=t.__vccOpts||t;for(const[c,e]of r)o[c]=e;return o};export{s as _}; 2 | -------------------------------------------------------------------------------- /fsembed/dist/assets/get-e68d4109.js: -------------------------------------------------------------------------------- 1 | import{ap as u,bS as f,bT as o,bU as p}from "./index-bbb52c5f.js";var h=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,y=/^\w*$/;function d(r, n){if(u(r))return!1;var e=typeof r;return e=="number"||e=="symbol"||e=="boolean"||r==null||f(r)?!0:y.test(r)||!h.test(r)||n!=null&&r in Object(n)}var m="Expected a function";function s(r, n){if(typeof r!="function"||n!=null&&typeof n!="function")throw new TypeError(m);var e=function(){var t=arguments,i=n?n.apply(this,t):t[0],a=e.cache;if(a.has(i))return a.get(i);var c=r.apply(this,t);return e.cache=a.set(i,c)||a,c};return e.cache=new(s.Cache||o),e}s.Cache=o;var g=500;function l(r){var n=s(r,function(t){return e.size===g&&e.clear(),t}),e=n.cache;return n}var E=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,C=/\\(\\)?/g,I=l(function(r){var n=[];return r.charCodeAt(0)===46&&n.push(""),r.replace(E,function(e, t, i, a){n.push(i?a.replace(C,"$1"):t||e)}),n});const P=I;function T(r, n){return u(r)?r:d(r,n)?[r]:P(p(r))}var w=1/0;function b(r){if(typeof r=="string"||f(r))return r;var n=r+"";return n=="0"&&1/r==-w?"-0":n}function z(r, n){n=T(n,r);for(var e=0,t=n.length; r!=null&&e{t.value=!0;const e=await c(...n);return o.value=n,e.result&&e.data?(s.value=e.data,u.value=void 0):(s.value=void 0,l.value=Number(e.code),u.value=String(e.message)),t.value=!1,e},f=()=>r(...o.value??[]);return m(d,()=>{i&&r(...v??[])},{immediate:!0,deep:!0}),{code:l,data:s,loading:t,error:u,run:r,refresh:f}}export{g as u}; 2 | -------------------------------------------------------------------------------- /fsembed/dist/assets/useTableData-66353f3c.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v-mars/jobor/dca6625b3560ddeebff83a14c90f0cac7b8688df/fsembed/dist/assets/useTableData-66353f3c.js.gz -------------------------------------------------------------------------------- /fsembed/dist/assets/validate-1d95410a.js: -------------------------------------------------------------------------------- 1 | function r(t=""){return/^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/.test(t)}function a(t=""){return/^https?:\/\//.test(t)}export{a,r as v}; 2 | -------------------------------------------------------------------------------- /fsembed/dist/assets/worker-2ff11057.js: -------------------------------------------------------------------------------- 1 | import{aj as e}from "./index-bbb52c5f.js";const k={getWorkerListAll(){return e("GET /jobor/workers",{params:{}},{cache:!0})},getWorkerById(r){return e(["GET /jobor/worker/:id",r],{})},getRoutingKeyList(){return e("GET /jobor/worker-routing-key",{})},getWorkerList(r){return e("GET /jobor/worker",{params:r})},createWorker(r){return e("POST /jobor/worker",{data:r})},editWorker(r, o){return e(["PUT /jobor/worker/:id",r],{data:o})},delWorker(r){return e(["DELETE /jobor/worker/:id",r])}};export{k as w}; 2 | -------------------------------------------------------------------------------- /fsembed/dist/iconfont.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v-mars/jobor/dca6625b3560ddeebff83a14c90f0cac7b8688df/fsembed/dist/iconfont.js.gz -------------------------------------------------------------------------------- /fsembed/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Jobor定时任务 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /fsembed/dist/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fsembed/fs.go: -------------------------------------------------------------------------------- 1 | package fsembed 2 | 3 | import "embed" 4 | 5 | //go:embed jobor3.sql 6 | var SqlFs embed.FS 7 | 8 | //go:embed dist/* 9 | //go:embed dist/assets/_* 10 | var DistFs embed.FS 11 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | pbapi "jobor/kitex_gen/pbapi" 5 | task "jobor/kitex_gen/task" 6 | ) 7 | 8 | // TaskServiceImpl implements the last service interface defined in the IDL. 9 | type TaskServiceImpl struct{} 10 | 11 | func (s *TaskServiceImpl) RunTask(req *task.TaskRequest, stream pbapi.TaskService_RunTaskServer) (err error) { 12 | println("RunTask called") 13 | return 14 | } 15 | -------------------------------------------------------------------------------- /idl/README.md: -------------------------------------------------------------------------------- 1 | ```text 2 | swagger: 3 | update_swag: 4 | swag init -pd true 5 | swag fmt 6 | #time swag init --parseDependency false --parseDepth 1 7 | 8 | init_api: 9 | hz new --model_dir biz/hertz_gen -mod github.com/cloudwego/hertz-examples/bizdemo/hertz_gorm -idl idl/api.thrift 10 | #hz new -I idl -idl idl/hello/hello.proto 11 | 12 | update_api: --unset_omitempty 13 | hz update --model_dir kitex_gen --unset_omitempty -idl idl/env.proto 14 | hz update --model_dir kitex_gen --unset_omitempty -idl idl/sys/kv.proto 15 | #hz update -I idl -idl idl/*/*/*.proto 16 | ``` -------------------------------------------------------------------------------- /idl/api.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | package api; 4 | 5 | import "google/protobuf/descriptor.proto"; 6 | 7 | option go_package = "/api"; 8 | 9 | extend google.protobuf.FieldOptions { 10 | optional string raw_body = 50101; 11 | optional string query = 50102; 12 | optional string header = 50103; 13 | optional string cookie = 50104; 14 | optional string body = 50105; 15 | optional string path = 50106; 16 | optional string vd = 50107; 17 | optional string form = 50108; 18 | optional string go_tag = 51001; 19 | optional string js_conv = 50109; 20 | } 21 | 22 | extend google.protobuf.MethodOptions { 23 | optional string get = 50201; 24 | optional string post = 50202; 25 | optional string put = 50203; 26 | optional string delete = 50204; 27 | optional string patch = 50205; 28 | optional string options = 50206; 29 | optional string head = 50207; 30 | optional string any = 50208; 31 | optional string gen_path = 50301; 32 | optional string api_version = 50302; 33 | optional string tag = 50303; 34 | optional string name = 50304; 35 | optional string api_level = 50305; 36 | optional string serializer = 50306; 37 | optional string param = 50307; 38 | optional string baseurl = 50308; 39 | } 40 | 41 | extend google.protobuf.EnumValueOptions { 42 | optional int32 http_code = 50401; 43 | } -------------------------------------------------------------------------------- /idl/audit.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package sys; 4 | 5 | option go_package = "audit"; 6 | 7 | import "api.proto"; 8 | 9 | message AuditReq { 10 | string User = 1[(api.query)="user",(api.body)="user"]; 11 | string Action = 2[(api.query)="action",(api.body)="action"]; 12 | string Method = 3[(api.query)="method",(api.body)="method"]; 13 | string UserIp = 4[(api.query)="user_ip",(api.body)="user_ip"]; 14 | optional int64 ObjId = 5[(api.query)="obj_id",(api.body)="obj_id"]; 15 | optional int64 HttpCode = 6[(api.query)="http_code",(api.body)="http_code"]; 16 | optional int64 UserId = 7[(api.query)="user_id",(api.body)="user_id"]; 17 | } 18 | 19 | message AuditResp { 20 | 21 | } 22 | 23 | // 登录历史记录和操作审计记录 24 | service AuditLog { 25 | rpc GetAuditLog(AuditReq) returns(AuditResp) { 26 | option (api.get) = "/api/v1/jobor/audit-log"; 27 | } 28 | } -------------------------------------------------------------------------------- /idl/base.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package base; 4 | 5 | option go_package = "base"; 6 | import "google/protobuf/struct.proto"; 7 | 8 | // base message 9 | message Empty {} 10 | 11 | message IDReq { 12 | uint64 ID = 1; 13 | } 14 | 15 | enum ErrCode { 16 | Success = 0; 17 | Fail = 1; 18 | } 19 | 20 | //message BaseResp { 21 | // string statusMessage = 1; 22 | // uint32 statusCode = 2; 23 | //} 24 | 25 | message PageInfoReq { 26 | uint64 page = 1; 27 | uint64 pageSize = 2; 28 | } 29 | 30 | message StatusCodeReq { 31 | uint64 ID = 1; 32 | uint64 status = 2; 33 | } 34 | 35 | message BaseResp { 36 | string message = 1; 37 | uint32 code = 2; 38 | string status = 3; 39 | } 40 | 41 | message BaseListReq { 42 | uint64 page = 1; 43 | uint64 pageSize = 2; 44 | optional BaseSearch condition = 3; 45 | } 46 | 47 | message BaseListResp { 48 | uint64 total = 1; 49 | google.protobuf.ListValue list = 2; 50 | } 51 | 52 | message BaseSearch { 53 | map condition = 1; 54 | } 55 | 56 | message BaseId { 57 | uint64 id = 1; 58 | } -------------------------------------------------------------------------------- /idl/dashboard.proto: -------------------------------------------------------------------------------- 1 | // idl/hello/hello.proto 2 | syntax = "proto3"; 3 | 4 | package dashboard; 5 | 6 | option go_package = "dashboard"; 7 | 8 | import "api.proto"; 9 | //import "base.proto"; 10 | //import "google/protobuf/struct.proto"; 11 | 12 | message dashboardQuery { 13 | int64 id = 1[(api.query)="id",(api.body)="id"]; 14 | string name = 2[(api.query)="name",(api.body)="name"]; 15 | repeated string range = 3[(api.query)="range",(api.body)="range"]; 16 | } 17 | 18 | message dashboardResp { 19 | int64 id = 1[(api.query)="id",(api.body)="id"]; 20 | string name = 2[(api.query)="name",(api.body)="name"]; 21 | string description = 3[(api.query)="description",(api.body)="description"]; 22 | } 23 | 24 | 25 | 26 | service DashboardService { 27 | rpc GetDashboard(dashboardQuery) returns(dashboardResp) { 28 | option (api.get) = "/api/v1/jobor/dashboard"; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /idl/kv.proto: -------------------------------------------------------------------------------- 1 | // idl/hello/hello.proto 2 | syntax = "proto3"; 3 | 4 | package kv; 5 | 6 | option go_package = "kv"; 7 | 8 | import "api.proto"; 9 | //import "base.proto"; 10 | //import "google/protobuf/struct.proto"; 11 | 12 | message KvQuery { 13 | int64 id = 1[(api.query)="id",(api.body)="id"]; 14 | string title = 2[(api.query)="title",(api.body)="title"]; 15 | string name = 3[(api.query)="name",(api.body)="name"]; 16 | string k = 4[(api.query)="k",(api.body)="k"]; 17 | string v = 5[(api.query)="v",(api.body)="v"]; 18 | uint64 page = 11; 19 | uint64 pageSize = 12; 20 | } 21 | 22 | message KvResp { 23 | int64 id = 1[(api.query)="id",(api.body)="id"]; 24 | string title = 2[(api.query)="title",(api.body)="title"]; 25 | string name = 3[(api.query)="name",(api.body)="name"]; 26 | string k = 4[(api.query)="k",(api.body)="k"]; 27 | string v = 5[(api.query)="v",(api.body)="v"]; 28 | string value = 6[(api.query)="value",(api.body)="value"]; 29 | } 30 | 31 | message KvAllResp { 32 | int64 id = 1[(api.query)="id",(api.body)="id"]; 33 | string title = 2[(api.query)="title",(api.body)="title"]; 34 | string name = 3[(api.query)="name",(api.body)="name"]; 35 | string k = 4[(api.query)="k",(api.body)="k"]; 36 | } 37 | 38 | message KvGetByIdResp { 39 | int64 id = 1[(api.query)="id"]; 40 | string title = 2[(api.query)="title",(api.body)="title"]; 41 | string name = 3[(api.query)="name",(api.body)="name"]; 42 | string k = 4[(api.query)="k",(api.body)="k"]; 43 | string v = 5[(api.query)="v",(api.body)="v"]; 44 | string value = 6[(api.query)="value",(api.body)="value"]; 45 | } 46 | 47 | 48 | message PostKvReq { 49 | string title = 2[(api.query)="title",(api.body)="title"]; 50 | string name = 3[(api.query)="name",(api.body)="name"]; 51 | string k = 4[(api.query)="k",(api.body)="k"]; 52 | string v = 5[(api.query)="v",(api.body)="v"]; 53 | string value = 6[(api.query)="value",(api.body)="value"]; 54 | } 55 | 56 | message PutKvReq { 57 | optional string title = 2[(api.query)="title",(api.body)="title,omitempty"]; 58 | optional string name = 3[(api.query)="name",(api.body)="name,omitempty"]; 59 | optional string k = 4[(api.query)="k",(api.body)="k,omitempty"]; 60 | optional string v = 5[(api.query)="v",(api.body)="v,omitempty"]; 61 | optional string value = 6[(api.query)="value",(api.body)="value,omitempty"]; 62 | } 63 | 64 | 65 | service KvService { 66 | rpc GetKvAll(KvQuery) returns(KvAllResp) { 67 | option (api.get) = "/api/v1/jobor/kvs"; 68 | } 69 | rpc GetKvById(KvQuery) returns(KvResp) { 70 | option (api.get) = "/api/v1/jobor/kv/:id"; 71 | } 72 | rpc GetKv(KvQuery) returns(KvResp) { 73 | option (api.get) = "/api/v1/jobor/kv"; 74 | } 75 | rpc PostKv(PostKvReq) returns(KvResp) { 76 | option (api.post) = "/api/v1/jobor/kv"; 77 | } 78 | rpc PutKv(PutKvReq) returns(KvResp) { 79 | option (api.put) = "/api/v1/jobor/kv/:id"; 80 | } 81 | rpc DeleteKv(KvQuery) returns(KvResp) { 82 | option (api.delete) = "/api/v1/jobor/kv/:id"; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /idl/role.proto: -------------------------------------------------------------------------------- 1 | // idl/sys/role.proto 2 | syntax = "proto3"; 3 | 4 | package role; 5 | 6 | option go_package = "role"; 7 | 8 | import "api.proto"; 9 | import "google/protobuf/struct.proto"; 10 | 11 | message RoleAll { 12 | int64 id = 1[(api.query)="id",(api.body)="id"]; 13 | string name = 2[(api.query)="name",(api.body)="name"]; 14 | string title = 3[(api.query)="title",(api.body)="title"]; 15 | } 16 | 17 | message RoleQuery { 18 | string name = 2[(api.query)="name",(api.body)="name"]; 19 | string title = 3[(api.query)="title",(api.body)="title"]; 20 | string client_name = 4[(api.query)="client_name",(api.body)="client_name"]; 21 | } 22 | 23 | message RolePost { 24 | string name = 2[(api.query)="name",(api.body)="name,required",(api.vd)="len($)>1"]; 25 | string title = 3[(api.query)="title",(api.body)="title,required"]; 26 | string description = 4[(api.query)="description",(api.body)="description"]; 27 | repeated int64 api_ids = 7[(api.query)="api_ids,omitempty",(api.body)="api_ids,omitempty"]; 28 | int64 parent_id = 8[(api.query)="parent_id",(api.body)="parent_id"]; 29 | // string Path = 9[(api.query)="path",(api.body)="path"]; 30 | google.protobuf.ListValue path = 11[(api.query)="path,omitempty",(api.body)="path,omitempty",(api.go_tag)="gorm:\"-\" ignore:\"yes\""]; 31 | int64 sort = 10[(api.query)="sort",(api.body)="sort"]; 32 | } 33 | 34 | message RolePut { 35 | optional string title = 3[(api.query)="title",(api.body)="title,omitempty"]; 36 | optional string description = 4[(api.query)="description",(api.body)="description,omitempty"]; 37 | google.protobuf.ListValue path = 11[(api.query)="path,omitempty",(api.body)="path,omitempty",(api.go_tag)=""]; 38 | google.protobuf.ListValue api_ids = 7[(api.query)="api_ids,omitempty",(api.body)="api_ids,omitempty",(api.go_tag)="gorm:\"-\" ignore:\"yes\""]; 39 | // repeated int64 ApiIds = 7[(api.query)="api_ids,omitempty",(api.body)="api_ids,omitempty",(api.go_tag)="gorm:\"-\" ignore:\"yes\""]; 40 | optional int64 parent_id = 8[(api.query)="parent_id",(api.body)="parent_id,omitempty"]; 41 | optional int64 sort = 10[(api.query)="sort",(api.body)="sort,omitempty"]; 42 | optional string updater = 60[(api.query)="-",(api.body)="-"]; 43 | // repeated Api Apis = 7[(api.query)="apis",(api.body)="apis"]; 44 | } 45 | 46 | message RoleDel { 47 | 48 | } 49 | 50 | message RoleResp { 51 | 52 | } 53 | 54 | service Role { 55 | rpc GetRoleAll(RoleQuery) returns(RoleResp) { 56 | option (api.get) = "/api/v1/sys/roles"; 57 | } 58 | rpc GetRoleTree(RoleQuery) returns(RoleResp) { 59 | option (api.get) = "/api/v1/sys/role-tree"; 60 | } 61 | rpc GetRole(RoleQuery) returns(RoleResp) { 62 | option (api.get) = "/api/v1/sys/role"; 63 | } 64 | rpc PostRole(RolePost) returns(RoleResp) { 65 | option (api.post) = "/api/v1/sys/role"; 66 | } 67 | rpc PutRole(RolePut) returns(RoleResp) { 68 | option (api.put) = "/api/v1/sys/role/:id"; 69 | } 70 | rpc DeleteRole(RoleDel) returns(RoleResp) { 71 | option (api.delete) = "/api/v1/sys/role/:id"; 72 | } 73 | } -------------------------------------------------------------------------------- /idl/rpc.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package srv_rpc; 4 | // kitex -module sso -service sso -I idl -record idl/rpc.proto 5 | // The greeting service definition. 6 | option go_package = "pbapi"; 7 | //option go_package = "kitex_gen/pbapi"; 8 | 9 | //import "base.proto"; 10 | //import "sys/user.proto"; 11 | 12 | 13 | message Request { 14 | string message = 1; 15 | } 16 | 17 | message GetUserReq { 18 | string username = 1; 19 | } 20 | message GetUserByIdReq { 21 | string id = 2; 22 | } 23 | message UserListResp { 24 | int64 page = 1; 25 | int64 pageSize = 2; 26 | int64 total = 3; 27 | // repeated user.UserInfo list =4; 28 | } 29 | 30 | service Sso { 31 | // rpc Demo(base.Empty) returns(base.Empty){} 32 | // rpc UserList(user.UserQuery) returns(UserListResp){} 33 | } -------------------------------------------------------------------------------- /idl/srv_rpc.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package srv_rpc; 4 | // kitex -module jobor -service jobor_rpc -no-fast-api -I idl -record idl/srv_rpc.proto 5 | // The greeting service definition. 6 | option go_package = "pbapi"; 7 | //option go_package = "kitex_gen/pbapi"; 8 | 9 | //import "base.proto"; 10 | 11 | 12 | 13 | // worker send Heartbeat to master server 14 | service Heartbeat { 15 | // registry worker 16 | rpc RegistryWorker(RegistryReq) returns (Empty) {}; 17 | // SendHeartbeat send to server req to itself alive 18 | rpc SendHeartbeat(HeartbeatReq) returns (Empty) {}; 19 | } 20 | 21 | message RegistryReq { 22 | string ip = 1; 23 | int32 port = 2; 24 | int32 weight = 3; 25 | string hostname = 4; 26 | string version = 5; 27 | string hostGroup = 6; 28 | string remark = 7; 29 | string routingKey = 8; 30 | } 31 | 32 | message HeartbeatReq { 33 | string ip = 1; 34 | int32 port = 2; 35 | repeated string running_task = 3; 36 | } 37 | 38 | message Empty {} 39 | 40 | 41 | -------------------------------------------------------------------------------- /idl/worker_rpc.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package srv_rpc; 4 | // kitex -module jobor -service jobor_rpc -no-fast-api -I idl -record idl/worker_rpc.proto 5 | // The greeting service definition. 6 | option go_package = "pbapi"; 7 | //option go_package = "kitex_gen/pbapi"; 8 | 9 | //import "base.proto"; 10 | import "task.proto"; 11 | 12 | 13 | message Request { 14 | string message = 1; 15 | } 16 | 17 | message GetUserReq { 18 | string username = 1; 19 | } 20 | message GetUserByIdReq { 21 | string id = 2; 22 | } 23 | message UserListResp { 24 | int64 page = 1; 25 | int64 pageSize = 2; 26 | int64 total = 3; 27 | // repeated user.UserInfo list =4; 28 | } 29 | 30 | //stream请求结构 31 | message StreamReqData { 32 | string data = 1; 33 | } 34 | //stream返回结构 35 | message StreamResData { 36 | string data = 1; 37 | } 38 | 39 | // The response message 40 | message Reply { 41 | string message = 1; 42 | } 43 | 44 | message Response { 45 | enum StatusCode { 46 | UNDEFINED = 0; 47 | SUCCESS = 200; 48 | FAILURE = 500; 49 | } 50 | StatusCode code = 1; 51 | Reply msg = 2; 52 | Reply data = 3; 53 | } 54 | 55 | message StreamResponse { 56 | bytes resp = 1; 57 | } 58 | //// The request message containing the user's name. 59 | //message TaskRequest { 60 | // int64 task_id = 1; 61 | // int32 task_type = 2; 62 | // string task_lang = 3; 63 | // bytes task_data = 4; 64 | // string name = 5; 65 | //} 66 | 67 | service TaskService { 68 | rpc RunTask (task.TaskRequest) returns (stream StreamResponse) {}; 69 | // rpc Demo(base.Empty) returns(base.Empty){} 70 | // rpc GetStream (StreamReqData) returns (stream StreamResData){} //客户端推送 服务端 71 | // rpc PutStream (stream StreamReqData) returns (StreamResData){} //服务端推送 客户端 72 | // rpc AllStream (stream StreamReqData) returns (stream StreamResData){} //客户端与 服务端 互相 推送 73 | } 74 | 75 | 76 | -------------------------------------------------------------------------------- /img/Wechatid.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v-mars/jobor/dca6625b3560ddeebff83a14c90f0cac7b8688df/img/Wechatid.jpeg -------------------------------------------------------------------------------- /img/dash3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v-mars/jobor/dca6625b3560ddeebff83a14c90f0cac7b8688df/img/dash3.png -------------------------------------------------------------------------------- /img/jobor-dash.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v-mars/jobor/dca6625b3560ddeebff83a14c90f0cac7b8688df/img/jobor-dash.jpeg -------------------------------------------------------------------------------- /img/log-detail3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v-mars/jobor/dca6625b3560ddeebff83a14c90f0cac7b8688df/img/log-detail3.png -------------------------------------------------------------------------------- /img/log3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v-mars/jobor/dca6625b3560ddeebff83a14c90f0cac7b8688df/img/log3.png -------------------------------------------------------------------------------- /img/notify-email.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v-mars/jobor/dca6625b3560ddeebff83a14c90f0cac7b8688df/img/notify-email.png -------------------------------------------------------------------------------- /img/struct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v-mars/jobor/dca6625b3560ddeebff83a14c90f0cac7b8688df/img/struct.png -------------------------------------------------------------------------------- /img/structv3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v-mars/jobor/dca6625b3560ddeebff83a14c90f0cac7b8688df/img/structv3.png -------------------------------------------------------------------------------- /img/task-list3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v-mars/jobor/dca6625b3560ddeebff83a14c90f0cac7b8688df/img/task-list3.png -------------------------------------------------------------------------------- /img/wechat.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v-mars/jobor/dca6625b3560ddeebff83a14c90f0cac7b8688df/img/wechat.jpeg -------------------------------------------------------------------------------- /img/worker3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v-mars/jobor/dca6625b3560ddeebff83a14c90f0cac7b8688df/img/worker3.png -------------------------------------------------------------------------------- /kitex_gen/pbapi/heartbeat/client.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.7.0. DO NOT EDIT. 2 | 3 | package heartbeat 4 | 5 | import ( 6 | "context" 7 | client "github.com/cloudwego/kitex/client" 8 | callopt "github.com/cloudwego/kitex/client/callopt" 9 | pbapi "jobor/kitex_gen/pbapi" 10 | ) 11 | 12 | // Client is designed to provide IDL-compatible methods with call-option parameter for kitex framework. 13 | type Client interface { 14 | RegistryWorker(ctx context.Context, Req *pbapi.RegistryReq, callOptions ...callopt.Option) (r *pbapi.Empty, err error) 15 | SendHeartbeat(ctx context.Context, Req *pbapi.HeartbeatReq, callOptions ...callopt.Option) (r *pbapi.Empty, err error) 16 | } 17 | 18 | // NewClient creates a client for the service defined in IDL. 19 | func NewClient(destService string, opts ...client.Option) (Client, error) { 20 | var options []client.Option 21 | options = append(options, client.WithDestService(destService)) 22 | 23 | options = append(options, opts...) 24 | 25 | kc, err := client.NewClient(serviceInfo(), options...) 26 | if err != nil { 27 | return nil, err 28 | } 29 | return &kHeartbeatClient{ 30 | kClient: newServiceClient(kc), 31 | }, nil 32 | } 33 | 34 | // MustNewClient creates a client for the service defined in IDL. It panics if any error occurs. 35 | func MustNewClient(destService string, opts ...client.Option) Client { 36 | kc, err := NewClient(destService, opts...) 37 | if err != nil { 38 | panic(err) 39 | } 40 | return kc 41 | } 42 | 43 | type kHeartbeatClient struct { 44 | *kClient 45 | } 46 | 47 | func (p *kHeartbeatClient) RegistryWorker(ctx context.Context, Req *pbapi.RegistryReq, callOptions ...callopt.Option) (r *pbapi.Empty, err error) { 48 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 49 | return p.kClient.RegistryWorker(ctx, Req) 50 | } 51 | 52 | func (p *kHeartbeatClient) SendHeartbeat(ctx context.Context, Req *pbapi.HeartbeatReq, callOptions ...callopt.Option) (r *pbapi.Empty, err error) { 53 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 54 | return p.kClient.SendHeartbeat(ctx, Req) 55 | } 56 | -------------------------------------------------------------------------------- /kitex_gen/pbapi/heartbeat/invoker.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.7.0. DO NOT EDIT. 2 | 3 | package heartbeat 4 | 5 | import ( 6 | server "github.com/cloudwego/kitex/server" 7 | pbapi "jobor/kitex_gen/pbapi" 8 | ) 9 | 10 | // NewInvoker creates a server.Invoker with the given handler and options. 11 | func NewInvoker(handler pbapi.Heartbeat, opts ...server.Option) server.Invoker { 12 | var options []server.Option 13 | 14 | options = append(options, opts...) 15 | 16 | s := server.NewInvoker(options...) 17 | if err := s.RegisterService(serviceInfo(), handler); err != nil { 18 | panic(err) 19 | } 20 | if err := s.Init(); err != nil { 21 | panic(err) 22 | } 23 | return s 24 | } 25 | -------------------------------------------------------------------------------- /kitex_gen/pbapi/heartbeat/server.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.7.0. DO NOT EDIT. 2 | package heartbeat 3 | 4 | import ( 5 | server "github.com/cloudwego/kitex/server" 6 | pbapi "jobor/kitex_gen/pbapi" 7 | ) 8 | 9 | // NewServer creates a server.Server with the given handler and options. 10 | func NewServer(handler pbapi.Heartbeat, opts ...server.Option) server.Server { 11 | var options []server.Option 12 | 13 | options = append(options, opts...) 14 | 15 | svr := server.NewServer(options...) 16 | if err := svr.RegisterService(serviceInfo(), handler); err != nil { 17 | panic(err) 18 | } 19 | return svr 20 | } 21 | -------------------------------------------------------------------------------- /kitex_gen/pbapi/taskservice/client.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.7.0. DO NOT EDIT. 2 | 3 | package taskservice 4 | 5 | import ( 6 | "context" 7 | client "github.com/cloudwego/kitex/client" 8 | callopt "github.com/cloudwego/kitex/client/callopt" 9 | streaming "github.com/cloudwego/kitex/pkg/streaming" 10 | transport "github.com/cloudwego/kitex/transport" 11 | pbapi "jobor/kitex_gen/pbapi" 12 | task "jobor/kitex_gen/task" 13 | ) 14 | 15 | // Client is designed to provide IDL-compatible methods with call-option parameter for kitex framework. 16 | type Client interface { 17 | RunTask(ctx context.Context, Req *task.TaskRequest, callOptions ...callopt.Option) (stream TaskService_RunTaskClient, err error) 18 | } 19 | 20 | type TaskService_RunTaskClient interface { 21 | streaming.Stream 22 | Recv() (*pbapi.StreamResponse, error) 23 | } 24 | 25 | // NewClient creates a client for the service defined in IDL. 26 | func NewClient(destService string, opts ...client.Option) (Client, error) { 27 | var options []client.Option 28 | options = append(options, client.WithDestService(destService)) 29 | 30 | options = append(options, client.WithTransportProtocol(transport.GRPC)) 31 | 32 | options = append(options, opts...) 33 | 34 | kc, err := client.NewClient(serviceInfo(), options...) 35 | if err != nil { 36 | return nil, err 37 | } 38 | return &kTaskServiceClient{ 39 | kClient: newServiceClient(kc), 40 | }, nil 41 | } 42 | 43 | // MustNewClient creates a client for the service defined in IDL. It panics if any error occurs. 44 | func MustNewClient(destService string, opts ...client.Option) Client { 45 | kc, err := NewClient(destService, opts...) 46 | if err != nil { 47 | panic(err) 48 | } 49 | return kc 50 | } 51 | 52 | type kTaskServiceClient struct { 53 | *kClient 54 | } 55 | 56 | func (p *kTaskServiceClient) RunTask(ctx context.Context, Req *task.TaskRequest, callOptions ...callopt.Option) (stream TaskService_RunTaskClient, err error) { 57 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 58 | return p.kClient.RunTask(ctx, Req) 59 | } 60 | -------------------------------------------------------------------------------- /kitex_gen/pbapi/taskservice/invoker.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.7.0. DO NOT EDIT. 2 | 3 | package taskservice 4 | 5 | import ( 6 | server "github.com/cloudwego/kitex/server" 7 | pbapi "jobor/kitex_gen/pbapi" 8 | ) 9 | 10 | // NewInvoker creates a server.Invoker with the given handler and options. 11 | func NewInvoker(handler pbapi.TaskService, opts ...server.Option) server.Invoker { 12 | var options []server.Option 13 | 14 | options = append(options, opts...) 15 | 16 | s := server.NewInvoker(options...) 17 | if err := s.RegisterService(serviceInfo(), handler); err != nil { 18 | panic(err) 19 | } 20 | if err := s.Init(); err != nil { 21 | panic(err) 22 | } 23 | return s 24 | } 25 | -------------------------------------------------------------------------------- /kitex_gen/pbapi/taskservice/server.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.7.0. DO NOT EDIT. 2 | package taskservice 3 | 4 | import ( 5 | server "github.com/cloudwego/kitex/server" 6 | pbapi "jobor/kitex_gen/pbapi" 7 | ) 8 | 9 | // NewServer creates a server.Server with the given handler and options. 10 | func NewServer(handler pbapi.TaskService, opts ...server.Option) server.Server { 11 | var options []server.Option 12 | 13 | options = append(options, opts...) 14 | 15 | svr := server.NewServer(options...) 16 | if err := svr.RegisterService(serviceInfo(), handler); err != nil { 17 | panic(err) 18 | } 19 | return svr 20 | } 21 | -------------------------------------------------------------------------------- /kitex_gen/role/index.go: -------------------------------------------------------------------------------- 1 | package role 2 | 3 | func (p *RolePut) GetApiIdsInt() []int { 4 | var arrayInt = make([]int, 0) 5 | if p.ApiIds != nil { 6 | for _, v := range p.ApiIds.GetValues() { 7 | v := v 8 | arrayInt = append(arrayInt, int(v.GetNumberValue())) 9 | } 10 | } 11 | return arrayInt 12 | } 13 | func (p *RolePut) GetPathInt() []int { 14 | var arrayInt = make([]int, 0) 15 | if p.Path != nil { 16 | for _, v := range p.Path.GetValues() { 17 | v := v 18 | arrayInt = append(arrayInt, int(v.GetNumberValue())) 19 | } 20 | } 21 | return arrayInt 22 | } 23 | -------------------------------------------------------------------------------- /kitex_gen/user/index.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "encoding/json" 5 | "jobor/pkg/utils" 6 | ) 7 | 8 | func (i *Userinfo) IsAdmin() bool { 9 | if utils.InOfStr("admin", i.Roles) || i.Username == "root" { 10 | return true 11 | } else { 12 | return false 13 | } 14 | } 15 | func (i *Userinfo) IsTaskAdmin() bool { 16 | if utils.InOfStr("task_rw", i.Roles) { 17 | return true 18 | } else { 19 | return false 20 | } 21 | } 22 | 23 | func (i *Userinfo) IsDevOps() bool { 24 | if utils.InOfStr("devops", i.Roles) { 25 | return true 26 | } else { 27 | return false 28 | } 29 | } 30 | 31 | func (i *Userinfo) IsOps() bool { 32 | if utils.InOfStr("ops", i.Roles) { 33 | return true 34 | } else { 35 | return false 36 | } 37 | } 38 | 39 | func (i *Userinfo) IsQa() bool { 40 | if utils.InOfStr("qa", i.Roles) { 41 | return true 42 | } else { 43 | return false 44 | } 45 | } 46 | 47 | func (i *Userinfo) IsDeveloper() bool { 48 | if utils.InOfStr("developer", i.Roles) { 49 | return true 50 | } else { 51 | return false 52 | } 53 | } 54 | 55 | func (i *Userinfo) JsonString() string { 56 | marshal, err := json.Marshal(i) 57 | if err != nil { 58 | panic(err) 59 | } 60 | return string(marshal) 61 | } 62 | 63 | func (i *Userinfo) Map() (mapRes map[string]interface{}) { 64 | err := utils.AnyToAny(i, &mapRes) 65 | if err != nil { 66 | panic(err) 67 | } 68 | return mapRes 69 | } 70 | 71 | func (p *PutUserReq) GetRoleIdsInt() []int { 72 | var arrayInt = make([]int, 0) 73 | if p.RoleIds != nil { 74 | for _, v := range p.RoleIds.GetValues() { 75 | v := v 76 | arrayInt = append(arrayInt, int(v.GetNumberValue())) 77 | } 78 | } 79 | return arrayInt 80 | } 81 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Code generated by hertz generator. 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | Cmd "jobor/cmd" 8 | "jobor/cmd/srv_http" 9 | "os" 10 | ) 11 | 12 | // @title Jobor 定时任务 API 13 | // @version 3.0 14 | // @description 支持host,server,network等 15 | // @description Authorization Bearer token 16 | // @description header: x-enc-data = yes 17 | 18 | // @securityDefinitions.apikey ApiKeyAuth 19 | // @in header 20 | // @name Authorization 21 | // @schema Bearer 22 | 23 | // @contact.name jobor 24 | // @contact.url https://github.com/v-mars/jobor 25 | // 26 | // //@host localhost:5002 27 | // 28 | // // @BasePath / 29 | // 30 | // //@schemes http 31 | func main() { 32 | //dal.Init() 33 | 34 | srv_http.RootCmd.AddCommand(Cmd.Version()) 35 | if err := srv_http.RootCmd.Execute(); err != nil { 36 | _ = fmt.Errorf("rootCmd.Execute failed %s", err.Error()) 37 | os.Exit(-1) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /pkg/convert/convert.go: -------------------------------------------------------------------------------- 1 | package convert 2 | 3 | import ( 4 | "encoding/binary" 5 | ) 6 | 7 | // Int64ToBytes int64 转 byte 8 | func Int64ToBytes(i int64) []byte { 9 | var buf = make([]byte, 8) 10 | binary.BigEndian.PutUint64(buf, uint64(i)) 11 | return buf 12 | } 13 | 14 | // BytesToInt64 byte 转 int64 15 | func BytesToInt64(buf []byte) int64 { 16 | return int64(binary.BigEndian.Uint64(buf)) 17 | } 18 | -------------------------------------------------------------------------------- /pkg/convert/structToJson.go: -------------------------------------------------------------------------------- 1 | package convert 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | ) 7 | 8 | // StructToJsonBytes struct 转成json防止特殊字符转义 9 | func StructToJsonBytes(src interface{}) (*bytes.Buffer, error) { 10 | bf := bytes.NewBuffer([]byte{}) 11 | jE := json.NewEncoder(bf) 12 | if err := jE.Encode(src); err != nil { 13 | return bf, err 14 | } 15 | return bf, nil 16 | } 17 | -------------------------------------------------------------------------------- /pkg/convert/time.go: -------------------------------------------------------------------------------- 1 | package convert 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | func TimeToString(t time.Time) (string, error) { 9 | //loc, _ := time.LoadLocation("PRC") 10 | //t, _ := time.ParseInLocation("2006-01-02 15:04:05", "2018-05-31 09:22:19", loc) 11 | //fmt.Println(t) 12 | //fmt.Sprintln(t.Format("2006-01-02 15:04:05")) 13 | return fmt.Sprintln(t.Format("2006-01-02 15:04:05")), nil 14 | } 15 | 16 | // 输出: 17 | // 2018-05-31 09:22:19 +0800 CST 18 | // 2018-05-31 09:22:19 19 | 20 | //var cstSh, _ = time.LoadLocation("Asia/Shanghai") 21 | //var a = pods.Items[i].PodStatus.StartTime.In(cstSh) 22 | 23 | func NowTimeString() string { 24 | return time.Now().Format("2006-01-02 15:04:05") 25 | } 26 | 27 | func GetNowTimeNoFormatStr() string { 28 | return time.Now().Format("20060102150405") 29 | } 30 | -------------------------------------------------------------------------------- /pkg/file_folder/folder.go: -------------------------------------------------------------------------------- 1 | package file_folder 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | ) 7 | 8 | func CreateDir(basePath, folderName string) (dirPath, dataString string) { 9 | folderPath := filepath.Join(basePath, folderName) 10 | if _, err := os.Stat(folderPath); os.IsNotExist(err) { 11 | // 必须分成两步 12 | // 先创建文件夹 13 | if err = os.Mkdir(folderPath, 0755); err != nil { 14 | panic(any(err)) 15 | } 16 | // 再修改权限 17 | //err = os.Chmod(folderPath, 0777) 18 | //if err != nil { 19 | // return "", "" 20 | //} 21 | } 22 | return folderPath, folderName 23 | } 24 | -------------------------------------------------------------------------------- /pkg/file_folder/utils_test.go: -------------------------------------------------------------------------------- 1 | package file_folder 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestKeep(t *testing.T) { 9 | err := KeepLastedFolder("./fss", 1, 3) 10 | if err != nil { 11 | fmt.Println("err:", err) 12 | return 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /pkg/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "os" 7 | ) 8 | 9 | 10 | var ( 11 | Info *log.Logger 12 | Warning *log.Logger 13 | Error *log.Logger 14 | ) 15 | 16 | type Logger struct { 17 | Info *log.Logger 18 | Warning *log.Logger 19 | Error *log.Logger 20 | } 21 | 22 | func Init(){ 23 | //errFile,err:=os.OpenFile("app.log",os.O_CREATE|os.O_WRONLY|os.O_APPEND,0666) 24 | logPath := "./logs" 25 | 26 | logFile,err:=os.OpenFile(logPath+"/app.log",os.O_CREATE|os.O_WRONLY|os.O_APPEND,os.ModePerm) 27 | errFile,err:=os.OpenFile(logPath+"/app_error.log",os.O_CREATE|os.O_WRONLY|os.O_APPEND,os.ModePerm) 28 | if err!=nil{ 29 | log.Fatalln("打开日志文件失败:",err) 30 | } 31 | 32 | Info = log.New(io.MultiWriter(os.Stdout, logFile),"[INFO] ",log.Ldate | log.Ltime | log.Lshortfile) 33 | Warning = log.New(io.MultiWriter(os.Stdout, logFile),"[WARNING] ",log.Ldate | log.Ltime | log.Lshortfile) 34 | Error = log.New(io.MultiWriter(os.Stderr,logFile, errFile),"[ERROR] ",log.Ldate | log.Ltime | log.Lshortfile) 35 | 36 | } 37 | 38 | //Info.Println("ocean:","http://www.flysnow.org") 39 | //Warning.Printf("ocean:%s\n","flysnow_org") 40 | //Error.Println("欢迎关注留言") 41 | -------------------------------------------------------------------------------- /pkg/log/logrus.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "github.com/lestrrat-go/file-rotatelogs" 6 | "github.com/rifflock/lfshook" 7 | log "github.com/sirupsen/logrus" 8 | "reflect" 9 | "time" 10 | ) 11 | 12 | 13 | 14 | func Rotate(logName string, maxRemainCnt uint) (*rotatelogs.RotateLogs, error) { // (rotatelogs.Clock, error) 15 | writer, err := rotatelogs.New( 16 | logName+".%Y%m%d%H", 17 | // WithLinkName为最新的日志建立软连接,以方便随着找到当前日志文件 18 | rotatelogs.WithLinkName(logName), 19 | 20 | // WithRotationTime设置日志分割的时间,这里设置为一小时分割一次 21 | rotatelogs.WithRotationTime(time.Second*60), 22 | 23 | // WithMaxAge和WithRotationCount二者只能设置一个, 24 | // WithMaxAge设置文件清理前的最长保存时间, 25 | // WithRotationCount设置文件清理前最多保存的个数。 26 | //rotatelogs.WithMaxAge(time.Hour*24), 27 | rotatelogs.WithRotationCount(maxRemainCnt), 28 | ) 29 | if err != nil{ 30 | return nil, err 31 | } 32 | fmt.Println(reflect.TypeOf(writer)) 33 | return writer, nil 34 | } 35 | 36 | 37 | func newLfsHook(logLevel *string, maxRemainCnt uint) log.Hook { 38 | logName := "app" 39 | writer, err := rotatelogs.New( 40 | logName+".%Y%m%d%H", 41 | // WithLinkName为最新的日志建立软连接,以方便随着找到当前日志文件 42 | rotatelogs.WithLinkName(logName), 43 | 44 | // WithRotationTime设置日志分割的时间,这里设置为一小时分割一次 45 | rotatelogs.WithRotationTime(time.Second*60), 46 | 47 | // WithMaxAge和WithRotationCount二者只能设置一个, 48 | // WithMaxAge设置文件清理前的最长保存时间, 49 | // WithRotationCount设置文件清理前最多保存的个数。 50 | //rotatelogs.WithMaxAge(time.Hour*24), 51 | rotatelogs.WithRotationCount(maxRemainCnt), 52 | ) 53 | 54 | if err != nil { 55 | log.Errorf("config local file system for logger error: %v", err) 56 | } 57 | 58 | //level, ok := logLevels[*logLevel] 59 | 60 | //if ok { 61 | // log.SetLevel(level) 62 | //} else { 63 | // log.SetLevel(log.WarnLevel) 64 | //} 65 | 66 | lfsHook := lfshook.NewHook(lfshook.WriterMap{ 67 | log.DebugLevel: writer, 68 | log.InfoLevel: writer, 69 | log.WarnLevel: writer, 70 | log.ErrorLevel: writer, 71 | log.FatalLevel: writer, 72 | log.PanicLevel: writer, 73 | }, &log.TextFormatter{DisableColors: true}) 74 | 75 | return lfsHook 76 | } -------------------------------------------------------------------------------- /pkg/logger/formatter.go: -------------------------------------------------------------------------------- 1 | /* 2 | Using https://github.com/t-tomalak/logrus-easy-formatter/ as formatter 3 | */ 4 | 5 | package logger 6 | 7 | import ( 8 | "strings" 9 | "time" 10 | 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | const ( 15 | // Default log format will output [INFO]: 2006-01-02T15:04:05Z07:00 - Log message 16 | defaultLogFormat = "[%lvl%] %time% - %msg%" 17 | defaultTimestampFormat = time.RFC3339 18 | ) 19 | 20 | // Formatter implements logrus.Formatter interface. 21 | type Formatter struct { 22 | // Timestamp format 23 | TimestampFormat string 24 | // Available standard keys: time, msg, lvl 25 | // Also can include custom fields but limited to strings. 26 | // All of fields need to be wrapped inside %% i.e %time% %msg% 27 | LogFormat string 28 | 29 | // Disables the truncation of the level text to 4 characters. 30 | DisableLevelTruncation bool 31 | } 32 | 33 | // Format building log message. 34 | func (f *Formatter) Format(entry *logrus.Entry) ([]byte, error) { 35 | output := f.LogFormat 36 | if output == "" { 37 | output = defaultLogFormat 38 | } 39 | 40 | timestampFormat := f.TimestampFormat 41 | if timestampFormat == "" { 42 | timestampFormat = defaultTimestampFormat 43 | } 44 | 45 | output = strings.Replace(output, "%time%", entry.Time.Format(timestampFormat), 1) 46 | output = strings.Replace(output, "%msg%", entry.Message, 1) 47 | level := strings.ToUpper(entry.Level.String()) 48 | if !f.DisableLevelTruncation { 49 | //level = level[:4] 50 | } 51 | output = strings.Replace(output, "%lvl%", level, 1) 52 | 53 | for k, v := range entry.Data { 54 | if s, ok := v.(string); ok { 55 | output = strings.Replace(output, "%"+k+"%", s, 1) 56 | } 57 | } 58 | output += "\n" 59 | 60 | return []byte(output), nil 61 | } 62 | -------------------------------------------------------------------------------- /pkg/logger/rotate.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/sirupsen/logrus" 7 | "gopkg.in/natefinch/lumberjack.v2" 8 | ) 9 | 10 | type RotateFileConfig struct { 11 | Filename string 12 | MaxSize int 13 | MaxBackups int 14 | MaxAge int 15 | Level logrus.Level 16 | LocalTime bool 17 | Formatter logrus.Formatter 18 | } 19 | 20 | type RotateFileHook struct { 21 | Config RotateFileConfig 22 | logWriter io.Writer 23 | } 24 | 25 | func NewRotateFileHook(config RotateFileConfig) (logrus.Hook, error) { 26 | hook := RotateFileHook{ 27 | Config: config, 28 | } 29 | hook.logWriter = &lumberjack.Logger{ 30 | Filename: config.Filename, 31 | MaxSize: config.MaxSize, 32 | MaxBackups: config.MaxBackups, 33 | MaxAge: config.MaxAge, 34 | LocalTime: config.LocalTime, 35 | } 36 | 37 | return &hook, nil 38 | } 39 | 40 | func (hook *RotateFileHook) Levels() []logrus.Level { 41 | return logrus.AllLevels[:hook.Config.Level+1] 42 | } 43 | 44 | func (hook *RotateFileHook) Fire(entry *logrus.Entry) (err error) { 45 | b, err := hook.Config.Formatter.Format(entry) 46 | if err != nil { 47 | return err 48 | } 49 | _, err = hook.logWriter.Write(b) 50 | return 51 | } 52 | -------------------------------------------------------------------------------- /pkg/notify/README.md: -------------------------------------------------------------------------------- 1 | ### Notify 2 | 3 | 封装了钉钉,邮件,slack,telegram,企业微信的消息通知组件 -------------------------------------------------------------------------------- /pkg/notify/dingding/README.md: -------------------------------------------------------------------------------- 1 | ### 如何使用钉钉群的机器人来接收通知 2 | - 添加群机器人 3 | 点击 **钉钉群设置**->**智能群助手**->**添加机器人**,选择**自定义**,然后点击**添加**, 4 | 填写**机器人名称**,**安全设置**,然后选择安全设置,有三类自定义关键词、加签、IP地址,任选一项然后点击完成 5 | 得到一个Webhook, 6 | - 初始化 7 | 使用`NewDing`来进行初始化,第一个参数就是这个Webhook url,第二个参数是安全设置,`CustomKey`是对应的的自定义关键字,`Sign`是加签,`IPCidr`是IP地址,注意如果选择了安全设置,这时会得到一个签名,然后把这个签名作为第三个参数传入,如果安全设置不是选择的加签,则第三个参数为空字符串即可。注意如果想在消息通知中@某个人则需要把这个人加入群众并且tos参数要穿入此人注册钉钉的手机号 8 | 9 | ### Example 10 | ```go 11 | var ( 12 | secret := "SEC..." 13 | webhook := "https://oapi.dingtalk.com/robot/send?access_token=..." 14 | ) 15 | func SendDing() { 16 | ding := NewDing(webhook, Sign, secret) 17 | err := ding.Send([]string{"..."}, "测试标题", "测试内容") 18 | 19 | if err != nil { 20 | t.Error(err) 21 | } 22 | } 23 | ``` -------------------------------------------------------------------------------- /pkg/notify/dingding/ding_test.go: -------------------------------------------------------------------------------- 1 | package dingding 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestNewDing(t *testing.T) { 9 | notify := NewDing( 10 | "https://oapi.dingtalk.com/robot/send?access_token=xxx", 11 | 2, 12 | "xxx") 13 | err := notify.Send([]string{}, "dingtitle", "test dingding") 14 | if err != nil { 15 | fmt.Println("err:", err) 16 | return 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /pkg/notify/dingding/dingding.go: -------------------------------------------------------------------------------- 1 | package dingding 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "encoding/json" 8 | "fmt" 9 | "jobor/pkg/notify" 10 | "net/http" 11 | "net/url" 12 | "strconv" 13 | "time" 14 | ) 15 | 16 | // getsign generate a sign when secure level is needsign 17 | func getsign(secret string, now string) string { 18 | signstr := now + "\n" + secret 19 | // HmacSHA256 20 | h := hmac.New(sha256.New, []byte(secret)) 21 | _, _ = h.Write([]byte(signstr)) 22 | hm := h.Sum(nil) 23 | // Base64 encode 24 | b := base64.StdEncoding.EncodeToString(hm) 25 | // urlEncode 26 | sign := url.QueryEscape(b) 27 | return sign 28 | } 29 | 30 | // Secrue dingding secrue setting 31 | // pls reading https://ding-doc.dingtalk.com/doc#/serverapi2/qf2nxq 32 | type Secrue int 33 | 34 | const ( 35 | // CustomKey Custom keywords 36 | CustomKey Secrue = iota + 1 37 | // Sign need sign up 38 | Sign 39 | // IPCdir IP addres 40 | IPCdir 41 | ) 42 | 43 | // Ding dingding alarm conf 44 | type Ding struct { 45 | MsgType string // text 46 | webhookurl string 47 | sl Secrue 48 | secret string 49 | } 50 | 51 | // Result post resp 52 | type Result struct { 53 | ErrCode int `json:"errcode"` 54 | ErrMsg string `json:"errmsg"` 55 | } 56 | 57 | type text struct { 58 | Content string `json:"content"` 59 | } 60 | 61 | type at struct { 62 | AtMobiles []string `json:"atMobiles"` 63 | IsAtAll bool `json:"isAtAll"` 64 | } 65 | 66 | // SendMsg post json data 67 | type SendMsg struct { 68 | MsgType string `json:"msgtype"` 69 | Text text `json:"text"` 70 | At at `json:"at"` 71 | } 72 | 73 | // NewDing init a Dingding send conf 74 | func NewDing(webhookurl string, sl Secrue, secret string) notify.Sender { 75 | d := Ding{ 76 | webhookurl: webhookurl, 77 | MsgType: "text", 78 | sl: sl, 79 | secret: secret, 80 | } 81 | 82 | return &d 83 | } 84 | 85 | // Send to notify tos is phone number 86 | func (d *Ding) Send(tos []string, title string, content string) error { 87 | var reqUrl = d.webhookurl 88 | if d.sl == Sign && len(d.secret) > 0 { 89 | now := strconv.FormatInt(time.Now().UnixNano()/1e6, 10) 90 | sign := getsign(d.secret, now) 91 | reqUrl += fmt.Sprintf("×tamp=%s&sign=%s", now, sign) 92 | } 93 | sendMsg := SendMsg{ 94 | MsgType: "text", 95 | Text: text{ 96 | Content: title + "\n" + content + "\n", 97 | }, 98 | At: at{ 99 | AtMobiles: tos, 100 | IsAtAll: false, 101 | }, 102 | } 103 | 104 | resp, err := notify.JSONPost(http.MethodPost, reqUrl, sendMsg, http.DefaultClient) 105 | if err != nil { 106 | return err 107 | } 108 | res := Result{} 109 | err = json.Unmarshal(resp, &res) 110 | if err != nil { 111 | return err 112 | } 113 | if res.ErrCode != 0 { 114 | return fmt.Errorf("errmsg: %s errcode: %d", res.ErrMsg, res.ErrCode) 115 | } 116 | return nil 117 | } 118 | -------------------------------------------------------------------------------- /pkg/notify/email/README.md: -------------------------------------------------------------------------------- 1 | ### 邮箱报警 2 | 如使用自建邮件系统请设置 skipVerify 为 true 以避免证书校验错误 3 | 4 | ### Example 5 | - SendEmailByTls 6 | ```go 7 | func SendTLS() { 8 | address := "smtp.163.com" 9 | from := "...@163.com" 10 | password := "..." 11 | // if use qq 12 | // https://service.mail.qq.com/cgi-bin/help?subtype=1&&no=1001256&&id=28 13 | address = "smtp.qq.com" 14 | from = "...@qq.com" 15 | password = "..." 16 | 17 | to := "to1@email.com" 18 | subject:= "" 19 | body:="" 20 | 21 | tls := true 22 | anonymous := false 23 | // 如使用自建邮件系统请设置 skipVerify 为 true 以避免证书校验错误 24 | skipVerify = true 25 | port := 465 26 | s := NewSMTP(address, from, password, from, html, tls, anonymous, skipVerify, port) 27 | err := s.Send([]string{to}, subject, body) 28 | if err != nil { 29 | fmt.Println(err) 30 | os.Exit(0) 31 | } 32 | } 33 | 34 | ``` 35 | 36 | - SendEmail 37 | ```go 38 | func Send() { 39 | address := "smtp.163.com" 40 | from := "...@163.com" 41 | password := "..." 42 | 43 | to := "to1@email.com" 44 | subject:= "" 45 | body:="" 46 | 47 | tls := true 48 | anonymous := false 49 | // 如使用自建邮件系统请设置 skipVerify 为 true 以避免证书校验错误 50 | skipVerify = true 51 | port := 25 52 | 53 | s := NewSMTP(address, from, password, from, plain, tls, anonymous, skipVerify, port) 54 | s.Send([]string{tos}, subject, body) 55 | } 56 | ``` 57 | 58 | - SendEmailAnonyMous 59 | ```go 60 | func SendAnonyMous() { 61 | address := "smtp.custom.com" 62 | from := "noreply@custom.com" 63 | 64 | to := "to1@email.com" 65 | subject:= "" 66 | body:="" 67 | 68 | tls := true 69 | anonymous := true 70 | // 如使用自建邮件系统请设置 skipVerify 为 true 以避免证书校验错误 71 | skipVerify = true 72 | port := 25 73 | 74 | s := NewSMTP(address, "", "", from, plain, tls, anonymous, skipVerify, 25) 75 | s.Send([]string{tos}, subject, body) 76 | } 77 | ``` -------------------------------------------------------------------------------- /pkg/notify/email/email_test.go: -------------------------------------------------------------------------------- 1 | package email 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestNewSMTP(t *testing.T) { 8 | // odftxhxilvfzcbci 9 | 10 | mail := NewMail("xx@qq.com", "xx", "smtp.qq.com", 11 | "xxx", 465, false) 12 | _ = mail.Send("测试用gomail发送邮件", "Good Good Study, Day Day Up!!!!!!", []string{`xx@qq.com`, "xxx"}, []string{}) 13 | } 14 | -------------------------------------------------------------------------------- /pkg/notify/lark/lark.go: -------------------------------------------------------------------------------- 1 | package lark 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "encoding/json" 8 | "fmt" 9 | "jobor/pkg/notify" 10 | "net/http" 11 | "time" 12 | ) 13 | 14 | func GenSign(secret string, timestamp int64) (string, error) { 15 | //timestamp + key 做sha256, 再进行base64 encode 16 | stringToSign := fmt.Sprintf("%v", timestamp) + "\n" + secret 17 | var data []byte 18 | h := hmac.New(sha256.New, []byte(stringToSign)) 19 | _, err := h.Write(data) 20 | if err != nil { 21 | return "", err 22 | } 23 | signature := base64.StdEncoding.EncodeToString(h.Sum(nil)) 24 | return signature, nil 25 | } 26 | 27 | type Secure uint 28 | 29 | const ( 30 | // CustomKey Custom keywords 31 | CustomKey Secure = iota + 1 32 | // Sign need sign up 33 | Sign 34 | // IPCdir IP addres 35 | IPCdir 36 | ) 37 | 38 | // Lark alarm conf 39 | type Lark struct { 40 | MsgType string `json:"msg_type"` 41 | WebHookUrl string `json:"web_hook_url"` 42 | Sl Secure `json:"sl"` 43 | Secret string `json:"secret"` 44 | } 45 | 46 | // Result post resp 47 | type Result struct { 48 | Code int `json:"code"` 49 | Msg string `json:"msg"` 50 | } 51 | 52 | type Content struct { 53 | Text string `json:"text"` 54 | } 55 | 56 | // SendMsg post json data 57 | type SendMsg struct { 58 | Timestamp string `json:"timestamp"` 59 | Sign string `json:"sign"` 60 | MsgType string `json:"msg_type"` 61 | Content Content `json:"content"` 62 | } 63 | 64 | // NewLark init a Lark send conf 65 | func NewLark(webHookUrl string, sl Secure, secret string) notify.Sender { 66 | d := Lark{ 67 | WebHookUrl: webHookUrl, 68 | MsgType: "text", 69 | Sl: sl, 70 | Secret: secret, 71 | } 72 | 73 | return &d 74 | } 75 | 76 | // Send to notify tos is phone number 77 | func (d *Lark) Send(tos []string, title string, content string) error { 78 | var reqUrl = d.WebHookUrl 79 | timestamp := time.Now().Unix() 80 | sign := "" 81 | if len(d.Secret) > 0 { 82 | var err error 83 | if sign, err = GenSign(d.Secret, timestamp); err != nil { 84 | return err 85 | } 86 | } 87 | 88 | sendMsg := SendMsg{ 89 | Timestamp: fmt.Sprintf("%d", timestamp), 90 | Sign: sign, 91 | MsgType: "text", 92 | Content: Content{ 93 | Text: title + "\n" + content + "\n", 94 | }, 95 | } 96 | 97 | resp, err := notify.JSONPost(http.MethodPost, reqUrl, sendMsg, http.DefaultClient) 98 | if err != nil { 99 | return err 100 | } 101 | res := Result{} 102 | err = json.Unmarshal(resp, &res) 103 | if err != nil { 104 | return err 105 | } 106 | if res.Code != 0 { 107 | return fmt.Errorf("errmsg: %s errcode: %d", res.Msg, res.Code) 108 | } 109 | return nil 110 | } 111 | -------------------------------------------------------------------------------- /pkg/notify/lark/lark_test.go: -------------------------------------------------------------------------------- 1 | package lark 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestNewLark(t *testing.T) { 9 | l := NewLark("https://open.feishu.cn/open-apis/bot/v2/hook/xxx", 10 | 1, "xxxx") 11 | err := l.Send([]string{}, "lark title", "test lark text example") 12 | if err != nil { 13 | fmt.Println("err:", err) 14 | return 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /pkg/notify/notify.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io/ioutil" 7 | "net/http" 8 | ) 9 | 10 | // Sender it send notify to user 11 | type Sender interface { 12 | Send(to []string, title string, content string) error 13 | } 14 | 15 | // alarm notify 16 | // mail 17 | // chat 18 | // dingding 19 | // slack 20 | // telegram 21 | // server jiang 22 | // lark 23 | 24 | // JSONPost Post req json data to url 25 | func JSONPost(method, url string, data interface{}, client *http.Client) ([]byte, error) { 26 | jsonBody, err := json.Marshal(data) 27 | if err != nil { 28 | return nil, err 29 | } 30 | req, err := http.NewRequest(method, url, bytes.NewReader(jsonBody)) 31 | if err != nil { 32 | return nil, err 33 | } 34 | req.Header.Set("Content-Type", "application/json;charset=utf-8") 35 | 36 | resp, err := client.Do(req) 37 | if err != nil { 38 | //log.Error("client.Do",err) 39 | return nil, err 40 | } 41 | 42 | body, err := ioutil.ReadAll(resp.Body) 43 | if err != nil { 44 | return nil, err 45 | } 46 | return body, err 47 | } 48 | -------------------------------------------------------------------------------- /pkg/notify/notify_test.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | ) 7 | 8 | func Test_JSONPost(t *testing.T) { 9 | _, err := JSONPost(http.MethodPost, "http://webhook.test", nil, http.DefaultClient) 10 | if err != nil { 11 | t.Fatal(err) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /pkg/notify/slack/README.md: -------------------------------------------------------------------------------- 1 | ### 如何使用slack channel来接收通知 2 | 3 | - 添加一个来接收通知的channel 4 | 点击最左边的`Channel`旁边的`加号`,然后输入一个你想创建的channel的名称,点击`Create`,这时一个channel就创建好了 5 | 然后点击slack pc端左下角`Add more apps`,然后在搜索框输入`Imcoming WebHooks`,点击`Add`,这时会在浏览器打开一个新的页面,再次点击`Add To Slack`会进入`Imcoming WebHooks`的配置页面,然后下面会出现一个`Post to Channel`,并且还有一个选择框,然后点击`Choose a channel`,然后选择刚才创建的channel,点击下面的蓝色的按钮完成添加,保存`webhook URL` 6 | 7 | 8 | ### Example 9 | ```go 10 | var ( 11 | webhook = "https://hooks.slack.com/services/TGM152H5E/BSXFZALEB/sc..." 12 | ) 13 | func Send() { 14 | slack := NewSlack(webhook) 15 | err := slack.Send([]string{"labulakalia"}, "测试标题", "测试内容") 16 | if err != nil { 17 | t.Error(err) 18 | } 19 | } 20 | ``` -------------------------------------------------------------------------------- /pkg/notify/slack/slack.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "fmt" 5 | "jobor/pkg/notify" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | // Slack send conf 11 | type Slack struct { 12 | webhookurl string 13 | httpclient *http.Client 14 | } 15 | 16 | // SendMsg post json data 17 | type SendMsg struct { 18 | Text string `json:"text"` 19 | // Channel string `json:"channel"` 20 | UserName string `json:"username"` 21 | // PreText string `json:"pretext"` 22 | } 23 | 24 | // NewSlack init 25 | func NewSlack(webhook string) notify.Sender { 26 | client := &http.Client{ 27 | Timeout: time.Second * 30, 28 | Transport: &http.Transport{ 29 | Proxy: http.ProxyFromEnvironment, 30 | }, 31 | } 32 | return &Slack{ 33 | webhookurl: webhook, 34 | httpclient: client, 35 | } 36 | } 37 | 38 | // Send will send send msg to slack channel 39 | func (s *Slack) Send(tos []string, title string, content string) error { 40 | sendmsg := SendMsg{ 41 | Text: content + "\n" + content, 42 | } 43 | resp, err := notify.JSONPost(http.MethodPost, s.webhookurl, sendmsg, s.httpclient) 44 | if err != nil { 45 | return err 46 | } 47 | if string(resp) != "ok" { 48 | err := fmt.Errorf("send data to slack failed error:%s", resp) 49 | return err 50 | } 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /pkg/notify/telegram/README.md: -------------------------------------------------------------------------------- 1 | ### 如何使用telegram bot来接收通知 2 | - 创建telegram bot 3 | 点击[telegram bot father](https://t.me/botfather),发送指令`/newbot`,然后会提示让输入bot的名称,发送你想创建bot的name,注意这个名字是bot显示的名称,发送之后还会提示让发送一个bot的username,这个username必须是不能重复的,因为使用这个名字才可以关注到这个bot,发送成功之后会给你发送一个`token` 4 | - 如何发送给指定用户消息 5 | ·· 6 | 7 | 8 | - Example 9 | ```go 10 | func Send() { 11 | token := "929493383:AA..." 12 | telegram, err := NewTelegram(token) 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | telegram.Send([]string{"chatid"}, "测试标题", "测试内容") 17 | 18 | time.Sleep(time.Second * 2) 19 | } 20 | ``` -------------------------------------------------------------------------------- /pkg/notify/telegram/telegram.go: -------------------------------------------------------------------------------- 1 | package telegram 2 | 3 | // 4 | //import ( 5 | // "jobor/pkg/notify" 6 | // "net/http" 7 | // "strconv" 8 | // "sync" 9 | // "time" 10 | // 11 | // tb "gopkg.in/tucnak/telebot.v2" 12 | //) 13 | // 14 | //var ( 15 | // once sync.Once 16 | //) 17 | // 18 | //// Telegram conf 19 | //type Telegram struct { 20 | // token string 21 | // bot *tb.Bot 22 | //} 23 | // 24 | //// NewTelegram init telegram 25 | //func NewTelegram(token string) (notify.Sender, error) { 26 | // telegram := &Telegram{ 27 | // token: token, 28 | // } 29 | // client := &http.Client{ 30 | // Timeout: time.Second * 10, 31 | // Transport: &http.Transport{ 32 | // Proxy: http.ProxyFromEnvironment, 33 | // }, 34 | // } 35 | // bot, err := tb.NewBot( 36 | // tb.Settings{ 37 | // Token: token, 38 | // Poller: &tb.LongPoller{Timeout: time.Second * 10}, 39 | // Client: client, 40 | // }) 41 | // if err != nil { 42 | // return nil, err 43 | // } 44 | // telegram.bot = bot 45 | // 46 | // go func() { 47 | // once.Do(func() { 48 | // bot.Start() 49 | // }) 50 | // }() 51 | // // wait bot start 52 | // time.Sleep(time.Second) 53 | // return telegram, nil 54 | //} 55 | // 56 | //// Send will send notify to channel 57 | //func (t *Telegram) Send(tos []string, title string, content string) error { 58 | // for _, id := range tos { 59 | // uid, err := strconv.Atoi(id) 60 | // if err != nil { 61 | // return err 62 | // } 63 | // go func(uid int) { 64 | // _, _ = t.bot.Send(&tb.User{Id: uid}, title+"\n"+title) 65 | // }(uid) 66 | // } 67 | // return nil 68 | //} 69 | -------------------------------------------------------------------------------- /pkg/notify/wechat/README.md: -------------------------------------------------------------------------------- 1 | ### 如何使用企业微信号来接收通知 2 | - [注册企业微信](https://work.weixin.qq.com/wework_admin/register_wx?from=myhome) 3 | - 获取corpid 4 | 每个企业都拥有唯一的corpid,获取此信息可在管理后台**我的企业**->**企业信息**->**企业ID**(需要有管理员权限) 5 | - 创建应用 6 | 点击**应用管理**->**自建**->**创建应用**,填写应用名称,添加成员,然后点击创建应用,然后点击进入新创建的应用,将**AgentId**和**Secret**两个参数记录下来 7 | - 在微信中接收通知 8 | 为了在微信中可以直接接收到消息,需要微信扫码关注微工作台,点击 **我的企业**->**微工作台**->**邀请关注**,使用微信关注即可接收到通知 9 | 10 | ### Example 11 | ```go 12 | var ( 13 | corpid = "wwb..." 14 | agentID = 1000002 15 | secret = "NgYcbPHa6DhR..." 16 | ) 17 | func Send() { 18 | client := NewWeChat(corpid, agentID, secret) 19 | err := client.Send([]string{"..."}, "测试标题", "测试消息的文本") 20 | if err != nil { 21 | t.Error(err) 22 | } 23 | } 24 | ``` 25 | -------------------------------------------------------------------------------- /pkg/utils/In_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestIn(t *testing.T) { 9 | r, err := In([]interface{}{1, "two", 3}, "two") 10 | //r, err := In([]interface{}{1, "two", 3}, "two") 11 | fmt.Println(r) 12 | fmt.Println(err) 13 | //var arr = []string{"aaa", "bbb", "ccc"} 14 | //fmt.Println("a") 15 | } 16 | -------------------------------------------------------------------------------- /pkg/utils/base64.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "encoding/base64" 4 | 5 | // Base64Enc base64编码 6 | func Base64Enc(input []byte) string { 7 | encodeString := base64.StdEncoding.EncodeToString(input) 8 | return encodeString 9 | } 10 | 11 | // Base64Dec base64解码 12 | func Base64Dec(encodeString string) string { 13 | decodeBytes, err := base64.StdEncoding.DecodeString(encodeString) 14 | //decodeBytes, err := base64.RawStdEncoding.DecodeString(encodeString) 15 | if err != nil { 16 | panic(any(err)) 17 | } 18 | return string(decodeBytes) 19 | } 20 | 21 | // Base64EncUrl base64 URL编码 22 | func Base64EncUrl(input []byte) string { 23 | encodeString := base64.URLEncoding.EncodeToString(input) 24 | return encodeString 25 | } 26 | 27 | // Base64DecUrl base64 URL解码 28 | func Base64DecUrl(encodeString string) string { 29 | decodeBytes, err := base64.URLEncoding.DecodeString(encodeString) 30 | if err != nil { 31 | panic(any(err)) 32 | } 33 | return string(decodeBytes) 34 | } 35 | -------------------------------------------------------------------------------- /pkg/utils/base64_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestBase64Enc(t *testing.T) { 9 | fmt.Println(Base64Enc([]byte("127.0.0.1:8080"))) 10 | } 11 | 12 | func TestBase64Des(t *testing.T) { 13 | fmt.Println(Base64Dec("xx")) 14 | } 15 | -------------------------------------------------------------------------------- /pkg/utils/cert.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/x509" 5 | "encoding/pem" 6 | "fmt" 7 | "io/ioutil" 8 | ) 9 | 10 | // CertInfo "test.pem" 11 | func CertInfo(certPath string, certBytes []byte) (*x509.Certificate, error) { 12 | var err error 13 | if len(certPath) > 0 { 14 | certBytes, err = ioutil.ReadFile(certPath) 15 | if err != nil { 16 | //fmt.Println(err.Error()) 17 | return nil, err 18 | } 19 | } 20 | 21 | pemBlock, _ := pem.Decode(certBytes) 22 | if pemBlock == nil { 23 | //fmt.Println("decode error") 24 | return nil, fmt.Errorf("decode pem/cert error") 25 | } 26 | 27 | cert, err := x509.ParseCertificate(pemBlock.Bytes) 28 | if err != nil { 29 | //fmt.Println(err.Error()) 30 | return cert, err 31 | } 32 | 33 | //fmt.Printf("Name %s\n", cert.Subject.CommonName) // 域名 34 | //fmt.Printf("Not before %s\n", cert.NotBefore.String()) // 有效期开始时间 35 | //fmt.Printf("Not after %s\n", cert.NotAfter.String()) // 有效期结束时间 36 | return cert, nil 37 | } 38 | -------------------------------------------------------------------------------- /pkg/utils/checkmail.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "net" 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | var ( 12 | errBadFormat = errors.New("invalid format") 13 | errUnresolvableHost = errors.New("unresolvable smtp host") 14 | emailRegexp = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") 15 | ) 16 | 17 | func validateFormat(email string) error { 18 | if !emailRegexp.MatchString(email) { 19 | return errBadFormat 20 | } 21 | return nil 22 | } 23 | 24 | func validateHost(email string) error { 25 | _, host := split(email) 26 | _, err := net.LookupMX(host) 27 | if err != nil { 28 | return errUnresolvableHost 29 | } 30 | return nil 31 | } 32 | func split(email string) (account, host string) { 33 | i := strings.LastIndexByte(email, '@') 34 | account = email[:i] 35 | host = email[i+1:] 36 | return 37 | } 38 | 39 | // CheckEmail will check a email is valid 40 | func CheckEmail(email string) error { 41 | err := validateFormat(email) 42 | if err != nil { 43 | return err 44 | } 45 | err = validateHost(email) 46 | if err != nil { 47 | return err 48 | } 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /pkg/utils/convert.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "encoding/json" 7 | "errors" 8 | "strings" 9 | ) 10 | 11 | func AnyToAny(src interface{}, des interface{}) error { 12 | bytesData, err := json.Marshal(src) 13 | if err != nil { 14 | return err 15 | } 16 | err = json.Unmarshal(bytesData, des) 17 | return err 18 | } 19 | 20 | func AnyToAnyV2(src interface{}, des interface{}) error { 21 | var buf bytes.Buffer 22 | err := json.NewEncoder(&buf).Encode(src) 23 | err = json.NewDecoder(&buf).Decode(&des) 24 | return err 25 | } 26 | 27 | func GobAnyToAny(src interface{}, des interface{}) error { 28 | var buf bytes.Buffer 29 | encoder := gob.NewEncoder(&buf) 30 | err := encoder.Encode(src) 31 | if err != nil { 32 | return err 33 | } 34 | decoder := gob.NewDecoder(bytes.NewReader(buf.Bytes())) 35 | //dec := gob.NewDecoder(&buf) 36 | err = decoder.Decode(des) 37 | return err 38 | } 39 | 40 | func Uri2map(uri string) (map[string]interface{}, error) { 41 | m := make(map[string]interface{}) 42 | if len(uri) < 1 { // 空字符串 43 | return m, errors.New("uri is none") 44 | } 45 | if uri[0:1] == "?" { // 有没有包含?,有的话忽略。 46 | uri = uri[1:] 47 | } 48 | 49 | // 首先分解&(sessionid=22222&token=3333 )变成 50 | // sessionid=222222 和 token=3333 51 | pars := strings.Split(uri, "&") 52 | for _, par := range pars { 53 | // 然后分解 sessionid=222222 和 token=3333 54 | parkv := strings.Split(par, "=") 55 | m[parkv[0]] = parkv[1] // 等号前面是key,后面是value 56 | } 57 | //log.Println(m) 58 | return m, nil 59 | } 60 | -------------------------------------------------------------------------------- /pkg/utils/crypto_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestDecryptDES(t *testing.T) { 9 | //TestAll() 10 | //AesDemo2() 11 | aa := DeTxtByAes("15bbddabdf3113087b72336ac2b3063c", "xxx") 12 | fmt.Println("aa:", aa) 13 | } 14 | -------------------------------------------------------------------------------- /pkg/utils/data.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | func GetMapDefault(k, defaultValue interface{}, m map[interface{}]interface{}) interface{} { 4 | if value, ok := m[k]; ok { 5 | return value 6 | } else { 7 | return defaultValue 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /pkg/utils/hash.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/md5" 5 | "crypto/sha1" 6 | "crypto/sha256" 7 | "fmt" 8 | ) 9 | 10 | // MD5Hash MD5哈希值 11 | func MD5Hash(b []byte) string { 12 | h := md5.New() 13 | h.Write(b) 14 | return fmt.Sprintf("%x", h.Sum(nil)) 15 | } 16 | 17 | // MD5HashString MD5哈希值 返回一个32位md5加密后的字符串 18 | func MD5HashString(s string) string { 19 | return MD5Hash([]byte(s)) 20 | } 21 | 22 | // MD5HashString16 返回一个16位md5加密后的字符串 23 | func MD5HashString16(data string) string { 24 | return MD5HashString(data)[8:24] 25 | } 26 | 27 | // SHA1Hash SHA1哈希值 28 | func SHA1Hash(b []byte) string { 29 | h := sha1.New() 30 | h.Write(b) 31 | return fmt.Sprintf("%x", h.Sum(nil)) 32 | } 33 | 34 | // SHA1HashString SHA1哈希值 35 | func SHA1HashString(s string) string { 36 | return SHA1Hash([]byte(s)) 37 | } 38 | 39 | // SHA256Hash SHA256哈希值 40 | func SHA256Hash(b []byte) string { 41 | h := sha256.New() 42 | h.Write(b) 43 | return fmt.Sprintf("%x", h.Sum(nil)) 44 | } 45 | 46 | // SHA256HashString SHA1哈希值 47 | func SHA256HashString(s string) string { 48 | return SHA256Hash([]byte(s)) 49 | } 50 | 51 | type IHash interface { 52 | MD5HashString(s string) string 53 | SHA1HashString(s string) string 54 | SHA256HashString(s string) string 55 | } 56 | 57 | type Hash struct{} 58 | 59 | func (h *Hash) SHA256HashString(s string) string { 60 | return SHA256Hash([]byte(s)) 61 | } 62 | 63 | func (h *Hash) SHA1HashString(s string) string { 64 | return SHA1Hash([]byte(s)) 65 | } 66 | 67 | // MD5HashString MD5哈希值 68 | func (h *Hash) MD5HashString(s string) string { 69 | return MD5Hash([]byte(s)) 70 | } 71 | -------------------------------------------------------------------------------- /pkg/utils/hash_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func Test(t *testing.T) { 9 | //fmt.Println(MD5HashString16("127.0.0.1:2889")) 10 | //fmt.Println(MD5HashString16("127.0.0.1:2888")) 11 | //fmt.Println(MD5HashString16("127.0.0.1:2887")) 12 | //fmt.Println(len(MD5HashString16("1.1.1.1"))) 13 | // admin $2a$10$lWjGqds.mVNxjuDHqsyJSeBjmEu75Hws8yDgQwGmwMFxHY8QuCkVi 14 | fmt.Println(HashAndSalt([]byte("admin"))) 15 | fmt.Println(HashAndSalt([]byte("admin"))) 16 | fmt.Println(ValidateSaltPasswords("$2a$10$O9GeHneK.st.qXkrZhX5auwWUxbJ7hWOJSN7zQsD.nhlQSECYYYQG", 17 | []byte("admin"))) 18 | // $2a$10$LBwJ2MIa8erQq7k6dkqvc.Wv1vLH9FsjYLu4VkMibcse8J6rvS4s6 19 | // $2a$10$iHz9anq5x2d8XikLCnMlWu5HJjEax2OUGdjdzRPwFLIXdiqPgqwe6 20 | // $2a$10$TB/VtGfygXrNUX0uszIjBurGZRNcQFjj2lyn4o.2NzcxEUy0f6cTm 21 | } 22 | -------------------------------------------------------------------------------- /pkg/utils/jsonMarshal.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | 7 | jsoniter "github.com/json-iterator/go" 8 | ) 9 | 10 | // 定义JSON操作 11 | var ( 12 | Json = jsoniter.ConfigCompatibleWithStandardLibrary 13 | JSONMarshal = Json.Marshal 14 | JSONUnmarshal = Json.Unmarshal 15 | JSONMarshalIndent = Json.MarshalIndent 16 | JSONNewDecoder = Json.NewDecoder 17 | JSONNewEncoder = Json.NewEncoder 18 | ) 19 | 20 | // JSONMarshalToString JSON编码为字符串 21 | func JSONMarshalToString(v interface{}) string { 22 | s, err := jsoniter.MarshalToString(v) 23 | if err != nil { 24 | return "" 25 | } 26 | return s 27 | } 28 | 29 | func PrettyJson(data interface{}) (string, error) { 30 | const ( 31 | empty = "" 32 | tab = "\t" 33 | ) 34 | buffer := new(bytes.Buffer) 35 | encoder := Json.NewEncoder(buffer) 36 | encoder.SetIndent(empty, tab) 37 | 38 | err := encoder.Encode(data) 39 | if err != nil { 40 | return empty, err 41 | } 42 | return buffer.String(), nil 43 | } 44 | 45 | func StructToJsonFormatted(v interface{}) string { 46 | data, err := json.MarshalIndent(v, "", " ") 47 | if err != nil { 48 | panic(err) 49 | } 50 | 51 | return string(data) 52 | } 53 | -------------------------------------------------------------------------------- /pkg/utils/random.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | func RandString(len int) string { 9 | r := rand.New(rand.NewSource(time.Now().Unix())) 10 | bytes := make([]byte, len) 11 | for i := 0; i < len; i++ { 12 | b := r.Intn(26) + 65 13 | bytes[i] = byte(b) 14 | } 15 | return string(bytes) 16 | } 17 | 18 | func GetRandomString(length int) string { 19 | str := "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" // abcdefghijklmnopqrstuvwxyz 20 | bytes := []byte(str) 21 | var result []byte 22 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 23 | for i := 0; i < length; i++ { 24 | result = append(result, bytes[r.Intn(len(bytes))]) 25 | } 26 | return string(result) 27 | } 28 | -------------------------------------------------------------------------------- /pkg/utils/randstr.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "encoding/binary" 7 | ) 8 | 9 | // Bytes generates n random bytes 10 | func Bytes(n int) []byte { 11 | b := make([]byte, n) 12 | _, err := rand.Read(b) 13 | if err != nil { 14 | panic(err) 15 | } 16 | return b 17 | } 18 | 19 | const Base64Chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/" 20 | const Base62Chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 21 | const HexChars = "0123456789abcdef" 22 | const DecChars = "0123456789" 23 | 24 | // Base64 generates a random base64 string with length of n 25 | func Base64(n int) string { return RandStr(n, Base64Chars) } 26 | 27 | // Base62 generates a random base62 string with length of n 28 | func Base62(s int) string { return RandStr(s, Base62Chars) } 29 | 30 | // Dec generates a random decimal number string with length of n 31 | func Dec(n int) string { return RandStr(n, DecChars) } 32 | 33 | // Hex generates a random hex string with length of n 34 | // e.g: 67aab2d956bd7cc621af22cfb169cba8 35 | func Hex(n int) string { return RandStr(n, HexChars) } 36 | 37 | // list of default letters that can be used to make a random string when calling String 38 | // function with no letters provided 39 | var defLetters = []rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 40 | 41 | // String generates a random string using only letters provided in the letters parameter 42 | // if user ommit letters parameters, this function will use defLetters instead 43 | func RandStr(n int, letters ...string) string { 44 | var letterRunes []rune 45 | if len(letters) == 0 { 46 | letterRunes = defLetters 47 | } else { 48 | letterRunes = []rune(letters[0]) 49 | } 50 | 51 | var bb bytes.Buffer 52 | bb.Grow(n) 53 | l := uint32(len(letterRunes)) 54 | // on each loop, generate one random rune and append to output 55 | for i := 0; i < n; i++ { 56 | bb.WriteRune(letterRunes[binary.BigEndian.Uint32(Bytes(4))%l]) 57 | } 58 | return bb.String() 59 | } 60 | -------------------------------------------------------------------------------- /pkg/utils/randstr_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func ExampleHex() { 8 | for i := 0; i < 5; i++ { 9 | token := Hex(16) 10 | fmt.Println(token) 11 | } 12 | // Output: 13 | // 67aab2d956bd7cc621af22cfb169cba8 14 | // 226eeb52947edbf3e97d1e6669e212c2 15 | // 5f3615e95d103d14ffb5b655aa0eec1e 16 | // ff3ab4efbd74025b87b14b59422d304c 17 | // a6705813c174ca73ed795ea0bab12726 18 | } 19 | 20 | func ExampleString() { 21 | for i := 0; i < 5; i++ { 22 | token := RandStr(16) 23 | fmt.Println(token) 24 | } 25 | // Output: 26 | // 7EbxkrHc1l3Ahmyr 27 | // I5XH2gc1EEHgbmGI 28 | // GlCycMpsxGkn9cDQ 29 | // U2OfBDQoak0z8FwV 30 | // kDX1m81u14YwEiCY 31 | } 32 | 33 | func ExampleDec() { 34 | for i := 0; i < 5; i++ { 35 | token := Dec(16) 36 | fmt.Println(token) 37 | } 38 | // Output: 39 | //1232392418047380 40 | //9160917876815937 41 | //6629264107419930 42 | //0271037110897873 43 | //0337735480322223 44 | } 45 | -------------------------------------------------------------------------------- /pkg/utils/routine.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | func SafeGoroutine(fn func()) { 10 | var err error 11 | go func() { 12 | defer func() { 13 | if r := recover(); r != nil { 14 | var ok bool 15 | err, ok = r.(error) 16 | if !ok { 17 | err = fmt.Errorf("%v", r) 18 | } 19 | log.Errorf("goroutine panic: %v", err) 20 | } 21 | }() 22 | fn() 23 | }() 24 | } 25 | -------------------------------------------------------------------------------- /pkg/utils/setList.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // Union 求并集 4 | func Union[T string | int | int64 | int32](slice1, slice2 []T) []T { 5 | m := make(map[T]int) 6 | for _, v := range slice1 { 7 | m[v]++ 8 | } 9 | 10 | for _, v := range slice2 { 11 | times, _ := m[v] 12 | if times == 0 { 13 | slice1 = append(slice1, v) 14 | } 15 | } 16 | return slice1 17 | } 18 | 19 | // Intersect 求交集 20 | func Intersect[T string | int | int64 | int32](slice1, slice2 []T) []T { 21 | m := make(map[T]int) 22 | nn := make([]T, 0) 23 | for _, v := range slice1 { 24 | m[v]++ 25 | } 26 | 27 | for _, v := range slice2 { 28 | times, _ := m[v] 29 | if times == 1 { 30 | nn = append(nn, v) 31 | } 32 | } 33 | return nn 34 | } 35 | 36 | // Difference 求差集 slice1-并集 37 | /** 38 | * Difference(slice1, slice2) slice1 多出来的元素 39 | */ 40 | func Difference[T string | int | int64 | int32](slice1, slice2 []T) []T { 41 | m := make(map[T]int) 42 | nn := make([]T, 0) 43 | inter := Intersect(slice1, slice2) 44 | for _, v := range inter { 45 | m[v]++ 46 | } 47 | 48 | for _, value := range slice1 { 49 | times, _ := m[value] 50 | if times == 0 { 51 | nn = append(nn, value) 52 | } 53 | } 54 | return nn 55 | } 56 | -------------------------------------------------------------------------------- /pkg/utils/slice.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | func DeleteSlice(a []int, elem int) []int { 4 | j := 0 5 | for _, v := range a { 6 | if v != elem { 7 | a[j] = v 8 | j++ 9 | } 10 | } 11 | return a[:j] 12 | } 13 | 14 | // 数组去重 15 | func RemoveDuplicate[T string | int](sliceList []T) []T { 16 | allKeys := make(map[T]bool) 17 | list := []T{} 18 | for _, item := range sliceList { 19 | if _, value := allKeys[item]; !value { 20 | allKeys[item] = true 21 | list = append(list, item) 22 | } 23 | } 24 | return list 25 | } 26 | -------------------------------------------------------------------------------- /pkg/utils/struct.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | func GetStructFieldsByReflect(obj interface{}, jsonNames *[]string, fieldNames *[]string) error { 9 | s := reflect.ValueOf(obj) 10 | t := reflect.TypeOf(obj) 11 | if s.Kind() == reflect.Ptr { 12 | obj = reflect.Value.Elem(s).Interface() 13 | s = reflect.ValueOf(obj) 14 | t = reflect.TypeOf(obj) 15 | } 16 | if t.Kind() != reflect.Struct { 17 | return fmt.Errorf("obj is not struct type") 18 | } 19 | for i := 0; i < s.NumField(); i++ { 20 | tField := t.Field(i) 21 | if tField.Type.Kind() == reflect.Struct && tField.Anonymous { 22 | //fmt.Println("13", tField.Name, tField.Type.Kind(), tField.Anonymous) 23 | if err := GetStructFieldsByReflect(s.Field(i).Interface(), jsonNames, fieldNames); err != nil { 24 | return err 25 | } 26 | } else { 27 | //fmt.Println("14", tField.Name, tField.Type.Kind(), tField.Anonymous) 28 | name := tField.Name 29 | // 获取struct的tag 30 | tag := tField.Tag.Get("json") 31 | if len(tag) > 0 { 32 | name = tag 33 | } 34 | if tag != "-" { 35 | for idx, n := range *jsonNames { 36 | n := n 37 | if n == name { 38 | *jsonNames = append((*jsonNames)[:idx], (*jsonNames)[idx+1:]...) 39 | } 40 | } 41 | for idx, n := range *fieldNames { 42 | n := n 43 | if n == tField.Name { 44 | *fieldNames = append((*fieldNames)[:idx], (*fieldNames)[idx+1:]...) 45 | } 46 | } 47 | *jsonNames = append(*jsonNames, name) 48 | *fieldNames = append(*fieldNames, tField.Name) 49 | } 50 | 51 | } 52 | 53 | } 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /pkg/utils/struct_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | func Json6() { 9 | type Stbb struct { 10 | Sdb string `json:"sdb"` 11 | Sdc string `json:"sdc"` 12 | } 13 | type Stru struct { 14 | ID int `json:"id"` 15 | Cd string `json:"cd"` 16 | CC string 17 | Stbb 18 | AA string `json:"aa"` 19 | BB string `json:"-"` 20 | St struct { 21 | Sta string `json:"sta"` 22 | Stb string `json:"stb"` 23 | } `json:"st"` 24 | } 25 | var st = Stru{AA: "a1", BB: "b1", CC: "c1", Cd: "cd1", ID: 10} 26 | st.St.Sta = "sta1" 27 | st.Sdb = "sdb1" 28 | marshal, err := json.Marshal(st) 29 | if err != nil { 30 | fmt.Println("err:", err) 31 | return 32 | } 33 | fmt.Println("marshal:", string(marshal)) 34 | var mm map[string]interface{} 35 | err = json.Unmarshal(marshal, &mm) 36 | if err != nil { 37 | fmt.Println("err:", err) 38 | return 39 | } 40 | fmt.Println("marshal:", mm) 41 | var names []string 42 | var names2 []string 43 | err = GetStructFieldsByReflect(st, &names, &names2) 44 | if err != nil { 45 | fmt.Println("names err:", err) 46 | return 47 | } 48 | fmt.Println("names:", names) 49 | } 50 | -------------------------------------------------------------------------------- /pkg/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "compress/gzip" 5 | "fmt" 6 | "io" 7 | "net" 8 | "os" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | func GetLocalIPv4Address() (string, error) { 14 | addr, err := net.InterfaceAddrs() 15 | if err != nil { 16 | return "", err 17 | } 18 | for _, addr := range addr { 19 | 20 | ipNet, isIpNet := addr.(*net.IPNet) 21 | if isIpNet && !ipNet.IP.IsLoopback() { 22 | ipv4 := ipNet.IP.To4() 23 | if ipv4 != nil { 24 | return ipv4.String(), nil 25 | } 26 | } 27 | } 28 | return "", fmt.Errorf("not found ipv4 address") 29 | } 30 | 31 | // GetOutBoundIP net.Dial("udp", "8.8.8.8:53") 32 | func GetOutBoundIP(network, addr string) (ip string) { 33 | conn, err := net.Dial(network, addr) 34 | if err != nil { 35 | //log.Errorf("get out bound ip err: %s\n", err) 36 | panic(any(err)) 37 | } 38 | var localAddr net.Addr 39 | if network == "tcp" { 40 | localAddr = conn.LocalAddr().(*net.TCPAddr) // .(*net.TCPAddr) 41 | } else { 42 | localAddr = conn.LocalAddr().(*net.UDPAddr) // .(*net.UDPAddr) 43 | } 44 | //fmt.Println(localAddr.String()) 45 | ip = strings.Split(localAddr.String(), ":")[0] 46 | return 47 | } 48 | 49 | func FileExists(name string) bool { 50 | if _, err := os.Stat(name); err != nil { 51 | if os.IsNotExist(err) { 52 | return false 53 | } 54 | } 55 | return true 56 | } 57 | 58 | func EnsureDirExist(name string) error { 59 | if !FileExists(name) { 60 | return os.MkdirAll(name, os.ModePerm) 61 | } 62 | return nil 63 | } 64 | 65 | func GzipCompressFile(srcPath, dstPath string) error { 66 | sf, err := os.Open(srcPath) 67 | if err != nil { 68 | return err 69 | } 70 | defer func(sf *os.File) { 71 | err := sf.Close() 72 | if err != nil { 73 | 74 | } 75 | }(sf) 76 | df, err := os.Create(dstPath) 77 | if err != nil { 78 | return err 79 | } 80 | defer func(df *os.File) { 81 | err := df.Close() 82 | if err != nil { 83 | 84 | } 85 | }(df) 86 | writer := gzip.NewWriter(df) 87 | writer.Name = dstPath 88 | writer.ModTime = time.Now().UTC() 89 | _, err = io.Copy(writer, sf) 90 | if err != nil { 91 | return err 92 | } 93 | if err := writer.Close(); err != nil { 94 | return err 95 | } 96 | return nil 97 | } 98 | 99 | func Sum(i []int) int { 100 | sum := 0 101 | for _, v := range i { 102 | sum += v 103 | } 104 | return sum 105 | } 106 | 107 | func Abs(x int) int { 108 | if x < 0 { 109 | return -x 110 | } 111 | return x 112 | } 113 | 114 | func CurrentUTCTime() string { 115 | return time.Now().UTC().Format("2006-01-02 15:04:05 +0000") 116 | } 117 | -------------------------------------------------------------------------------- /pkg/utils/uuid_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestNewMD5(t *testing.T) { 9 | fmt.Println(NewMD5(UUID{16}, []byte("1233"))) 10 | } 11 | -------------------------------------------------------------------------------- /profile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v-mars/jobor/dca6625b3560ddeebff83a14c90f0cac7b8688df/profile -------------------------------------------------------------------------------- /rpc_biz/mw/access_log.go: -------------------------------------------------------------------------------- 1 | package mw 2 | 3 | import ( 4 | "context" 5 | "github.com/cloudwego/kitex/pkg/endpoint" 6 | "github.com/cloudwego/kitex/pkg/klog" 7 | "time" 8 | ) 9 | 10 | func AccessLogMW(next endpoint.Endpoint) endpoint.Endpoint { 11 | return func(ctx context.Context, request, response interface{}) error { 12 | start := time.Now() 13 | //fmt.Printf("request: %v\n", request) 14 | defer func() { 15 | un := "" 16 | if un == "" { 17 | un = "Anonymous" 18 | } 19 | end := time.Now() 20 | latency := end.Sub(start).String() 21 | //fmt.Println("ctx:", ctx) 22 | klog.CtxDebugf(ctx, "cost=%s", latency) 23 | }() 24 | err := next(ctx, request, response) 25 | //fmt.Printf("response: %v", response) 26 | return err 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /rpc_biz/registry/registry_mysql.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "fmt" 5 | "jobor/biz/model" 6 | "jobor/rpc_biz" 7 | "time" 8 | ) 9 | 10 | type Mysql struct { 11 | Subscriptions 12 | } 13 | 14 | func (m *Mysql) Registry(port int32, ttl int64) error { 15 | 16 | //var d = time.Second * time.Duration(ttl) 17 | ticker := time.NewTicker(rpc_biz.DefaultHeartbeatInterval) 18 | go func() { 19 | for { 20 | //_, err = hbClient.SendHb(context.Background(), &pb.HeartbeatReq{Port: port}) 21 | //if err != nil { 22 | // time.Sleep(time.Second) 23 | // fmt.Printf("send hearbeat failed:%s", err) 24 | // return 25 | //} 26 | fmt.Println("get ticker", time.Now().Format("2006-01-02 15:04:05")) 27 | <-ticker.C 28 | } 29 | }() 30 | 31 | return nil 32 | } 33 | 34 | // Keepalive 35 | // 保持服务器与mysql的同步更新 36 | func (m *Mysql) Keepalive(interval int64) error { 37 | var data = model.JoborWorker{Ip: m.Ip, Hostname: m.Hostname, Addr: m.Addr, Version: m.Version, RoutingKey: m.RoutingKey, 38 | Weight: m.Weight, LeaseUpdate: time.Now().Unix(), Status: "running"} 39 | return model.CreateOrUpdate(data) 40 | } 41 | 42 | func (m *Mysql) UnRegistry() error { 43 | var data = model.JoborWorker{Ip: m.Ip, Hostname: m.Hostname, Addr: m.Ip, Version: m.Version, RoutingKey: m.RoutingKey, 44 | Weight: m.Weight, LeaseUpdate: time.Now().Unix(), Status: "offline"} 45 | return model.CreateOrUpdate(data) 46 | } 47 | -------------------------------------------------------------------------------- /rpc_biz/registry/registry_redis.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/redis/go-redis/v9" 7 | "time" 8 | ) 9 | 10 | // SUBSCRIPTIONS_ 11 | 12 | type Redis struct { 13 | c *redis.Client 14 | Subscriptions 15 | } 16 | 17 | func (m *Redis) Registry(port int32, ttl int64) error { 18 | var d = time.Second * time.Duration(ttl) 19 | ticker := time.NewTicker(d) 20 | go func() { 21 | for { 22 | err := m.Keepalive(ttl) 23 | if err != nil { 24 | fmt.Printf("保持连接失败:%s", err) 25 | } 26 | //fmt.Println("get ticker1", time.Now().Format("2006-01-02 15:04:05")) 27 | <-ticker.C 28 | } 29 | }() 30 | 31 | return nil 32 | } 33 | 34 | // 保持服务器与redis的同步更新 35 | func (m *Redis) Keepalive(interval int64) error { 36 | var key = fmt.Sprintf("routing_key.%s__%s", m.RoutingKey, m.Ip) 37 | _ = m.c.Set(context.TODO(), key, "aa", time.Second*time.Duration(interval+1)) 38 | return nil 39 | } 40 | 41 | func (m *Redis) UnRegistry() error { 42 | var key = fmt.Sprintf("routing_key.%s__%s", m.RoutingKey, m.Ip) 43 | _ = m.c.Del(context.TODO(), key) 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /rpc_biz/rpc_consts.go: -------------------------------------------------------------------------------- 1 | package rpc_biz 2 | 3 | import ( 4 | "google.golang.org/grpc/keepalive" 5 | "time" 6 | ) 7 | 8 | const ( 9 | // DefaultRPCTimeout RPC connect timeout 10 | DefaultRPCTimeout = time.Second * 3 11 | // DefaultHeartbeatInterval worker send heartbeat ttl 12 | //DefaultHeartbeatInterval2 = time.Second * 5 // 13 | DefaultHeartbeatInterval = time.Second * 5 // maxWorkerTTL int64 = 20 14 | DefaultLastFailHearBeatInterval = time.Second * 3 15 | // DefaultMaxRetryGetWorker max retry get host time for func Next 16 | DefaultMaxRetryGetWorker = 3 17 | ) 18 | 19 | var Kaep = keepalive.EnforcementPolicy{ 20 | MinTime: 5 * time.Second, // If a client pings more than once every 5 seconds, terminate the connection 21 | PermitWithoutStream: true, // Allow pings even when there are no active streams 22 | } 23 | 24 | var Kasp = keepalive.ServerParameters{ 25 | MaxConnectionIdle: 15 * time.Second, // If a client is idle for 15 seconds, send a GOAWAY 26 | MaxConnectionAge: 0 * time.Second, // If any connection is alive for more than 30 seconds, send a GOAWAY 27 | MaxConnectionAgeGrace: 5 * time.Second, // Allow 5 seconds for pending RPCs to complete before forcibly closing connections 28 | Time: 5 * time.Second, // Ping the client if it is idle for 5 seconds to ensure the connection is still active 29 | Timeout: 1 * time.Second, // Wait 1 second for the ping ack before assuming the connection is dead 30 | } 31 | 32 | var Kacp = keepalive.ClientParameters{ 33 | Time: 5 * time.Second, // send pings every 10 seconds if there is no activity 34 | Timeout: time.Second, // wait 1 second for ping ack before considering the connection dead 35 | PermitWithoutStream: true, // send pings even without active streams 36 | } 37 | -------------------------------------------------------------------------------- /rpc_biz/svc/server_rpc.go: -------------------------------------------------------------------------------- 1 | package svc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/cloudwego/hertz/pkg/common/hlog" 7 | pbapi "jobor/kitex_gen/pbapi" 8 | "jobor/rpc_biz/registry" 9 | ) 10 | 11 | // HeartbeatImpl implements the last service interface defined in the IDL. 12 | type HeartbeatImpl struct{} 13 | 14 | // RegistryWorker implements the HeartbeatImpl interface. 15 | func (s *HeartbeatImpl) RegistryWorker(ctx context.Context, req *pbapi.RegistryReq) (resp *pbapi.Empty, err error) { 16 | // TODO: Your code here... 17 | 18 | //p, ok := peer.FromContext(ctx) 19 | //if !ok { 20 | // return &pbapi.Empty{}, fmt.Errorf("registry failed, %s", p) 21 | //} 22 | //ip, _, _ := net.SplitHostPort(p.Addr.String()) 23 | hlog.Debugf("`registry host` new worker req %s", req) 24 | //req.Ip = ip 25 | 26 | if req.Ip == "::1" || req.Ip == "[::]" { 27 | req.Ip = "127.0.0.1" 28 | } 29 | addr := fmt.Sprintf("%s:%d", req.Ip, req.Port) 30 | 31 | subscriptions := registry.Subscriptions{Ip: req.Ip, Hostname: req.Hostname, Port: req.Port, Weight: req.Weight, 32 | Version: req.Version, RoutingKey: req.RoutingKey, Addr: addr} 33 | var regCenter = registry.GetRegistryCenter(registry.MysqlCenter, subscriptions) 34 | if err := regCenter.Keepalive(5); err != nil { 35 | hlog.Errorf(err.Error()) 36 | } 37 | hlog.Infof("New Worker SendHeartbeatAndRegistryWorker Success addr %s", addr) 38 | return &pbapi.Empty{}, nil 39 | } 40 | 41 | // SendHeartbeat implements the HeartbeatImpl interface. 42 | func (s *HeartbeatImpl) SendHeartbeat(ctx context.Context, req *pbapi.HeartbeatReq) (resp *pbapi.Empty, err error) { 43 | // TODO: Your code here... 44 | //p, ok := peer.FromContext(ctx) 45 | //if !ok { 46 | // return &pbapi.Empty{}, fmt.Errorf("get peer failed") 47 | //} 48 | //ip, _, _ := net.SplitHostPort(p.Addr.String()) 49 | hlog.Debugf("heartbeat host keepalive req %s", req) 50 | if req.Ip == "::1" || req.Ip == "::" { 51 | req.Ip = "127.0.0.1" 52 | } 53 | hlog.Debugf("recv hearbeat addr %s", fmt.Sprintf("%s:%d", req.Ip, req.Port)) 54 | subscriptions := registry.Subscriptions{Ip: req.Ip, Port: req.Port, Addr: fmt.Sprintf("%s:%d", req.Ip, req.Port)} 55 | var registryStore registry.Center = ®istry.Mysql{Subscriptions: subscriptions} 56 | 57 | if err := registryStore.Keepalive(5); err != nil { 58 | hlog.Errorf(err.Error()) 59 | } 60 | 61 | return &pbapi.Empty{}, nil 62 | } 63 | -------------------------------------------------------------------------------- /rpc_biz/svc/worker_rpc.go: -------------------------------------------------------------------------------- 1 | package svc 2 | 3 | import ( 4 | "context" 5 | "jobor/biz/pack/dispatcher" 6 | pbapi "jobor/kitex_gen/pbapi" 7 | task "jobor/kitex_gen/task" 8 | ) 9 | 10 | // TaskServiceImpl implements the last service interface defined in the IDL. 11 | type TaskServiceImpl struct{} 12 | 13 | func (s *TaskServiceImpl) RunTask(req *task.TaskRequest, stream pbapi.TaskService_RunTaskServer) (err error) { 14 | //println("RunTask called") 15 | 16 | err = dispatcher.TaskWorker(context.Background(), string(req.TaskData), req.TaskId, req.TaskLang, stream) 17 | if err != nil { 18 | return err 19 | } 20 | return 21 | } 22 | -------------------------------------------------------------------------------- /script/bootstrap.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | CURDIR=$(cd $(dirname $0); pwd) 3 | 4 | if [ "X$1" != "X" ]; then 5 | RUNTIME_ROOT=$1 6 | else 7 | RUNTIME_ROOT=${CURDIR} 8 | fi 9 | 10 | export KITEX_RUNTIME_ROOT=$RUNTIME_ROOT 11 | export KITEX_LOG_DIR="$RUNTIME_ROOT/log" 12 | 13 | if [ ! -d "$KITEX_LOG_DIR/app" ]; then 14 | mkdir -p "$KITEX_LOG_DIR/app" 15 | fi 16 | 17 | if [ ! -d "$KITEX_LOG_DIR/rpc" ]; then 18 | mkdir -p "$KITEX_LOG_DIR/rpc" 19 | fi 20 | 21 | exec "$CURDIR/bin/jobor_rpc" 22 | 23 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # shellcheck disable=SC2034 4 | version=3.0.1 5 | 6 | cd ../ 7 | make Makefile build-linux-drawin 8 | # shellcheck disable=SC2164 9 | cd ./build/package 10 | rm -rf ./jobor-darwin-amd64/bin/jobor 11 | rm -rf ./jobor-linux-amd64/bin/jobor 12 | mv linux-amd64 ./jobor-linux-amd64/bin/jobor 13 | mv darwin-amd64 ./jobor-darwin-amd64/bin/jobor 14 | 15 | tar zcvf jobor-linux-amd64-${version}.tar.gz ./jobor-linux-amd64/ 16 | tar zcvf jobor-darwin-amd64-${version}.tar.gz ./jobor-darwin-amd64/ --------------------------------------------------------------------------------