From b5aa970766951b6ebca60dd8d913a5023376529f Mon Sep 17 00:00:00 2001 From: FamousMai <906631095@qq.com> Date: Fri, 28 Mar 2025 15:18:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9Esandbox-full=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 300 +- README_DIFY.md | 192 + admin/.gitattributes | 2 + admin/.gitignore | 35 + admin/CODE_OF_CONDUCT.md | 76 + admin/CONTRIBUTING.md | 19 + admin/LICENSE | 201 + admin/Makefile | 78 + .../docker-compose/docker-compose-dev.yaml | 99 + .../deploy/docker-compose/docker-compose.yaml | 90 + admin/deploy/docker/Dockerfile | 18 + admin/deploy/docker/entrypoint.sh | 19 + .../server/gva-server-configmap.yaml | 148 + .../server/gva-server-deployment.yaml | 74 + .../kubernetes/server/gva-server-service.yaml | 21 + .../kubernetes/web/gva-web-configmap.yaml | 32 + .../kubernetes/web/gva-web-deploymemt.yaml | 51 + .../kubernetes/web/gva-web-ingress.yaml | 18 + .../kubernetes/web/gva-web-service.yaml | 21 + admin/docs/gin-vue-admin.png | Bin 0 -> 105776 bytes admin/gin-vue-admin.code-workspace | 49 + admin/server/Dockerfile | 33 + admin/server/README.md | 54 + admin/server/api/v1/enter.go | 15 + admin/server/api/v1/example/enter.go | 13 + .../api/v1/example/exa_breakpoint_continue.go | 150 + admin/server/api/v1/example/exa_customer.go | 176 + .../v1/example/exa_file_upload_download.go | 133 + admin/server/api/v1/gaia/dashboard.go | 159 + admin/server/api/v1/gaia/enter.go | 19 + admin/server/api/v1/gaia/quota.go | 73 + admin/server/api/v1/gaia/system.go | 51 + admin/server/api/v1/gaia/tenants.go | 80 + admin/server/api/v1/gaia/test.go | 141 + .../server/api/v1/system/auto_code_history.go | 115 + .../server/api/v1/system/auto_code_package.go | 100 + .../server/api/v1/system/auto_code_plugin.go | 119 + .../api/v1/system/auto_code_template.go | 121 + admin/server/api/v1/system/enter.go | 48 + admin/server/api/v1/system/sys_api.go | 323 + admin/server/api/v1/system/sys_authority.go | 202 + .../server/api/v1/system/sys_authority_btn.go | 80 + admin/server/api/v1/system/sys_auto_code.go | 155 + admin/server/api/v1/system/sys_captcha.go | 70 + admin/server/api/v1/system/sys_casbin.go | 69 + admin/server/api/v1/system/sys_dictionary.go | 129 + .../api/v1/system/sys_dictionary_detail.go | 148 + .../api/v1/system/sys_export_template.go | 258 + admin/server/api/v1/system/sys_initdb.go | 66 + .../server/api/v1/system/sys_jwt_blacklist.go | 33 + admin/server/api/v1/system/sys_menu.go | 265 + .../api/v1/system/sys_operation_record.go | 149 + admin/server/api/v1/system/sys_params.go | 171 + admin/server/api/v1/system/sys_system.go | 88 + admin/server/api/v1/system/sys_user.go | 503 + admin/server/api/v1/system/sys_user_extend.go | 158 + admin/server/config.docker.yaml | 173 + admin/server/config.yaml | 71 + admin/server/config/auto_code.go | 22 + admin/server/config/captcha.go | 9 + admin/server/config/config.go | 44 + admin/server/config/cors.go | 14 + admin/server/config/db_list.go | 52 + admin/server/config/disk.go | 9 + admin/server/config/email.go | 11 + admin/server/config/excel.go | 5 + admin/server/config/gaia.go | 8 + admin/server/config/gorm_mssql.go | 10 + admin/server/config/gorm_mysql.go | 9 + admin/server/config/gorm_oracle.go | 10 + admin/server/config/gorm_pgsql.go | 17 + admin/server/config/gorm_sqlite.go | 13 + admin/server/config/jwt.go | 8 + admin/server/config/mongo.go | 41 + admin/server/config/oa_login.go | 9 + admin/server/config/oss_aliyun.go | 10 + admin/server/config/oss_aws.go | 13 + admin/server/config/oss_cloudflare.go | 10 + admin/server/config/oss_huawei.go | 9 + admin/server/config/oss_local.go | 6 + admin/server/config/oss_minio.go | 11 + admin/server/config/oss_qiniu.go | 11 + admin/server/config/oss_tencent.go | 10 + admin/server/config/redis.go | 10 + admin/server/config/system.go | 18 + admin/server/config/zap.go | 71 + admin/server/core/internal/constant.go | 9 + admin/server/core/internal/cutter.go | 121 + admin/server/core/internal/zap_core.go | 68 + admin/server/core/server.go | 44 + admin/server/core/server_other.go | 19 + admin/server/core/server_win.go | 21 + admin/server/core/viper.go | 72 + admin/server/core/zap.go | 32 + admin/server/corn/main.go | 82 + admin/server/docs/docs.go | 8104 ++++++++++++++++ admin/server/docs/swagger.json | 8078 ++++++++++++++++ admin/server/docs/swagger.yaml | 4927 ++++++++++ admin/server/global/global.go | 67 + admin/server/global/model.go | 14 + admin/server/go.mod | 185 + admin/server/go.sum | 859 ++ admin/server/initialize/db_list.go | 36 + admin/server/initialize/ensure_tables.go | 119 + admin/server/initialize/gorm.go | 85 + admin/server/initialize/gorm_biz.go | 14 + admin/server/initialize/gorm_mssql.go | 59 + admin/server/initialize/gorm_mysql.go | 55 + admin/server/initialize/gorm_oracle.go | 52 + admin/server/initialize/gorm_pgsql.go | 50 + admin/server/initialize/gorm_sqlite.go | 42 + admin/server/initialize/internal/gorm.go | 48 + .../initialize/internal/gorm_logger_writer.go | 37 + admin/server/initialize/internal/mongo.go | 29 + admin/server/initialize/mongo.go | 151 + admin/server/initialize/other.go | 32 + admin/server/initialize/plugin.go | 15 + admin/server/initialize/plugin_biz_v1.go | 34 + admin/server/initialize/plugin_biz_v2.go | 16 + admin/server/initialize/redis.go | 71 + admin/server/initialize/register_init.go | 10 + admin/server/initialize/router.go | 110 + admin/server/initialize/router_biz.go | 24 + admin/server/initialize/timer.go | 37 + admin/server/initialize/validator.go | 22 + admin/server/main.go | 38 + admin/server/middleware/casbin_rbac.go | 36 + admin/server/middleware/cors.go | 73 + admin/server/middleware/email.go | 60 + admin/server/middleware/error.go | 61 + admin/server/middleware/jwt.go | 95 + admin/server/middleware/limit_ip.go | 92 + admin/server/middleware/loadtls.go | 27 + admin/server/middleware/logger.go | 89 + admin/server/middleware/operation.go | 133 + admin/server/model/common/basetypes.go | 36 + admin/server/model/common/clearDB.go | 7 + admin/server/model/common/request/common.go | 48 + admin/server/model/common/response/common.go | 8 + .../server/model/common/response/response.go | 63 + .../model/example/exa_breakpoint_continue.go | 24 + admin/server/model/example/exa_customer.go | 15 + .../model/example/exa_file_upload_download.go | 17 + .../response/exa_breakpoint_continue.go | 11 + .../model/example/response/exa_customer.go | 7 + .../response/exa_file_upload_download.go | 7 + admin/server/model/gaia/ForwardingExtend.go | 48 + admin/server/model/gaia/account.go | 82 + .../server/model/gaia/account_money_extend.go | 22 + admin/server/model/gaia/api_tokens.go | 71 + admin/server/model/gaia/apps.go | 48 + admin/server/model/gaia/end_user.go | 22 + admin/server/model/gaia/messages.go | 46 + admin/server/model/gaia/request/dashboard.go | 36 + admin/server/model/gaia/request/quota.go | 6 + admin/server/model/gaia/request/tenants.go | 11 + admin/server/model/gaia/request/test.go | 39 + admin/server/model/gaia/response/dashboard.go | 51 + admin/server/model/gaia/response/quota.go | 9 + admin/server/model/gaia/response/test.go | 12 + admin/server/model/gaia/system_integration.go | 23 + admin/server/model/gaia/tenants.go | 48 + admin/server/model/gaia/test.go | 34 + .../model/gaia/workflow_node_executions.go | 32 + admin/server/model/gaia/workflow_runs.go | 32 + admin/server/model/system/request/jwt.go | 33 + admin/server/model/system/request/oa_login.go | 5 + admin/server/model/system/request/sys_api.go | 14 + .../model/system/request/sys_authority_btn.go | 7 + .../model/system/request/sys_auto_code.go | 282 + .../system/request/sys_auto_code_package.go | 31 + .../model/system/request/sys_auto_history.go | 56 + .../server/model/system/request/sys_casbin.go | 27 + .../system/request/sys_dictionary_detail.go | 11 + .../system/request/sys_export_template.go | 14 + admin/server/model/system/request/sys_init.go | 123 + admin/server/model/system/request/sys_menu.go | 27 + .../system/request/sys_operation_record.go | 11 + .../server/model/system/request/sys_params.go | 14 + admin/server/model/system/request/sys_user.go | 86 + .../server/model/system/response/oa_login.go | 18 + admin/server/model/system/response/sys_api.go | 18 + .../model/system/response/sys_authority.go | 12 + .../system/response/sys_authority_btn.go | 5 + .../model/system/response/sys_auto_code.go | 17 + .../model/system/response/sys_captcha.go | 8 + .../model/system/response/sys_casbin.go | 9 + .../server/model/system/response/sys_menu.go | 15 + .../model/system/response/sys_system.go | 7 + .../server/model/system/response/sys_user.go | 15 + admin/server/model/system/sys_api.go | 28 + admin/server/model/system/sys_authority.go | 23 + .../server/model/system/sys_authority_btn.go | 8 + .../server/model/system/sys_authority_menu.go | 19 + .../model/system/sys_auto_code_history.go | 67 + .../model/system/sys_auto_code_package.go | 18 + admin/server/model/system/sys_base_menu.go | 42 + admin/server/model/system/sys_dictionary.go | 20 + .../model/system/sys_dictionary_detail.go | 21 + .../model/system/sys_export_template.go | 44 + .../server/model/system/sys_jwt_blacklist.go | 10 + admin/server/model/system/sys_menu_btn.go | 10 + .../model/system/sys_operation_record.go | 24 + admin/server/model/system/sys_params.go | 20 + admin/server/model/system/sys_system.go | 10 + admin/server/model/system/sys_user.go | 116 + .../server/model/system/sys_user_authority.go | 11 + admin/server/model/system/sys_user_extend.go | 4 + admin/server/plugin/announcement/api/enter.go | 10 + admin/server/plugin/announcement/api/info.go | 183 + .../plugin/announcement/config/config.go | 4 + admin/server/plugin/announcement/gen/gen.go | 17 + .../plugin/announcement/initialize/api.go | 49 + .../plugin/announcement/initialize/gorm.go | 20 + .../plugin/announcement/initialize/menu.go | 22 + .../plugin/announcement/initialize/router.go | 15 + .../plugin/announcement/initialize/viper.go | 17 + .../server/plugin/announcement/model/info.go | 20 + .../plugin/announcement/model/request/info.go | 12 + admin/server/plugin/announcement/plugin.go | 26 + .../plugin/announcement/plugin/plugin.go | 5 + .../plugin/announcement/router/enter.go | 10 + .../server/plugin/announcement/router/info.go | 31 + .../plugin/announcement/service/enter.go | 5 + .../plugin/announcement/service/info.go | 78 + admin/server/plugin/email/README.MD | 75 + admin/server/plugin/email/api/enter.go | 7 + admin/server/plugin/email/api/sys_email.go | 53 + admin/server/plugin/email/config/email.go | 11 + admin/server/plugin/email/global/gloabl.go | 5 + admin/server/plugin/email/main.go | 28 + .../plugin/email/model/response/email.go | 7 + admin/server/plugin/email/router/enter.go | 7 + admin/server/plugin/email/router/sys_email.go | 19 + admin/server/plugin/email/service/enter.go | 7 + .../server/plugin/email/service/sys_email.go | 32 + admin/server/plugin/email/utils/email.go | 82 + .../server/plugin/plugin-tool/utils/check.go | 50 + admin/server/resource/function/api.go.tpl | 40 + admin/server/resource/function/api.js.tpl | 32 + admin/server/resource/function/server.go.tpl | 25 + admin/server/resource/package/readme.txt.tpl | 7 + .../resource/package/server/api/api.go.tpl | 212 + .../resource/package/server/api/enter.go.tpl | 4 + .../package/server/model/model.go.tpl | 86 + .../server/model/request/request.go.tpl | 58 + .../package/server/router/enter.go.tpl | 4 + .../package/server/router/router.go.tpl | 42 + .../package/server/service/enter.go.tpl | 4 + .../package/server/service/service.go.tpl | 218 + .../resource/package/web/api/api.js.tpl | 130 + .../resource/package/web/view/form.vue.tpl | 413 + .../resource/package/web/view/table.vue.tpl | 1266 +++ .../plugin/server/api/api.go.template | 207 + .../plugin/server/api/enter.go.template | 6 + .../plugin/server/config/config.go.template | 4 + .../plugin/server/gen/gen.go.template | 18 + .../plugin/server/initialize/api.go.template | 12 + .../plugin/server/initialize/gorm.go.template | 17 + .../plugin/server/initialize/menu.go.template | 12 + .../server/initialize/router.go.template | 14 + .../server/initialize/viper.go.template | 17 + .../plugin/server/model/model.go.template | 83 + .../server/model/request/request.go.template | 57 + .../resource/plugin/server/plugin.go.template | 26 + .../plugin/server/plugin/plugin.go.template | 5 + .../plugin/server/router/enter.go.template | 6 + .../plugin/server/router/router.go.template | 46 + .../plugin/server/service/enter.go.template | 7 + .../plugin/server/service/service.go.template | 225 + .../resource/plugin/web/api/api.js.template | 127 + .../plugin/web/form/form.vue.template | 413 + .../plugin/web/view/view.vue.template | 1269 +++ admin/server/router/enter.go | 15 + admin/server/router/example/enter.go | 15 + admin/server/router/example/exa_customer.go | 22 + .../example/exa_file_upload_and_download.go | 22 + admin/server/router/gaia/dashboard.go | 19 + admin/server/router/gaia/enter.go | 19 + admin/server/router/gaia/quota.go | 16 + admin/server/router/gaia/system.go | 16 + admin/server/router/gaia/tenants.go | 17 + admin/server/router/gaia/test.go | 18 + admin/server/router/system/enter.go | 44 + admin/server/router/system/sys_api.go | 33 + admin/server/router/system/sys_authority.go | 23 + .../server/router/system/sys_authority_btn.go | 19 + admin/server/router/system/sys_auto_code.go | 40 + .../router/system/sys_auto_code_history.go | 17 + admin/server/router/system/sys_base.go | 17 + admin/server/router/system/sys_casbin.go | 19 + admin/server/router/system/sys_dictionary.go | 22 + .../router/system/sys_dictionary_detail.go | 22 + .../router/system/sys_export_template.go | 28 + admin/server/router/system/sys_initdb.go | 15 + admin/server/router/system/sys_jwt.go | 14 + admin/server/router/system/sys_menu.go | 27 + .../router/system/sys_operation_record.go | 19 + admin/server/router/system/sys_params.go | 25 + admin/server/router/system/sys_system.go | 22 + admin/server/router/system/sys_user.go | 29 + admin/server/service/enter.go | 15 + admin/server/service/example/enter.go | 6 + .../example/exa_breakpoint_continue.go | 71 + admin/server/service/example/exa_customer.go | 87 + .../example/exa_file_upload_download.go | 118 + admin/server/service/gaia/account.go | 230 + admin/server/service/gaia/copy.go | 267 + admin/server/service/gaia/dashboard.go | 478 + admin/server/service/gaia/encode.go | 59 + admin/server/service/gaia/enter.go | 9 + admin/server/service/gaia/quota.go | 119 + admin/server/service/gaia/system.go | 136 + admin/server/service/gaia/tenants.go | 42 + admin/server/service/gaia/test.go | 564 ++ .../service/system/auto_code_history.go | 217 + .../service/system/auto_code_package.go | 673 ++ .../service/system/auto_code_package_test.go | 105 + .../server/service/system/auto_code_plugin.go | 249 + .../service/system/auto_code_template.go | 444 + .../service/system/auto_code_template_test.go | 84 + admin/server/service/system/enter.go | 25 + admin/server/service/system/jwt_black_list.go | 84 + admin/server/service/system/sys_api.go | 325 + admin/server/service/system/sys_authority.go | 327 + .../service/system/sys_authority_btn.go | 60 + .../service/system/sys_auto_code_interface.go | 55 + .../service/system/sys_auto_code_mssql.go | 83 + .../service/system/sys_auto_code_mysql.go | 83 + .../service/system/sys_auto_code_oracle.go | 72 + .../service/system/sys_auto_code_pgsql.go | 135 + .../service/system/sys_auto_code_sqlite.go | 84 + admin/server/service/system/sys_base_menu.go | 146 + admin/server/service/system/sys_casbin.go | 221 + admin/server/service/system/sys_dictionary.go | 112 + .../service/system/sys_dictionary_detail.go | 118 + .../service/system/sys_export_template.go | 423 + admin/server/service/system/sys_initdb.go | 189 + .../server/service/system/sys_initdb_mssql.go | 92 + .../server/service/system/sys_initdb_mysql.go | 97 + .../server/service/system/sys_initdb_pgsql.go | 118 + .../service/system/sys_initdb_sqlite.go | 88 + admin/server/service/system/sys_menu.go | 289 + .../service/system/sys_operation_record.go | 88 + admin/server/service/system/sys_params.go | 82 + admin/server/service/system/sys_system.go | 62 + admin/server/service/system/sys_user.go | 456 + .../server/service/system/sys_user_extend.go | 156 + .../source/example/file_upload_download.go | 65 + admin/server/source/system/api.go | 223 + admin/server/source/system/api_ignore.go | 77 + .../server/source/system/authorities_menus.go | 89 + admin/server/source/system/authority.go | 89 + admin/server/source/system/casbin.go | 309 + admin/server/source/system/dictionary.go | 71 + .../server/source/system/dictionary_detail.go | 121 + admin/server/source/system/excel_template.go | 75 + admin/server/source/system/menu.go | 115 + admin/server/source/system/user.go | 108 + admin/server/task/clearTable.go | 51 + admin/server/utils/ast/ast.go | 231 + admin/server/utils/ast/ast_auto_enter.go | 47 + admin/server/utils/ast/ast_enter.go | 181 + admin/server/utils/ast/ast_gorm.go | 166 + admin/server/utils/ast/ast_init_test.go | 11 + admin/server/utils/ast/ast_rollback.go | 173 + admin/server/utils/ast/ast_router.go | 135 + admin/server/utils/ast/ast_test.go | 32 + admin/server/utils/ast/ast_type.go | 53 + admin/server/utils/ast/import.go | 94 + admin/server/utils/ast/interfaces.go | 17 + admin/server/utils/ast/interfaces_base.go | 76 + admin/server/utils/ast/package_enter.go | 85 + admin/server/utils/ast/package_enter_test.go | 154 + .../utils/ast/package_initialize_gorm.go | 196 + .../utils/ast/package_initialize_gorm_test.go | 171 + .../utils/ast/package_initialize_router.go | 150 + .../ast/package_initialize_router_test.go | 158 + .../server/utils/ast/package_module_enter.go | 180 + .../utils/ast/package_module_enter_test.go | 185 + admin/server/utils/ast/plugin_enter.go | 167 + admin/server/utils/ast/plugin_enter_test.go | 200 + admin/server/utils/ast/plugin_gen.go | 189 + admin/server/utils/ast/plugin_gen_test.go | 127 + .../utils/ast/plugin_initialize_gorm.go | 111 + .../utils/ast/plugin_initialize_gorm_test.go | 138 + .../utils/ast/plugin_initialize_router.go | 124 + .../ast/plugin_initialize_router_test.go | 155 + .../server/utils/ast/plugin_initialize_v2.go | 52 + .../utils/ast/plugin_initialize_v2_test.go | 100 + admin/server/utils/breakpoint_continue.go | 112 + admin/server/utils/captcha/redis.go | 60 + admin/server/utils/claims.go | 169 + admin/server/utils/directory.go | 124 + admin/server/utils/encode.go | 82 + admin/server/utils/fmt_plus.go | 82 + admin/server/utils/fun.go | 71 + admin/server/utils/hash.go | 31 + admin/server/utils/human_duration.go | 29 + admin/server/utils/human_duration_test.go | 49 + admin/server/utils/json.go | 34 + admin/server/utils/json_test.go | 53 + admin/server/utils/jwt.go | 93 + admin/server/utils/plugin/plugin.go | 18 + admin/server/utils/plugin/v2/plugin.go | 11 + admin/server/utils/reload.go | 18 + admin/server/utils/request/http.go | 62 + admin/server/utils/server.go | 126 + admin/server/utils/timer/timed_task.go | 229 + admin/server/utils/timer/timed_task_test.go | 72 + admin/server/utils/upload/aliyun_oss.go | 75 + admin/server/utils/upload/aws_s3.go | 97 + admin/server/utils/upload/cloudflare_r2.go | 85 + admin/server/utils/upload/local.go | 109 + admin/server/utils/upload/minio_oss.go | 98 + admin/server/utils/upload/obs.go | 69 + admin/server/utils/upload/qiniu.go | 96 + admin/server/utils/upload/tencent_cos.go | 61 + admin/server/utils/upload/upload.go | 46 + admin/server/utils/validator.go | 294 + admin/server/utils/validator_test.go | 37 + admin/server/utils/verify.go | 19 + admin/server/utils/verify_extend.go | 5 + admin/server/utils/zip.go | 53 + .../web/.docker-compose/nginx/conf.d/my.conf | 27 + .../.docker-compose/nginx/conf.d/nginx.conf | 32 + admin/web/.dockerignore | 1 + admin/web/.env.development | 15 + admin/web/.env.production | 7 + admin/web/.eslintignore | 4 + admin/web/.eslintrc.js | 17 + admin/web/.gitignore | 5 + admin/web/Dockerfile | 17 + admin/web/babel.config.js | 8 + admin/web/index.html | 102 + admin/web/jsconfig.json | 10 + admin/web/limit.js | 37 + admin/web/package.json | 73 + admin/web/postcss.config.js | 6 + admin/web/public/favicon.ico | Bin 0 -> 845 bytes admin/web/public/logo.png | Bin 0 -> 5805 bytes admin/web/src/App.vue | 41 + admin/web/src/api/api.js | 179 + admin/web/src/api/authority.js | 85 + admin/web/src/api/authorityBtn.js | 27 + admin/web/src/api/autoCode.js | 189 + admin/web/src/api/breakpoint.js | 43 + admin/web/src/api/casbin.js | 32 + admin/web/src/api/customer.js | 80 + admin/web/src/api/email.js | 14 + admin/web/src/api/exportTemplate.js | 97 + admin/web/src/api/fileUploadAndDownload.js | 57 + admin/web/src/api/gaia/dashboard.js | 84 + admin/web/src/api/gaia/providers.js | 49 + admin/web/src/api/gaia/system.js | 28 + admin/web/src/api/gaia/tenants.js | 49 + admin/web/src/api/gaia/test.js | 43 + admin/web/src/api/initdb.js | 27 + admin/web/src/api/jwt.js | 14 + admin/web/src/api/menu.js | 113 + admin/web/src/api/sysDictionary.js | 80 + admin/web/src/api/sysDictionaryDetail.js | 80 + admin/web/src/api/sysOperationRecord.js | 48 + admin/web/src/api/sysParams.js | 111 + admin/web/src/api/system.js | 56 + admin/web/src/api/user.js | 223 + admin/web/src/api/user_extend.js | 13 + admin/web/src/assets/404.png | Bin 0 -> 43988 bytes admin/web/src/assets/background.svg | 1 + admin/web/src/assets/banner.jpg | Bin 0 -> 40498 bytes admin/web/src/assets/banner2.jpg | Bin 0 -> 45773 bytes admin/web/src/assets/dashboard.png | Bin 0 -> 72153 bytes admin/web/src/assets/docs.png | Bin 0 -> 4701 bytes admin/web/src/assets/flipped-aurora.png | Bin 0 -> 72652 bytes admin/web/src/assets/github.png | Bin 0 -> 7793 bytes admin/web/src/assets/icons/ai-gva.svg | 1 + admin/web/src/assets/icons/customer-gva.svg | 1 + admin/web/src/assets/kefu.png | Bin 0 -> 6770 bytes admin/web/src/assets/login_background.jpg | Bin 0 -> 41154 bytes admin/web/src/assets/login_background.svg | 33 + admin/web/src/assets/login_left.svg | 123 + admin/web/src/assets/login_right_banner.jpg | Bin 0 -> 719028 bytes admin/web/src/assets/logo.jpg | Bin 0 -> 27616 bytes admin/web/src/assets/logo.png | Bin 0 -> 8060 bytes admin/web/src/assets/logo_login.png | Bin 0 -> 25312 bytes admin/web/src/assets/nav_logo.png | Bin 0 -> 76683 bytes admin/web/src/assets/noBody.png | Bin 0 -> 4097 bytes admin/web/src/assets/notFound.png | Bin 0 -> 19669 bytes admin/web/src/assets/qm.png | Bin 0 -> 4821 bytes admin/web/src/assets/video.png | Bin 0 -> 5160 bytes .../src/components/arrayCtrl/arrayCtrl.vue | 68 + .../src/components/bottomInfo/bottomInfo.vue | 41 + admin/web/src/components/charts/index.vue | 56 + .../web/src/components/commandMenu/index.vue | 197 + admin/web/src/components/customPic/index.vue | 104 + .../components/exportExcel/exportExcel.vue | 58 + .../components/exportExcel/exportTemplate.vue | 28 + .../components/exportExcel/importExcel.vue | 42 + admin/web/src/components/office/docx.vue | 35 + admin/web/src/components/office/excel.vue | 33 + admin/web/src/components/office/index.vue | 53 + admin/web/src/components/office/pdf.vue | 36 + .../web/src/components/richtext/rich-edit.vue | 90 + .../web/src/components/richtext/rich-view.vue | 62 + .../src/components/selectFile/selectFile.vue | 87 + .../selectImage/selectComponent.vue | 65 + .../components/selectImage/selectImage.vue | 276 + admin/web/src/components/svgIcon/svgIcon.vue | 39 + admin/web/src/components/upload/common.vue | 76 + admin/web/src/components/upload/image.vue | 92 + .../src/components/warningBar/warningBar.vue | 33 + admin/web/src/core/config.js | 31 + admin/web/src/core/gin-vue-admin.js | 13 + admin/web/src/core/global.js | 53 + admin/web/src/directive/auth.js | 41 + admin/web/src/hooks/charts.js | 19 + admin/web/src/hooks/responsive.js | 35 + admin/web/src/hooks/use-windows-resize.js | 23 + admin/web/src/main.js | 29 + admin/web/src/pathInfo.json | 85 + admin/web/src/permission.js | 139 + admin/web/src/pinia/index.js | 13 + admin/web/src/pinia/modules/app.js | 147 + admin/web/src/pinia/modules/dictionary.js | 40 + admin/web/src/pinia/modules/router.js | 147 + admin/web/src/pinia/modules/user.js | 194 + admin/web/src/plugin/announcement/api/info.js | 110 + .../web/src/plugin/announcement/form/info.vue | 121 + .../web/src/plugin/announcement/view/info.vue | 418 + admin/web/src/plugin/email/api/email.js | 30 + admin/web/src/plugin/email/view/index.vue | 63 + admin/web/src/router/index.js | 38 + admin/web/src/style/element/index.scss | 24 + admin/web/src/style/element_visiable.scss | 138 + admin/web/src/style/iconfont.css | 47 + admin/web/src/style/main.scss | 53 + admin/web/src/style/reset.scss | 507 + admin/web/src/utils/asyncRouter.js | 32 + admin/web/src/utils/btnAuth.js | 6 + admin/web/src/utils/bus.js | 6 + admin/web/src/utils/closeThisPage.js | 5 + admin/web/src/utils/date.js | 51 + admin/web/src/utils/dictionary.js | 35 + admin/web/src/utils/doc.js | 3 + admin/web/src/utils/downloadImg.js | 19 + admin/web/src/utils/event.js | 29 + admin/web/src/utils/fmtRouterTitle.js | 13 + admin/web/src/utils/format.js | 139 + admin/web/src/utils/image.js | 113 + admin/web/src/utils/page.js | 9 + admin/web/src/utils/passwd.js | 26 + admin/web/src/utils/request.js | 156 + admin/web/src/utils/stringFun.js | 29 + admin/web/src/view/about/index.vue | 4 + .../src/view/dashboard/components/banner.vue | 40 + .../src/view/dashboard/components/card.vue | 51 + .../components/charts-content-numbers.vue | 183 + .../components/charts-people-numbers.vue | 141 + .../src/view/dashboard/components/charts.vue | 58 + .../src/view/dashboard/components/index.js | 19 + .../src/view/dashboard/components/notice.vue | 78 + .../view/dashboard/components/pluginTable.vue | 63 + .../view/dashboard/components/quickLinks.vue | 91 + .../src/view/dashboard/components/table.vue | 54 + .../src/view/dashboard/components/wiki.vue | 42 + admin/web/src/view/dashboard/index.vue | 47 + admin/web/src/view/error/index.vue | 28 + admin/web/src/view/error/reload.vue | 14 + .../view/example/breakpoint/breakpoint.vue | 295 + .../src/view/example/customer/customer.vue | 235 + admin/web/src/view/example/index.vue | 22 + admin/web/src/view/example/upload/upload.vue | 304 + .../components/accountMoneyTable.vue | 104 + .../components/appTokenDailyQuotaNumbers.vue | 208 + .../dashboard/components/appTokenQuota.vue | 117 + .../components/appTokenQuotaTable.vue | 126 + .../view/gaia/dashboard/components/card.vue | 51 + .../components/charts-people-numbers.vue | 141 + .../view/gaia/dashboard/components/charts.vue | 58 + .../view/gaia/dashboard/components/index.js | 91 + admin/web/src/view/gaia/dashboard/index.vue | 29 + admin/web/src/view/gaia/tenants/tenants.vue | 235 + admin/web/src/view/init/index.vue | 396 + .../aside/asideComponent/asyncSubmenu.vue | 67 + .../layout/aside/asideComponent/index.vue | 48 + .../layout/aside/asideComponent/menuItem.vue | 47 + .../src/view/layout/aside/combinationMode.vue | 138 + admin/web/src/view/layout/aside/headMode.vue | 91 + admin/web/src/view/layout/aside/index.vue | 25 + .../web/src/view/layout/aside/normalMode.vue | 105 + admin/web/src/view/layout/header/index.vue | 140 + admin/web/src/view/layout/header/tools.vue | 126 + admin/web/src/view/layout/index.vue | 106 + .../web/src/view/layout/screenfull/index.vue | 69 + admin/web/src/view/layout/search/search.vue | 104 + admin/web/src/view/layout/setting/index.vue | 200 + admin/web/src/view/layout/tabs/index.vue | 413 + admin/web/src/view/login/callback.vue | 46 + admin/web/src/view/login/index.vue | 313 + admin/web/src/view/person/person.vue | 507 + admin/web/src/view/quota/index.vue | 236 + admin/web/src/view/routerHolder.vue | 25 + admin/web/src/view/superAdmin/api/api.vue | 891 ++ .../view/superAdmin/authority/authority.vue | 457 + .../superAdmin/authority/components/apis.vue | 173 + .../superAdmin/authority/components/datas.vue | 144 + .../superAdmin/authority/components/menus.vue | 248 + .../superAdmin/dictionary/sysDictionary.vue | 215 + .../dictionary/sysDictionaryDetail.vue | 362 + admin/web/src/view/superAdmin/index.vue | 23 + .../menu/components/components-cascader.vue | 123 + admin/web/src/view/superAdmin/menu/icon.vue | 1184 +++ admin/web/src/view/superAdmin/menu/menu.vue | 799 ++ .../operation/sysOperationRecord.vue | 325 + .../src/view/superAdmin/params/sysParams.vue | 454 + admin/web/src/view/superAdmin/user/user.vue | 559 ++ admin/web/src/view/system/state.vue | 251 + .../view/systemIntegrated/dingTalk/index.vue | 300 + admin/web/src/view/systemIntegrated/index.vue | 23 + .../autoCode/component/fieldDialog.vue | 498 + .../autoCode/component/previewCodeDialog.vue | 116 + .../src/view/systemTools/autoCode/index.vue | 1511 +++ .../view/systemTools/autoCodeAdmin/index.vue | 546 ++ .../src/view/systemTools/autoPkg/autoPkg.vue | 253 + .../view/systemTools/exportTemplate/code.js | 32 + .../exportTemplate/exportTemplate.vue | 1008 ++ .../src/view/systemTools/formCreate/index.vue | 20 + admin/web/src/view/systemTools/index.vue | 23 + .../view/systemTools/installPlugin/index.vue | 39 + .../src/view/systemTools/pubPlug/pubPlug.vue | 234 + .../src/view/systemTools/system/system.vue | 823 ++ admin/web/src/view/test/appRequest/index.vue | 261 + admin/web/src/view/test/appRequest/list.vue | 302 + admin/web/src/view/test/index.vue | 23 + admin/web/src/view/user/index.vue | 697 ++ admin/web/tailwind.config.js | 27 + admin/web/vite.config.js | 110 + admin/web/vitePlugin/componentName/index.js | 80 + admin/web/vitePlugin/secret/index.js | 6 + api/.env.example | 36 +- api/Dockerfile | 2 +- api/configs/app_config.py | 3 + api/configs/extend/__init__.py | 67 + api/controllers/console/__init__.py | 17 +- api/controllers/console/apikey.py | 141 +- api/controllers/console/app/app.py | 4 +- api/controllers/console/app/app_extend.py | 75 + api/controllers/console/app/completion.py | 3 + .../console/app/ding_talk_extend.py | 42 + api/controllers/console/app/error_extend.py | 13 + .../console/app/passport_extend.py | 95 + api/controllers/console/app/statistic.py | 51 + api/controllers/console/app/workflow.py | 3 + .../console/app/workflow_statistic.py | 30 + api/controllers/console/auth/oauth.py | 14 +- .../console/auth/register_extend.py | 71 + api/controllers/console/error_extend.py | 7 + api/controllers/console/explore/completion.py | 16 + api/controllers/console/explore/workflow.py | 10 + api/controllers/console/money_extend.py | 30 + api/controllers/console/tag/tags.py | 4 + .../console/workspace/account_extend.py | 27 + api/controllers/console/workspace/models.py | 10 + .../console/workspace/workspace.py | 4 + api/controllers/service_api/app/completion.py | 23 +- .../service_api/app/error_extend.py | 19 + api/controllers/service_api/app/message.py | 4 +- api/controllers/service_api/app/workflow.py | 8 +- api/controllers/service_api/wraps.py | 89 +- api/controllers/web/completion.py | 87 + api/controllers/web/error_extend.py | 13 + api/controllers/web/workflow.py | 24 + .../app/apps/advanced_chat/app_generator.py | 16 +- .../advanced_chat/generate_task_pipeline.py | 12 +- api/core/app/apps/agent_chat/app_generator.py | 9 + api/core/app/apps/chat/app_generator.py | 9 + api/core/app/apps/completion/app_generator.py | 9 + api/core/app/apps/workflow/app_generator.py | 13 +- .../apps/workflow/generate_task_pipeline.py | 12 +- .../task_pipeline/workflow_cycle_manage.py | 9 + .../helper/code_executor/code_executor.py | 10 +- .../bedrock/get_bedrock_client.py | 11 + .../llm/anthropic.claude-3-sonnet-v1.7.yaml | 60 + .../model_providers/bedrock/llm/llm.py | 9 + .../openai_api_compatible.yaml | 55 + .../text_embedding/text_embedding.py | 32 +- .../builtin/code/tools/simple_code.py | 2 +- api/core/workflow/nodes/code/code_node.py | 5 + .../workflow/nodes/code/control_extend.py | 39 + api/docker/entrypoint.sh | 4 +- api/events/event_handlers/__init__.py | 1 + ...count_money_when_messaeg_created_extend.py | 63 + api/extensions/ext_blueprints.py | 6 +- api/extensions/ext_celery.py | 21 + api/fields/app_fields.py | 1 + api/fields/app_fields_extend.py | 35 + api/fields/member_fields_extend.py | 6 + api/libs/oauth.py | 84 + ...29024_recommended_list_sorted_by_usage_.py | 40 + .../09633b4cf949_add_account_money_extend.py | 42 + ...821be_add_end_user_account_joins_extend.py | 45 + ...0_add_account_money_monthly_stat_extend.py | 44 + .../versions/2024_08_20_0337-9cb135c9d1f8_.py | 22 + ...1a08e_add_account_layover_record_extend.py | 43 + ...1b804f8bbd28_add_api_token_money_extend.py | 105 + ...68_forwarding_address_extend_add_status.py | 36 + ...4907d127b_update_quota_precision_extend.py | 104 + ...pdate_account_money_monthly_stat_quota_.py | 48 + ...drop_api_token_money_stat_column_extend.py | 112 + .../versions/2024_09_03_0658-aa93d972c8c9_.py | 22 + ..._add_description_api_token_money_extend.py | 33 + ...af_the_proxy_billing_log_table_account_.py | 33 + .../versions/2024_09_11_0853-39714b40d774_.py | 22 + .../versions/2024_09_19_0407-0853e971b36e_.py | 22 + .../versions/2024_10_31_1253-ca79d9b5973b_.py | 25 + .../versions/2024_12_03_0410-4faed5bbdb91_.py | 25 + .../versions/2024_12_25_1619-62dd723ee92b_.py | 25 + .../versions/2025_01_23_1053-37e5bf7a1e53_.py | 25 + ...572_add_recommended_apps_category_join_.py | 58 + api/migrations/versions/59fc25e84ae2_.py | 22 + ...6d_ai_billing_and_forwarding_two_extend.py | 61 + ...929f29057c_add_tenant_model_sync_extend.py | 56 + api/models/__init__.py | 2 + api/models/account_money_extend.py | 34 + .../account_money_monthly_stat_extend.py | 19 + api/models/api_token_money_extend.py | 82 + api/models/model.py | 56 +- api/models/model_extend.py | 19 + api/models/system_extend.py | 51 + api/models/tenant_model_sync_extend.py | 50 + api/models/workflow.py | 47 +- api/poetry.lock | 6559 ++++++++----- api/pyproject.toml | 9 + .../update_account_used_quota_extend.py | 44 + ..._api_token_daily_used_quota_task_extend.py | 44 + ...pi_token_monthly_used_quota_task_extend.py | 44 + api/services/account_service_extend.py | 158 + api/services/app_generate_service_extend.py | 25 + api/services/app_service.py | 40 +- api/services/ding_talk_extend.py | 200 + api/services/feature_service.py | 16 + api/services/model_provider_service_extend.py | 140 + api/services/model_service_extend.py | 70 + .../database/database_retrieval.py | 65 +- .../recommended_app_service_extend.py | 188 + api/services/workspace_service.py | 11 + ..._workflow_node_execution_created_extend.py | 103 + .../code_executor/test_code_javascript.py | 6 +- .../nodes/code_executor/test_code_jinja2.py | 2 +- .../nodes/code_executor/test_code_python3.py | 4 +- docker/docker-compose.dify-plus.yaml | 653 ++ images/dify-plus/API密钥列表.png | Bin 0 -> 34096 bytes images/dify-plus/API调用测试.jpg | Bin 0 -> 87092 bytes images/dify-plus/个人额度修改.jpg | Bin 0 -> 93367 bytes images/dify-plus/创建API密钥.jpg | Bin 0 -> 20062 bytes images/dify-plus/同步至应用模版.jpg | Bin 0 -> 8976 bytes images/dify-plus/密钥使用分析.jpg | Bin 0 -> 94750 bytes images/dify-plus/应用中心.jpg | Bin 0 -> 116685 bytes images/dify-plus/每月密钥额度花费.jpg | Bin 0 -> 87488 bytes images/dify-plus/用户额度显示.jpg | Bin 0 -> 3649 bytes images/dify-plus/费用报表.png | Bin 0 -> 138820 bytes images/dify_plus.png | Bin 0 -> 20324 bytes web/.env.example | 7 + web/Dockerfile | 4 +- .../app/(appDetailLayout)/[appId]/layout.tsx | 21 +- .../[appId]/user_overview_extend/page.tsx | 110 + web/app/(commonLayout)/apps/AppCard.tsx | 115 +- web/app/(commonLayout)/apps/Apps.tsx | 3 +- .../explore/apps-center-extend/page.tsx | 8 + web/app/components/app/overview/appCard.tsx | 2 +- web/app/components/app/overview/appChart.tsx | 1 + .../base/auto-select-extend/index.tsx | 329 + .../base/auto-select-extend/locale.tsx | 101 + .../chat/chat-with-history/chat-wrapper.tsx | 2 +- .../base/chat/chat-with-history/index.tsx | 21 +- web/app/components/base/chat/chat/index.tsx | 15 + .../base/chat/embedded-chatbot/index.tsx | 17 + .../components/base/image-uploader/hooks.ts | 13 +- web/app/components/base/mermaid/index.tsx | 55 +- .../components/base/mermaid/modal.module.css | 28 + .../base/param-item/day-limit-item-extend.tsx | 48 + .../param-item/month-limit-item-extend.tsx | 48 + .../react-multi-email-extend/index.module.css | 12 + .../base/react-multi-email-extend/index.tsx | 67 + .../components/base/tag-management/filter.tsx | 25 +- web/app/components/develop/index.tsx | 4 +- .../develop/secret-key/assets/edit-hover.svg | 1 + .../develop/secret-key/assets/edit.svg | 1 + .../develop/secret-key/secret-key-modal.tsx | 115 +- .../secret-key-quota-set-modal-extend.tsx | 92 + .../develop/secret-key/style.module.css | 23 +- .../explore/app-card-extend/index.tsx | 98 + .../explore/app-list-center-extend/index.tsx | 265 + .../app-list-center-extend/style.module.css | 35 + web/app/components/explore/app-list/index.tsx | 160 +- web/app/components/explore/category.tsx | 12 +- .../explore/item-operation/index.tsx | 13 +- .../explore/sidebar/app-nav-item/index.tsx | 26 +- web/app/components/explore/sidebar/index.tsx | 36 +- .../header/account-dropdown/index.tsx | 9 + .../header/account-money-extend/index.tsx | 33 + .../header/account-setting/index.tsx | 13 +- .../members-page/invite-modal/index.tsx | 8 +- .../parameter-item-extend.tsx | 236 + .../model-parameter-modal/parameter-item.tsx | 2 - .../components/header/explore-nav/index.tsx | 2 +- web/app/components/header/index.tsx | 15 +- .../share/text-generation/index.tsx | 11 + .../run-batch/csv-reader/index.tsx | 126 +- web/app/components/share/utils.ts | 6 +- web/app/components/swr-initor.tsx | 8 + web/app/components/tools/provider/detail.tsx | 1 + web/app/layout.tsx | 10 +- web/app/signin/assets/ding.svg | 3 + web/app/signin/assets/oauth2.svg | 9 + web/app/signin/components/dingtalk-auth.tsx | 45 + .../components/mail-and-password-auth.tsx | 11 +- web/app/signin/normalForm.tsx | 88 +- web/app/signin/page.module.css | 20 +- web/context/app-context.tsx | 12 +- web/context/debug-configuration.ts | 13 + web/i18n/de-DE/extend.ts | 63 + web/i18n/de-DE/login.ts | 1 + web/i18n/en-US/extend.ts | 63 + web/i18n/en-US/login.ts | 1 + web/i18n/es-ES/extend.ts | 63 + web/i18n/fa-IR/extend.ts | 63 + web/i18n/fr-FR/extend.ts | 63 + web/i18n/fr-FR/login.ts | 1 + web/i18n/hi-IN/extend.ts | 63 + web/i18n/i18next-config.ts | 1 + web/i18n/it-IT/extend.ts | 63 + web/i18n/ja-JP/extend.ts | 63 + web/i18n/ja-JP/login.ts | 1 + web/i18n/ko-KR/extend.ts | 63 + web/i18n/ko-KR/login.ts | 1 + web/i18n/pl-PL/extend.ts | 63 + web/i18n/pl-PL/login.ts | 1 + web/i18n/pt-BR/extend.ts | 63 + web/i18n/pt-BR/login.ts | 1 + web/i18n/ro-RO/extend.ts | 63 + web/i18n/ro-RO/login.ts | 1 + web/i18n/ru-RU/extend.ts | 63 + web/i18n/sl-SI/extend.ts | 63 + web/i18n/th-TH/extend.ts | 63 + web/i18n/tr-TR/extend.ts | 63 + web/i18n/uk-UA/extend.ts | 62 + web/i18n/uk-UA/login.ts | 1 + web/i18n/vi-VN/extend.ts | 63 + web/i18n/vi-VN/login.ts | 1 + web/i18n/zh-Hans/extend.ts | 63 + web/i18n/zh-Hans/login.ts | 1 + web/i18n/zh-Hant/extend.ts | 43 + web/i18n/zh-Hant/login.ts | 1 + web/models/app.ts | 21 + web/models/common-extend.ts | 4 + web/models/common.ts | 44 + web/models/explore.ts | 3 +- web/next.config.js | 2 +- web/package.json | 4 + web/service/apps.ts | 16 +- web/service/base.ts | 6 + web/service/common-extend.ts | 6 + web/service/common.ts | 2 +- web/service/explore.ts | 9 + web/service/web-extend.ts | 10 + web/types/feature.ts | 8 + web/types/workspace-extend.ts | 10 + web/yarn.lock | 8251 +++++++++-------- 869 files changed, 99420 insertions(+), 6830 deletions(-) create mode 100644 README_DIFY.md create mode 100644 admin/.gitattributes create mode 100644 admin/.gitignore create mode 100644 admin/CODE_OF_CONDUCT.md create mode 100644 admin/CONTRIBUTING.md create mode 100644 admin/LICENSE create mode 100644 admin/Makefile create mode 100644 admin/deploy/docker-compose/docker-compose-dev.yaml create mode 100644 admin/deploy/docker-compose/docker-compose.yaml create mode 100644 admin/deploy/docker/Dockerfile create mode 100644 admin/deploy/docker/entrypoint.sh create mode 100644 admin/deploy/kubernetes/server/gva-server-configmap.yaml create mode 100644 admin/deploy/kubernetes/server/gva-server-deployment.yaml create mode 100644 admin/deploy/kubernetes/server/gva-server-service.yaml create mode 100644 admin/deploy/kubernetes/web/gva-web-configmap.yaml create mode 100644 admin/deploy/kubernetes/web/gva-web-deploymemt.yaml create mode 100644 admin/deploy/kubernetes/web/gva-web-ingress.yaml create mode 100644 admin/deploy/kubernetes/web/gva-web-service.yaml create mode 100644 admin/docs/gin-vue-admin.png create mode 100644 admin/gin-vue-admin.code-workspace create mode 100644 admin/server/Dockerfile create mode 100644 admin/server/README.md create mode 100644 admin/server/api/v1/enter.go create mode 100644 admin/server/api/v1/example/enter.go create mode 100644 admin/server/api/v1/example/exa_breakpoint_continue.go create mode 100644 admin/server/api/v1/example/exa_customer.go create mode 100644 admin/server/api/v1/example/exa_file_upload_download.go create mode 100644 admin/server/api/v1/gaia/dashboard.go create mode 100644 admin/server/api/v1/gaia/enter.go create mode 100644 admin/server/api/v1/gaia/quota.go create mode 100644 admin/server/api/v1/gaia/system.go create mode 100644 admin/server/api/v1/gaia/tenants.go create mode 100644 admin/server/api/v1/gaia/test.go create mode 100644 admin/server/api/v1/system/auto_code_history.go create mode 100644 admin/server/api/v1/system/auto_code_package.go create mode 100644 admin/server/api/v1/system/auto_code_plugin.go create mode 100644 admin/server/api/v1/system/auto_code_template.go create mode 100644 admin/server/api/v1/system/enter.go create mode 100644 admin/server/api/v1/system/sys_api.go create mode 100644 admin/server/api/v1/system/sys_authority.go create mode 100644 admin/server/api/v1/system/sys_authority_btn.go create mode 100644 admin/server/api/v1/system/sys_auto_code.go create mode 100644 admin/server/api/v1/system/sys_captcha.go create mode 100644 admin/server/api/v1/system/sys_casbin.go create mode 100644 admin/server/api/v1/system/sys_dictionary.go create mode 100644 admin/server/api/v1/system/sys_dictionary_detail.go create mode 100644 admin/server/api/v1/system/sys_export_template.go create mode 100644 admin/server/api/v1/system/sys_initdb.go create mode 100644 admin/server/api/v1/system/sys_jwt_blacklist.go create mode 100644 admin/server/api/v1/system/sys_menu.go create mode 100644 admin/server/api/v1/system/sys_operation_record.go create mode 100644 admin/server/api/v1/system/sys_params.go create mode 100644 admin/server/api/v1/system/sys_system.go create mode 100644 admin/server/api/v1/system/sys_user.go create mode 100644 admin/server/api/v1/system/sys_user_extend.go create mode 100644 admin/server/config.docker.yaml create mode 100644 admin/server/config.yaml create mode 100644 admin/server/config/auto_code.go create mode 100644 admin/server/config/captcha.go create mode 100644 admin/server/config/config.go create mode 100644 admin/server/config/cors.go create mode 100644 admin/server/config/db_list.go create mode 100644 admin/server/config/disk.go create mode 100644 admin/server/config/email.go create mode 100644 admin/server/config/excel.go create mode 100644 admin/server/config/gaia.go create mode 100644 admin/server/config/gorm_mssql.go create mode 100644 admin/server/config/gorm_mysql.go create mode 100644 admin/server/config/gorm_oracle.go create mode 100644 admin/server/config/gorm_pgsql.go create mode 100644 admin/server/config/gorm_sqlite.go create mode 100644 admin/server/config/jwt.go create mode 100644 admin/server/config/mongo.go create mode 100644 admin/server/config/oa_login.go create mode 100644 admin/server/config/oss_aliyun.go create mode 100644 admin/server/config/oss_aws.go create mode 100644 admin/server/config/oss_cloudflare.go create mode 100644 admin/server/config/oss_huawei.go create mode 100644 admin/server/config/oss_local.go create mode 100644 admin/server/config/oss_minio.go create mode 100644 admin/server/config/oss_qiniu.go create mode 100644 admin/server/config/oss_tencent.go create mode 100644 admin/server/config/redis.go create mode 100644 admin/server/config/system.go create mode 100644 admin/server/config/zap.go create mode 100644 admin/server/core/internal/constant.go create mode 100644 admin/server/core/internal/cutter.go create mode 100644 admin/server/core/internal/zap_core.go create mode 100644 admin/server/core/server.go create mode 100644 admin/server/core/server_other.go create mode 100644 admin/server/core/server_win.go create mode 100644 admin/server/core/viper.go create mode 100644 admin/server/core/zap.go create mode 100644 admin/server/corn/main.go create mode 100644 admin/server/docs/docs.go create mode 100644 admin/server/docs/swagger.json create mode 100644 admin/server/docs/swagger.yaml create mode 100644 admin/server/global/global.go create mode 100644 admin/server/global/model.go create mode 100644 admin/server/go.mod create mode 100644 admin/server/go.sum create mode 100644 admin/server/initialize/db_list.go create mode 100644 admin/server/initialize/ensure_tables.go create mode 100644 admin/server/initialize/gorm.go create mode 100644 admin/server/initialize/gorm_biz.go create mode 100644 admin/server/initialize/gorm_mssql.go create mode 100644 admin/server/initialize/gorm_mysql.go create mode 100644 admin/server/initialize/gorm_oracle.go create mode 100644 admin/server/initialize/gorm_pgsql.go create mode 100644 admin/server/initialize/gorm_sqlite.go create mode 100644 admin/server/initialize/internal/gorm.go create mode 100644 admin/server/initialize/internal/gorm_logger_writer.go create mode 100644 admin/server/initialize/internal/mongo.go create mode 100644 admin/server/initialize/mongo.go create mode 100644 admin/server/initialize/other.go create mode 100644 admin/server/initialize/plugin.go create mode 100644 admin/server/initialize/plugin_biz_v1.go create mode 100644 admin/server/initialize/plugin_biz_v2.go create mode 100644 admin/server/initialize/redis.go create mode 100644 admin/server/initialize/register_init.go create mode 100644 admin/server/initialize/router.go create mode 100644 admin/server/initialize/router_biz.go create mode 100644 admin/server/initialize/timer.go create mode 100644 admin/server/initialize/validator.go create mode 100644 admin/server/main.go create mode 100644 admin/server/middleware/casbin_rbac.go create mode 100644 admin/server/middleware/cors.go create mode 100644 admin/server/middleware/email.go create mode 100644 admin/server/middleware/error.go create mode 100644 admin/server/middleware/jwt.go create mode 100644 admin/server/middleware/limit_ip.go create mode 100644 admin/server/middleware/loadtls.go create mode 100644 admin/server/middleware/logger.go create mode 100644 admin/server/middleware/operation.go create mode 100644 admin/server/model/common/basetypes.go create mode 100644 admin/server/model/common/clearDB.go create mode 100644 admin/server/model/common/request/common.go create mode 100644 admin/server/model/common/response/common.go create mode 100644 admin/server/model/common/response/response.go create mode 100644 admin/server/model/example/exa_breakpoint_continue.go create mode 100644 admin/server/model/example/exa_customer.go create mode 100644 admin/server/model/example/exa_file_upload_download.go create mode 100644 admin/server/model/example/response/exa_breakpoint_continue.go create mode 100644 admin/server/model/example/response/exa_customer.go create mode 100644 admin/server/model/example/response/exa_file_upload_download.go create mode 100644 admin/server/model/gaia/ForwardingExtend.go create mode 100644 admin/server/model/gaia/account.go create mode 100644 admin/server/model/gaia/account_money_extend.go create mode 100644 admin/server/model/gaia/api_tokens.go create mode 100644 admin/server/model/gaia/apps.go create mode 100644 admin/server/model/gaia/end_user.go create mode 100644 admin/server/model/gaia/messages.go create mode 100644 admin/server/model/gaia/request/dashboard.go create mode 100644 admin/server/model/gaia/request/quota.go create mode 100644 admin/server/model/gaia/request/tenants.go create mode 100644 admin/server/model/gaia/request/test.go create mode 100644 admin/server/model/gaia/response/dashboard.go create mode 100644 admin/server/model/gaia/response/quota.go create mode 100644 admin/server/model/gaia/response/test.go create mode 100644 admin/server/model/gaia/system_integration.go create mode 100644 admin/server/model/gaia/tenants.go create mode 100644 admin/server/model/gaia/test.go create mode 100644 admin/server/model/gaia/workflow_node_executions.go create mode 100644 admin/server/model/gaia/workflow_runs.go create mode 100644 admin/server/model/system/request/jwt.go create mode 100644 admin/server/model/system/request/oa_login.go create mode 100644 admin/server/model/system/request/sys_api.go create mode 100644 admin/server/model/system/request/sys_authority_btn.go create mode 100644 admin/server/model/system/request/sys_auto_code.go create mode 100644 admin/server/model/system/request/sys_auto_code_package.go create mode 100644 admin/server/model/system/request/sys_auto_history.go create mode 100644 admin/server/model/system/request/sys_casbin.go create mode 100644 admin/server/model/system/request/sys_dictionary_detail.go create mode 100644 admin/server/model/system/request/sys_export_template.go create mode 100644 admin/server/model/system/request/sys_init.go create mode 100644 admin/server/model/system/request/sys_menu.go create mode 100644 admin/server/model/system/request/sys_operation_record.go create mode 100644 admin/server/model/system/request/sys_params.go create mode 100644 admin/server/model/system/request/sys_user.go create mode 100644 admin/server/model/system/response/oa_login.go create mode 100644 admin/server/model/system/response/sys_api.go create mode 100644 admin/server/model/system/response/sys_authority.go create mode 100644 admin/server/model/system/response/sys_authority_btn.go create mode 100644 admin/server/model/system/response/sys_auto_code.go create mode 100644 admin/server/model/system/response/sys_captcha.go create mode 100644 admin/server/model/system/response/sys_casbin.go create mode 100644 admin/server/model/system/response/sys_menu.go create mode 100644 admin/server/model/system/response/sys_system.go create mode 100644 admin/server/model/system/response/sys_user.go create mode 100644 admin/server/model/system/sys_api.go create mode 100644 admin/server/model/system/sys_authority.go create mode 100644 admin/server/model/system/sys_authority_btn.go create mode 100644 admin/server/model/system/sys_authority_menu.go create mode 100644 admin/server/model/system/sys_auto_code_history.go create mode 100644 admin/server/model/system/sys_auto_code_package.go create mode 100644 admin/server/model/system/sys_base_menu.go create mode 100644 admin/server/model/system/sys_dictionary.go create mode 100644 admin/server/model/system/sys_dictionary_detail.go create mode 100644 admin/server/model/system/sys_export_template.go create mode 100644 admin/server/model/system/sys_jwt_blacklist.go create mode 100644 admin/server/model/system/sys_menu_btn.go create mode 100644 admin/server/model/system/sys_operation_record.go create mode 100644 admin/server/model/system/sys_params.go create mode 100644 admin/server/model/system/sys_system.go create mode 100644 admin/server/model/system/sys_user.go create mode 100644 admin/server/model/system/sys_user_authority.go create mode 100644 admin/server/model/system/sys_user_extend.go create mode 100644 admin/server/plugin/announcement/api/enter.go create mode 100644 admin/server/plugin/announcement/api/info.go create mode 100644 admin/server/plugin/announcement/config/config.go create mode 100644 admin/server/plugin/announcement/gen/gen.go create mode 100644 admin/server/plugin/announcement/initialize/api.go create mode 100644 admin/server/plugin/announcement/initialize/gorm.go create mode 100644 admin/server/plugin/announcement/initialize/menu.go create mode 100644 admin/server/plugin/announcement/initialize/router.go create mode 100644 admin/server/plugin/announcement/initialize/viper.go create mode 100644 admin/server/plugin/announcement/model/info.go create mode 100644 admin/server/plugin/announcement/model/request/info.go create mode 100644 admin/server/plugin/announcement/plugin.go create mode 100644 admin/server/plugin/announcement/plugin/plugin.go create mode 100644 admin/server/plugin/announcement/router/enter.go create mode 100644 admin/server/plugin/announcement/router/info.go create mode 100644 admin/server/plugin/announcement/service/enter.go create mode 100644 admin/server/plugin/announcement/service/info.go create mode 100644 admin/server/plugin/email/README.MD create mode 100644 admin/server/plugin/email/api/enter.go create mode 100644 admin/server/plugin/email/api/sys_email.go create mode 100644 admin/server/plugin/email/config/email.go create mode 100644 admin/server/plugin/email/global/gloabl.go create mode 100644 admin/server/plugin/email/main.go create mode 100644 admin/server/plugin/email/model/response/email.go create mode 100644 admin/server/plugin/email/router/enter.go create mode 100644 admin/server/plugin/email/router/sys_email.go create mode 100644 admin/server/plugin/email/service/enter.go create mode 100644 admin/server/plugin/email/service/sys_email.go create mode 100644 admin/server/plugin/email/utils/email.go create mode 100644 admin/server/plugin/plugin-tool/utils/check.go create mode 100644 admin/server/resource/function/api.go.tpl create mode 100644 admin/server/resource/function/api.js.tpl create mode 100644 admin/server/resource/function/server.go.tpl create mode 100644 admin/server/resource/package/readme.txt.tpl create mode 100644 admin/server/resource/package/server/api/api.go.tpl create mode 100644 admin/server/resource/package/server/api/enter.go.tpl create mode 100644 admin/server/resource/package/server/model/model.go.tpl create mode 100644 admin/server/resource/package/server/model/request/request.go.tpl create mode 100644 admin/server/resource/package/server/router/enter.go.tpl create mode 100644 admin/server/resource/package/server/router/router.go.tpl create mode 100644 admin/server/resource/package/server/service/enter.go.tpl create mode 100644 admin/server/resource/package/server/service/service.go.tpl create mode 100644 admin/server/resource/package/web/api/api.js.tpl create mode 100644 admin/server/resource/package/web/view/form.vue.tpl create mode 100644 admin/server/resource/package/web/view/table.vue.tpl create mode 100644 admin/server/resource/plugin/server/api/api.go.template create mode 100644 admin/server/resource/plugin/server/api/enter.go.template create mode 100644 admin/server/resource/plugin/server/config/config.go.template create mode 100644 admin/server/resource/plugin/server/gen/gen.go.template create mode 100644 admin/server/resource/plugin/server/initialize/api.go.template create mode 100644 admin/server/resource/plugin/server/initialize/gorm.go.template create mode 100644 admin/server/resource/plugin/server/initialize/menu.go.template create mode 100644 admin/server/resource/plugin/server/initialize/router.go.template create mode 100644 admin/server/resource/plugin/server/initialize/viper.go.template create mode 100644 admin/server/resource/plugin/server/model/model.go.template create mode 100644 admin/server/resource/plugin/server/model/request/request.go.template create mode 100644 admin/server/resource/plugin/server/plugin.go.template create mode 100644 admin/server/resource/plugin/server/plugin/plugin.go.template create mode 100644 admin/server/resource/plugin/server/router/enter.go.template create mode 100644 admin/server/resource/plugin/server/router/router.go.template create mode 100644 admin/server/resource/plugin/server/service/enter.go.template create mode 100644 admin/server/resource/plugin/server/service/service.go.template create mode 100644 admin/server/resource/plugin/web/api/api.js.template create mode 100644 admin/server/resource/plugin/web/form/form.vue.template create mode 100644 admin/server/resource/plugin/web/view/view.vue.template create mode 100644 admin/server/router/enter.go create mode 100644 admin/server/router/example/enter.go create mode 100644 admin/server/router/example/exa_customer.go create mode 100644 admin/server/router/example/exa_file_upload_and_download.go create mode 100644 admin/server/router/gaia/dashboard.go create mode 100644 admin/server/router/gaia/enter.go create mode 100644 admin/server/router/gaia/quota.go create mode 100644 admin/server/router/gaia/system.go create mode 100644 admin/server/router/gaia/tenants.go create mode 100644 admin/server/router/gaia/test.go create mode 100644 admin/server/router/system/enter.go create mode 100644 admin/server/router/system/sys_api.go create mode 100644 admin/server/router/system/sys_authority.go create mode 100644 admin/server/router/system/sys_authority_btn.go create mode 100644 admin/server/router/system/sys_auto_code.go create mode 100644 admin/server/router/system/sys_auto_code_history.go create mode 100644 admin/server/router/system/sys_base.go create mode 100644 admin/server/router/system/sys_casbin.go create mode 100644 admin/server/router/system/sys_dictionary.go create mode 100644 admin/server/router/system/sys_dictionary_detail.go create mode 100644 admin/server/router/system/sys_export_template.go create mode 100644 admin/server/router/system/sys_initdb.go create mode 100644 admin/server/router/system/sys_jwt.go create mode 100644 admin/server/router/system/sys_menu.go create mode 100644 admin/server/router/system/sys_operation_record.go create mode 100644 admin/server/router/system/sys_params.go create mode 100644 admin/server/router/system/sys_system.go create mode 100644 admin/server/router/system/sys_user.go create mode 100644 admin/server/service/enter.go create mode 100644 admin/server/service/example/enter.go create mode 100644 admin/server/service/example/exa_breakpoint_continue.go create mode 100644 admin/server/service/example/exa_customer.go create mode 100644 admin/server/service/example/exa_file_upload_download.go create mode 100644 admin/server/service/gaia/account.go create mode 100644 admin/server/service/gaia/copy.go create mode 100644 admin/server/service/gaia/dashboard.go create mode 100644 admin/server/service/gaia/encode.go create mode 100644 admin/server/service/gaia/enter.go create mode 100644 admin/server/service/gaia/quota.go create mode 100644 admin/server/service/gaia/system.go create mode 100644 admin/server/service/gaia/tenants.go create mode 100644 admin/server/service/gaia/test.go create mode 100644 admin/server/service/system/auto_code_history.go create mode 100644 admin/server/service/system/auto_code_package.go create mode 100644 admin/server/service/system/auto_code_package_test.go create mode 100644 admin/server/service/system/auto_code_plugin.go create mode 100644 admin/server/service/system/auto_code_template.go create mode 100644 admin/server/service/system/auto_code_template_test.go create mode 100644 admin/server/service/system/enter.go create mode 100644 admin/server/service/system/jwt_black_list.go create mode 100644 admin/server/service/system/sys_api.go create mode 100644 admin/server/service/system/sys_authority.go create mode 100644 admin/server/service/system/sys_authority_btn.go create mode 100644 admin/server/service/system/sys_auto_code_interface.go create mode 100644 admin/server/service/system/sys_auto_code_mssql.go create mode 100644 admin/server/service/system/sys_auto_code_mysql.go create mode 100644 admin/server/service/system/sys_auto_code_oracle.go create mode 100644 admin/server/service/system/sys_auto_code_pgsql.go create mode 100644 admin/server/service/system/sys_auto_code_sqlite.go create mode 100644 admin/server/service/system/sys_base_menu.go create mode 100644 admin/server/service/system/sys_casbin.go create mode 100644 admin/server/service/system/sys_dictionary.go create mode 100644 admin/server/service/system/sys_dictionary_detail.go create mode 100644 admin/server/service/system/sys_export_template.go create mode 100644 admin/server/service/system/sys_initdb.go create mode 100644 admin/server/service/system/sys_initdb_mssql.go create mode 100644 admin/server/service/system/sys_initdb_mysql.go create mode 100644 admin/server/service/system/sys_initdb_pgsql.go create mode 100644 admin/server/service/system/sys_initdb_sqlite.go create mode 100644 admin/server/service/system/sys_menu.go create mode 100644 admin/server/service/system/sys_operation_record.go create mode 100644 admin/server/service/system/sys_params.go create mode 100644 admin/server/service/system/sys_system.go create mode 100644 admin/server/service/system/sys_user.go create mode 100644 admin/server/service/system/sys_user_extend.go create mode 100644 admin/server/source/example/file_upload_download.go create mode 100644 admin/server/source/system/api.go create mode 100644 admin/server/source/system/api_ignore.go create mode 100644 admin/server/source/system/authorities_menus.go create mode 100644 admin/server/source/system/authority.go create mode 100644 admin/server/source/system/casbin.go create mode 100644 admin/server/source/system/dictionary.go create mode 100644 admin/server/source/system/dictionary_detail.go create mode 100644 admin/server/source/system/excel_template.go create mode 100644 admin/server/source/system/menu.go create mode 100644 admin/server/source/system/user.go create mode 100644 admin/server/task/clearTable.go create mode 100644 admin/server/utils/ast/ast.go create mode 100644 admin/server/utils/ast/ast_auto_enter.go create mode 100644 admin/server/utils/ast/ast_enter.go create mode 100644 admin/server/utils/ast/ast_gorm.go create mode 100644 admin/server/utils/ast/ast_init_test.go create mode 100644 admin/server/utils/ast/ast_rollback.go create mode 100644 admin/server/utils/ast/ast_router.go create mode 100644 admin/server/utils/ast/ast_test.go create mode 100644 admin/server/utils/ast/ast_type.go create mode 100644 admin/server/utils/ast/import.go create mode 100644 admin/server/utils/ast/interfaces.go create mode 100644 admin/server/utils/ast/interfaces_base.go create mode 100644 admin/server/utils/ast/package_enter.go create mode 100644 admin/server/utils/ast/package_enter_test.go create mode 100644 admin/server/utils/ast/package_initialize_gorm.go create mode 100644 admin/server/utils/ast/package_initialize_gorm_test.go create mode 100644 admin/server/utils/ast/package_initialize_router.go create mode 100644 admin/server/utils/ast/package_initialize_router_test.go create mode 100644 admin/server/utils/ast/package_module_enter.go create mode 100644 admin/server/utils/ast/package_module_enter_test.go create mode 100644 admin/server/utils/ast/plugin_enter.go create mode 100644 admin/server/utils/ast/plugin_enter_test.go create mode 100644 admin/server/utils/ast/plugin_gen.go create mode 100644 admin/server/utils/ast/plugin_gen_test.go create mode 100644 admin/server/utils/ast/plugin_initialize_gorm.go create mode 100644 admin/server/utils/ast/plugin_initialize_gorm_test.go create mode 100644 admin/server/utils/ast/plugin_initialize_router.go create mode 100644 admin/server/utils/ast/plugin_initialize_router_test.go create mode 100644 admin/server/utils/ast/plugin_initialize_v2.go create mode 100644 admin/server/utils/ast/plugin_initialize_v2_test.go create mode 100644 admin/server/utils/breakpoint_continue.go create mode 100644 admin/server/utils/captcha/redis.go create mode 100644 admin/server/utils/claims.go create mode 100644 admin/server/utils/directory.go create mode 100644 admin/server/utils/encode.go create mode 100644 admin/server/utils/fmt_plus.go create mode 100644 admin/server/utils/fun.go create mode 100644 admin/server/utils/hash.go create mode 100644 admin/server/utils/human_duration.go create mode 100644 admin/server/utils/human_duration_test.go create mode 100644 admin/server/utils/json.go create mode 100644 admin/server/utils/json_test.go create mode 100644 admin/server/utils/jwt.go create mode 100644 admin/server/utils/plugin/plugin.go create mode 100644 admin/server/utils/plugin/v2/plugin.go create mode 100644 admin/server/utils/reload.go create mode 100644 admin/server/utils/request/http.go create mode 100644 admin/server/utils/server.go create mode 100644 admin/server/utils/timer/timed_task.go create mode 100644 admin/server/utils/timer/timed_task_test.go create mode 100644 admin/server/utils/upload/aliyun_oss.go create mode 100644 admin/server/utils/upload/aws_s3.go create mode 100644 admin/server/utils/upload/cloudflare_r2.go create mode 100644 admin/server/utils/upload/local.go create mode 100644 admin/server/utils/upload/minio_oss.go create mode 100644 admin/server/utils/upload/obs.go create mode 100644 admin/server/utils/upload/qiniu.go create mode 100644 admin/server/utils/upload/tencent_cos.go create mode 100644 admin/server/utils/upload/upload.go create mode 100644 admin/server/utils/validator.go create mode 100644 admin/server/utils/validator_test.go create mode 100644 admin/server/utils/verify.go create mode 100644 admin/server/utils/verify_extend.go create mode 100644 admin/server/utils/zip.go create mode 100644 admin/web/.docker-compose/nginx/conf.d/my.conf create mode 100644 admin/web/.docker-compose/nginx/conf.d/nginx.conf create mode 100644 admin/web/.dockerignore create mode 100644 admin/web/.env.development create mode 100644 admin/web/.env.production create mode 100644 admin/web/.eslintignore create mode 100644 admin/web/.eslintrc.js create mode 100644 admin/web/.gitignore create mode 100644 admin/web/Dockerfile create mode 100644 admin/web/babel.config.js create mode 100644 admin/web/index.html create mode 100644 admin/web/jsconfig.json create mode 100644 admin/web/limit.js create mode 100644 admin/web/package.json create mode 100644 admin/web/postcss.config.js create mode 100644 admin/web/public/favicon.ico create mode 100644 admin/web/public/logo.png create mode 100644 admin/web/src/App.vue create mode 100644 admin/web/src/api/api.js create mode 100644 admin/web/src/api/authority.js create mode 100644 admin/web/src/api/authorityBtn.js create mode 100644 admin/web/src/api/autoCode.js create mode 100644 admin/web/src/api/breakpoint.js create mode 100644 admin/web/src/api/casbin.js create mode 100644 admin/web/src/api/customer.js create mode 100644 admin/web/src/api/email.js create mode 100644 admin/web/src/api/exportTemplate.js create mode 100644 admin/web/src/api/fileUploadAndDownload.js create mode 100644 admin/web/src/api/gaia/dashboard.js create mode 100644 admin/web/src/api/gaia/providers.js create mode 100644 admin/web/src/api/gaia/system.js create mode 100644 admin/web/src/api/gaia/tenants.js create mode 100644 admin/web/src/api/gaia/test.js create mode 100644 admin/web/src/api/initdb.js create mode 100644 admin/web/src/api/jwt.js create mode 100644 admin/web/src/api/menu.js create mode 100644 admin/web/src/api/sysDictionary.js create mode 100644 admin/web/src/api/sysDictionaryDetail.js create mode 100644 admin/web/src/api/sysOperationRecord.js create mode 100644 admin/web/src/api/sysParams.js create mode 100644 admin/web/src/api/system.js create mode 100644 admin/web/src/api/user.js create mode 100644 admin/web/src/api/user_extend.js create mode 100644 admin/web/src/assets/404.png create mode 100644 admin/web/src/assets/background.svg create mode 100644 admin/web/src/assets/banner.jpg create mode 100644 admin/web/src/assets/banner2.jpg create mode 100644 admin/web/src/assets/dashboard.png create mode 100644 admin/web/src/assets/docs.png create mode 100644 admin/web/src/assets/flipped-aurora.png create mode 100644 admin/web/src/assets/github.png create mode 100644 admin/web/src/assets/icons/ai-gva.svg create mode 100644 admin/web/src/assets/icons/customer-gva.svg create mode 100644 admin/web/src/assets/kefu.png create mode 100644 admin/web/src/assets/login_background.jpg create mode 100644 admin/web/src/assets/login_background.svg create mode 100644 admin/web/src/assets/login_left.svg create mode 100644 admin/web/src/assets/login_right_banner.jpg create mode 100644 admin/web/src/assets/logo.jpg create mode 100644 admin/web/src/assets/logo.png create mode 100644 admin/web/src/assets/logo_login.png create mode 100644 admin/web/src/assets/nav_logo.png create mode 100644 admin/web/src/assets/noBody.png create mode 100644 admin/web/src/assets/notFound.png create mode 100644 admin/web/src/assets/qm.png create mode 100644 admin/web/src/assets/video.png create mode 100644 admin/web/src/components/arrayCtrl/arrayCtrl.vue create mode 100644 admin/web/src/components/bottomInfo/bottomInfo.vue create mode 100644 admin/web/src/components/charts/index.vue create mode 100644 admin/web/src/components/commandMenu/index.vue create mode 100644 admin/web/src/components/customPic/index.vue create mode 100644 admin/web/src/components/exportExcel/exportExcel.vue create mode 100644 admin/web/src/components/exportExcel/exportTemplate.vue create mode 100644 admin/web/src/components/exportExcel/importExcel.vue create mode 100644 admin/web/src/components/office/docx.vue create mode 100644 admin/web/src/components/office/excel.vue create mode 100644 admin/web/src/components/office/index.vue create mode 100644 admin/web/src/components/office/pdf.vue create mode 100644 admin/web/src/components/richtext/rich-edit.vue create mode 100644 admin/web/src/components/richtext/rich-view.vue create mode 100644 admin/web/src/components/selectFile/selectFile.vue create mode 100644 admin/web/src/components/selectImage/selectComponent.vue create mode 100644 admin/web/src/components/selectImage/selectImage.vue create mode 100644 admin/web/src/components/svgIcon/svgIcon.vue create mode 100644 admin/web/src/components/upload/common.vue create mode 100644 admin/web/src/components/upload/image.vue create mode 100644 admin/web/src/components/warningBar/warningBar.vue create mode 100644 admin/web/src/core/config.js create mode 100644 admin/web/src/core/gin-vue-admin.js create mode 100644 admin/web/src/core/global.js create mode 100644 admin/web/src/directive/auth.js create mode 100644 admin/web/src/hooks/charts.js create mode 100644 admin/web/src/hooks/responsive.js create mode 100644 admin/web/src/hooks/use-windows-resize.js create mode 100644 admin/web/src/main.js create mode 100644 admin/web/src/pathInfo.json create mode 100644 admin/web/src/permission.js create mode 100644 admin/web/src/pinia/index.js create mode 100644 admin/web/src/pinia/modules/app.js create mode 100644 admin/web/src/pinia/modules/dictionary.js create mode 100644 admin/web/src/pinia/modules/router.js create mode 100644 admin/web/src/pinia/modules/user.js create mode 100644 admin/web/src/plugin/announcement/api/info.js create mode 100644 admin/web/src/plugin/announcement/form/info.vue create mode 100644 admin/web/src/plugin/announcement/view/info.vue create mode 100644 admin/web/src/plugin/email/api/email.js create mode 100644 admin/web/src/plugin/email/view/index.vue create mode 100644 admin/web/src/router/index.js create mode 100644 admin/web/src/style/element/index.scss create mode 100644 admin/web/src/style/element_visiable.scss create mode 100644 admin/web/src/style/iconfont.css create mode 100644 admin/web/src/style/main.scss create mode 100644 admin/web/src/style/reset.scss create mode 100644 admin/web/src/utils/asyncRouter.js create mode 100644 admin/web/src/utils/btnAuth.js create mode 100644 admin/web/src/utils/bus.js create mode 100644 admin/web/src/utils/closeThisPage.js create mode 100644 admin/web/src/utils/date.js create mode 100644 admin/web/src/utils/dictionary.js create mode 100644 admin/web/src/utils/doc.js create mode 100644 admin/web/src/utils/downloadImg.js create mode 100644 admin/web/src/utils/event.js create mode 100644 admin/web/src/utils/fmtRouterTitle.js create mode 100644 admin/web/src/utils/format.js create mode 100644 admin/web/src/utils/image.js create mode 100644 admin/web/src/utils/page.js create mode 100644 admin/web/src/utils/passwd.js create mode 100644 admin/web/src/utils/request.js create mode 100644 admin/web/src/utils/stringFun.js create mode 100644 admin/web/src/view/about/index.vue create mode 100644 admin/web/src/view/dashboard/components/banner.vue create mode 100644 admin/web/src/view/dashboard/components/card.vue create mode 100644 admin/web/src/view/dashboard/components/charts-content-numbers.vue create mode 100644 admin/web/src/view/dashboard/components/charts-people-numbers.vue create mode 100644 admin/web/src/view/dashboard/components/charts.vue create mode 100644 admin/web/src/view/dashboard/components/index.js create mode 100644 admin/web/src/view/dashboard/components/notice.vue create mode 100644 admin/web/src/view/dashboard/components/pluginTable.vue create mode 100644 admin/web/src/view/dashboard/components/quickLinks.vue create mode 100644 admin/web/src/view/dashboard/components/table.vue create mode 100644 admin/web/src/view/dashboard/components/wiki.vue create mode 100644 admin/web/src/view/dashboard/index.vue create mode 100644 admin/web/src/view/error/index.vue create mode 100644 admin/web/src/view/error/reload.vue create mode 100644 admin/web/src/view/example/breakpoint/breakpoint.vue create mode 100644 admin/web/src/view/example/customer/customer.vue create mode 100644 admin/web/src/view/example/index.vue create mode 100644 admin/web/src/view/example/upload/upload.vue create mode 100644 admin/web/src/view/gaia/dashboard/components/accountMoneyTable.vue create mode 100644 admin/web/src/view/gaia/dashboard/components/appTokenDailyQuotaNumbers.vue create mode 100644 admin/web/src/view/gaia/dashboard/components/appTokenQuota.vue create mode 100644 admin/web/src/view/gaia/dashboard/components/appTokenQuotaTable.vue create mode 100644 admin/web/src/view/gaia/dashboard/components/card.vue create mode 100644 admin/web/src/view/gaia/dashboard/components/charts-people-numbers.vue create mode 100644 admin/web/src/view/gaia/dashboard/components/charts.vue create mode 100644 admin/web/src/view/gaia/dashboard/components/index.js create mode 100644 admin/web/src/view/gaia/dashboard/index.vue create mode 100644 admin/web/src/view/gaia/tenants/tenants.vue create mode 100644 admin/web/src/view/init/index.vue create mode 100644 admin/web/src/view/layout/aside/asideComponent/asyncSubmenu.vue create mode 100644 admin/web/src/view/layout/aside/asideComponent/index.vue create mode 100644 admin/web/src/view/layout/aside/asideComponent/menuItem.vue create mode 100644 admin/web/src/view/layout/aside/combinationMode.vue create mode 100644 admin/web/src/view/layout/aside/headMode.vue create mode 100644 admin/web/src/view/layout/aside/index.vue create mode 100644 admin/web/src/view/layout/aside/normalMode.vue create mode 100644 admin/web/src/view/layout/header/index.vue create mode 100644 admin/web/src/view/layout/header/tools.vue create mode 100644 admin/web/src/view/layout/index.vue create mode 100644 admin/web/src/view/layout/screenfull/index.vue create mode 100644 admin/web/src/view/layout/search/search.vue create mode 100644 admin/web/src/view/layout/setting/index.vue create mode 100644 admin/web/src/view/layout/tabs/index.vue create mode 100644 admin/web/src/view/login/callback.vue create mode 100644 admin/web/src/view/login/index.vue create mode 100644 admin/web/src/view/person/person.vue create mode 100644 admin/web/src/view/quota/index.vue create mode 100644 admin/web/src/view/routerHolder.vue create mode 100644 admin/web/src/view/superAdmin/api/api.vue create mode 100644 admin/web/src/view/superAdmin/authority/authority.vue create mode 100644 admin/web/src/view/superAdmin/authority/components/apis.vue create mode 100644 admin/web/src/view/superAdmin/authority/components/datas.vue create mode 100644 admin/web/src/view/superAdmin/authority/components/menus.vue create mode 100644 admin/web/src/view/superAdmin/dictionary/sysDictionary.vue create mode 100644 admin/web/src/view/superAdmin/dictionary/sysDictionaryDetail.vue create mode 100644 admin/web/src/view/superAdmin/index.vue create mode 100644 admin/web/src/view/superAdmin/menu/components/components-cascader.vue create mode 100644 admin/web/src/view/superAdmin/menu/icon.vue create mode 100644 admin/web/src/view/superAdmin/menu/menu.vue create mode 100644 admin/web/src/view/superAdmin/operation/sysOperationRecord.vue create mode 100644 admin/web/src/view/superAdmin/params/sysParams.vue create mode 100644 admin/web/src/view/superAdmin/user/user.vue create mode 100644 admin/web/src/view/system/state.vue create mode 100644 admin/web/src/view/systemIntegrated/dingTalk/index.vue create mode 100644 admin/web/src/view/systemIntegrated/index.vue create mode 100644 admin/web/src/view/systemTools/autoCode/component/fieldDialog.vue create mode 100644 admin/web/src/view/systemTools/autoCode/component/previewCodeDialog.vue create mode 100644 admin/web/src/view/systemTools/autoCode/index.vue create mode 100644 admin/web/src/view/systemTools/autoCodeAdmin/index.vue create mode 100644 admin/web/src/view/systemTools/autoPkg/autoPkg.vue create mode 100644 admin/web/src/view/systemTools/exportTemplate/code.js create mode 100644 admin/web/src/view/systemTools/exportTemplate/exportTemplate.vue create mode 100644 admin/web/src/view/systemTools/formCreate/index.vue create mode 100644 admin/web/src/view/systemTools/index.vue create mode 100644 admin/web/src/view/systemTools/installPlugin/index.vue create mode 100644 admin/web/src/view/systemTools/pubPlug/pubPlug.vue create mode 100644 admin/web/src/view/systemTools/system/system.vue create mode 100644 admin/web/src/view/test/appRequest/index.vue create mode 100644 admin/web/src/view/test/appRequest/list.vue create mode 100644 admin/web/src/view/test/index.vue create mode 100644 admin/web/src/view/user/index.vue create mode 100644 admin/web/tailwind.config.js create mode 100644 admin/web/vite.config.js create mode 100644 admin/web/vitePlugin/componentName/index.js create mode 100644 admin/web/vitePlugin/secret/index.js create mode 100644 api/configs/extend/__init__.py create mode 100644 api/controllers/console/app/app_extend.py create mode 100644 api/controllers/console/app/ding_talk_extend.py create mode 100644 api/controllers/console/app/error_extend.py create mode 100644 api/controllers/console/app/passport_extend.py create mode 100644 api/controllers/console/auth/register_extend.py create mode 100644 api/controllers/console/error_extend.py create mode 100644 api/controllers/console/money_extend.py create mode 100644 api/controllers/console/workspace/account_extend.py create mode 100644 api/controllers/service_api/app/error_extend.py create mode 100644 api/controllers/web/error_extend.py create mode 100644 api/core/model_runtime/model_providers/bedrock/llm/anthropic.claude-3-sonnet-v1.7.yaml create mode 100644 api/core/workflow/nodes/code/control_extend.py create mode 100644 api/events/event_handlers/update_account_money_when_messaeg_created_extend.py create mode 100644 api/fields/app_fields_extend.py create mode 100644 api/fields/member_fields_extend.py create mode 100644 api/migrations/versions/06b18b329024_recommended_list_sorted_by_usage_.py create mode 100644 api/migrations/versions/09633b4cf949_add_account_money_extend.py create mode 100644 api/migrations/versions/2024_08_03_0724-a6f3333821be_add_end_user_account_joins_extend.py create mode 100644 api/migrations/versions/2024_08_05_0626-fb321d6d1ef0_add_account_money_monthly_stat_extend.py create mode 100644 api/migrations/versions/2024_08_20_0337-9cb135c9d1f8_.py create mode 100644 api/migrations/versions/2024_08_27_0308-fbd1f511a08e_add_account_layover_record_extend.py create mode 100644 api/migrations/versions/2024_08_29_0715-1b804f8bbd28_add_api_token_money_extend.py create mode 100644 api/migrations/versions/2024_08_30_0935-afc9c19af168_forwarding_address_extend_add_status.py create mode 100644 api/migrations/versions/2024_09_02_0246-cfe4907d127b_update_quota_precision_extend.py create mode 100644 api/migrations/versions/2024_09_02_0440-871d5faaa862_update_account_money_monthly_stat_quota_.py create mode 100644 api/migrations/versions/2024_09_02_0701-dd130cfd98f8_drop_api_token_money_stat_column_extend.py create mode 100644 api/migrations/versions/2024_09_03_0658-aa93d972c8c9_.py create mode 100644 api/migrations/versions/2024_09_04_1036-7eaee114bcee_add_description_api_token_money_extend.py create mode 100644 api/migrations/versions/2024_09_10_1713-0205d1137aaf_the_proxy_billing_log_table_account_.py create mode 100644 api/migrations/versions/2024_09_11_0853-39714b40d774_.py create mode 100644 api/migrations/versions/2024_09_19_0407-0853e971b36e_.py create mode 100644 api/migrations/versions/2024_10_31_1253-ca79d9b5973b_.py create mode 100644 api/migrations/versions/2024_12_03_0410-4faed5bbdb91_.py create mode 100644 api/migrations/versions/2024_12_25_1619-62dd723ee92b_.py create mode 100644 api/migrations/versions/2025_01_23_1053-37e5bf7a1e53_.py create mode 100644 api/migrations/versions/41e6e402d572_add_recommended_apps_category_join_.py create mode 100644 api/migrations/versions/59fc25e84ae2_.py create mode 100644 api/migrations/versions/9e52f36c2d6d_ai_billing_and_forwarding_two_extend.py create mode 100644 api/migrations/versions/d8929f29057c_add_tenant_model_sync_extend.py create mode 100644 api/models/account_money_extend.py create mode 100644 api/models/account_money_monthly_stat_extend.py create mode 100644 api/models/api_token_money_extend.py create mode 100644 api/models/model_extend.py create mode 100644 api/models/system_extend.py create mode 100644 api/models/tenant_model_sync_extend.py create mode 100644 api/schedule/update_account_used_quota_extend.py create mode 100644 api/schedule/update_api_token_daily_used_quota_task_extend.py create mode 100644 api/schedule/update_api_token_monthly_used_quota_task_extend.py create mode 100644 api/services/account_service_extend.py create mode 100644 api/services/app_generate_service_extend.py create mode 100644 api/services/ding_talk_extend.py create mode 100644 api/services/model_provider_service_extend.py create mode 100644 api/services/model_service_extend.py create mode 100644 api/services/recommended_app_service_extend.py create mode 100644 api/tasks/extend/update_account_money_when_workflow_node_execution_created_extend.py create mode 100644 docker/docker-compose.dify-plus.yaml create mode 100644 images/dify-plus/API密钥列表.png create mode 100644 images/dify-plus/API调用测试.jpg create mode 100644 images/dify-plus/个人额度修改.jpg create mode 100644 images/dify-plus/创建API密钥.jpg create mode 100644 images/dify-plus/同步至应用模版.jpg create mode 100644 images/dify-plus/密钥使用分析.jpg create mode 100644 images/dify-plus/应用中心.jpg create mode 100644 images/dify-plus/每月密钥额度花费.jpg create mode 100644 images/dify-plus/用户额度显示.jpg create mode 100644 images/dify-plus/费用报表.png create mode 100644 images/dify_plus.png create mode 100644 web/app/(commonLayout)/app/(appDetailLayout)/[appId]/user_overview_extend/page.tsx create mode 100644 web/app/(commonLayout)/explore/apps-center-extend/page.tsx create mode 100644 web/app/components/base/auto-select-extend/index.tsx create mode 100644 web/app/components/base/auto-select-extend/locale.tsx create mode 100644 web/app/components/base/mermaid/modal.module.css create mode 100644 web/app/components/base/param-item/day-limit-item-extend.tsx create mode 100644 web/app/components/base/param-item/month-limit-item-extend.tsx create mode 100644 web/app/components/base/react-multi-email-extend/index.module.css create mode 100644 web/app/components/base/react-multi-email-extend/index.tsx create mode 100644 web/app/components/develop/secret-key/assets/edit-hover.svg create mode 100644 web/app/components/develop/secret-key/assets/edit.svg create mode 100644 web/app/components/develop/secret-key/secret-key-quota-set-modal-extend.tsx create mode 100644 web/app/components/explore/app-card-extend/index.tsx create mode 100644 web/app/components/explore/app-list-center-extend/index.tsx create mode 100644 web/app/components/explore/app-list-center-extend/style.module.css create mode 100644 web/app/components/header/account-money-extend/index.tsx create mode 100644 web/app/components/header/account-setting/model-provider-page/model-parameter-modal/parameter-item-extend.tsx create mode 100644 web/app/signin/assets/ding.svg create mode 100644 web/app/signin/assets/oauth2.svg create mode 100644 web/app/signin/components/dingtalk-auth.tsx create mode 100644 web/i18n/de-DE/extend.ts create mode 100644 web/i18n/en-US/extend.ts create mode 100644 web/i18n/es-ES/extend.ts create mode 100644 web/i18n/fa-IR/extend.ts create mode 100644 web/i18n/fr-FR/extend.ts create mode 100644 web/i18n/hi-IN/extend.ts create mode 100644 web/i18n/it-IT/extend.ts create mode 100644 web/i18n/ja-JP/extend.ts create mode 100644 web/i18n/ko-KR/extend.ts create mode 100644 web/i18n/pl-PL/extend.ts create mode 100644 web/i18n/pt-BR/extend.ts create mode 100644 web/i18n/ro-RO/extend.ts create mode 100644 web/i18n/ru-RU/extend.ts create mode 100644 web/i18n/sl-SI/extend.ts create mode 100644 web/i18n/th-TH/extend.ts create mode 100644 web/i18n/tr-TR/extend.ts create mode 100644 web/i18n/uk-UA/extend.ts create mode 100644 web/i18n/vi-VN/extend.ts create mode 100644 web/i18n/zh-Hans/extend.ts create mode 100644 web/i18n/zh-Hant/extend.ts create mode 100644 web/models/common-extend.ts create mode 100644 web/service/common-extend.ts create mode 100644 web/service/web-extend.ts create mode 100644 web/types/workspace-extend.ts diff --git a/README.md b/README.md index 3920ff107..676f7bb2a 100644 --- a/README.md +++ b/README.md @@ -1,193 +1,149 @@ -![cover-v5-optimized](https://github.com/langgenius/dify/assets/13230914/f9e19af5-61ba-4119-b926-d10c4c06ebab) +# Dify-Plus -

- 📌 Introducing Dify Workflow File Upload: Recreate Google NotebookLM Podcast -

+## 项目介绍 -

- Dify Cloud · - Self-hosting · - Documentation · - Enterprise inquiry -

+在原有 Dify 的基础中,该项目做了一些二开以及新增了管理中心的功能,原先这些功能只是在我们企业内部使用,对外交流后发现很多伙伴也遇到我们相同一些痛点,故将我们的二开内容进行开源,欢迎大家一起交流。 -

- - Static Badge - - Static Badge - - chat on Discord - - join Reddit - - follow on X(Twitter) - - follow on LinkedIn - - Docker Pulls - - Commits last month - - Issues closed - - Discussion posts -

+简而言之:该项目基于 [gin-vue-admin](https://github.com/flipped-aurora/gin-vue-admin) 做了 Dify 的管理中心,基于 [Dify](https://github.com/langgenius/dify) 做了一些适合企业场景的二开功能。 -

- README in English - 简体中文版自述文件 - 日本語のREADME - README en Español - README en Français - README tlhIngan Hol - README in Korean - README بالعربية - Türkçe README - README Tiếng Việt -

+即 *Dify-Plus* = *管理中心* + *Dify 二开* + +## 名字说明 + +Dify-Plus,该名字不是说比 Dify 项目牛的意思,意思是想说比 Dify 多做了一些针对企业场景多了一些二开的功能而已。 + +## 新增功能介绍 + +### 一. Dify 二开功能 +1. 新增:用户额度 + 1. 对话余额限制判断 + 2. 异步计算用户额度逻辑 + 3. 左上角新增使用额度显示 + 4. 新增个人监测页 +2. 新增:密钥额度设置 + 1. 新增应用 API 调用余额限制判断 +3. 新增 :Web 公开页登录鉴权 +4. 新增:管理员同步应用到应用模版 +5. 新增:后台创建用户,自动邀请进管理员空间 +6. 新增:可以鉴权的 cookie +7. 新增:同步应用到模版中心 +8. 新增:应用中心页面 +9. 调整 :默认跳转到应用中心 +10. 新增:应用使用次数记录、应用中心按照使用次数排序 +11. 权限调整 + 1. 调整:不允许普通成员关闭模型 + 2. 调整:空间普通成员不渲染“模型供应商”标签 + 3. 调整:非管理员,隐藏密钥显示 + 4. 优化: csv 编码监测,修复批量请求,windows 下载后保存再上传问题 + 5. 优化: markdown 图片放大问题优化 +## 二. 管理中心 +> 代码所在目录:/admin +1. JWT 与 Dify 打通 +2. 用户同步 +3. 用户额度修改 +4. 费用报表 + +## 部分功能页面展示截图 + +1. 应用中心 + + ![应用中心.jpg](images/dify-plus/应用中心.jpg) + +1. API密钥列表 + + ![API密钥列表.png](images/dify-plus/API密钥列表.png) + +1. 创建API密钥 + + ![创建API密钥.jpg](images/dify-plus/创建API密钥.jpg) + +1. 用户额度显示 + + ![用户额度显示.jpg](images/dify-plus/用户额度显示.jpg) + +1. 同步至应用模版 + + ![同步至应用模版.jpg](images/dify-plus/同步至应用模版.jpg) + +1. API调用测试 + + ![API调用测试.jpg](images/dify-plus/API调用测试.jpg) + +1. 个人额度修改 + + ![个人额度修改.jpg](images/dify-plus/个人额度修改.jpg) + +1. 费用报表 + + ![费用报表.jpg.png](images/dify-plus/费用报表.png) + +1. 密钥使用分析 + + ![密钥使用分析.jpg](images/dify-plus/密钥使用分析.jpg) + +1. 每月密钥额度花费 + + ![每月密钥额度花费.jpg](images/dify-plus/每月密钥额度花费.jpg) -Dify is an open-source LLM app development platform. Its intuitive interface combines agentic AI workflow, RAG pipeline, agent capabilities, model management, observability features and more, letting you quickly go from prototype to production. +## 版本更新说明 -## Quick start -> Before installing Dify, make sure your machine meets the following minimum system requirements: -> ->- CPU >= 2 Core ->- RAM >= 4 GiB +1. 会持续跟随 gin-vue-admin 和 Dify 两个开源项目的版本。 +2. 为了标志二开的部分,我们特意在注释、文件名、方法名、表名都加上`extend`,可通过搜索这个关键字,查看我们二开的代码 -
+## 整体服务 -The easiest way to start the Dify server is through [docker compose](docker/docker-compose.yaml). Before running Dify with the following commands, make sure that [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/) are installed on your machine: +![dify-plus.png](images/dify_plus.png) -```bash -cd dify -cd docker -cp .env.example .env -docker compose up -d + +## 启动方式(docker-compose) +见文档:[部署详细步骤(docker‐compose)](https://github.com/YFGaia/dify-plus/wiki/%E9%83%A8%E7%BD%B2%E8%AF%A6%E7%BB%86%E6%AD%A5%E9%AA%A4%EF%BC%88docker%E2%80%90compose%EF%BC%89) + +## 启动方式(源码) +见文档:[部署详细步骤(源码)](https://github.com/YFGaia/dify-plus/wiki/%E9%83%A8%E7%BD%B2%E8%AF%A6%E7%BB%86%E6%AD%A5%E9%AA%A4%EF%BC%88%E6%BA%90%E7%A0%81%E9%83%A8%E7%BD%B2%EF%BC%89) + +## 用户问的较多的问题 +见文档:[Q&A](https://github.com/YFGaia/dify-plus/wiki/Q&A) + + +## 相关配置说明 +- Dify 相关配置说明:https://docs.dify.ai/zh-hans/getting-started/install-self-hosted/environments +- 管理中心 相关配置说明: + - 后端:https://gin-vue-admin.com/guide/server/config.html + - 前端:https://gin-vue-admin.com/guide/web/env.html +- Dify-plus 新增环境变量说明 + ``` + 待补充 + ``` + +## 联系我们 +### email +- toxingwang@gmail.com +- 906631095@qq.com + +### 微信交流群 +1. 防止广告进群,添加微信,输入以下代码执行结果 +```python +encoded_str = "5Yqg5YWlZGlmeS1wbHVz5Lqk5rWB576kMgo=" +decoded_bytes = base64.b64decode(encoded_str) +decoded_str = decoded_bytes.decode('utf-8') +print(decoded_str) ``` +2. 微信二维码: +image -After running, you can access the Dify dashboard in your browser at [http://localhost/install](http://localhost/install) and start the initialization process. - -#### Seeking help -Please refer to our [FAQ](https://docs.dify.ai/getting-started/install-self-hosted/faqs) if you encounter problems setting up Dify. Reach out to [the community and us](#community--contact) if you are still having issues. - -> If you'd like to contribute to Dify or do additional development, refer to our [guide to deploying from source code](https://docs.dify.ai/getting-started/install-self-hosted/local-source-code) - -## Key features -**1. Workflow**: - Build and test powerful AI workflows on a visual canvas, leveraging all the following features and beyond. - - - https://github.com/langgenius/dify/assets/13230914/356df23e-1604-483d-80a6-9517ece318aa +### 请作者喝咖啡~ +image -**2. Comprehensive model support**: - Seamless integration with hundreds of proprietary / open-source LLMs from dozens of inference providers and self-hosted solutions, covering GPT, Mistral, Llama3, and any OpenAI API-compatible models. A full list of supported model providers can be found [here](https://docs.dify.ai/getting-started/readme/model-providers). +### Star History -![providers-v5](https://github.com/langgenius/dify/assets/13230914/5a17bdbe-097a-4100-8363-40255b70f6e3) +[![Star History Chart](https://api.star-history.com/svg?repos=YFGaia/dify-plus&type=Date)](https://star-history.com/#YFGaia/dify-plus&Date) -**3. Prompt IDE**: - Intuitive interface for crafting prompts, comparing model performance, and adding additional features such as text-to-speech to a chat-based app. - -**4. RAG Pipeline**: - Extensive RAG capabilities that cover everything from document ingestion to retrieval, with out-of-box support for text extraction from PDFs, PPTs, and other common document formats. - -**5. Agent capabilities**: - You can define agents based on LLM Function Calling or ReAct, and add pre-built or custom tools for the agent. Dify provides 50+ built-in tools for AI agents, such as Google Search, DALL·E, Stable Diffusion and WolframAlpha. - -**6. LLMOps**: - Monitor and analyze application logs and performance over time. You could continuously improve prompts, datasets, and models based on production data and annotations. - -**7. Backend-as-a-Service**: - All of Dify's offerings come with corresponding APIs, so you could effortlessly integrate Dify into your own business logic. - - -## Using Dify - -- **Cloud
** -We host a [Dify Cloud](https://dify.ai) service for anyone to try with zero setup. It provides all the capabilities of the self-deployed version, and includes 200 free GPT-4 calls in the sandbox plan. - -- **Self-hosting Dify Community Edition
** -Quickly get Dify running in your environment with this [starter guide](#quick-start). -Use our [documentation](https://docs.dify.ai) for further references and more in-depth instructions. - -- **Dify for enterprise / organizations
** -We provide additional enterprise-centric features. [Log your questions for us through this chatbot](https://udify.app/chat/22L1zSxg6yW1cWQg) or [send us an email](mailto:business@dify.ai?subject=[GitHub]Business%20License%20Inquiry) to discuss enterprise needs.
- > For startups and small businesses using AWS, check out [Dify Premium on AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-t22mebxzwjhu6) and deploy it to your own AWS VPC with one-click. It's an affordable AMI offering with the option to create apps with custom logo and branding. - - -## Staying ahead - -Star Dify on GitHub and be instantly notified of new releases. - -![star-us](https://github.com/langgenius/dify/assets/13230914/b823edc1-6388-4e25-ad45-2f6b187adbb4) - - -## Advanced Setup - -If you need to customize the configuration, please refer to the comments in our [.env.example](docker/.env.example) file and update the corresponding values in your `.env` file. Additionally, you might need to make adjustments to the `docker-compose.yaml` file itself, such as changing image versions, port mappings, or volume mounts, based on your specific deployment environment and requirements. After making any changes, please re-run `docker-compose up -d`. You can find the full list of available environment variables [here](https://docs.dify.ai/getting-started/install-self-hosted/environments). - -If you'd like to configure a highly-available setup, there are community-contributed [Helm Charts](https://helm.sh/) and YAML files which allow Dify to be deployed on Kubernetes. - -- [Helm Chart by @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify) -- [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm) -- [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes) - -#### Using Terraform for Deployment - -Deploy Dify to Cloud Platform with a single click using [terraform](https://www.terraform.io/) - -##### Azure Global -- [Azure Terraform by @nikawang](https://github.com/nikawang/dify-azure-terraform) - -##### Google Cloud -- [Google Cloud Terraform by @sotazum](https://github.com/DeNA/dify-google-cloud-terraform) - -#### Using AWS CDK for Deployment - -Deploy Dify to AWS with [CDK](https://aws.amazon.com/cdk/) - -##### AWS -- [AWS CDK by @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws) - -## Contributing - -For those who'd like to contribute code, see our [Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md). -At the same time, please consider supporting Dify by sharing it on social media and at events and conferences. - - -> We are looking for contributors to help with translating Dify to languages other than Mandarin or English. If you are interested in helping, please see the [i18n README](https://github.com/langgenius/dify/blob/main/web/i18n/README.md) for more information, and leave us a comment in the `global-users` channel of our [Discord Community Server](https://discord.gg/8Tpq4AcN9c). - -## Community & contact - -* [Github Discussion](https://github.com/langgenius/dify/discussions). Best for: sharing feedback and asking questions. -* [GitHub Issues](https://github.com/langgenius/dify/issues). Best for: bugs you encounter using Dify.AI, and feature proposals. See our [Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md). -* [Discord](https://discord.gg/FngNHpbcY7). Best for: sharing your applications and hanging out with the community. -* [X(Twitter)](https://twitter.com/dify_ai). Best for: sharing your applications and hanging out with the community. - -**Contributors** - - - - - -## Star history - -[![Star History Chart](https://api.star-history.com/svg?repos=langgenius/dify&type=Date)](https://star-history.com/#langgenius/dify&Date) - - -## Security disclosure - -To protect your privacy, please avoid posting security issues on GitHub. Instead, send your questions to security@dify.ai and we will provide you with a more detailed answer. - ## License -This repository is available under the [Dify Open Source License](LICENSE), which is essentially Apache 2.0 with a few additional restrictions. +版权说明:本项目在 Dify 项目基础上进行二开,需要遵守 Dify 的开源协议,如下 +This repository is available under the [Dify Open Source License](LICENSE), which is essentially Apache 2.0 with a few additional restrictions. diff --git a/README_DIFY.md b/README_DIFY.md new file mode 100644 index 000000000..e2b7cdea6 --- /dev/null +++ b/README_DIFY.md @@ -0,0 +1,192 @@ +![cover-v5-optimized](https://github.com/langgenius/dify/assets/13230914/f9e19af5-61ba-4119-b926-d10c4c06ebab) + +

+ 📌 Introducing Dify Workflow File Upload: Recreate Google NotebookLM Podcast +

+ +

+ Dify Cloud · + Self-hosting · + Documentation · + Enterprise inquiry +

+ +

+ + Static Badge + + Static Badge + + chat on Discord + + join Reddit + + follow on X(Twitter) + + follow on LinkedIn + + Docker Pulls + + Commits last month + + Issues closed + + Discussion posts +

+ +

+ README in English + 简体中文版自述文件 + 日本語のREADME + README en Español + README en Français + README tlhIngan Hol + README in Korean + README بالعربية + Türkçe README + README Tiếng Việt +

+ + +Dify is an open-source LLM app development platform. Its intuitive interface combines agentic AI workflow, RAG pipeline, agent capabilities, model management, observability features and more, letting you quickly go from prototype to production. + +## Quick start +> Before installing Dify, make sure your machine meets the following minimum system requirements: +> +>- CPU >= 2 Core +>- RAM >= 4 GiB + +
+ +The easiest way to start the Dify server is through [docker compose](docker/docker-compose.yaml). Before running Dify with the following commands, make sure that [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/) are installed on your machine: + +```bash +cd dify +cd docker +cp .env.example .env +docker compose up -d +``` + +After running, you can access the Dify dashboard in your browser at [http://localhost/install](http://localhost/install) and start the initialization process. + +#### Seeking help +Please refer to our [FAQ](https://docs.dify.ai/getting-started/install-self-hosted/faqs) if you encounter problems setting up Dify. Reach out to [the community and us](#community--contact) if you are still having issues. + +> If you'd like to contribute to Dify or do additional development, refer to our [guide to deploying from source code](https://docs.dify.ai/getting-started/install-self-hosted/local-source-code) + +## Key features +**1. Workflow**: +Build and test powerful AI workflows on a visual canvas, leveraging all the following features and beyond. + + +https://github.com/langgenius/dify/assets/13230914/356df23e-1604-483d-80a6-9517ece318aa + + + +**2. Comprehensive model support**: +Seamless integration with hundreds of proprietary / open-source LLMs from dozens of inference providers and self-hosted solutions, covering GPT, Mistral, Llama3, and any OpenAI API-compatible models. A full list of supported model providers can be found [here](https://docs.dify.ai/getting-started/readme/model-providers). + +![providers-v5](https://github.com/langgenius/dify/assets/13230914/5a17bdbe-097a-4100-8363-40255b70f6e3) + + +**3. Prompt IDE**: +Intuitive interface for crafting prompts, comparing model performance, and adding additional features such as text-to-speech to a chat-based app. + +**4. RAG Pipeline**: +Extensive RAG capabilities that cover everything from document ingestion to retrieval, with out-of-box support for text extraction from PDFs, PPTs, and other common document formats. + +**5. Agent capabilities**: +You can define agents based on LLM Function Calling or ReAct, and add pre-built or custom tools for the agent. Dify provides 50+ built-in tools for AI agents, such as Google Search, DALL·E, Stable Diffusion and WolframAlpha. + +**6. LLMOps**: +Monitor and analyze application logs and performance over time. You could continuously improve prompts, datasets, and models based on production data and annotations. + +**7. Backend-as-a-Service**: +All of Dify's offerings come with corresponding APIs, so you could effortlessly integrate Dify into your own business logic. + + +## Using Dify + +- **Cloud
** + We host a [Dify Cloud](https://dify.ai) service for anyone to try with zero setup. It provides all the capabilities of the self-deployed version, and includes 200 free GPT-4 calls in the sandbox plan. + +- **Self-hosting Dify Community Edition
** + Quickly get Dify running in your environment with this [starter guide](#quick-start). + Use our [documentation](https://docs.dify.ai) for further references and more in-depth instructions. + +- **Dify for enterprise / organizations
** + We provide additional enterprise-centric features. [Log your questions for us through this chatbot](https://udify.app/chat/22L1zSxg6yW1cWQg) or [send us an email](mailto:business@dify.ai?subject=[GitHub]Business%20License%20Inquiry) to discuss enterprise needs.
+ > For startups and small businesses using AWS, check out [Dify Premium on AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-t22mebxzwjhu6) and deploy it to your own AWS VPC with one-click. It's an affordable AMI offering with the option to create apps with custom logo and branding. + + +## Staying ahead + +Star Dify on GitHub and be instantly notified of new releases. + +![star-us](https://github.com/langgenius/dify/assets/13230914/b823edc1-6388-4e25-ad45-2f6b187adbb4) + + +## Advanced Setup + +If you need to customize the configuration, please refer to the comments in our [.env.example](docker/.env.example) file and update the corresponding values in your `.env` file. Additionally, you might need to make adjustments to the `docker-compose.yaml` file itself, such as changing image versions, port mappings, or volume mounts, based on your specific deployment environment and requirements. After making any changes, please re-run `docker-compose up -d`. You can find the full list of available environment variables [here](https://docs.dify.ai/getting-started/install-self-hosted/environments). + +If you'd like to configure a highly-available setup, there are community-contributed [Helm Charts](https://helm.sh/) and YAML files which allow Dify to be deployed on Kubernetes. + +- [Helm Chart by @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify) +- [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm) +- [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes) + +#### Using Terraform for Deployment + +Deploy Dify to Cloud Platform with a single click using [terraform](https://www.terraform.io/) + +##### Azure Global +- [Azure Terraform by @nikawang](https://github.com/nikawang/dify-azure-terraform) + +##### Google Cloud +- [Google Cloud Terraform by @sotazum](https://github.com/DeNA/dify-google-cloud-terraform) + +#### Using AWS CDK for Deployment + +Deploy Dify to AWS with [CDK](https://aws.amazon.com/cdk/) + +##### AWS +- [AWS CDK by @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws) + +## Contributing + +For those who'd like to contribute code, see our [Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md). +At the same time, please consider supporting Dify by sharing it on social media and at events and conferences. + + +> We are looking for contributors to help with translating Dify to languages other than Mandarin or English. If you are interested in helping, please see the [i18n README](https://github.com/langgenius/dify/blob/main/web/i18n/README.md) for more information, and leave us a comment in the `global-users` channel of our [Discord Community Server](https://discord.gg/8Tpq4AcN9c). + +## Community & contact + +* [Github Discussion](https://github.com/langgenius/dify/discussions). Best for: sharing feedback and asking questions. +* [GitHub Issues](https://github.com/langgenius/dify/issues). Best for: bugs you encounter using Dify.AI, and feature proposals. See our [Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md). +* [Discord](https://discord.gg/FngNHpbcY7). Best for: sharing your applications and hanging out with the community. +* [X(Twitter)](https://twitter.com/dify_ai). Best for: sharing your applications and hanging out with the community. + +**Contributors** + + + + + +## Star history + +[![Star History Chart](https://api.star-history.com/svg?repos=langgenius/dify&type=Date)](https://star-history.com/#langgenius/dify&Date) + + +## Security disclosure + +To protect your privacy, please avoid posting security issues on GitHub. Instead, send your questions to security@dify.ai and we will provide you with a more detailed answer. + +## License + +This repository is available under the [Dify Open Source License](LICENSE), which is essentially Apache 2.0 with a few additional restrictions. \ No newline at end of file diff --git a/admin/.gitattributes b/admin/.gitattributes new file mode 100644 index 000000000..a4970acdb --- /dev/null +++ b/admin/.gitattributes @@ -0,0 +1,2 @@ +*.sql linguist-language=GO +*.html linguist-language=GO diff --git a/admin/.gitignore b/admin/.gitignore new file mode 100644 index 000000000..5f7b8d380 --- /dev/null +++ b/admin/.gitignore @@ -0,0 +1,35 @@ +.idea/ +/web/node_modules +/web/dist + +.DS_Store + +# local env files +.env.local +.env.*.local + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +rm_file/ +/server/log/ +/server/gva +/server/server +/server/latest_log +/server/__debug_bin* +server/uploads/ + +*.iml +web/.pnpm-debug.log +web/pnpm-lock.yaml diff --git a/admin/CODE_OF_CONDUCT.md b/admin/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..bcd5c45ec --- /dev/null +++ b/admin/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at 303176530@qq.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/admin/CONTRIBUTING.md b/admin/CONTRIBUTING.md new file mode 100644 index 000000000..b3076b25c --- /dev/null +++ b/admin/CONTRIBUTING.md @@ -0,0 +1,19 @@ + +### Contributing Guide +#### 1 Issue Guidelines + +- Issues are exclusively for bug reports, feature requests and design-related topics. Other questions may be closed directly. If any questions come up when you are using Element, please hit [Gitter](https://gitter.im/element-en/Lobby) for help. + +- Before submitting an issue, please check if similar problems have already been issued. + +#### 2 Pull Request Guidelines + +- Fork this repository to your own account. Do not create branches here. + +- Commit info should be formatted as `[File Name]: Info about commit.` (e.g. `README.md: Fix xxx bug`) + +- Make sure PRs are created to `develop` branch instead of `master` branch. + +- If your PR fixes a bug, please provide a description about the related bug. + +- Merging a PR takes two maintainers: one approves the changes after reviewing, and then the other reviews and merges. diff --git a/admin/LICENSE b/admin/LICENSE new file mode 100644 index 000000000..e0dca2e47 --- /dev/null +++ b/admin/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2019 北京翻转极光科技有限责任公司 + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/admin/Makefile b/admin/Makefile new file mode 100644 index 000000000..4b193fc6d --- /dev/null +++ b/admin/Makefile @@ -0,0 +1,78 @@ +SHELL = /bin/bash + +#SCRIPT_DIR = $(shell pwd)/etc/script +#请选择golang版本 +BUILD_IMAGE_SERVER = golang:1.22 +#请选择node版本 +BUILD_IMAGE_WEB = node:20 +#项目名称 +PROJECT_NAME = github.com/flipped-aurora/gin-vue-admin/server +#配置文件目录 +CONFIG_FILE = config.yaml +#镜像仓库命名空间 +IMAGE_NAME = gva +#镜像地址 +REPOSITORY = registry.cn-hangzhou.aliyuncs.com/${IMAGE_NAME} + +ifeq ($(TAGS_OPT),) +TAGS_OPT = latest +else +endif + +ifeq ($(PLUGIN),) +PLUGIN = email +else +endif + +#容器环境前后端共同打包 +build: build-web build-server + docker run --name build-local --rm -v $(shell pwd):/go/src/${PROJECT_NAME} -w /go/src/${PROJECT_NAME} ${BUILD_IMAGE_SERVER} make build-local + +#容器环境打包前端 +build-web: + docker run --name build-web-local --rm -v $(shell pwd):/go/src/${PROJECT_NAME} -w /go/src/${PROJECT_NAME} ${BUILD_IMAGE_WEB} make build-web-local + +#容器环境打包后端 +build-server: + docker run --name build-server-local --rm -v $(shell pwd):/go/src/${PROJECT_NAME} -w /go/src/${PROJECT_NAME} ${BUILD_IMAGE_SERVER} make build-server-local + +#构建web镜像 +build-image-web: + @cd web/ && docker build -t ${REPOSITORY}/web:${TAGS_OPT} . + +#构建server镜像 +build-image-server: + @cd server/ && docker build -t ${REPOSITORY}/server:${TAGS_OPT} . + +#本地环境打包前后端 +build-local: + if [ -d "build" ];then rm -rf build; else echo "OK!"; fi \ + && if [ -f "/.dockerenv" ];then echo "OK!"; else make build-web-local && make build-server-local; fi \ + && mkdir build && cp -r web/dist build/ && cp server/server build/ && cp -r server/resource build/resource + +#本地环境打包前端 +build-web-local: + @cd web/ && if [ -d "dist" ];then rm -rf dist; else echo "OK!"; fi \ + && yarn config set registry http://mirrors.cloud.tencent.com/npm/ && yarn install && yarn build + +#本地环境打包后端 +build-server-local: + @cd server/ && if [ -f "server" ];then rm -rf server; else echo "OK!"; fi \ + && go env -w GO111MODULE=on && go env -w GOPROXY=https://goproxy.cn,direct \ + && go env -w CGO_ENABLED=0 && go env && go mod tidy \ + && go build -ldflags "-B 0x$(shell head -c20 /dev/urandom|od -An -tx1|tr -d ' \n') -X main.Version=${TAGS_OPT}" -v + +#打包前后端二合一镜像 +image: build + docker build -t ${REPOSITORY}/gin-vue-admin:${TAGS_OPT} -f deploy/docker/Dockerfile . + +#尝鲜版 +images: build build-image-web build-image-server + docker build -t ${REPOSITORY}/all:${TAGS_OPT} -f deploy/docker/Dockerfile . + +#插件快捷打包: make plugin PLUGIN="这里是插件文件夹名称,默认为email" +plugin: + if [ -d ".plugin" ];then rm -rf .plugin ; else echo "OK!"; fi && mkdir -p .plugin/${PLUGIN}/{server/plugin,web/plugin} \ + && if [ -d "server/plugin/${PLUGIN}" ];then cp -r server/plugin/${PLUGIN} .plugin/${PLUGIN}/server/plugin/ ; else echo "OK!"; fi \ + && if [ -d "web/src/plugin/${PLUGIN}" ];then cp -r web/src/plugin/${PLUGIN} .plugin/${PLUGIN}/web/plugin/ ; else echo "OK!"; fi \ + && cd .plugin && zip -r ${PLUGIN}.zip ${PLUGIN} && mv ${PLUGIN}.zip ../ && cd .. diff --git a/admin/deploy/docker-compose/docker-compose-dev.yaml b/admin/deploy/docker-compose/docker-compose-dev.yaml new file mode 100644 index 000000000..fd0a3b661 --- /dev/null +++ b/admin/deploy/docker-compose/docker-compose-dev.yaml @@ -0,0 +1,99 @@ +version: "3" + +# 声明一个名为network的networks,subnet为network的子网地址,默认网关是177.7.0.1 +networks: + network: + ipam: + driver: default + config: + - subnet: '177.7.0.0/16' + +# 设置mysql,redis持久化保存 +volumes: + mysql: + redis: + +services: + web: + image: node:20 + container_name: gva-web + hostname: gva-web #可以通过容器名访问 + restart: always + ports: + - '8080:8080' + depends_on: + - server + working_dir: /web # 如果docker 设置了workdir 则此处不需要设置 + #若网络不太好,请自行换源,如下 + #command: bash -c "yarn config set registry https://registry.npmmirror.com --global && yarn install && yarn serve" + command: bash -c "yarn install && yarn serve" + volumes: + - ../../web:/web + networks: + network: + ipv4_address: 177.7.0.11 + + server: + image: golang:1.22 + container_name: gva-server + hostname: gva-server + restart: always + ports: + - '8888:8888' + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_healthy + volumes: + - ../../server:/server + working_dir: /server # 如果docker 设置了workdir 则此处不需要设置 + command: bash -c "go env -w GOPROXY=https://goproxy.cn,direct && go mod tidy && go run main.go" + links: + - mysql + - redis + networks: + network: + ipv4_address: 177.7.0.12 + + mysql: + image: mysql:8.0.21 # 如果您是 arm64 架构:如 MacOS 的 M1,请修改镜像为 image: mysql/mysql-server:8.0.21 + container_name: gva-mysql + hostname: gva-mysql + command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci #设置utf8字符集 + restart: always + ports: + - "13306:3306" # host物理直接映射端口为13306 + environment: + #MYSQL_ROOT_PASSWORD: 'Aa@6447985' # root管理员用户密码 + MYSQL_DATABASE: 'qmPlus' # 初始化启动时要创建的数据库的名称 + MYSQL_USER: 'gva' + MYSQL_PASSWORD: 'Aa@6447985' + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "gva", "-pAa@6447985"] + interval: 10s + timeout: 5s + retries: 3 + volumes: + - mysql:/var/lib/mysql + networks: + network: + ipv4_address: 177.7.0.13 + + redis: + image: redis:6.0.6 + container_name: gva-redis # 容器名 + hostname: gva-redis + restart: always + ports: + - '16379:6379' + healthcheck: + test: ["CMD-SHELL", "redis-cli ping | grep PONG || exit 1"] + interval: 10s + timeout: 5s + retries: 3 + volumes: + - redis:/data + networks: + network: + ipv4_address: 177.7.0.14 diff --git a/admin/deploy/docker-compose/docker-compose.yaml b/admin/deploy/docker-compose/docker-compose.yaml new file mode 100644 index 000000000..1e304c2e1 --- /dev/null +++ b/admin/deploy/docker-compose/docker-compose.yaml @@ -0,0 +1,90 @@ +version: "3" + +# 声明一个名为network的networks,subnet为network的子网地址,默认网关是177.7.0.1 +networks: + network: + ipam: + driver: default + config: + - subnet: '177.7.0.0/16' + +# 设置mysql,redis持久化保存 +volumes: + mysql: + redis: + +services: + web: + build: + context: ../../web + dockerfile: ./Dockerfile + container_name: gva-web + restart: always + ports: + - '8080:8080' + depends_on: + - server + command: [ 'nginx-debug', '-g', 'daemon off;' ] + networks: + network: + ipv4_address: 177.7.0.11 + + server: + build: + context: ../../server + dockerfile: ./Dockerfile + container_name: gva-server + restart: always + ports: + - '8888:8888' + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_healthy + links: + - mysql + - redis + networks: + network: + ipv4_address: 177.7.0.12 + + mysql: + image: mysql:8.0.21 # 如果您是 arm64 架构:如 MacOS 的 M1,请修改镜像为 image: mysql/mysql-server:8.0.21 + container_name: gva-mysql + command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci #设置utf8字符集 + restart: always + ports: + - "13306:3306" # host物理直接映射端口为13306 + environment: + #MYSQL_ROOT_PASSWORD: 'Aa@6447985' # root管理员用户密码 + MYSQL_DATABASE: 'qmPlus' # 初始化启动时要创建的数据库的名称 + MYSQL_USER: 'gva' + MYSQL_PASSWORD: 'Aa@6447985' + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "gva", "-pAa@6447985"] + interval: 10s + timeout: 5s + retries: 3 + volumes: + - mysql:/var/lib/mysql + networks: + network: + ipv4_address: 177.7.0.13 + + redis: + image: redis:6.0.6 + container_name: gva-redis # 容器名 + restart: always + ports: + - '16379:6379' + healthcheck: + test: ["CMD-SHELL", "redis-cli ping | grep PONG || exit 1"] + interval: 10s + timeout: 5s + retries: 3 + volumes: + - redis:/data + networks: + network: + ipv4_address: 177.7.0.14 diff --git a/admin/deploy/docker/Dockerfile b/admin/deploy/docker/Dockerfile new file mode 100644 index 000000000..da238053d --- /dev/null +++ b/admin/deploy/docker/Dockerfile @@ -0,0 +1,18 @@ +FROM centos:7 +WORKDIR /opt +ENV LANG=en_US.utf8 +COPY deploy/docker/entrypoint.sh . +COPY build/ /usr/share/nginx/html/ +COPY server/config.yaml /usr/share/nginx/html/config.yaml +COPY web/.docker-compose/nginx/conf.d/nginx.conf /etc/nginx/conf.d/nginx.conf +RUN set -ex \ + && echo "LANG=en_US.utf8" > /etc/locale.conf \ + && echo "net.core.somaxconn = 1024" >> /etc/sysctl.conf \ + && echo "vm.overcommit_memory = 1" >> /etc/sysctl.conf \ + && yum -y install epel-release \ + && yum -y localinstall http://mirrors.ustc.edu.cn/mysql-repo/mysql57-community-release-el7.rpm \ + && yum -y install mysql-community-server git redis nginx go npm --nogpgcheck && chmod +x ./entrypoint.sh \ + && npm install -g yarn && go env -w GO111MODULE=on && go env -w GOPROXY=https://goproxy.cn,direct \ + && echo "start" > /dev/null +EXPOSE 80 +ENTRYPOINT ["./entrypoint.sh"] \ No newline at end of file diff --git a/admin/deploy/docker/entrypoint.sh b/admin/deploy/docker/entrypoint.sh new file mode 100644 index 000000000..0f6dd1379 --- /dev/null +++ b/admin/deploy/docker/entrypoint.sh @@ -0,0 +1,19 @@ +#!/bin/bash +if [ ! -d "/var/lib/mysql/gva" ]; then + mysqld --initialize-insecure --user=mysql --datadir=/var/lib/mysql + mysqld --daemonize --user=mysql + sleep 5s + mysql -uroot -e "create database gva default charset 'utf8' collate 'utf8_bin'; grant all on gva.* to 'root'@'127.0.0.1' identified by '123456'; flush privileges;" +else + mysqld --daemonize --user=mysql +fi +redis-server & +if [ "$1" = "actions" ]; then + cd /opt/gva/server && go run main.go & + cd /opt/gva/web/ && yarn serve & +else + /usr/sbin/nginx & + cd /usr/share/nginx/html/ && ./server & +fi +echo "gva ALL start!!!" +tail -f /dev/null \ No newline at end of file diff --git a/admin/deploy/kubernetes/server/gva-server-configmap.yaml b/admin/deploy/kubernetes/server/gva-server-configmap.yaml new file mode 100644 index 000000000..fb889fb7e --- /dev/null +++ b/admin/deploy/kubernetes/server/gva-server-configmap.yaml @@ -0,0 +1,148 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: config.yaml + annotations: + flipped-aurora/gin-vue-admin: backend + github: "https://github.com/flipped-aurora/gin-vue-admin.git" + app.kubernetes.io/version: 0.0.1 + labels: + app: gva-server + version: gva-vue3 +data: + config.yaml: | + # github.com/flipped-aurora/gin-vue-admin/server Global Configuration + + # jwt configuration + jwt: + signing-key: 'qmPlus' + expires-time: 604800 + buffer-time: 86400 + + # zap logger configuration + zap: + level: 'info' + format: 'console' + prefix: '[github.com/flipped-aurora/gin-vue-admin/server]' + director: 'log' + link-name: 'latest_log' + show-line: true + encode-level: 'LowercaseColorLevelEncoder' + stacktrace-key: 'stacktrace' + log-in-console: true + + # redis configuration + redis: + db: 0 + addr: '127.0.0.1:6379' + password: '' + + # email configuration + email: + to: 'xxx@qq.com' + port: 465 + from: 'xxx@163.com' + host: 'smtp.163.com' + is-ssl: true + secret: 'xxx' + nickname: 'test' + + # casbin configuration + casbin: + model-path: './resource/rbac_model.conf' + + # system configuration + system: + env: 'develop' # Change to "develop" to skip authentication for development mode + addr: 8888 + db-type: 'mysql' + oss-type: 'local' # 控制oss选择走本期还是 七牛等其他仓 自行增加其他oss仓可以在 server/utils/upload/upload.go 中 NewOss函数配置 + use-multipoint: false + + # captcha configuration + captcha: + key-long: 6 + img-width: 240 + img-height: 80 + + # mysql connect configuration + # 未初始化之前请勿手动修改数据库信息!!!如果一定要手动初始化请看(https://www.github.com/flipped-aurora/gin-vue-admin/server.com/docs/first) + mysql: + path: '' + config: '' + db-name: '' + username: '' + password: '' + max-idle-conns: 10 + max-open-conns: 100 + log-mode: false + log-zap: "" + + # local configuration + local: + path: 'uploads/file' + + # autocode configuration + autocode: + transfer-restart: true + root: "" + server: /server + server-api: /api/v1/autocode + server-initialize: /initialize + server-model: /model/autocode + server-request: /model/autocode/request/ + server-router: /router/autocode + server-service: /service/autocode + web: /web/src + web-api: /api + web-flow: /view + web-form: /view + web-table: /view + + # qiniu configuration (请自行七牛申请对应的 公钥 私钥 bucket 和 域名地址) + qiniu: + zone: 'ZoneHuaDong' + bucket: '' + img-path: '' + use-https: false + access-key: '' + secret-key: '' + use-cdn-domains: false + + + # aliyun oss configuration + aliyun-oss: + endpoint: 'yourEndpoint' + access-key-id: 'yourAccessKeyId' + access-key-secret: 'yourAccessKeySecret' + bucket-name: 'yourBucketName' + bucket-url: 'yourBucketUrl' + base-path: 'yourBasePath' + + # tencent cos configuration + tencent-cos: + bucket: 'xxxxx-10005608' + region: 'ap-shanghai' + secret-id: 'xxxxxxxx' + secret-key: 'xxxxxxxx' + base-url: 'https://gin.vue.admin' + path-prefix: 'github.com/flipped-aurora/gin-vue-admin/server' + + # excel configuration + excel: + dir: './resource/excel/' + + + # timer task db clear table + Timer: + start: true + spec: "@daily" # 定时任务详细配置参考 https://pkg.go.dev/github.com/robfig/cron/v3 + detail: [ + # tableName: 需要清理的表名 + # compareField: 需要比较时间的字段 + # interval: 时间间隔, 具体配置详看 time.ParseDuration() 中字符串表示 且不能为负数 + # 2160h = 24 * 30 * 3 -> 三个月 + { tableName: "sys_operation_records" , compareField: "created_at", interval: "2160h" }, + { tableName: "jwt_blacklists" , compareField: "created_at", interval: "168h" } + #{ tableName: "log2" , compareField: "created_at", interval: "2160h" } + ] diff --git a/admin/deploy/kubernetes/server/gva-server-deployment.yaml b/admin/deploy/kubernetes/server/gva-server-deployment.yaml new file mode 100644 index 000000000..7da9d2001 --- /dev/null +++ b/admin/deploy/kubernetes/server/gva-server-deployment.yaml @@ -0,0 +1,74 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: gva-server + annotations: + flipped-aurora/gin-vue-admin: backend + github: "https://github.com/flipped-aurora/gin-vue-admin.git" + app.kubernetes.io/version: 0.0.1 + labels: + app: gva-server + version: gva-vue3 +spec: + replicas: 1 + selector: + matchLabels: + app: gva-server + version: gva-vue3 + template: + metadata: + labels: + app: gva-server + version: gva-vue3 + spec: + containers: + - name: gin-vue-admin-container + image: registry.cn-hangzhou.aliyuncs.com/gva/server:latest + imagePullPolicy: Always + ports: + - containerPort: 8888 + name: http + volumeMounts: + - mountPath: /go/src/github.com/flipped-aurora/gin-vue-admin/server/config.docker.yaml + name: config + subPath: config.yaml + - mountPath: /etc/localtime + name: localtime + resources: + limits: + cpu: 1000m + memory: 2000Mi + requests: + cpu: 100m + memory: 200Mi + livenessProbe: + failureThreshold: 1 + periodSeconds: 5 + successThreshold: 1 + tcpSocket: + port: 8888 + timeoutSeconds: 1 + readinessProbe: + failureThreshold: 3 + initialDelaySeconds: 30 + periodSeconds: 5 + successThreshold: 1 + tcpSocket: + port: 8888 + timeoutSeconds: 1 + startupProbe: + failureThreshold: 40 + periodSeconds: 5 + successThreshold: 1 + tcpSocket: + port: 8888 + timeoutSeconds: 1 + #imagePullSecrets: + # - name: docker-registry + volumes: + - name: localtime + hostPath: + path: /etc/localtime + - name: config + configMap: + name: config.yaml \ No newline at end of file diff --git a/admin/deploy/kubernetes/server/gva-server-service.yaml b/admin/deploy/kubernetes/server/gva-server-service.yaml new file mode 100644 index 000000000..17aaef2c8 --- /dev/null +++ b/admin/deploy/kubernetes/server/gva-server-service.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Service +metadata: + name: gva-server + annotations: + flipped-aurora/gin-vue-admin: backend + github: "https://github.com/flipped-aurora/gin-vue-admin.git" + app.kubernetes.io/version: 0.0.1 + labels: + app: gva-server + version: gva-vue3 +spec: + selector: + app: gva-server + version: gva-vue3 + ports: + - port: 8888 + name: http + targetPort: 8888 + type: ClusterIP +# type: NodePort diff --git a/admin/deploy/kubernetes/web/gva-web-configmap.yaml b/admin/deploy/kubernetes/web/gva-web-configmap.yaml new file mode 100644 index 000000000..189b8617e --- /dev/null +++ b/admin/deploy/kubernetes/web/gva-web-configmap.yaml @@ -0,0 +1,32 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: my.conf +data: + my.conf: | + server { + listen 8080; + server_name localhost; + + #charset koi8-r; + #access_log logs/host.access.log main; + + location / { + root /usr/share/nginx/html; + add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0'; + try_files $uri $uri/ /index.html; + } + + location /api { + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + rewrite ^/api/(.*)$ /$1 break; #重写 + proxy_pass http://gva-server:8888; # 设置代理服务器的协议和地址 + } + + location /api/swagger/index.html { + proxy_pass http://gva-server:8888/swagger/index.html; + } + } \ No newline at end of file diff --git a/admin/deploy/kubernetes/web/gva-web-deploymemt.yaml b/admin/deploy/kubernetes/web/gva-web-deploymemt.yaml new file mode 100644 index 000000000..e6d15bceb --- /dev/null +++ b/admin/deploy/kubernetes/web/gva-web-deploymemt.yaml @@ -0,0 +1,51 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: gva-web + annotations: + flipped-aurora/gin-vue-admin: ui + github: "https://github.com/flipped-aurora/gin-vue-admin.git" + app.kubernetes.io/version: 0.0.1 + labels: + app: gva-web + version: gva-vue3 +spec: + replicas: 1 + selector: + matchLabels: + app: gva-web + version: gva-vue3 + template: + metadata: + labels: + app: gva-web + version: gva-vue3 + spec: + containers: + - name: gin-vue-admin-nginx-container + image: registry.cn-hangzhou.aliyuncs.com/gva/web:latest + imagePullPolicy: Always + ports: + - containerPort: 8080 + name: http + readinessProbe: + tcpSocket: + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 10 + successThreshold: 1 + failureThreshold: 3 + resources: + limits: + cpu: 500m + memory: 1000Mi + requests: + cpu: 100m + memory: 100Mi + volumeMounts: + - mountPath: /etc/nginx/conf.d/ + name: nginx-config + volumes: + - name: nginx-config + configMap: + name: my.conf diff --git a/admin/deploy/kubernetes/web/gva-web-ingress.yaml b/admin/deploy/kubernetes/web/gva-web-ingress.yaml new file mode 100644 index 000000000..81922f17a --- /dev/null +++ b/admin/deploy/kubernetes/web/gva-web-ingress.yaml @@ -0,0 +1,18 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: gva-ingress + annotations: + kubernetes.io/ingress.class: "nginx" +spec: + rules: + - host: demo.gin-vue-admin.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: gva-web + port: + number: 8080 \ No newline at end of file diff --git a/admin/deploy/kubernetes/web/gva-web-service.yaml b/admin/deploy/kubernetes/web/gva-web-service.yaml new file mode 100644 index 000000000..c374bdb56 --- /dev/null +++ b/admin/deploy/kubernetes/web/gva-web-service.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Service +metadata: + name: gva-web + annotations: + flipped-aurora/gin-vue-admin: ui + github: "https://github.com/flipped-aurora/gin-vue-admin.git" + app.kubernetes.io/version: 0.0.1 + labels: + app: gva-web + version: gva-vue3 +spec: +# type: NodePort + type: ClusterIP + ports: + - name: http + port: 8080 + targetPort: 8080 + selector: + app: gva-web + version: gva-vue3 diff --git a/admin/docs/gin-vue-admin.png b/admin/docs/gin-vue-admin.png new file mode 100644 index 0000000000000000000000000000000000000000..bbe0549e18c09229c29a8b1e3e84edc5b1cc14e0 GIT binary patch literal 105776 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 literal 0 HcmV?d00001 diff --git a/admin/gin-vue-admin.code-workspace b/admin/gin-vue-admin.code-workspace new file mode 100644 index 000000000..c9fb1ec4d --- /dev/null +++ b/admin/gin-vue-admin.code-workspace @@ -0,0 +1,49 @@ +{ + "folders": [ + { + "path": "server", + "name": "backend" + }, + { + "path": "web", + "name": "frontend" + }, + { + "path": ".", + "name": "root" + } + ], + "settings": { + "go.toolsEnvVars": { + "GOPROXY": "https://goproxy.cn,direct", + "GONOPROXY": "none;" + } + }, + "launch": { + "version": "0.2.0", + "configurations": [ + { + "type": "go", + "request": "launch", + "name": "Backend", + "cwd": "${workspaceFolder:backend}", + "program": "${workspaceFolder:backend}/" + }, + { + "type": "node", + "request": "launch", + "cwd": "${workspaceFolder:frontend}", + "name": "Frontend", + "runtimeExecutable": "npm", + "runtimeArgs": ["run-script", "serve"] + } + ], + "compounds": [ + { + "name": "Both (Backend & Frontend)", + "configurations": ["Backend", "Frontend"], + "stopAll": true + } + ] + } +} diff --git a/admin/server/Dockerfile b/admin/server/Dockerfile new file mode 100644 index 000000000..259edaed4 --- /dev/null +++ b/admin/server/Dockerfile @@ -0,0 +1,33 @@ +FROM golang:alpine as builder + +RUN mkdir /app +WORKDIR /app +COPY . . + +RUN go env -w GO111MODULE=on \ + && go env -w GOPROXY=https://goproxy.cn,direct \ + && go env -w CGO_ENABLED=0 \ + && go env \ + && go mod tidy \ + && go build -o server . + +FROM alpine:latest + +LABEL MAINTAINER="SliverHorn@sliver_horn@qq.com" +# 设置时区 +ENV TZ=Asia/Shanghai +RUN mkdir /app \ + && apk update && apk add --no-cache tzdata openntpd \ + && ln -sf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +WORKDIR /app + +COPY --from=0 /app/server ./ +COPY --from=0 /app/resource ./resource/ +COPY --from=0 /app/config.docker.yaml ./ + +# 挂载目录:如果使用了sqlite数据库,容器命令示例:docker run -d -v /宿主机路径/gva.db:/app/gva.db -p 8888:8888 --name gva-server-v1 gva-server:1.0 +# VOLUME ["/app"] + +EXPOSE 8888 +ENTRYPOINT ./server -c config.docker.yaml diff --git a/admin/server/README.md b/admin/server/README.md new file mode 100644 index 000000000..9a34870bc --- /dev/null +++ b/admin/server/README.md @@ -0,0 +1,54 @@ +## server项目结构 + +```shell +├── api +│   └── v1 +├── config +├── core +├── docs +├── global +├── initialize +│   └── internal +├── middleware +├── model +│   ├── request +│   └── response +├── packfile +├── resource +│   ├── excel +│   ├── page +│   └── template +├── router +├── service +├── source +└── utils + ├── timer + └── upload +``` + +| 文件夹 | 说明 | 描述 | +| ------------ | ----------------------- | --------------------------- | +| `api` | api层 | api层 | +| `--v1` | v1版本接口 | v1版本接口 | +| `config` | 配置包 | config.yaml对应的配置结构体 | +| `core` | 核心文件 | 核心组件(zap, viper, server)的初始化 | +| `docs` | swagger文档目录 | swagger文档目录 | +| `global` | 全局对象 | 全局对象 | +| `initialize` | 初始化 | router,redis,gorm,validator, timer的初始化 | +| `--internal` | 初始化内部函数 | gorm 的 longger 自定义,在此文件夹的函数只能由 `initialize` 层进行调用 | +| `middleware` | 中间件层 | 用于存放 `gin` 中间件代码 | +| `model` | 模型层 | 模型对应数据表 | +| `--request` | 入参结构体 | 接收前端发送到后端的数据。 | +| `--response` | 出参结构体 | 返回给前端的数据结构体 | +| `packfile` | 静态文件打包 | 静态文件打包 | +| `resource` | 静态资源文件夹 | 负责存放静态文件 | +| `--excel` | excel导入导出默认路径 | excel导入导出默认路径 | +| `--page` | 表单生成器 | 表单生成器 打包后的dist | +| `--template` | 模板 | 模板文件夹,存放的是代码生成器的模板 | +| `router` | 路由层 | 路由层 | +| `service` | service层 | 存放业务逻辑问题 | +| `source` | source层 | 存放初始化数据的函数 | +| `utils` | 工具包 | 工具函数封装 | +| `--timer` | timer | 定时器接口封装 | +| `--upload` | oss | oss接口封装 | + diff --git a/admin/server/api/v1/enter.go b/admin/server/api/v1/enter.go new file mode 100644 index 000000000..7d9ebadc9 --- /dev/null +++ b/admin/server/api/v1/enter.go @@ -0,0 +1,15 @@ +package v1 + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/api/v1/example" + "github.com/flipped-aurora/gin-vue-admin/server/api/v1/gaia" + "github.com/flipped-aurora/gin-vue-admin/server/api/v1/system" +) + +var ApiGroupApp = new(ApiGroup) + +type ApiGroup struct { + SystemApiGroup system.ApiGroup + ExampleApiGroup example.ApiGroup + GaiaApiGroup gaia.ApiGroup +} diff --git a/admin/server/api/v1/example/enter.go b/admin/server/api/v1/example/enter.go new file mode 100644 index 000000000..c182328ea --- /dev/null +++ b/admin/server/api/v1/example/enter.go @@ -0,0 +1,13 @@ +package example + +import "github.com/flipped-aurora/gin-vue-admin/server/service" + +type ApiGroup struct { + CustomerApi + FileUploadAndDownloadApi +} + +var ( + customerService = service.ServiceGroupApp.ExampleServiceGroup.CustomerService + fileUploadAndDownloadService = service.ServiceGroupApp.ExampleServiceGroup.FileUploadAndDownloadService +) diff --git a/admin/server/api/v1/example/exa_breakpoint_continue.go b/admin/server/api/v1/example/exa_breakpoint_continue.go new file mode 100644 index 000000000..8f39cb160 --- /dev/null +++ b/admin/server/api/v1/example/exa_breakpoint_continue.go @@ -0,0 +1,150 @@ +package example + +import ( + "fmt" + "io" + "mime/multipart" + "strconv" + + "github.com/flipped-aurora/gin-vue-admin/server/model/example" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + exampleRes "github.com/flipped-aurora/gin-vue-admin/server/model/example/response" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +// BreakpointContinue +// @Tags ExaFileUploadAndDownload +// @Summary 断点续传到服务器 +// @Security ApiKeyAuth +// @accept multipart/form-data +// @Produce application/json +// @Param file formData file true "an example for breakpoint resume, 断点续传示例" +// @Success 200 {object} response.Response{msg=string} "断点续传到服务器" +// @Router /fileUploadAndDownload/breakpointContinue [post] +func (b *FileUploadAndDownloadApi) BreakpointContinue(c *gin.Context) { + fileMd5 := c.Request.FormValue("fileMd5") + fileName := c.Request.FormValue("fileName") + chunkMd5 := c.Request.FormValue("chunkMd5") + chunkNumber, _ := strconv.Atoi(c.Request.FormValue("chunkNumber")) + chunkTotal, _ := strconv.Atoi(c.Request.FormValue("chunkTotal")) + _, FileHeader, err := c.Request.FormFile("file") + if err != nil { + global.GVA_LOG.Error("接收文件失败!", zap.Error(err)) + response.FailWithMessage("接收文件失败", c) + return + } + f, err := FileHeader.Open() + if err != nil { + global.GVA_LOG.Error("文件读取失败!", zap.Error(err)) + response.FailWithMessage("文件读取失败", c) + return + } + defer func(f multipart.File) { + err := f.Close() + if err != nil { + fmt.Println(err) + } + }(f) + cen, _ := io.ReadAll(f) + if !utils.CheckMd5(cen, chunkMd5) { + global.GVA_LOG.Error("检查md5失败!", zap.Error(err)) + response.FailWithMessage("检查md5失败", c) + return + } + file, err := fileUploadAndDownloadService.FindOrCreateFile(fileMd5, fileName, chunkTotal) + if err != nil { + global.GVA_LOG.Error("查找或创建记录失败!", zap.Error(err)) + response.FailWithMessage("查找或创建记录失败", c) + return + } + pathC, err := utils.BreakPointContinue(cen, fileName, chunkNumber, chunkTotal, fileMd5) + if err != nil { + global.GVA_LOG.Error("断点续传失败!", zap.Error(err)) + response.FailWithMessage("断点续传失败", c) + return + } + + if err = fileUploadAndDownloadService.CreateFileChunk(file.ID, pathC, chunkNumber); err != nil { + global.GVA_LOG.Error("创建文件记录失败!", zap.Error(err)) + response.FailWithMessage("创建文件记录失败", c) + return + } + response.OkWithMessage("切片创建成功", c) +} + +// FindFile +// @Tags ExaFileUploadAndDownload +// @Summary 查找文件 +// @Security ApiKeyAuth +// @accept multipart/form-data +// @Produce application/json +// @Param file formData file true "Find the file, 查找文件" +// @Success 200 {object} response.Response{data=exampleRes.FileResponse,msg=string} "查找文件,返回包括文件详情" +// @Router /fileUploadAndDownload/findFile [get] +func (b *FileUploadAndDownloadApi) FindFile(c *gin.Context) { + fileMd5 := c.Query("fileMd5") + fileName := c.Query("fileName") + chunkTotal, _ := strconv.Atoi(c.Query("chunkTotal")) + file, err := fileUploadAndDownloadService.FindOrCreateFile(fileMd5, fileName, chunkTotal) + if err != nil { + global.GVA_LOG.Error("查找失败!", zap.Error(err)) + response.FailWithMessage("查找失败", c) + } else { + response.OkWithDetailed(exampleRes.FileResponse{File: file}, "查找成功", c) + } +} + +// BreakpointContinueFinish +// @Tags ExaFileUploadAndDownload +// @Summary 创建文件 +// @Security ApiKeyAuth +// @accept multipart/form-data +// @Produce application/json +// @Param file formData file true "上传文件完成" +// @Success 200 {object} response.Response{data=exampleRes.FilePathResponse,msg=string} "创建文件,返回包括文件路径" +// @Router /fileUploadAndDownload/findFile [post] +func (b *FileUploadAndDownloadApi) BreakpointContinueFinish(c *gin.Context) { + fileMd5 := c.Query("fileMd5") + fileName := c.Query("fileName") + filePath, err := utils.MakeFile(fileName, fileMd5) + if err != nil { + global.GVA_LOG.Error("文件创建失败!", zap.Error(err)) + response.FailWithDetailed(exampleRes.FilePathResponse{FilePath: filePath}, "文件创建失败", c) + } else { + response.OkWithDetailed(exampleRes.FilePathResponse{FilePath: filePath}, "文件创建成功", c) + } +} + +// RemoveChunk +// @Tags ExaFileUploadAndDownload +// @Summary 删除切片 +// @Security ApiKeyAuth +// @accept multipart/form-data +// @Produce application/json +// @Param file formData file true "删除缓存切片" +// @Success 200 {object} response.Response{msg=string} "删除切片" +// @Router /fileUploadAndDownload/removeChunk [post] +func (b *FileUploadAndDownloadApi) RemoveChunk(c *gin.Context) { + var file example.ExaFile + err := c.ShouldBindJSON(&file) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.RemoveChunk(file.FileMd5) + if err != nil { + global.GVA_LOG.Error("缓存切片删除失败!", zap.Error(err)) + return + } + err = fileUploadAndDownloadService.DeleteFileChunk(file.FileMd5, file.FilePath) + if err != nil { + global.GVA_LOG.Error(err.Error(), zap.Error(err)) + response.FailWithMessage(err.Error(), c) + return + } + response.OkWithMessage("缓存切片删除成功", c) +} diff --git a/admin/server/api/v1/example/exa_customer.go b/admin/server/api/v1/example/exa_customer.go new file mode 100644 index 000000000..5d9ef1c02 --- /dev/null +++ b/admin/server/api/v1/example/exa_customer.go @@ -0,0 +1,176 @@ +package example + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/example" + exampleRes "github.com/flipped-aurora/gin-vue-admin/server/model/example/response" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type CustomerApi struct{} + +// CreateExaCustomer +// @Tags ExaCustomer +// @Summary 创建客户 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body example.ExaCustomer true "客户用户名, 客户手机号码" +// @Success 200 {object} response.Response{msg=string} "创建客户" +// @Router /customer/customer [post] +func (e *CustomerApi) CreateExaCustomer(c *gin.Context) { + var customer example.ExaCustomer + err := c.ShouldBindJSON(&customer) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(customer, utils.CustomerVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + customer.SysUserID = utils.GetUserID(c) + customer.SysUserAuthorityID = utils.GetUserAuthorityId(c) + err = customerService.CreateExaCustomer(customer) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败", c) + return + } + response.OkWithMessage("创建成功", c) +} + +// DeleteExaCustomer +// @Tags ExaCustomer +// @Summary 删除客户 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body example.ExaCustomer true "客户ID" +// @Success 200 {object} response.Response{msg=string} "删除客户" +// @Router /customer/customer [delete] +func (e *CustomerApi) DeleteExaCustomer(c *gin.Context) { + var customer example.ExaCustomer + err := c.ShouldBindJSON(&customer) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(customer.GVA_MODEL, utils.IdVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = customerService.DeleteExaCustomer(customer) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败", c) + return + } + response.OkWithMessage("删除成功", c) +} + +// UpdateExaCustomer +// @Tags ExaCustomer +// @Summary 更新客户信息 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body example.ExaCustomer true "客户ID, 客户信息" +// @Success 200 {object} response.Response{msg=string} "更新客户信息" +// @Router /customer/customer [put] +func (e *CustomerApi) UpdateExaCustomer(c *gin.Context) { + var customer example.ExaCustomer + err := c.ShouldBindJSON(&customer) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(customer.GVA_MODEL, utils.IdVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(customer, utils.CustomerVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = customerService.UpdateExaCustomer(&customer) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败", c) + return + } + response.OkWithMessage("更新成功", c) +} + +// GetExaCustomer +// @Tags ExaCustomer +// @Summary 获取单一客户信息 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query example.ExaCustomer true "客户ID" +// @Success 200 {object} response.Response{data=exampleRes.ExaCustomerResponse,msg=string} "获取单一客户信息,返回包括客户详情" +// @Router /customer/customer [get] +func (e *CustomerApi) GetExaCustomer(c *gin.Context) { + var customer example.ExaCustomer + err := c.ShouldBindQuery(&customer) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(customer.GVA_MODEL, utils.IdVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + data, err := customerService.GetExaCustomer(customer.ID) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(exampleRes.ExaCustomerResponse{Customer: data}, "获取成功", c) +} + +// GetExaCustomerList +// @Tags ExaCustomer +// @Summary 分页获取权限客户列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query request.PageInfo true "页码, 每页大小" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页获取权限客户列表,返回包括列表,总数,页码,每页数量" +// @Router /customer/customerList [get] +func (e *CustomerApi) GetExaCustomerList(c *gin.Context) { + var pageInfo request.PageInfo + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(pageInfo, utils.PageInfoVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + customerList, total, err := customerService.GetCustomerInfoList(utils.GetUserAuthorityId(c), pageInfo) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败"+err.Error(), c) + return + } + response.OkWithDetailed(response.PageResult{ + List: customerList, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.PageSize, + }, "获取成功", c) +} diff --git a/admin/server/api/v1/example/exa_file_upload_download.go b/admin/server/api/v1/example/exa_file_upload_download.go new file mode 100644 index 000000000..6905936d7 --- /dev/null +++ b/admin/server/api/v1/example/exa_file_upload_download.go @@ -0,0 +1,133 @@ +package example + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/example" + exampleRes "github.com/flipped-aurora/gin-vue-admin/server/model/example/response" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type FileUploadAndDownloadApi struct{} + +// UploadFile +// @Tags ExaFileUploadAndDownload +// @Summary 上传文件示例 +// @Security ApiKeyAuth +// @accept multipart/form-data +// @Produce application/json +// @Param file formData file true "上传文件示例" +// @Success 200 {object} response.Response{data=exampleRes.ExaFileResponse,msg=string} "上传文件示例,返回包括文件详情" +// @Router /fileUploadAndDownload/upload [post] +func (b *FileUploadAndDownloadApi) UploadFile(c *gin.Context) { + var file example.ExaFileUploadAndDownload + noSave := c.DefaultQuery("noSave", "0") + _, header, err := c.Request.FormFile("file") + if err != nil { + global.GVA_LOG.Error("接收文件失败!", zap.Error(err)) + response.FailWithMessage("接收文件失败", c) + return + } + file, err = fileUploadAndDownloadService.UploadFile(header, noSave) // 文件上传后拿到文件路径 + if err != nil { + global.GVA_LOG.Error("上传文件失败!", zap.Error(err)) + response.FailWithMessage("上传文件失败", c) + return + } + response.OkWithDetailed(exampleRes.ExaFileResponse{File: file}, "上传成功", c) +} + +// EditFileName 编辑文件名或者备注 +func (b *FileUploadAndDownloadApi) EditFileName(c *gin.Context) { + var file example.ExaFileUploadAndDownload + err := c.ShouldBindJSON(&file) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = fileUploadAndDownloadService.EditFileName(file) + if err != nil { + global.GVA_LOG.Error("编辑失败!", zap.Error(err)) + response.FailWithMessage("编辑失败", c) + return + } + response.OkWithMessage("编辑成功", c) +} + +// DeleteFile +// @Tags ExaFileUploadAndDownload +// @Summary 删除文件 +// @Security ApiKeyAuth +// @Produce application/json +// @Param data body example.ExaFileUploadAndDownload true "传入文件里面id即可" +// @Success 200 {object} response.Response{msg=string} "删除文件" +// @Router /fileUploadAndDownload/deleteFile [post] +func (b *FileUploadAndDownloadApi) DeleteFile(c *gin.Context) { + var file example.ExaFileUploadAndDownload + err := c.ShouldBindJSON(&file) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if err := fileUploadAndDownloadService.DeleteFile(file); err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败", c) + return + } + response.OkWithMessage("删除成功", c) +} + +// GetFileList +// @Tags ExaFileUploadAndDownload +// @Summary 分页文件列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.PageInfo true "页码, 每页大小" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页文件列表,返回包括列表,总数,页码,每页数量" +// @Router /fileUploadAndDownload/getFileList [post] +func (b *FileUploadAndDownloadApi) GetFileList(c *gin.Context) { + var pageInfo request.PageInfo + err := c.ShouldBindJSON(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := fileUploadAndDownloadService.GetFileRecordInfoList(pageInfo) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.PageSize, + }, "获取成功", c) +} + +// ImportURL +// @Tags ExaFileUploadAndDownload +// @Summary 导入URL +// @Security ApiKeyAuth +// @Produce application/json +// @Param data body example.ExaFileUploadAndDownload true "对象" +// @Success 200 {object} response.Response{msg=string} "导入URL" +// @Router /fileUploadAndDownload/importURL [post] +func (b *FileUploadAndDownloadApi) ImportURL(c *gin.Context) { + var file []example.ExaFileUploadAndDownload + err := c.ShouldBindJSON(&file) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if err := fileUploadAndDownloadService.ImportURL(&file); err != nil { + global.GVA_LOG.Error("导入URL失败!", zap.Error(err)) + response.FailWithMessage("导入URL失败", c) + return + } + response.OkWithMessage("导入URL成功", c) +} diff --git a/admin/server/api/v1/gaia/dashboard.go b/admin/server/api/v1/gaia/dashboard.go new file mode 100644 index 000000000..7ccd0d6ae --- /dev/null +++ b/admin/server/api/v1/gaia/dashboard.go @@ -0,0 +1,159 @@ +package gaia + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + gaiaReq "github.com/flipped-aurora/gin-vue-admin/server/model/gaia/request" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type DashboardApi struct{} + +// GetAccountQuotaRankingData 分页获取账号额度排名 +// @Tags Dashboard +// @Summary 分页获取dashboard表列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query gaiaReq.DashboardSearch true "分页获取账号额度排名列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /dashboard/getAccountQuotaRankingData [get] +func (dashboardApi *DashboardApi) GetAccountQuotaRankingData(c *gin.Context) { + var pageInfo gaiaReq.GetAccountQuotaRankingDataReq + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := dashboardService.GetAccountQuotaRankingData(pageInfo) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败:"+err.Error(), c) + return + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.PageSize, + }, "获取成功", c) +} + +// GetAppQuotaRankingData 分页获取【应用】配额排名数据 +// @Tags Dashboard +// @Summary 分页获取dashboard表列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query gaiaReq.DashboardSearch true "分页获取【应用】配额排名数据" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /dashboard/getAppQuotaRankingData [get] +func (dashboardApi *DashboardApi) GetAppQuotaRankingData(c *gin.Context) { + var pageInfo gaiaReq.GetAppQuotaRankingDataReq + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := dashboardService.GetAppQuotaRankingData(pageInfo) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败:"+err.Error(), c) + return + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.PageSize, + }, "获取成功", c) +} + +// GetAppTokenQuotaRankingData 分页获取【应用密钥】配额排名数据列表 +// @Tags Dashboard +// @Summary 分页获取dashboard表列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query gaiaReq.DashboardSearch true "分页获取【应用密钥】配额排名数据列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /dashboard/getAppTokenQuotaRankingData [get] +func (dashboardApi *DashboardApi) GetAppTokenQuotaRankingData(c *gin.Context) { + var pageInfo gaiaReq.GetAppTokenQuotaRankingDataReq + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := dashboardService.GetAppTokenQuotaRankingData(pageInfo) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败:"+err.Error(), c) + return + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.PageSize, + }, "获取成功", c) +} + +// GetAppTokenDailyQuotaData 获取每天密钥花费数据列表 +// @Tags Dashboard +// @Summary 分页获取dashboard表列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query gaiaReq.DashboardSearch true "获取每天密钥花费数据列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /dashboard/getAppTokenDailyQuotaData [get] +func (dashboardApi *DashboardApi) GetAppTokenDailyQuotaData(c *gin.Context) { + var pageInfo gaiaReq.GetAppTokenDailyQuotaDataReq + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, err := dashboardService.GetAppTokenDailyQuotaData(pageInfo) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败:"+err.Error(), c) + return + } + response.OkWithDetailed(response.PageResult{ + List: list, + Page: pageInfo.Page, + PageSize: pageInfo.PageSize, + }, "获取成功", c) +} + +// GetAiImageQuotaRankingData 获取每天ai图片花费数据列表 +// @Tags Dashboard +// @Summary 获取每天ai图片花费数据列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query gaiaReq.DashboardSearch true "获取每天ai图片花费数据列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /dashboard/getAppTokenDailyQuotaData [get] +func (dashboardApi *DashboardApi) GetAiImageQuotaRankingData(c *gin.Context) { + var pageInfo gaiaReq.GetAiImageQuotaRankingDataReq + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, err := dashboardService.GetAiImageQuotaRankingData(pageInfo) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败:"+err.Error(), c) + return + } + response.OkWithDetailed(response.PageResult{ + List: list, + Page: pageInfo.Page, + PageSize: pageInfo.PageSize, + }, "获取成功", c) +} diff --git a/admin/server/api/v1/gaia/enter.go b/admin/server/api/v1/gaia/enter.go new file mode 100644 index 000000000..262a11a20 --- /dev/null +++ b/admin/server/api/v1/gaia/enter.go @@ -0,0 +1,19 @@ +package gaia + +import "github.com/flipped-aurora/gin-vue-admin/server/service" + +type ApiGroup struct { + DashboardApi + QuotaApi + TenantsApi + SystemApi + TestApi +} + +var ( + dashboardService = service.ServiceGroupApp.GaiaServiceGroup.DashboardService + tenantsService = service.ServiceGroupApp.GaiaServiceGroup.TenantsService + systemIntegratedService = service.ServiceGroupApp.GaiaServiceGroup.SystemIntegratedService +) +var QuotaService = service.ServiceGroupApp.GaiaServiceGroup.QuotaService +var TestService = service.ServiceGroupApp.GaiaServiceGroup.TestService diff --git a/admin/server/api/v1/gaia/quota.go b/admin/server/api/v1/gaia/quota.go new file mode 100644 index 000000000..8c635dfd4 --- /dev/null +++ b/admin/server/api/v1/gaia/quota.go @@ -0,0 +1,73 @@ +package gaia + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + gaiaReq "github.com/flipped-aurora/gin-vue-admin/server/model/gaia/request" + "github.com/gin-gonic/gin" + "github.com/gofrs/uuid/v5" + "go.uber.org/zap" +) + +type QuotaApi struct{} + +// QuotaManagementList +// @Tags Quota +// @Summary 额度管理列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query gaiaReq.DashboardSearch true "分页获取账号额度排名列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /gaia/quota/getManagementList [get] +func (quotaApi *QuotaApi) QuotaManagementList(c *gin.Context) { + var pageInfo gaiaReq.GetAccountQuotaRankingDataReq + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := QuotaService.GetQuotaManagementData(pageInfo) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败:"+err.Error(), c) + return + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.PageSize, + }, "获取成功", c) +} + +// SetUserQuota +// @Tags Quota +// @Summary 设置用户额度 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query gaiaReq.DashboardSearch true "分页获取账号额度排名列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /gaia/quota/setUserQuota [post] +func (quotaApi *QuotaApi) SetUserQuota(c *gin.Context) { + var err error + var uid uuid.UUID + var pageInfo gaiaReq.SetUserQuotaRequest + if err = c.ShouldBindJSON(&pageInfo); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + // + if uid, err = uuid.FromString(pageInfo.Uid); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + if err = QuotaService.SetUserQuota(uid, pageInfo.Quota); err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败:"+err.Error(), c) + return + } + response.OkWithDetailed("ok", "修改成功", c) +} diff --git a/admin/server/api/v1/gaia/system.go b/admin/server/api/v1/gaia/system.go new file mode 100644 index 000000000..b5bf6735d --- /dev/null +++ b/admin/server/api/v1/gaia/system.go @@ -0,0 +1,51 @@ +package gaia + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/gaia" + "github.com/gin-gonic/gin" +) + +type SystemApi struct{} + +// GetDingTalk 获取钉钉系统配置 +// @Tags System +// @Summary 获取钉钉系统配置 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query gaia.Tenants true "用id查询tenants表" +// @Success 200 {object} response.Response{data=gaia.Tenants,msg=string} "查询成功" +// @Router /gaia/system/dingtalk [get] +func (systemApi *SystemApi) GetDingTalk(c *gin.Context) { + var config = make(map[string]interface{}) + config["host"] = global.GVA_CONFIG.Gaia.Url + config["config"] = systemIntegratedService.GetIntegratedConfig(gaia.SystemIntegrationDingTalk) + response.OkWithData(config, c) +} + +// SetDingTalk 设置钉钉系统配置 +// @Tags System +// @Summary 设置钉钉系统配置 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query gaia.Tenants true "用id查询tenants表" +// @Success 200 {object} response.Response{data=gaia.Tenants,msg=string} "查询成功" +// @Router /gaia/system/dingtalk [post] +func (systemApi *SystemApi) SetDingTalk(c *gin.Context) { + var err error + var req gaia.SystemIntegration + if err = c.ShouldBindJSON(&req); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + // update + req.Classify = gaia.SystemIntegrationDingTalk + if err = systemIntegratedService.SetIntegratedConfig(req, req.Test); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + response.OkWithData("ok", c) +} diff --git a/admin/server/api/v1/gaia/tenants.go b/admin/server/api/v1/gaia/tenants.go new file mode 100644 index 000000000..194de06d2 --- /dev/null +++ b/admin/server/api/v1/gaia/tenants.go @@ -0,0 +1,80 @@ +package gaia + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + gaiaReq "github.com/flipped-aurora/gin-vue-admin/server/model/gaia/request" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type TenantsApi struct{} + +// FindTenants 用id查询tenants表 +// @Tags Tenants +// @Summary 用id查询tenants表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query gaia.Tenants true "用id查询tenants表" +// @Success 200 {object} response.Response{data=gaia.Tenants,msg=string} "查询成功" +// @Router /tenants/findTenants [get] +func (tenantsApi *TenantsApi) FindTenants(c *gin.Context) { + id := c.Query("id") + retenants, err := tenantsService.GetTenants(id) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败:"+err.Error(), c) + return + } + response.OkWithData(retenants, c) +} + +// GetTenantsList 分页获取tenants表列表 +// @Tags Tenants +// @Summary 分页获取tenants表列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query gaiaReq.TenantsSearch true "分页获取tenants表列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /tenants/getTenantsList [get] +func (tenantsApi *TenantsApi) GetTenantsList(c *gin.Context) { + var pageInfo gaiaReq.TenantsSearch + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := tenantsService.GetTenantsInfoList(pageInfo) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败:"+err.Error(), c) + return + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.PageSize, + }, "获取成功", c) +} + +// GetAllTenants 获取所有工作区 +// @Tags Tenants +// @Summary 获取所有工作区 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query gaia.Tenants true "获取所有工作区" +// @Success 200 {object} response.Response{data=gaia.Tenants,msg=string} "查询成功" +// @Router /tenants/getAllTenants [get] +func (tenantsApi *TenantsApi) GetAllTenants(c *gin.Context) { + retenants, err := tenantsService.GetAllTenants() + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败:"+err.Error(), c) + return + } + response.OkWithData(retenants, c) +} diff --git a/admin/server/api/v1/gaia/test.go b/admin/server/api/v1/gaia/test.go new file mode 100644 index 000000000..029358ef1 --- /dev/null +++ b/admin/server/api/v1/gaia/test.go @@ -0,0 +1,141 @@ +package gaia + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/gaia" + "github.com/flipped-aurora/gin-vue-admin/server/model/gaia/request" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type TestApi struct{} + +// SyncDatabaseTableData +// @Tags Test +// @Summary 同步数据库表数据 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query gaiaReq.DashboardSearch true "分页获取账号额度排名列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /gaia/test/sync/database [post] +func (quotaApi *TestApi) SyncDatabaseTableData(c *gin.Context) { + var data request.SyncDatabaseTableData + if err := c.ShouldBindJSON(&data); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + // 是否都不为空 + if len(data.LogTable) > 0 && len(data.NewTable) > 0 && len(data.KeyName) > 0 && len(data.OrderName) > 0 { + go TestService.SyncDatabaseTableData(data.LogTable, data.NewTable, data.KeyName, data.OrderName, data.GroupName) + response.OkWithDetailed("ok", "获取成功", c) + } else { + response.FailWithMessage("传参有误", c) + } + +} + +// GaiaAppRequestTest +// @Tags Test +// @Summary 发起gaia应用请求测试 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query gaiaReq.DashboardSearch true "分页获取账号额度排名列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /gaia/test/app/request [post] +func (quotaApi *TestApi) GaiaAppRequestTest(c *gin.Context) { + err := TestService.AppRequestTest() + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败:"+err.Error(), c) + return + } + response.OkWithDetailed("ok", "获取成功", c) +} + +// GaiaAppRequestTestList +// @Tags Test +// @Summary gaia应用请求测试结果列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query gaiaReq.DashboardSearch true "分页获取账号额度排名列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /gaia/test/app/request/list [get] +func (quotaApi *TestApi) GaiaAppRequestTestList(c *gin.Context) { + var pageInfo request.GetAppRequestTestRequest + if err := c.ShouldBindQuery(&pageInfo); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + // list + lock, list, total, err := TestService.AppRequestTestList(pageInfo) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败:"+err.Error(), c) + return + } + var appIdList []string + var appList []map[string]string + var batchInfo []gaia.AppRequestTest + if err = global.GVA_DB.Select("app_id").Where("batch_id = ?", pageInfo.BatchId).Group( + "app_id").Find(&batchInfo).Error; err == nil { + for _, v := range batchInfo { + appIdList = append(appIdList, v.AppID) + } + } + // 查询相关的app列表 + if len(appIdList) > 0 { + var apps []gaia.Apps + if err = global.GVA_DB.Select("id", "name").Where( + "id IN (?)", appIdList).Find(&apps).Error; err == nil { + for _, v := range apps { + appList = append(appList, map[string]string{ + "value": v.ID.String(), + "label": v.Name, + }) + } + } + } + response.OkWithDetailed(map[string]interface{}{ + "lock": lock, + "list": list, + "total": total, + "apps": appList, + "page": pageInfo.Page, + "page_size": pageInfo.PageSize, + }, "获取成功", c) +} + +// GaiaAppRequestTestBatch +// @Tags Test +// @Summary gaia应用请求测试批次列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query gaiaReq.DashboardSearch true "分页获取账号额度排名列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /gaia/test/app/request/batch [get] +func (quotaApi *TestApi) GaiaAppRequestTestBatch(c *gin.Context) { + var pageInfo request.GetAppRequestTestRequest + if err := c.ShouldBindQuery(&pageInfo); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + // list + lock, list, total, err := TestService.AppRequestTestBatch(pageInfo) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败:"+err.Error(), c) + return + } + response.OkWithDetailed(map[string]interface{}{ + "lock": lock, + "list": list, + "total": total, + "page": pageInfo.Page, + "page_size": pageInfo.PageSize, + }, "获取成功", c) +} diff --git a/admin/server/api/v1/system/auto_code_history.go b/admin/server/api/v1/system/auto_code_history.go new file mode 100644 index 000000000..065ddd86c --- /dev/null +++ b/admin/server/api/v1/system/auto_code_history.go @@ -0,0 +1,115 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + common "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + request "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type AutoCodeHistoryApi struct{} + +// First +// @Tags AutoCode +// @Summary 获取meta信息 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.GetById true "请求参数" +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "获取meta信息" +// @Router /autoCode/getMeta [post] +func (a *AutoCodeHistoryApi) First(c *gin.Context) { + var info common.GetById + err := c.ShouldBindJSON(&info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + data, err := autoCodeHistoryService.First(c.Request.Context(), info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + response.OkWithDetailed(gin.H{"meta": data}, "获取成功", c) +} + +// Delete +// @Tags AutoCode +// @Summary 删除回滚记录 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.GetById true "请求参数" +// @Success 200 {object} response.Response{msg=string} "删除回滚记录" +// @Router /autoCode/delSysHistory [post] +func (a *AutoCodeHistoryApi) Delete(c *gin.Context) { + var info common.GetById + err := c.ShouldBindJSON(&info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = autoCodeHistoryService.Delete(c.Request.Context(), info) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败", c) + return + } + response.OkWithMessage("删除成功", c) +} + +// RollBack +// @Tags AutoCode +// @Summary 回滚自动生成代码 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.SysAutoHistoryRollBack true "请求参数" +// @Success 200 {object} response.Response{msg=string} "回滚自动生成代码" +// @Router /autoCode/rollback [post] +func (a *AutoCodeHistoryApi) RollBack(c *gin.Context) { + var info request.SysAutoHistoryRollBack + err := c.ShouldBindJSON(&info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = autoCodeHistoryService.RollBack(c.Request.Context(), info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + response.OkWithMessage("回滚成功", c) +} + +// GetList +// @Tags AutoCode +// @Summary 查询回滚记录 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body common.PageInfo true "请求参数" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "查询回滚记录,返回包括列表,总数,页码,每页数量" +// @Router /autoCode/getSysHistory [post] +func (a *AutoCodeHistoryApi) GetList(c *gin.Context) { + var info common.PageInfo + err := c.ShouldBindJSON(&info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := autoCodeHistoryService.GetList(c.Request.Context(), info) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: info.Page, + PageSize: info.PageSize, + }, "获取成功", c) +} diff --git a/admin/server/api/v1/system/auto_code_package.go b/admin/server/api/v1/system/auto_code_package.go new file mode 100644 index 000000000..655f29ab2 --- /dev/null +++ b/admin/server/api/v1/system/auto_code_package.go @@ -0,0 +1,100 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + common "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/gin-gonic/gin" + "go.uber.org/zap" + "strings" +) + +type AutoCodePackageApi struct{} + +// Create +// @Tags AutoCodePackage +// @Summary 创建package +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.SysAutoCodePackageCreate true "创建package" +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "创建package成功" +// @Router /autoCode/createPackage [post] +func (a *AutoCodePackageApi) Create(c *gin.Context) { + var info request.SysAutoCodePackageCreate + _ = c.ShouldBindJSON(&info) + if err := utils.Verify(info, utils.AutoPackageVerify); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if strings.Contains(info.PackageName, "\\") || strings.Contains(info.PackageName, "/") || strings.Contains(info.PackageName, "..") { + response.FailWithMessage("包名不合法", c) + return + } // PackageName可能导致路径穿越的问题 / 和 \ 都要防止 + err := autoCodePackageService.Create(c.Request.Context(), &info) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败", c) + return + } + response.OkWithMessage("创建成功", c) +} + +// Delete +// @Tags AutoCode +// @Summary 删除package +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body common.GetById true "创建package" +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "删除package成功" +// @Router /autoCode/delPackage [post] +func (a *AutoCodePackageApi) Delete(c *gin.Context) { + var info common.GetById + _ = c.ShouldBindJSON(&info) + err := autoCodePackageService.Delete(c.Request.Context(), info) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败", c) + return + } + response.OkWithMessage("删除成功", c) +} + +// All +// @Tags AutoCodePackage +// @Summary 获取package +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "创建package成功" +// @Router /autoCode/getPackage [post] +func (a *AutoCodePackageApi) All(c *gin.Context) { + data, err := autoCodePackageService.All(c.Request.Context()) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(gin.H{"pkgs": data}, "获取成功", c) +} + +// Templates +// @Tags AutoCodePackage +// @Summary 获取package +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "创建package成功" +// @Router /autoCode/getTemplates [get] +func (a *AutoCodePackageApi) Templates(c *gin.Context) { + data, err := autoCodePackageService.Templates(c.Request.Context()) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(data, "获取成功", c) +} diff --git a/admin/server/api/v1/system/auto_code_plugin.go b/admin/server/api/v1/system/auto_code_plugin.go new file mode 100644 index 000000000..30029feb2 --- /dev/null +++ b/admin/server/api/v1/system/auto_code_plugin.go @@ -0,0 +1,119 @@ +package system + +import ( + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type AutoCodePluginApi struct{} + +// Install +// @Tags AutoCodePlugin +// @Summary 安装插件 +// @Security ApiKeyAuth +// @accept multipart/form-data +// @Produce application/json +// @Param plug formData file true "this is a test file" +// @Success 200 {object} response.Response{data=[]interface{},msg=string} "安装插件成功" +// @Router /autoCode/installPlugin [post] +func (a *AutoCodePluginApi) Install(c *gin.Context) { + header, err := c.FormFile("plug") + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + web, server, err := autoCodePluginService.Install(header) + webStr := "web插件安装成功" + serverStr := "server插件安装成功" + if web == -1 { + webStr = "web端插件未成功安装,请按照文档自行解压安装,如果为纯后端插件请忽略此条提示" + } + if server == -1 { + serverStr = "server端插件未成功安装,请按照文档自行解压安装,如果为纯前端插件请忽略此条提示" + } + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + response.OkWithData([]interface{}{ + gin.H{ + "code": web, + "msg": webStr, + }, + gin.H{ + "code": server, + "msg": serverStr, + }}, c) +} + +// Packaged +// @Tags AutoCodePlugin +// @Summary 打包插件 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param plugName query string true "插件名称" +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "打包插件成功" +// @Router /autoCode/pubPlug [post] +func (a *AutoCodePluginApi) Packaged(c *gin.Context) { + plugName := c.Query("plugName") + zipPath, err := autoCodePluginService.PubPlug(plugName) + if err != nil { + global.GVA_LOG.Error("打包失败!", zap.Error(err)) + response.FailWithMessage("打包失败"+err.Error(), c) + return + } + response.OkWithMessage(fmt.Sprintf("打包成功,文件路径为:%s", zipPath), c) +} + +// Packaged +// @Tags AutoCodePlugin +// @Summary 打包插件 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "打包插件成功" +// @Router /autoCode/initMenu [post] +func (a *AutoCodePluginApi) InitMenu(c *gin.Context) { + var menuInfo request.InitMenu + err := c.ShouldBindJSON(&menuInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = autoCodePluginService.InitMenu(menuInfo) + if err != nil { + global.GVA_LOG.Error("创建初始化Menu失败!", zap.Error(err)) + response.FailWithMessage("创建初始化Menu失败"+err.Error(), c) + return + } + response.OkWithMessage("文件变更成功", c) +} + +// Packaged +// @Tags AutoCodePlugin +// @Summary 打包插件 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "打包插件成功" +// @Router /autoCode/initAPI [post] +func (a *AutoCodePluginApi) InitAPI(c *gin.Context) { + var apiInfo request.InitApi + err := c.ShouldBindJSON(&apiInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = autoCodePluginService.InitAPI(apiInfo) + if err != nil { + global.GVA_LOG.Error("创建初始化API失败!", zap.Error(err)) + response.FailWithMessage("创建初始化API失败"+err.Error(), c) + return + } + response.OkWithMessage("文件变更成功", c) +} diff --git a/admin/server/api/v1/system/auto_code_template.go b/admin/server/api/v1/system/auto_code_template.go new file mode 100644 index 000000000..1a4ce601c --- /dev/null +++ b/admin/server/api/v1/system/auto_code_template.go @@ -0,0 +1,121 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type AutoCodeTemplateApi struct{} + +// Preview +// @Tags AutoCodeTemplate +// @Summary 预览创建后的代码 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.AutoCode true "预览创建代码" +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "预览创建后的代码" +// @Router /autoCode/preview [post] +func (a *AutoCodeTemplateApi) Preview(c *gin.Context) { + var info request.AutoCode + err := c.ShouldBindJSON(&info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(info, utils.AutoCodeVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = info.Pretreatment() + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + info.PackageT = utils.FirstUpper(info.Package) + autoCode, err := autoCodeTemplateService.Preview(c.Request.Context(), info) + if err != nil { + global.GVA_LOG.Error("预览失败!", zap.Error(err)) + response.FailWithMessage("预览失败", c) + } else { + response.OkWithDetailed(gin.H{"autoCode": autoCode}, "预览成功", c) + } +} + +// Create +// @Tags AutoCodeTemplate +// @Summary 自动代码模板 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.AutoCode true "创建自动代码" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" +// @Router /autoCode/createTemp [post] +func (a *AutoCodeTemplateApi) Create(c *gin.Context) { + var info request.AutoCode + err := c.ShouldBindJSON(&info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(info, utils.AutoCodeVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = info.Pretreatment() + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = autoCodeTemplateService.Create(c.Request.Context(), info) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage(err.Error(), c) + } else { + response.OkWithMessage("创建成功", c) + } +} + +// Create +// @Tags AddFunc +// @Summary 增加方法 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.AutoCode true "增加方法" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" +// @Router /autoCode/addFunc [post] +func (a *AutoCodeTemplateApi) AddFunc(c *gin.Context) { + var info request.AutoFunc + err := c.ShouldBindJSON(&info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + var tempMap map[string]string + if info.IsPreview { + info.Router = "填充router" + info.FuncName = "填充funcName" + info.Method = "填充method" + info.Description = "填充description" + tempMap, err = autoCodeTemplateService.GetApiAndServer(info) + } else { + err = autoCodeTemplateService.AddFunc(info) + } + if err != nil { + global.GVA_LOG.Error("注入失败!", zap.Error(err)) + response.FailWithMessage("注入失败", c) + } else { + if info.IsPreview { + response.OkWithDetailed(tempMap, "注入成功", c) + return + } + response.OkWithMessage("注入成功", c) + } +} diff --git a/admin/server/api/v1/system/enter.go b/admin/server/api/v1/system/enter.go new file mode 100644 index 000000000..eeda23594 --- /dev/null +++ b/admin/server/api/v1/system/enter.go @@ -0,0 +1,48 @@ +package system + +import "github.com/flipped-aurora/gin-vue-admin/server/service" + +type ApiGroup struct { + DBApi + JwtApi + BaseApi + SystemApi + CasbinApi + AutoCodeApi + SystemApiApi + AuthorityApi + DictionaryApi + AuthorityMenuApi + OperationRecordApi + DictionaryDetailApi + AuthorityBtnApi + SysExportTemplateApi + AutoCodePluginApi + AutoCodePackageApi + AutoCodeHistoryApi + AutoCodeTemplateApi + SysParamsApi +} + +var ( + apiService = service.ServiceGroupApp.SystemServiceGroup.ApiService + jwtService = service.ServiceGroupApp.SystemServiceGroup.JwtService + menuService = service.ServiceGroupApp.SystemServiceGroup.MenuService + userService = service.ServiceGroupApp.SystemServiceGroup.UserService + userExtendService = service.ServiceGroupApp.SystemServiceGroup.UserExtendService + initDBService = service.ServiceGroupApp.SystemServiceGroup.InitDBService + casbinService = service.ServiceGroupApp.SystemServiceGroup.CasbinService + baseMenuService = service.ServiceGroupApp.SystemServiceGroup.BaseMenuService + authorityService = service.ServiceGroupApp.SystemServiceGroup.AuthorityService + dictionaryService = service.ServiceGroupApp.SystemServiceGroup.DictionaryService + authorityBtnService = service.ServiceGroupApp.SystemServiceGroup.AuthorityBtnService + systemConfigService = service.ServiceGroupApp.SystemServiceGroup.SystemConfigService + sysParamsService = service.ServiceGroupApp.SystemServiceGroup.SysParamsService + operationRecordService = service.ServiceGroupApp.SystemServiceGroup.OperationRecordService + dictionaryDetailService = service.ServiceGroupApp.SystemServiceGroup.DictionaryDetailService + autoCodeService = service.ServiceGroupApp.SystemServiceGroup.AutoCodeService + autoCodePluginService = service.ServiceGroupApp.SystemServiceGroup.AutoCodePlugin + autoCodePackageService = service.ServiceGroupApp.SystemServiceGroup.AutoCodePackage + autoCodeHistoryService = service.ServiceGroupApp.SystemServiceGroup.AutoCodeHistory + autoCodeTemplateService = service.ServiceGroupApp.SystemServiceGroup.AutoCodeTemplate +) diff --git a/admin/server/api/v1/system/sys_api.go b/admin/server/api/v1/system/sys_api.go new file mode 100644 index 000000000..7c34f2cdb --- /dev/null +++ b/admin/server/api/v1/system/sys_api.go @@ -0,0 +1,323 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type SystemApiApi struct{} + +// CreateApi +// @Tags SysApi +// @Summary 创建基础api +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysApi true "api路径, api中文描述, api组, 方法" +// @Success 200 {object} response.Response{msg=string} "创建基础api" +// @Router /api/createApi [post] +func (s *SystemApiApi) CreateApi(c *gin.Context) { + var api system.SysApi + err := c.ShouldBindJSON(&api) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(api, utils.ApiVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = apiService.CreateApi(api) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败", c) + return + } + response.OkWithMessage("创建成功", c) +} + +// SyncApi +// @Tags SysApi +// @Summary 同步API +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "同步API" +// @Router /api/syncApi [get] +func (s *SystemApiApi) SyncApi(c *gin.Context) { + newApis, deleteApis, ignoreApis, err := apiService.SyncApi() + if err != nil { + global.GVA_LOG.Error("同步失败!", zap.Error(err)) + response.FailWithMessage("同步失败", c) + return + } + response.OkWithData(gin.H{ + "newApis": newApis, + "deleteApis": deleteApis, + "ignoreApis": ignoreApis, + }, c) +} + +// GetApiGroups +// @Tags SysApi +// @Summary 获取API分组 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "获取API分组" +// @Router /api/getApiGroups [get] +func (s *SystemApiApi) GetApiGroups(c *gin.Context) { + groups, apiGroupMap, err := apiService.GetApiGroups() + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithData(gin.H{ + "groups": groups, + "apiGroupMap": apiGroupMap, + }, c) +} + +// IgnoreApi +// @Tags IgnoreApi +// @Summary 忽略API +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "同步API" +// @Router /api/ignoreApi [post] +func (s *SystemApiApi) IgnoreApi(c *gin.Context) { + var ignoreApi system.SysIgnoreApi + err := c.ShouldBindJSON(&ignoreApi) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = apiService.IgnoreApi(ignoreApi) + if err != nil { + global.GVA_LOG.Error("忽略失败!", zap.Error(err)) + response.FailWithMessage("忽略失败", c) + return + } + response.Ok(c) +} + +// EnterSyncApi +// @Tags SysApi +// @Summary 确认同步API +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "确认同步API" +// @Router /api/enterSyncApi [post] +func (s *SystemApiApi) EnterSyncApi(c *gin.Context) { + var syncApi systemRes.SysSyncApis + err := c.ShouldBindJSON(&syncApi) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = apiService.EnterSyncApi(syncApi) + if err != nil { + global.GVA_LOG.Error("忽略失败!", zap.Error(err)) + response.FailWithMessage("忽略失败", c) + return + } + response.Ok(c) +} + +// DeleteApi +// @Tags SysApi +// @Summary 删除api +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysApi true "ID" +// @Success 200 {object} response.Response{msg=string} "删除api" +// @Router /api/deleteApi [post] +func (s *SystemApiApi) DeleteApi(c *gin.Context) { + var api system.SysApi + err := c.ShouldBindJSON(&api) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(api.GVA_MODEL, utils.IdVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = apiService.DeleteApi(api) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败", c) + return + } + response.OkWithMessage("删除成功", c) +} + +// GetApiList +// @Tags SysApi +// @Summary 分页获取API列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body systemReq.SearchApiParams true "分页获取API列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页获取API列表,返回包括列表,总数,页码,每页数量" +// @Router /api/getApiList [post] +func (s *SystemApiApi) GetApiList(c *gin.Context) { + var pageInfo systemReq.SearchApiParams + err := c.ShouldBindJSON(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(pageInfo.PageInfo, utils.PageInfoVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := apiService.GetAPIInfoList(pageInfo.SysApi, pageInfo.PageInfo, pageInfo.OrderKey, pageInfo.Desc) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.PageSize, + }, "获取成功", c) +} + +// GetApiById +// @Tags SysApi +// @Summary 根据id获取api +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.GetById true "根据id获取api" +// @Success 200 {object} response.Response{data=systemRes.SysAPIResponse} "根据id获取api,返回包括api详情" +// @Router /api/getApiById [post] +func (s *SystemApiApi) GetApiById(c *gin.Context) { + var idInfo request.GetById + err := c.ShouldBindJSON(&idInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(idInfo, utils.IdVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + api, err := apiService.GetApiById(idInfo.ID) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(systemRes.SysAPIResponse{Api: api}, "获取成功", c) +} + +// UpdateApi +// @Tags SysApi +// @Summary 修改基础api +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysApi true "api路径, api中文描述, api组, 方法" +// @Success 200 {object} response.Response{msg=string} "修改基础api" +// @Router /api/updateApi [post] +func (s *SystemApiApi) UpdateApi(c *gin.Context) { + var api system.SysApi + err := c.ShouldBindJSON(&api) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(api, utils.ApiVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = apiService.UpdateApi(api) + if err != nil { + global.GVA_LOG.Error("修改失败!", zap.Error(err)) + response.FailWithMessage("修改失败", c) + return + } + response.OkWithMessage("修改成功", c) +} + +// GetAllApis +// @Tags SysApi +// @Summary 获取所有的Api 不分页 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=systemRes.SysAPIListResponse,msg=string} "获取所有的Api 不分页,返回包括api列表" +// @Router /api/getAllApis [post] +func (s *SystemApiApi) GetAllApis(c *gin.Context) { + authorityID := utils.GetUserAuthorityId(c) + apis, err := apiService.GetAllApis(authorityID) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(systemRes.SysAPIListResponse{Apis: apis}, "获取成功", c) +} + +// DeleteApisByIds +// @Tags SysApi +// @Summary 删除选中Api +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "ID" +// @Success 200 {object} response.Response{msg=string} "删除选中Api" +// @Router /api/deleteApisByIds [delete] +func (s *SystemApiApi) DeleteApisByIds(c *gin.Context) { + var ids request.IdsReq + err := c.ShouldBindJSON(&ids) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = apiService.DeleteApisByIds(ids) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败", c) + return + } + response.OkWithMessage("删除成功", c) +} + +// FreshCasbin +// @Tags SysApi +// @Summary 刷新casbin缓存 +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "刷新成功" +// @Router /api/freshCasbin [get] +func (s *SystemApiApi) FreshCasbin(c *gin.Context) { + err := casbinService.FreshCasbin() + if err != nil { + global.GVA_LOG.Error("刷新失败!", zap.Error(err)) + response.FailWithMessage("刷新失败", c) + return + } + response.OkWithMessage("刷新成功", c) +} diff --git a/admin/server/api/v1/system/sys_authority.go b/admin/server/api/v1/system/sys_authority.go new file mode 100644 index 000000000..b34fc3a0b --- /dev/null +++ b/admin/server/api/v1/system/sys_authority.go @@ -0,0 +1,202 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type AuthorityApi struct{} + +// CreateAuthority +// @Tags Authority +// @Summary 创建角色 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysAuthority true "权限id, 权限名, 父角色id" +// @Success 200 {object} response.Response{data=systemRes.SysAuthorityResponse,msg=string} "创建角色,返回包括系统角色详情" +// @Router /authority/createAuthority [post] +func (a *AuthorityApi) CreateAuthority(c *gin.Context) { + var authority, authBack system.SysAuthority + var err error + + if err = c.ShouldBindJSON(&authority); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + if err = utils.Verify(authority, utils.AuthorityVerify); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + if *authority.ParentId == 0 && global.GVA_CONFIG.System.UseStrictAuth { + authority.ParentId = utils.Pointer(utils.GetUserAuthorityId(c)) + } + + if authBack, err = authorityService.CreateAuthority(authority); err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败"+err.Error(), c) + return + } + err = casbinService.FreshCasbin() + if err != nil { + global.GVA_LOG.Error("创建成功,权限刷新失败。", zap.Error(err)) + response.FailWithMessage("创建成功,权限刷新失败。"+err.Error(), c) + return + } + response.OkWithDetailed(systemRes.SysAuthorityResponse{Authority: authBack}, "创建成功", c) +} + +// CopyAuthority +// @Tags Authority +// @Summary 拷贝角色 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body response.SysAuthorityCopyResponse true "旧角色id, 新权限id, 新权限名, 新父角色id" +// @Success 200 {object} response.Response{data=systemRes.SysAuthorityResponse,msg=string} "拷贝角色,返回包括系统角色详情" +// @Router /authority/copyAuthority [post] +func (a *AuthorityApi) CopyAuthority(c *gin.Context) { + var copyInfo systemRes.SysAuthorityCopyResponse + err := c.ShouldBindJSON(©Info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(copyInfo, utils.OldAuthorityVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(copyInfo.Authority, utils.AuthorityVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + adminAuthorityID := utils.GetUserAuthorityId(c) + authBack, err := authorityService.CopyAuthority(adminAuthorityID, copyInfo) + if err != nil { + global.GVA_LOG.Error("拷贝失败!", zap.Error(err)) + response.FailWithMessage("拷贝失败"+err.Error(), c) + return + } + response.OkWithDetailed(systemRes.SysAuthorityResponse{Authority: authBack}, "拷贝成功", c) +} + +// DeleteAuthority +// @Tags Authority +// @Summary 删除角色 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysAuthority true "删除角色" +// @Success 200 {object} response.Response{msg=string} "删除角色" +// @Router /authority/deleteAuthority [post] +func (a *AuthorityApi) DeleteAuthority(c *gin.Context) { + var authority system.SysAuthority + var err error + if err = c.ShouldBindJSON(&authority); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if err = utils.Verify(authority, utils.AuthorityIdVerify); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + // 删除角色之前需要判断是否有用户正在使用此角色 + if err = authorityService.DeleteAuthority(&authority); err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败"+err.Error(), c) + return + } + _ = casbinService.FreshCasbin() + response.OkWithMessage("删除成功", c) +} + +// UpdateAuthority +// @Tags Authority +// @Summary 更新角色信息 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysAuthority true "权限id, 权限名, 父角色id" +// @Success 200 {object} response.Response{data=systemRes.SysAuthorityResponse,msg=string} "更新角色信息,返回包括系统角色详情" +// @Router /authority/updateAuthority [put] +func (a *AuthorityApi) UpdateAuthority(c *gin.Context) { + var auth system.SysAuthority + err := c.ShouldBindJSON(&auth) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(auth, utils.AuthorityVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + authority, err := authorityService.UpdateAuthority(auth) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败"+err.Error(), c) + return + } + response.OkWithDetailed(systemRes.SysAuthorityResponse{Authority: authority}, "更新成功", c) +} + +// GetAuthorityList +// @Tags Authority +// @Summary 分页获取角色列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.PageInfo true "页码, 每页大小" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页获取角色列表,返回包括列表,总数,页码,每页数量" +// @Router /authority/getAuthorityList [post] +func (a *AuthorityApi) GetAuthorityList(c *gin.Context) { + authorityID := utils.GetUserAuthorityId(c) + list, err := authorityService.GetAuthorityInfoList(authorityID) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败"+err.Error(), c) + return + } + response.OkWithDetailed(list, "获取成功", c) +} + +// SetDataAuthority +// @Tags Authority +// @Summary 设置角色资源权限 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysAuthority true "设置角色资源权限" +// @Success 200 {object} response.Response{msg=string} "设置角色资源权限" +// @Router /authority/setDataAuthority [post] +func (a *AuthorityApi) SetDataAuthority(c *gin.Context) { + var auth system.SysAuthority + err := c.ShouldBindJSON(&auth) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(auth, utils.AuthorityIdVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + adminAuthorityID := utils.GetUserAuthorityId(c) + err = authorityService.SetDataAuthority(adminAuthorityID, auth) + if err != nil { + global.GVA_LOG.Error("设置失败!", zap.Error(err)) + response.FailWithMessage("设置失败"+err.Error(), c) + return + } + response.OkWithMessage("设置成功", c) +} diff --git a/admin/server/api/v1/system/sys_authority_btn.go b/admin/server/api/v1/system/sys_authority_btn.go new file mode 100644 index 000000000..94f02a00e --- /dev/null +++ b/admin/server/api/v1/system/sys_authority_btn.go @@ -0,0 +1,80 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type AuthorityBtnApi struct{} + +// GetAuthorityBtn +// @Tags AuthorityBtn +// @Summary 获取权限按钮 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.SysAuthorityBtnReq true "菜单id, 角色id, 选中的按钮id" +// @Success 200 {object} response.Response{data=response.SysAuthorityBtnRes,msg=string} "返回列表成功" +// @Router /authorityBtn/getAuthorityBtn [post] +func (a *AuthorityBtnApi) GetAuthorityBtn(c *gin.Context) { + var req request.SysAuthorityBtnReq + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + res, err := authorityBtnService.GetAuthorityBtn(req) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败", c) + return + } + response.OkWithDetailed(res, "查询成功", c) +} + +// SetAuthorityBtn +// @Tags AuthorityBtn +// @Summary 设置权限按钮 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.SysAuthorityBtnReq true "菜单id, 角色id, 选中的按钮id" +// @Success 200 {object} response.Response{msg=string} "返回列表成功" +// @Router /authorityBtn/setAuthorityBtn [post] +func (a *AuthorityBtnApi) SetAuthorityBtn(c *gin.Context) { + var req request.SysAuthorityBtnReq + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = authorityBtnService.SetAuthorityBtn(req) + if err != nil { + global.GVA_LOG.Error("分配失败!", zap.Error(err)) + response.FailWithMessage("分配失败", c) + return + } + response.OkWithMessage("分配成功", c) +} + +// CanRemoveAuthorityBtn +// @Tags AuthorityBtn +// @Summary 设置权限按钮 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "删除成功" +// @Router /authorityBtn/canRemoveAuthorityBtn [post] +func (a *AuthorityBtnApi) CanRemoveAuthorityBtn(c *gin.Context) { + id := c.Query("id") + err := authorityBtnService.CanRemoveAuthorityBtn(id) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage(err.Error(), c) + return + } + response.OkWithMessage("删除成功", c) +} diff --git a/admin/server/api/v1/system/sys_auto_code.go b/admin/server/api/v1/system/sys_auto_code.go new file mode 100644 index 000000000..02f458bb3 --- /dev/null +++ b/admin/server/api/v1/system/sys_auto_code.go @@ -0,0 +1,155 @@ +package system + +import ( + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/model/common" + "github.com/goccy/go-json" + "io" + "strings" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/utils/request" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type AutoCodeApi struct{} + +// GetDB +// @Tags AutoCode +// @Summary 获取当前所有数据库 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "获取当前所有数据库" +// @Router /autoCode/getDB [get] +func (autoApi *AutoCodeApi) GetDB(c *gin.Context) { + businessDB := c.Query("businessDB") + dbs, err := autoCodeService.Database(businessDB).GetDB(businessDB) + var dbList []map[string]interface{} + for _, db := range global.GVA_CONFIG.DBList { + var item = make(map[string]interface{}) + item["aliasName"] = db.AliasName + item["dbName"] = db.Dbname + item["disable"] = db.Disable + item["dbtype"] = db.Type + dbList = append(dbList, item) + } + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + } else { + response.OkWithDetailed(gin.H{"dbs": dbs, "dbList": dbList}, "获取成功", c) + } +} + +// GetTables +// @Tags AutoCode +// @Summary 获取当前数据库所有表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "获取当前数据库所有表" +// @Router /autoCode/getTables [get] +func (autoApi *AutoCodeApi) GetTables(c *gin.Context) { + dbName := c.Query("dbName") + businessDB := c.Query("businessDB") + if dbName == "" { + dbName = *global.GVA_ACTIVE_DBNAME + if businessDB != "" { + for _, db := range global.GVA_CONFIG.DBList { + if db.AliasName == businessDB { + dbName = db.Dbname + } + } + } + } + + tables, err := autoCodeService.Database(businessDB).GetTables(businessDB, dbName) + if err != nil { + global.GVA_LOG.Error("查询table失败!", zap.Error(err)) + response.FailWithMessage("查询table失败", c) + } else { + response.OkWithDetailed(gin.H{"tables": tables}, "获取成功", c) + } +} + +// GetColumn +// @Tags AutoCode +// @Summary 获取当前表所有字段 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "获取当前表所有字段" +// @Router /autoCode/getColumn [get] +func (autoApi *AutoCodeApi) GetColumn(c *gin.Context) { + businessDB := c.Query("businessDB") + dbName := c.Query("dbName") + if dbName == "" { + dbName = *global.GVA_ACTIVE_DBNAME + if businessDB != "" { + for _, db := range global.GVA_CONFIG.DBList { + if db.AliasName == businessDB { + dbName = db.Dbname + } + } + } + } + tableName := c.Query("tableName") + columns, err := autoCodeService.Database(businessDB).GetColumn(businessDB, tableName, dbName) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + } else { + response.OkWithDetailed(gin.H{"columns": columns}, "获取成功", c) + } +} + +func (autoApi *AutoCodeApi) LLMAuto(c *gin.Context) { + var llm common.JSONMap + err := c.ShouldBindJSON(&llm) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if global.GVA_CONFIG.AutoCode.AiPath == "" { + response.FailWithMessage("请先前往插件市场个人中心获取AiPath并填入config.yaml中", c) + return + } + + path := strings.ReplaceAll(global.GVA_CONFIG.AutoCode.AiPath, "{FUNC}", fmt.Sprintf("api/chat/%s", llm["mode"])) + res, err := request.HttpRequest( + path, + "POST", + nil, + nil, + llm, + ) + if err != nil { + global.GVA_LOG.Error("大模型生成失败!", zap.Error(err)) + response.FailWithMessage("大模型生成失败"+err.Error(), c) + return + } + var resStruct response.Response + b, err := io.ReadAll(res.Body) + defer res.Body.Close() + if err != nil { + global.GVA_LOG.Error("大模型生成失败!", zap.Error(err)) + response.FailWithMessage("大模型生成失败"+err.Error(), c) + return + } + err = json.Unmarshal(b, &resStruct) + if err != nil { + global.GVA_LOG.Error("大模型生成失败!", zap.Error(err)) + response.FailWithMessage("大模型生成失败"+err.Error(), c) + return + } + + if resStruct.Code == 7 { + global.GVA_LOG.Error("大模型生成失败!"+resStruct.Msg, zap.Error(err)) + response.FailWithMessage("大模型生成失败"+resStruct.Msg, c) + return + } + response.OkWithData(resStruct.Data, c) +} diff --git a/admin/server/api/v1/system/sys_captcha.go b/admin/server/api/v1/system/sys_captcha.go new file mode 100644 index 000000000..b9f2110ec --- /dev/null +++ b/admin/server/api/v1/system/sys_captcha.go @@ -0,0 +1,70 @@ +package system + +import ( + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" + "github.com/gin-gonic/gin" + "github.com/mojocn/base64Captcha" + "go.uber.org/zap" +) + +// 当开启多服务器部署时,替换下面的配置,使用redis共享存储验证码 +// var store = captcha.NewDefaultRedisStore() +var store = base64Captcha.DefaultMemStore + +type BaseApi struct{} + +// Captcha +// @Tags Base +// @Summary 生成验证码 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=systemRes.SysCaptchaResponse,msg=string} "生成验证码,返回包括随机数id,base64,验证码长度,是否开启验证码" +// @Router /base/captcha [post] +func (b *BaseApi) Captcha(c *gin.Context) { + // 判断验证码是否开启 + openCaptcha := global.GVA_CONFIG.Captcha.OpenCaptcha // 是否开启防爆次数 + openCaptchaTimeOut := global.GVA_CONFIG.Captcha.OpenCaptchaTimeOut // 缓存超时时间 + key := c.ClientIP() + v, ok := global.BlackCache.Get(key) + if !ok { + global.BlackCache.Set(key, 1, time.Second*time.Duration(openCaptchaTimeOut)) + } + + var oc bool + if openCaptcha == 0 || openCaptcha < interfaceToInt(v) { + oc = true + } + // 字符,公式,验证码配置 + // 生成默认数字的driver + driver := base64Captcha.NewDriverDigit(global.GVA_CONFIG.Captcha.ImgHeight, global.GVA_CONFIG.Captcha.ImgWidth, global.GVA_CONFIG.Captcha.KeyLong, 0.7, 80) + // cp := base64Captcha.NewCaptcha(driver, store.UseWithCtx(c)) // v8下使用redis + cp := base64Captcha.NewCaptcha(driver, store) + id, b64s, _, err := cp.Generate() + if err != nil { + global.GVA_LOG.Error("验证码获取失败!", zap.Error(err)) + response.FailWithMessage("验证码获取失败", c) + return + } + response.OkWithDetailed(systemRes.SysCaptchaResponse{ + CaptchaId: id, + PicPath: b64s, + CaptchaLength: global.GVA_CONFIG.Captcha.KeyLong, + OpenCaptcha: oc, + }, "验证码获取成功", c) +} + +// 类型转换 +func interfaceToInt(v interface{}) (i int) { + switch v := v.(type) { + case int: + i = v + default: + i = 0 + } + return +} diff --git a/admin/server/api/v1/system/sys_casbin.go b/admin/server/api/v1/system/sys_casbin.go new file mode 100644 index 000000000..c1bf54894 --- /dev/null +++ b/admin/server/api/v1/system/sys_casbin.go @@ -0,0 +1,69 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type CasbinApi struct{} + +// UpdateCasbin +// @Tags Casbin +// @Summary 更新角色api权限 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.CasbinInReceive true "权限id, 权限模型列表" +// @Success 200 {object} response.Response{msg=string} "更新角色api权限" +// @Router /casbin/UpdateCasbin [post] +func (cas *CasbinApi) UpdateCasbin(c *gin.Context) { + var cmr request.CasbinInReceive + err := c.ShouldBindJSON(&cmr) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(cmr, utils.AuthorityIdVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + adminAuthorityID := utils.GetUserAuthorityId(c) + err = casbinService.UpdateCasbin(adminAuthorityID, cmr.AuthorityId, cmr.CasbinInfos) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败", c) + return + } + response.OkWithMessage("更新成功", c) +} + +// GetPolicyPathByAuthorityId +// @Tags Casbin +// @Summary 获取权限列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.CasbinInReceive true "权限id, 权限模型列表" +// @Success 200 {object} response.Response{data=systemRes.PolicyPathResponse,msg=string} "获取权限列表,返回包括casbin详情列表" +// @Router /casbin/getPolicyPathByAuthorityId [post] +func (cas *CasbinApi) GetPolicyPathByAuthorityId(c *gin.Context) { + var casbin request.CasbinInReceive + err := c.ShouldBindJSON(&casbin) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(casbin, utils.AuthorityIdVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + paths := casbinService.GetPolicyPathByAuthorityId(casbin.AuthorityId) + response.OkWithDetailed(systemRes.PolicyPathResponse{Paths: paths}, "获取成功", c) +} diff --git a/admin/server/api/v1/system/sys_dictionary.go b/admin/server/api/v1/system/sys_dictionary.go new file mode 100644 index 000000000..1dfe9d089 --- /dev/null +++ b/admin/server/api/v1/system/sys_dictionary.go @@ -0,0 +1,129 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type DictionaryApi struct{} + +// CreateSysDictionary +// @Tags SysDictionary +// @Summary 创建SysDictionary +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysDictionary true "SysDictionary模型" +// @Success 200 {object} response.Response{msg=string} "创建SysDictionary" +// @Router /sysDictionary/createSysDictionary [post] +func (s *DictionaryApi) CreateSysDictionary(c *gin.Context) { + var dictionary system.SysDictionary + err := c.ShouldBindJSON(&dictionary) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = dictionaryService.CreateSysDictionary(dictionary) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败", c) + return + } + response.OkWithMessage("创建成功", c) +} + +// DeleteSysDictionary +// @Tags SysDictionary +// @Summary 删除SysDictionary +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysDictionary true "SysDictionary模型" +// @Success 200 {object} response.Response{msg=string} "删除SysDictionary" +// @Router /sysDictionary/deleteSysDictionary [delete] +func (s *DictionaryApi) DeleteSysDictionary(c *gin.Context) { + var dictionary system.SysDictionary + err := c.ShouldBindJSON(&dictionary) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = dictionaryService.DeleteSysDictionary(dictionary) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败", c) + return + } + response.OkWithMessage("删除成功", c) +} + +// UpdateSysDictionary +// @Tags SysDictionary +// @Summary 更新SysDictionary +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysDictionary true "SysDictionary模型" +// @Success 200 {object} response.Response{msg=string} "更新SysDictionary" +// @Router /sysDictionary/updateSysDictionary [put] +func (s *DictionaryApi) UpdateSysDictionary(c *gin.Context) { + var dictionary system.SysDictionary + err := c.ShouldBindJSON(&dictionary) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = dictionaryService.UpdateSysDictionary(&dictionary) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败", c) + return + } + response.OkWithMessage("更新成功", c) +} + +// FindSysDictionary +// @Tags SysDictionary +// @Summary 用id查询SysDictionary +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query system.SysDictionary true "ID或字典英名" +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "用id查询SysDictionary" +// @Router /sysDictionary/findSysDictionary [get] +func (s *DictionaryApi) FindSysDictionary(c *gin.Context) { + var dictionary system.SysDictionary + err := c.ShouldBindQuery(&dictionary) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + sysDictionary, err := dictionaryService.GetSysDictionary(dictionary.Type, dictionary.ID, dictionary.Status) + if err != nil { + global.GVA_LOG.Error("字典未创建或未开启!", zap.Error(err)) + response.FailWithMessage("字典未创建或未开启", c) + return + } + response.OkWithDetailed(gin.H{"resysDictionary": sysDictionary}, "查询成功", c) +} + +// GetSysDictionaryList +// @Tags SysDictionary +// @Summary 分页获取SysDictionary列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页获取SysDictionary列表,返回包括列表,总数,页码,每页数量" +// @Router /sysDictionary/getSysDictionaryList [get] +func (s *DictionaryApi) GetSysDictionaryList(c *gin.Context) { + list, err := dictionaryService.GetSysDictionaryInfoList() + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(list, "获取成功", c) +} diff --git a/admin/server/api/v1/system/sys_dictionary_detail.go b/admin/server/api/v1/system/sys_dictionary_detail.go new file mode 100644 index 000000000..754af1be6 --- /dev/null +++ b/admin/server/api/v1/system/sys_dictionary_detail.go @@ -0,0 +1,148 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type DictionaryDetailApi struct{} + +// CreateSysDictionaryDetail +// @Tags SysDictionaryDetail +// @Summary 创建SysDictionaryDetail +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysDictionaryDetail true "SysDictionaryDetail模型" +// @Success 200 {object} response.Response{msg=string} "创建SysDictionaryDetail" +// @Router /sysDictionaryDetail/createSysDictionaryDetail [post] +func (s *DictionaryDetailApi) CreateSysDictionaryDetail(c *gin.Context) { + var detail system.SysDictionaryDetail + err := c.ShouldBindJSON(&detail) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = dictionaryDetailService.CreateSysDictionaryDetail(detail) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败", c) + return + } + response.OkWithMessage("创建成功", c) +} + +// DeleteSysDictionaryDetail +// @Tags SysDictionaryDetail +// @Summary 删除SysDictionaryDetail +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysDictionaryDetail true "SysDictionaryDetail模型" +// @Success 200 {object} response.Response{msg=string} "删除SysDictionaryDetail" +// @Router /sysDictionaryDetail/deleteSysDictionaryDetail [delete] +func (s *DictionaryDetailApi) DeleteSysDictionaryDetail(c *gin.Context) { + var detail system.SysDictionaryDetail + err := c.ShouldBindJSON(&detail) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = dictionaryDetailService.DeleteSysDictionaryDetail(detail) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败", c) + return + } + response.OkWithMessage("删除成功", c) +} + +// UpdateSysDictionaryDetail +// @Tags SysDictionaryDetail +// @Summary 更新SysDictionaryDetail +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysDictionaryDetail true "更新SysDictionaryDetail" +// @Success 200 {object} response.Response{msg=string} "更新SysDictionaryDetail" +// @Router /sysDictionaryDetail/updateSysDictionaryDetail [put] +func (s *DictionaryDetailApi) UpdateSysDictionaryDetail(c *gin.Context) { + var detail system.SysDictionaryDetail + err := c.ShouldBindJSON(&detail) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = dictionaryDetailService.UpdateSysDictionaryDetail(&detail) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败", c) + return + } + response.OkWithMessage("更新成功", c) +} + +// FindSysDictionaryDetail +// @Tags SysDictionaryDetail +// @Summary 用id查询SysDictionaryDetail +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query system.SysDictionaryDetail true "用id查询SysDictionaryDetail" +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "用id查询SysDictionaryDetail" +// @Router /sysDictionaryDetail/findSysDictionaryDetail [get] +func (s *DictionaryDetailApi) FindSysDictionaryDetail(c *gin.Context) { + var detail system.SysDictionaryDetail + err := c.ShouldBindQuery(&detail) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(detail, utils.IdVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + reSysDictionaryDetail, err := dictionaryDetailService.GetSysDictionaryDetail(detail.ID) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败", c) + return + } + response.OkWithDetailed(gin.H{"reSysDictionaryDetail": reSysDictionaryDetail}, "查询成功", c) +} + +// GetSysDictionaryDetailList +// @Tags SysDictionaryDetail +// @Summary 分页获取SysDictionaryDetail列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query request.SysDictionaryDetailSearch true "页码, 每页大小, 搜索条件" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页获取SysDictionaryDetail列表,返回包括列表,总数,页码,每页数量" +// @Router /sysDictionaryDetail/getSysDictionaryDetailList [get] +func (s *DictionaryDetailApi) GetSysDictionaryDetailList(c *gin.Context) { + var pageInfo request.SysDictionaryDetailSearch + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := dictionaryDetailService.GetSysDictionaryDetailInfoList(pageInfo) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.PageSize, + }, "获取成功", c) +} diff --git a/admin/server/api/v1/system/sys_export_template.go b/admin/server/api/v1/system/sys_export_template.go new file mode 100644 index 000000000..38b22965b --- /dev/null +++ b/admin/server/api/v1/system/sys_export_template.go @@ -0,0 +1,258 @@ +package system + +import ( + "fmt" + "net/http" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/flipped-aurora/gin-vue-admin/server/service" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type SysExportTemplateApi struct { +} + +var sysExportTemplateService = service.ServiceGroupApp.SystemServiceGroup.SysExportTemplateService + +// CreateSysExportTemplate 创建导出模板 +// @Tags SysExportTemplate +// @Summary 创建导出模板 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysExportTemplate true "创建导出模板" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" +// @Router /sysExportTemplate/createSysExportTemplate [post] +func (sysExportTemplateApi *SysExportTemplateApi) CreateSysExportTemplate(c *gin.Context) { + var sysExportTemplate system.SysExportTemplate + err := c.ShouldBindJSON(&sysExportTemplate) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + verify := utils.Rules{ + "Name": {utils.NotEmpty()}, + } + if err := utils.Verify(sysExportTemplate, verify); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if err := sysExportTemplateService.CreateSysExportTemplate(&sysExportTemplate); err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败", c) + } else { + response.OkWithMessage("创建成功", c) + } +} + +// DeleteSysExportTemplate 删除导出模板 +// @Tags SysExportTemplate +// @Summary 删除导出模板 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysExportTemplate true "删除导出模板" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /sysExportTemplate/deleteSysExportTemplate [delete] +func (sysExportTemplateApi *SysExportTemplateApi) DeleteSysExportTemplate(c *gin.Context) { + var sysExportTemplate system.SysExportTemplate + err := c.ShouldBindJSON(&sysExportTemplate) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if err := sysExportTemplateService.DeleteSysExportTemplate(sysExportTemplate); err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败", c) + } else { + response.OkWithMessage("删除成功", c) + } +} + +// DeleteSysExportTemplateByIds 批量删除导出模板 +// @Tags SysExportTemplate +// @Summary 批量删除导出模板 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "批量删除导出模板" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"批量删除成功"}" +// @Router /sysExportTemplate/deleteSysExportTemplateByIds [delete] +func (sysExportTemplateApi *SysExportTemplateApi) DeleteSysExportTemplateByIds(c *gin.Context) { + var IDS request.IdsReq + err := c.ShouldBindJSON(&IDS) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if err := sysExportTemplateService.DeleteSysExportTemplateByIds(IDS); err != nil { + global.GVA_LOG.Error("批量删除失败!", zap.Error(err)) + response.FailWithMessage("批量删除失败", c) + } else { + response.OkWithMessage("批量删除成功", c) + } +} + +// UpdateSysExportTemplate 更新导出模板 +// @Tags SysExportTemplate +// @Summary 更新导出模板 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysExportTemplate true "更新导出模板" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}" +// @Router /sysExportTemplate/updateSysExportTemplate [put] +func (sysExportTemplateApi *SysExportTemplateApi) UpdateSysExportTemplate(c *gin.Context) { + var sysExportTemplate system.SysExportTemplate + err := c.ShouldBindJSON(&sysExportTemplate) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + verify := utils.Rules{ + "Name": {utils.NotEmpty()}, + } + if err := utils.Verify(sysExportTemplate, verify); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if err := sysExportTemplateService.UpdateSysExportTemplate(sysExportTemplate); err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败", c) + } else { + response.OkWithMessage("更新成功", c) + } +} + +// FindSysExportTemplate 用id查询导出模板 +// @Tags SysExportTemplate +// @Summary 用id查询导出模板 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query system.SysExportTemplate true "用id查询导出模板" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" +// @Router /sysExportTemplate/findSysExportTemplate [get] +func (sysExportTemplateApi *SysExportTemplateApi) FindSysExportTemplate(c *gin.Context) { + var sysExportTemplate system.SysExportTemplate + err := c.ShouldBindQuery(&sysExportTemplate) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if resysExportTemplate, err := sysExportTemplateService.GetSysExportTemplate(sysExportTemplate.ID); err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败", c) + } else { + response.OkWithData(gin.H{"resysExportTemplate": resysExportTemplate}, c) + } +} + +// GetSysExportTemplateList 分页获取导出模板列表 +// @Tags SysExportTemplate +// @Summary 分页获取导出模板列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query systemReq.SysExportTemplateSearch true "分页获取导出模板列表" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /sysExportTemplate/getSysExportTemplateList [get] +func (sysExportTemplateApi *SysExportTemplateApi) GetSysExportTemplateList(c *gin.Context) { + var pageInfo systemReq.SysExportTemplateSearch + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if list, total, err := sysExportTemplateService.GetSysExportTemplateInfoList(pageInfo); err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + } else { + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.PageSize, + }, "获取成功", c) + } +} + +// ExportExcel 导出表格 +// @Tags SysExportTemplate +// @Summary 导出表格 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Router /sysExportTemplate/exportExcel [get] +func (sysExportTemplateApi *SysExportTemplateApi) ExportExcel(c *gin.Context) { + templateID := c.Query("templateID") + queryParams := c.Request.URL.Query() + if templateID == "" { + response.FailWithMessage("模板ID不能为空", c) + return + } + if file, name, err := sysExportTemplateService.ExportExcel(templateID, queryParams); err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + } else { + c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", name+utils.RandomString(6)+".xlsx")) // 对下载的文件重命名 + c.Header("success", "true") + c.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", file.Bytes()) + } +} + +// ExportTemplate 导出表格模板 +// @Tags SysExportTemplate +// @Summary 导出表格模板 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Router /sysExportTemplate/exportExcel [get] +func (sysExportTemplateApi *SysExportTemplateApi) ExportTemplate(c *gin.Context) { + templateID := c.Query("templateID") + if templateID == "" { + response.FailWithMessage("模板ID不能为空", c) + return + } + if file, name, err := sysExportTemplateService.ExportTemplate(templateID); err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + } else { + c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", name+"模板.xlsx")) // 对下载的文件重命名 + c.Header("success", "true") + c.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", file.Bytes()) + } +} + +// ImportExcel 导入表格 +// @Tags SysImportTemplate +// @Summary 导入表格 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Router /sysExportTemplate/importExcel [post] +func (sysExportTemplateApi *SysExportTemplateApi) ImportExcel(c *gin.Context) { + templateID := c.Query("templateID") + if templateID == "" { + response.FailWithMessage("模板ID不能为空", c) + return + } + file, err := c.FormFile("file") + if err != nil { + global.GVA_LOG.Error("文件获取失败!", zap.Error(err)) + response.FailWithMessage("文件获取失败", c) + return + } + if err := sysExportTemplateService.ImportExcel(templateID, file); err != nil { + global.GVA_LOG.Error(err.Error(), zap.Error(err)) + response.FailWithMessage(err.Error(), c) + } else { + response.OkWithMessage("导入成功", c) + } +} diff --git a/admin/server/api/v1/system/sys_initdb.go b/admin/server/api/v1/system/sys_initdb.go new file mode 100644 index 000000000..f8ad8263b --- /dev/null +++ b/admin/server/api/v1/system/sys_initdb.go @@ -0,0 +1,66 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/flipped-aurora/gin-vue-admin/server/service/system" + "go.uber.org/zap" + + "github.com/gin-gonic/gin" +) + +type DBApi struct{} + +// InitDB +// @Tags InitDB +// @Summary 初始化用户数据库 +// @Produce application/json +// @Param data body request.InitDB true "初始化数据库参数" +// @Success 200 {object} response.Response{data=string} "初始化用户数据库" +// @Router /init/initdb [post] +func (i *DBApi) InitDB(c *gin.Context) { + if global.GVA_DB != nil { + global.GVA_LOG.Error("已存在数据库配置!") + response.FailWithMessage("已存在数据库配置", c) + return + } + var dbInfo request.InitDB + if err := c.ShouldBindJSON(&dbInfo); err != nil { + global.GVA_LOG.Error("参数校验不通过!", zap.Error(err)) + response.FailWithMessage("参数校验不通过", c) + return + } + if err := initDBService.InitDB(dbInfo); err != nil { + global.GVA_LOG.Error("自动创建数据库失败!", zap.Error(err)) + response.FailWithMessage("自动创建数据库失败,请查看后台日志,检查后在进行初始化", c) + return + } + + // 二开部分:新增同步管理员账号 + user := system.UserExtendService{} + go user.SyncUser() + + response.OkWithMessage("自动创建数据库成功", c) +} + +// CheckDB +// @Tags CheckDB +// @Summary 初始化用户数据库 +// @Produce application/json +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "初始化用户数据库" +// @Router /init/checkdb [post] +func (i *DBApi) CheckDB(c *gin.Context) { + var ( + message = "前往初始化数据库" + needInit = true + ) + + if global.GVA_DB != nil { + message = "数据库无需初始化" + needInit = false + } + + global.GVA_LOG.Info(message) + response.OkWithDetailed(gin.H{"needInit": needInit}, message, c) +} diff --git a/admin/server/api/v1/system/sys_jwt_blacklist.go b/admin/server/api/v1/system/sys_jwt_blacklist.go new file mode 100644 index 000000000..f66c155c7 --- /dev/null +++ b/admin/server/api/v1/system/sys_jwt_blacklist.go @@ -0,0 +1,33 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type JwtApi struct{} + +// JsonInBlacklist +// @Tags Jwt +// @Summary jwt加入黑名单 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "jwt加入黑名单" +// @Router /jwt/jsonInBlacklist [post] +func (j *JwtApi) JsonInBlacklist(c *gin.Context) { + token := utils.GetToken(c) + jwt := system.JwtBlacklist{Jwt: token} + err := jwtService.JsonInBlacklist(jwt) + if err != nil { + global.GVA_LOG.Error("jwt作废失败!", zap.Error(err)) + response.FailWithMessage("jwt作废失败", c) + return + } + utils.ClearToken(c) + response.OkWithMessage("jwt作废成功", c) +} diff --git a/admin/server/api/v1/system/sys_menu.go b/admin/server/api/v1/system/sys_menu.go new file mode 100644 index 000000000..864b61f38 --- /dev/null +++ b/admin/server/api/v1/system/sys_menu.go @@ -0,0 +1,265 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type AuthorityMenuApi struct{} + +// GetMenu +// @Tags AuthorityMenu +// @Summary 获取用户动态路由 +// @Security ApiKeyAuth +// @Produce application/json +// @Param data body request.Empty true "空" +// @Success 200 {object} response.Response{data=systemRes.SysMenusResponse,msg=string} "获取用户动态路由,返回包括系统菜单详情列表" +// @Router /menu/getMenu [post] +func (a *AuthorityMenuApi) GetMenu(c *gin.Context) { + menus, err := menuService.GetMenuTree(utils.GetUserAuthorityId(c)) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + if menus == nil { + menus = []system.SysMenu{} + } + response.OkWithDetailed(systemRes.SysMenusResponse{Menus: menus}, "获取成功", c) +} + +// GetBaseMenuTree +// @Tags AuthorityMenu +// @Summary 获取用户动态路由 +// @Security ApiKeyAuth +// @Produce application/json +// @Param data body request.Empty true "空" +// @Success 200 {object} response.Response{data=systemRes.SysBaseMenusResponse,msg=string} "获取用户动态路由,返回包括系统菜单列表" +// @Router /menu/getBaseMenuTree [post] +func (a *AuthorityMenuApi) GetBaseMenuTree(c *gin.Context) { + authority := utils.GetUserAuthorityId(c) + menus, err := menuService.GetBaseMenuTree(authority) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(systemRes.SysBaseMenusResponse{Menus: menus}, "获取成功", c) +} + +// AddMenuAuthority +// @Tags AuthorityMenu +// @Summary 增加menu和角色关联关系 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body systemReq.AddMenuAuthorityInfo true "角色ID" +// @Success 200 {object} response.Response{msg=string} "增加menu和角色关联关系" +// @Router /menu/addMenuAuthority [post] +func (a *AuthorityMenuApi) AddMenuAuthority(c *gin.Context) { + var authorityMenu systemReq.AddMenuAuthorityInfo + err := c.ShouldBindJSON(&authorityMenu) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if err := utils.Verify(authorityMenu, utils.AuthorityIdVerify); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + adminAuthorityID := utils.GetUserAuthorityId(c) + if err := menuService.AddMenuAuthority(authorityMenu.Menus, adminAuthorityID, authorityMenu.AuthorityId); err != nil { + global.GVA_LOG.Error("添加失败!", zap.Error(err)) + response.FailWithMessage("添加失败", c) + } else { + response.OkWithMessage("添加成功", c) + } +} + +// GetMenuAuthority +// @Tags AuthorityMenu +// @Summary 获取指定角色menu +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.GetAuthorityId true "角色ID" +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "获取指定角色menu" +// @Router /menu/getMenuAuthority [post] +func (a *AuthorityMenuApi) GetMenuAuthority(c *gin.Context) { + var param request.GetAuthorityId + err := c.ShouldBindJSON(¶m) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(param, utils.AuthorityIdVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + menus, err := menuService.GetMenuAuthority(¶m) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithDetailed(systemRes.SysMenusResponse{Menus: menus}, "获取失败", c) + return + } + response.OkWithDetailed(gin.H{"menus": menus}, "获取成功", c) +} + +// AddBaseMenu +// @Tags Menu +// @Summary 新增菜单 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysBaseMenu true "路由path, 父菜单ID, 路由name, 对应前端文件路径, 排序标记" +// @Success 200 {object} response.Response{msg=string} "新增菜单" +// @Router /menu/addBaseMenu [post] +func (a *AuthorityMenuApi) AddBaseMenu(c *gin.Context) { + var menu system.SysBaseMenu + err := c.ShouldBindJSON(&menu) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(menu, utils.MenuVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(menu.Meta, utils.MenuMetaVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = menuService.AddBaseMenu(menu) + if err != nil { + global.GVA_LOG.Error("添加失败!", zap.Error(err)) + response.FailWithMessage("添加失败", c) + return + } + response.OkWithMessage("添加成功", c) +} + +// DeleteBaseMenu +// @Tags Menu +// @Summary 删除菜单 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.GetById true "菜单id" +// @Success 200 {object} response.Response{msg=string} "删除菜单" +// @Router /menu/deleteBaseMenu [post] +func (a *AuthorityMenuApi) DeleteBaseMenu(c *gin.Context) { + var menu request.GetById + err := c.ShouldBindJSON(&menu) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(menu, utils.IdVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = baseMenuService.DeleteBaseMenu(menu.ID) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败:"+err.Error(), c) + return + } + response.OkWithMessage("删除成功", c) +} + +// UpdateBaseMenu +// @Tags Menu +// @Summary 更新菜单 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysBaseMenu true "路由path, 父菜单ID, 路由name, 对应前端文件路径, 排序标记" +// @Success 200 {object} response.Response{msg=string} "更新菜单" +// @Router /menu/updateBaseMenu [post] +func (a *AuthorityMenuApi) UpdateBaseMenu(c *gin.Context) { + var menu system.SysBaseMenu + err := c.ShouldBindJSON(&menu) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(menu, utils.MenuVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(menu.Meta, utils.MenuMetaVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = baseMenuService.UpdateBaseMenu(menu) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败", c) + return + } + response.OkWithMessage("更新成功", c) +} + +// GetBaseMenuById +// @Tags Menu +// @Summary 根据id获取菜单 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.GetById true "菜单id" +// @Success 200 {object} response.Response{data=systemRes.SysBaseMenuResponse,msg=string} "根据id获取菜单,返回包括系统菜单列表" +// @Router /menu/getBaseMenuById [post] +func (a *AuthorityMenuApi) GetBaseMenuById(c *gin.Context) { + var idInfo request.GetById + err := c.ShouldBindJSON(&idInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(idInfo, utils.IdVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + menu, err := baseMenuService.GetBaseMenuById(idInfo.ID) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(systemRes.SysBaseMenuResponse{Menu: menu}, "获取成功", c) +} + +// GetMenuList +// @Tags Menu +// @Summary 分页获取基础menu列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.PageInfo true "页码, 每页大小" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页获取基础menu列表,返回包括列表,总数,页码,每页数量" +// @Router /menu/getMenuList [post] +func (a *AuthorityMenuApi) GetMenuList(c *gin.Context) { + authorityID := utils.GetUserAuthorityId(c) + menuList, err := menuService.GetInfoList(authorityID) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(menuList, "获取成功", c) +} diff --git a/admin/server/api/v1/system/sys_operation_record.go b/admin/server/api/v1/system/sys_operation_record.go new file mode 100644 index 000000000..40daeb98d --- /dev/null +++ b/admin/server/api/v1/system/sys_operation_record.go @@ -0,0 +1,149 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type OperationRecordApi struct{} + +// CreateSysOperationRecord +// @Tags SysOperationRecord +// @Summary 创建SysOperationRecord +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysOperationRecord true "创建SysOperationRecord" +// @Success 200 {object} response.Response{msg=string} "创建SysOperationRecord" +// @Router /sysOperationRecord/createSysOperationRecord [post] +func (s *OperationRecordApi) CreateSysOperationRecord(c *gin.Context) { + var sysOperationRecord system.SysOperationRecord + err := c.ShouldBindJSON(&sysOperationRecord) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = operationRecordService.CreateSysOperationRecord(sysOperationRecord) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败", c) + return + } + response.OkWithMessage("创建成功", c) +} + +// DeleteSysOperationRecord +// @Tags SysOperationRecord +// @Summary 删除SysOperationRecord +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysOperationRecord true "SysOperationRecord模型" +// @Success 200 {object} response.Response{msg=string} "删除SysOperationRecord" +// @Router /sysOperationRecord/deleteSysOperationRecord [delete] +func (s *OperationRecordApi) DeleteSysOperationRecord(c *gin.Context) { + var sysOperationRecord system.SysOperationRecord + err := c.ShouldBindJSON(&sysOperationRecord) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = operationRecordService.DeleteSysOperationRecord(sysOperationRecord) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败", c) + return + } + response.OkWithMessage("删除成功", c) +} + +// DeleteSysOperationRecordByIds +// @Tags SysOperationRecord +// @Summary 批量删除SysOperationRecord +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "批量删除SysOperationRecord" +// @Success 200 {object} response.Response{msg=string} "批量删除SysOperationRecord" +// @Router /sysOperationRecord/deleteSysOperationRecordByIds [delete] +func (s *OperationRecordApi) DeleteSysOperationRecordByIds(c *gin.Context) { + var IDS request.IdsReq + err := c.ShouldBindJSON(&IDS) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = operationRecordService.DeleteSysOperationRecordByIds(IDS) + if err != nil { + global.GVA_LOG.Error("批量删除失败!", zap.Error(err)) + response.FailWithMessage("批量删除失败", c) + return + } + response.OkWithMessage("批量删除成功", c) +} + +// FindSysOperationRecord +// @Tags SysOperationRecord +// @Summary 用id查询SysOperationRecord +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query system.SysOperationRecord true "Id" +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "用id查询SysOperationRecord" +// @Router /sysOperationRecord/findSysOperationRecord [get] +func (s *OperationRecordApi) FindSysOperationRecord(c *gin.Context) { + var sysOperationRecord system.SysOperationRecord + err := c.ShouldBindQuery(&sysOperationRecord) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(sysOperationRecord, utils.IdVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + reSysOperationRecord, err := operationRecordService.GetSysOperationRecord(sysOperationRecord.ID) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败", c) + return + } + response.OkWithDetailed(gin.H{"reSysOperationRecord": reSysOperationRecord}, "查询成功", c) +} + +// GetSysOperationRecordList +// @Tags SysOperationRecord +// @Summary 分页获取SysOperationRecord列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query request.SysOperationRecordSearch true "页码, 每页大小, 搜索条件" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页获取SysOperationRecord列表,返回包括列表,总数,页码,每页数量" +// @Router /sysOperationRecord/getSysOperationRecordList [get] +func (s *OperationRecordApi) GetSysOperationRecordList(c *gin.Context) { + var pageInfo systemReq.SysOperationRecordSearch + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := operationRecordService.GetSysOperationRecordInfoList(pageInfo) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.PageSize, + }, "获取成功", c) +} diff --git a/admin/server/api/v1/system/sys_params.go b/admin/server/api/v1/system/sys_params.go new file mode 100644 index 000000000..45fb1dfb8 --- /dev/null +++ b/admin/server/api/v1/system/sys_params.go @@ -0,0 +1,171 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type SysParamsApi struct{} + +// CreateSysParams 创建参数 +// @Tags SysParams +// @Summary 创建参数 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysParams true "创建参数" +// @Success 200 {object} response.Response{msg=string} "创建成功" +// @Router /sysParams/createSysParams [post] +func (sysParamsApi *SysParamsApi) CreateSysParams(c *gin.Context) { + var sysParams system.SysParams + err := c.ShouldBindJSON(&sysParams) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = sysParamsService.CreateSysParams(&sysParams) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败:"+err.Error(), c) + return + } + response.OkWithMessage("创建成功", c) +} + +// DeleteSysParams 删除参数 +// @Tags SysParams +// @Summary 删除参数 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysParams true "删除参数" +// @Success 200 {object} response.Response{msg=string} "删除成功" +// @Router /sysParams/deleteSysParams [delete] +func (sysParamsApi *SysParamsApi) DeleteSysParams(c *gin.Context) { + ID := c.Query("ID") + err := sysParamsService.DeleteSysParams(ID) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败:"+err.Error(), c) + return + } + response.OkWithMessage("删除成功", c) +} + +// DeleteSysParamsByIds 批量删除参数 +// @Tags SysParams +// @Summary 批量删除参数 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "批量删除成功" +// @Router /sysParams/deleteSysParamsByIds [delete] +func (sysParamsApi *SysParamsApi) DeleteSysParamsByIds(c *gin.Context) { + IDs := c.QueryArray("IDs[]") + err := sysParamsService.DeleteSysParamsByIds(IDs) + if err != nil { + global.GVA_LOG.Error("批量删除失败!", zap.Error(err)) + response.FailWithMessage("批量删除失败:"+err.Error(), c) + return + } + response.OkWithMessage("批量删除成功", c) +} + +// UpdateSysParams 更新参数 +// @Tags SysParams +// @Summary 更新参数 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysParams true "更新参数" +// @Success 200 {object} response.Response{msg=string} "更新成功" +// @Router /sysParams/updateSysParams [put] +func (sysParamsApi *SysParamsApi) UpdateSysParams(c *gin.Context) { + var sysParams system.SysParams + err := c.ShouldBindJSON(&sysParams) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = sysParamsService.UpdateSysParams(sysParams) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败:"+err.Error(), c) + return + } + response.OkWithMessage("更新成功", c) +} + +// FindSysParams 用id查询参数 +// @Tags SysParams +// @Summary 用id查询参数 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query system.SysParams true "用id查询参数" +// @Success 200 {object} response.Response{data=system.SysParams,msg=string} "查询成功" +// @Router /sysParams/findSysParams [get] +func (sysParamsApi *SysParamsApi) FindSysParams(c *gin.Context) { + ID := c.Query("ID") + resysParams, err := sysParamsService.GetSysParams(ID) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败:"+err.Error(), c) + return + } + response.OkWithData(resysParams, c) +} + +// GetSysParamsList 分页获取参数列表 +// @Tags SysParams +// @Summary 分页获取参数列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query systemReq.SysParamsSearch true "分页获取参数列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /sysParams/getSysParamsList [get] +func (sysParamsApi *SysParamsApi) GetSysParamsList(c *gin.Context) { + var pageInfo systemReq.SysParamsSearch + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := sysParamsService.GetSysParamsInfoList(pageInfo) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败:"+err.Error(), c) + return + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.PageSize, + }, "获取成功", c) +} + +// GetSysParam 根据key获取参数value +// @Tags SysParams +// @Summary 根据key获取参数value +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param key query string true "key" +// @Success 200 {object} response.Response{data=system.SysParams,msg=string} "获取成功" +// @Router /sysParams/getSysParam [get] +func (sysParamsApi *SysParamsApi) GetSysParam(c *gin.Context) { + k := c.Query("key") + params, err := sysParamsService.GetSysParam(k) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败:"+err.Error(), c) + return + } + response.OkWithDetailed(params, "获取成功", c) +} diff --git a/admin/server/api/v1/system/sys_system.go b/admin/server/api/v1/system/sys_system.go new file mode 100644 index 000000000..aa41c2f46 --- /dev/null +++ b/admin/server/api/v1/system/sys_system.go @@ -0,0 +1,88 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type SystemApi struct{} + +// GetSystemConfig +// @Tags System +// @Summary 获取配置文件内容 +// @Security ApiKeyAuth +// @Produce application/json +// @Success 200 {object} response.Response{data=systemRes.SysConfigResponse,msg=string} "获取配置文件内容,返回包括系统配置" +// @Router /system/getSystemConfig [post] +func (s *SystemApi) GetSystemConfig(c *gin.Context) { + config, err := systemConfigService.GetSystemConfig() + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(systemRes.SysConfigResponse{Config: config}, "获取成功", c) +} + +// SetSystemConfig +// @Tags System +// @Summary 设置配置文件内容 +// @Security ApiKeyAuth +// @Produce application/json +// @Param data body system.System true "设置配置文件内容" +// @Success 200 {object} response.Response{data=string} "设置配置文件内容" +// @Router /system/setSystemConfig [post] +func (s *SystemApi) SetSystemConfig(c *gin.Context) { + var sys system.System + err := c.ShouldBindJSON(&sys) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = systemConfigService.SetSystemConfig(sys) + if err != nil { + global.GVA_LOG.Error("设置失败!", zap.Error(err)) + response.FailWithMessage("设置失败", c) + return + } + response.OkWithMessage("设置成功", c) +} + +// ReloadSystem +// @Tags System +// @Summary 重启系统 +// @Security ApiKeyAuth +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "重启系统" +// @Router /system/reloadSystem [post] +func (s *SystemApi) ReloadSystem(c *gin.Context) { + err := utils.Reload() + if err != nil { + global.GVA_LOG.Error("重启系统失败!", zap.Error(err)) + response.FailWithMessage("重启系统失败", c) + return + } + response.OkWithMessage("重启系统成功", c) +} + +// GetServerInfo +// @Tags System +// @Summary 获取服务器信息 +// @Security ApiKeyAuth +// @Produce application/json +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "获取服务器信息" +// @Router /system/getServerInfo [post] +func (s *SystemApi) GetServerInfo(c *gin.Context) { + server, err := systemConfigService.GetServerInfo() + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(gin.H{"server": server}, "获取成功", c) +} diff --git a/admin/server/api/v1/system/sys_user.go b/admin/server/api/v1/system/sys_user.go new file mode 100644 index 000000000..f36089155 --- /dev/null +++ b/admin/server/api/v1/system/sys_user.go @@ -0,0 +1,503 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/model/common" + "github.com/flipped-aurora/gin-vue-admin/server/service/gaia" + "strconv" + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + + "github.com/gin-gonic/gin" + "github.com/redis/go-redis/v9" + "go.uber.org/zap" +) + +// Login +// @Tags Base +// @Summary 用户登录 +// @Produce application/json +// @Param data body systemReq.Login true "用户名, 密码, 验证码" +// @Success 200 {object} response.Response{data=systemRes.LoginResponse,msg=string} "返回包括用户信息,token,过期时间" +// @Router /base/login [post] +func (b *BaseApi) Login(c *gin.Context) { + var l systemReq.Login + err := c.ShouldBindJSON(&l) + key := c.ClientIP() + + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(l, utils.LoginVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + // 判断验证码是否开启 + openCaptcha := global.GVA_CONFIG.Captcha.OpenCaptcha // 是否开启防爆次数 + openCaptchaTimeOut := global.GVA_CONFIG.Captcha.OpenCaptchaTimeOut // 缓存超时时间 + v, ok := global.BlackCache.Get(key) + if !ok { + global.BlackCache.Set(key, 1, time.Second*time.Duration(openCaptchaTimeOut)) + } + + var oc bool = openCaptcha == 0 || openCaptcha < interfaceToInt(v) + + if !oc || (l.CaptchaId != "" && l.Captcha != "" && store.Verify(l.CaptchaId, l.Captcha, true)) { + u := &system.SysUser{Username: l.Username, Password: l.Password} + user, err := userService.Login(u) + if err != nil { + global.GVA_LOG.Error("登陆失败! 用户名不存在或者密码错误!", zap.Error(err)) + // 验证码次数+1 + global.BlackCache.Increment(key, 1) + response.FailWithMessage("用户名不存在或者密码错误", c) + return + } + if user.Enable != 1 { + global.GVA_LOG.Error("登陆失败! 用户被禁止登录!") + // 验证码次数+1 + global.BlackCache.Increment(key, 1) + response.FailWithMessage("用户被禁止登录", c) + return + } + b.TokenNext(c, *user) + return + } + // 验证码次数+1 + global.BlackCache.Increment(key, 1) + response.FailWithMessage("验证码错误", c) +} + +// TokenNext 登录以后签发jwt +func (b *BaseApi) TokenNext(c *gin.Context, user system.SysUser) { + token, claims, err := utils.LoginToken(&user) + if err != nil { + global.GVA_LOG.Error("获取token失败!", zap.Error(err)) + response.FailWithMessage("获取token失败", c) + return + } + if !global.GVA_CONFIG.System.UseMultipoint { + utils.SetToken(c, token, int(claims.RegisteredClaims.ExpiresAt.Unix()-time.Now().Unix())) + response.OkWithDetailed(systemRes.LoginResponse{ + User: user, + Token: token, + ExpiresAt: claims.RegisteredClaims.ExpiresAt.Unix() * 1000, + }, "登录成功", c) + return + } + + if jwtStr, err := jwtService.GetRedisJWT(user.Username); err == redis.Nil { + if err := jwtService.SetRedisJWT(token, user.Username); err != nil { + global.GVA_LOG.Error("设置登录状态失败!", zap.Error(err)) + response.FailWithMessage("设置登录状态失败", c) + return + } + utils.SetToken(c, token, int(claims.RegisteredClaims.ExpiresAt.Unix()-time.Now().Unix())) + response.OkWithDetailed(systemRes.LoginResponse{ + User: user, + Token: token, + ExpiresAt: claims.RegisteredClaims.ExpiresAt.Unix() * 1000, + }, "登录成功", c) + } else if err != nil { + global.GVA_LOG.Error("设置登录状态失败!", zap.Error(err)) + response.FailWithMessage("设置登录状态失败", c) + } else { + var blackJWT system.JwtBlacklist + blackJWT.Jwt = jwtStr + if err := jwtService.JsonInBlacklist(blackJWT); err != nil { + response.FailWithMessage("jwt作废失败", c) + return + } + if err := jwtService.SetRedisJWT(token, user.GetUsername()); err != nil { + response.FailWithMessage("设置登录状态失败", c) + return + } + utils.SetToken(c, token, int(claims.RegisteredClaims.ExpiresAt.Unix()-time.Now().Unix())) + response.OkWithDetailed(systemRes.LoginResponse{ + User: user, + Token: token, + ExpiresAt: claims.RegisteredClaims.ExpiresAt.Unix() * 1000, + }, "登录成功", c) + } +} + +// Register +// @Tags SysUser +// @Summary 用户注册账号 +// @Produce application/json +// @Param data body systemReq.Register true "用户名, 昵称, 密码, 角色ID" +// @Success 200 {object} response.Response{data=systemRes.SysUserResponse,msg=string} "用户注册账号,返回包括用户信息" +// @Router /user/admin_register [post] +func (b *BaseApi) Register(c *gin.Context) { + var r systemReq.Register + err := c.ShouldBindJSON(&r) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(r, utils.RegisterVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if err = gaia.IsUserPasswordValid(r.Password); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + // Extend: Start admin init + r.AuthorityId = system.AdminGroupID + // Extend: stop admin init + var authorities []system.SysAuthority + for _, v := range r.AuthorityIds { + authorities = append(authorities, system.SysAuthority{ + AuthorityId: v, + }) + } + token := utils.GetToken(c) + user := &system.SysUser{Username: r.Username, NickName: r.NickName, Password: r.Password, HeaderImg: r.HeaderImg, AuthorityId: r.AuthorityId, Authorities: authorities, Enable: r.Enable, Phone: r.Phone, Email: r.Email} + userReturn, err := userService.Register(*user, token) + if err != nil { + global.GVA_LOG.Error("注册失败!", zap.Error(err)) + response.FailWithDetailed(systemRes.SysUserResponse{User: userReturn}, "注册失败", c) + return + } + response.OkWithDetailed(systemRes.SysUserResponse{User: userReturn}, "注册成功", c) +} + +// ChangePassword +// @Tags SysUser +// @Summary 用户修改密码 +// @Security ApiKeyAuth +// @Produce application/json +// @Param data body systemReq.ChangePasswordReq true "用户名, 原密码, 新密码" +// @Success 200 {object} response.Response{msg=string} "用户修改密码" +// @Router /user/changePassword [post] +func (b *BaseApi) ChangePassword(c *gin.Context) { + var req systemReq.ChangePasswordReq + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(req, utils.ChangePasswordVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + uid := utils.GetUserID(c) + if err = gaia.IsUserPasswordValid(req.Password); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + u := &system.SysUser{GVA_MODEL: global.GVA_MODEL{ID: uid}, Password: req.Password} + _, err = userService.ChangePassword(u, req.NewPassword) + if err != nil { + global.GVA_LOG.Error("修改失败!", zap.Error(err)) + response.FailWithMessage("修改失败,原密码与当前账户不符", c) + return + } + response.OkWithMessage("修改成功", c) +} + +// GetUserList +// @Tags SysUser +// @Summary 分页获取用户列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body systemReq.GetUserList true "页码, 每页大小" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页获取用户列表,返回包括列表,总数,页码,每页数量" +// @Router /user/getUserList [post] +func (b *BaseApi) GetUserList(c *gin.Context) { + var pageInfo systemReq.GetUserList + err := c.ShouldBindJSON(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(pageInfo, utils.PageInfoVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := userService.GetUserInfoList(pageInfo) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.PageSize, + }, "获取成功", c) +} + +// SetUserAuthority +// @Tags SysUser +// @Summary 更改用户权限 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body systemReq.SetUserAuth true "用户UUID, 角色ID" +// @Success 200 {object} response.Response{msg=string} "设置用户权限" +// @Router /user/setUserAuthority [post] +func (b *BaseApi) SetUserAuthority(c *gin.Context) { + var sua systemReq.SetUserAuth + err := c.ShouldBindJSON(&sua) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if UserVerifyErr := utils.Verify(sua, utils.SetUserAuthorityVerify); UserVerifyErr != nil { + response.FailWithMessage(UserVerifyErr.Error(), c) + return + } + userID := utils.GetUserID(c) + err = userService.SetUserAuthority(userID, sua.AuthorityId) + if err != nil { + global.GVA_LOG.Error("修改失败!", zap.Error(err)) + response.FailWithMessage(err.Error(), c) + return + } + claims := utils.GetUserInfo(c) + j := &utils.JWT{SigningKey: []byte(global.GVA_CONFIG.JWT.SigningKey)} // 唯一签名 + claims.AuthorityId = sua.AuthorityId + if token, err := j.CreateToken(*claims); err != nil { + global.GVA_LOG.Error("修改失败!", zap.Error(err)) + response.FailWithMessage(err.Error(), c) + } else { + c.Header("new-token", token) + c.Header("new-expires-at", strconv.FormatInt(claims.ExpiresAt.Unix(), 10)) + utils.SetToken(c, token, int((claims.ExpiresAt.Unix()-time.Now().Unix())/60)) + response.OkWithMessage("修改成功", c) + } +} + +// SetUserAuthorities +// @Tags SysUser +// @Summary 设置用户权限 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body systemReq.SetUserAuthorities true "用户UUID, 角色ID" +// @Success 200 {object} response.Response{msg=string} "设置用户权限" +// @Router /user/setUserAuthorities [post] +func (b *BaseApi) SetUserAuthorities(c *gin.Context) { + var sua systemReq.SetUserAuthorities + err := c.ShouldBindJSON(&sua) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + authorityID := utils.GetUserAuthorityId(c) + err = userService.SetUserAuthorities(authorityID, sua.ID, sua.AuthorityIds) + if err != nil { + global.GVA_LOG.Error("修改失败!", zap.Error(err)) + response.FailWithMessage("修改失败", c) + return + } + response.OkWithMessage("修改成功", c) +} + +// DeleteUser +// @Tags SysUser +// @Summary 删除用户 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.GetById true "用户ID" +// @Success 200 {object} response.Response{msg=string} "删除用户" +// @Router /user/deleteUser [delete] +func (b *BaseApi) DeleteUser(c *gin.Context) { + var reqId request.GetById + err := c.ShouldBindJSON(&reqId) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(reqId, utils.IdVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + jwtId := utils.GetUserID(c) + if jwtId == uint(reqId.ID) { + response.FailWithMessage("删除失败, 无法删除自己。", c) + return + } + err = userService.DeleteUser(reqId.ID) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败", c) + return + } + response.OkWithMessage("删除成功", c) +} + +// SetUserInfo +// @Tags SysUser +// @Summary 设置用户信息 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysUser true "ID, 用户名, 昵称, 头像链接" +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "设置用户信息" +// @Router /user/setUserInfo [put] +func (b *BaseApi) SetUserInfo(c *gin.Context) { + var user systemReq.ChangeUserInfo + err := c.ShouldBindJSON(&user) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(user, utils.IdVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if len(user.AuthorityIds) != 0 { + authorityID := utils.GetUserAuthorityId(c) + err = userService.SetUserAuthorities(authorityID, user.ID, user.AuthorityIds) + if err != nil { + global.GVA_LOG.Error("设置失败!", zap.Error(err)) + response.FailWithMessage("设置失败", c) + return + } + } + err = userService.SetUserInfo(system.SysUser{ + GVA_MODEL: global.GVA_MODEL{ + ID: user.ID, + }, + NickName: user.NickName, + HeaderImg: user.HeaderImg, + Phone: user.Phone, + Email: user.Email, + Enable: user.Enable, + }, user.GlobalCode) + if err != nil { + global.GVA_LOG.Error("设置失败!", zap.Error(err)) + response.FailWithMessage("设置失败", c) + return + } + response.OkWithMessage("设置成功", c) +} + +// SetSelfInfo +// @Tags SysUser +// @Summary 设置用户信息 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysUser true "ID, 用户名, 昵称, 头像链接" +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "设置用户信息" +// @Router /user/SetSelfInfo [put] +func (b *BaseApi) SetSelfInfo(c *gin.Context) { + var user systemReq.ChangeUserInfo + err := c.ShouldBindJSON(&user) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + user.ID = utils.GetUserID(c) + err = userService.SetSelfInfo(system.SysUser{ + GVA_MODEL: global.GVA_MODEL{ + ID: user.ID, + }, + NickName: user.NickName, + HeaderImg: user.HeaderImg, + Phone: user.Phone, + Email: user.Email, + Enable: user.Enable, + }) + if err != nil { + global.GVA_LOG.Error("设置失败!", zap.Error(err)) + response.FailWithMessage("设置失败", c) + return + } + response.OkWithMessage("设置成功", c) +} + +// SetSelfSetting +// @Tags SysUser +// @Summary 设置用户配置 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body map[string]interface{} true "用户配置数据" +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "设置用户配置" +// @Router /user/SetSelfSetting [put] +func (b *BaseApi) SetSelfSetting(c *gin.Context) { + var req common.JSONMap + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + err = userService.SetSelfSetting(req, utils.GetUserID(c)) + if err != nil { + global.GVA_LOG.Error("设置失败!", zap.Error(err)) + response.FailWithMessage("设置失败", c) + return + } + response.OkWithMessage("设置成功", c) +} + +// GetUserInfo +// @Tags SysUser +// @Summary 获取用户信息 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "获取用户信息" +// @Router /user/getUserInfo [get] +func (b *BaseApi) GetUserInfo(c *gin.Context) { + uuid := utils.GetUserUuid(c) + ReqUser, err := userService.GetUserInfo(uuid) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(gin.H{"userInfo": ReqUser}, "获取成功", c) +} + +// ResetPassword +// @Tags SysUser +// @Summary 重置用户密码 +// @Security ApiKeyAuth +// @Produce application/json +// @Param data body system.SysUser true "ID" +// @Success 200 {object} response.Response{msg=string} "重置用户密码" +// @Router /user/resetPassword [post] +func (b *BaseApi) ResetPassword(c *gin.Context) { + // Extend Start: update password + var user systemReq.UpdateUserPasswd + err := c.ShouldBindJSON(&user) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if err = gaia.IsUserPasswordValid(user.Password); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = userService.ResetPassword(user.ID, user.Password) + // Extend Stop: update password + if err != nil { + global.GVA_LOG.Error("重置失败!", zap.Error(err)) + response.FailWithMessage("重置失败"+err.Error(), c) + return + } + response.OkWithMessage("重置成功", c) +} diff --git a/admin/server/api/v1/system/sys_user_extend.go b/admin/server/api/v1/system/sys_user_extend.go new file mode 100644 index 000000000..853fb3ec7 --- /dev/null +++ b/admin/server/api/v1/system/sys_user_extend.go @@ -0,0 +1,158 @@ +package system + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/gin-gonic/gin" + "github.com/go-resty/resty/v2" + "go.uber.org/zap" + "gorm.io/gorm" +) + +// Extend Start: sync user + +// SyncUser +// @Tags Base +// @Summary 用户同步 +// @Produce application/json +// @Param data body systemReq.Login true "用户名, 密码, 验证码" +// @Success 200 {object} response.Response{data=systemRes.LoginResponse,msg=string} "返回包括用户信息,token,过期时间" +// @Router /user/sync [post] +func (b *BaseApi) SyncUser(c *gin.Context) { + userExtendService.SyncUser() + response.OkWithMessage("同步中", c) +} + +// Extend Stop: sync user + +// OaLogin +// @Tags Base +// @Summary 用户登录 +// @Produce application/json +// @Param data body systemReq.Login true "用户名, 密码, 验证码" +// @Success 200 {object} response.Response{data=systemRes.LoginResponse,msg=string} "返回包括用户信息,token,过期时间" +// @Router /base/oaLogin [post] +func (b *BaseApi) OaLogin(c *gin.Context) { + var l systemReq.OaLoginReq + err := c.ShouldBindJSON(&l) + + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(l, utils.OaLoginVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + /** + 请求获取accessToken + */ + clientGetToken := resty.New().R() + var accessTokenResponse *resty.Response + getTokenUrl := fmt.Sprintf("%s%s", global.GVA_CONFIG.OaLogin.Url, global.GVA_CONFIG.OaLogin.GetTokenByCodeApiPath) + var postParams = map[string]string{ + "client_id": global.GVA_CONFIG.OaLogin.Oauth2ClientId, + "client_secret": global.GVA_CONFIG.OaLogin.Oauth2ClientSecret, + "code": l.AuthorizeCode, + "grant_type": "authorization_code", + "redirect_uri": "", + } + accessTokenResponse, err = clientGetToken. + SetFormData(postParams). + Post(getTokenUrl) + if err != nil { + global.GVA_LOG.Error("请求OA用户信息失败,响应数据为:", zap.Error(errors.New(accessTokenResponse.String()))) + response.FailWithMessage("请求OA用户信息失败:"+err.Error(), c) + return + } + tokenRes := accessTokenResponse.String() + var oaAccessToken systemRes.OaAccessTokenRes + err = json.Unmarshal([]byte(tokenRes), &oaAccessToken) + if err != nil { + global.GVA_LOG.Error("解析OA AccessToken接口返回数据失败,响应数据为:", zap.Error(errors.New(accessTokenResponse.String()))) + response.FailWithMessage("解析OA AccessToken接口返回数据失败:"+err.Error(), c) + return + } + + /** + 请求OA,返回用户信息 + */ + getUserInfoUrl := fmt.Sprintf("%s%s", global.GVA_CONFIG.OaLogin.Url, global.GVA_CONFIG.OaLogin.GetUserApiPath) + clientGetUser := resty.New().R() + var userInfoResponse *resty.Response + userInfoResponse, err = clientGetUser.SetHeader("Authorization", oaAccessToken.AccessToken).Post(getUserInfoUrl) + if err != nil { + global.GVA_LOG.Error("请求OA用户信息失败,响应数据为:", zap.Error(errors.New(userInfoResponse.String()))) + response.FailWithMessage("请求OA用户信息失败:"+err.Error(), c) + return + } + userInfoRes := userInfoResponse.String() + var oaUserInfo systemRes.OaUserInfoRes + err = json.Unmarshal([]byte(userInfoRes), &oaUserInfo) + if err != nil { + global.GVA_LOG.Error("解析OA用户信息接口返回数据失败,响应数据为:", zap.Error(errors.New(userInfoRes))) + global.GVA_LOG.Error("解析OA用户信息接口返回数据失败,请求Token为:", zap.String("", oaAccessToken.AccessToken)) + global.GVA_LOG.Error("解析OA用户信息接口返回数据失败,请求Token为:", zap.String("", accessTokenResponse.String())) + response.FailWithMessage("解析OA用户信息接口返回数据失败:"+err.Error(), c) + return + } + + // 查询数据库数据 + sysUser := &system.SysUser{} + err = global.GVA_DB.Where("email", oaUserInfo.Data.Email).First(&sysUser).Error + if err != nil && err != gorm.ErrRecordNotFound { + response.FailWithMessage("查询数据库信息失败:"+err.Error(), c) + return + } + // 判断是否需要注册 + if sysUser.ID == 0 { + // TODO 从ldap中获取用户详细数据 + + // 注册用户 + sysUser = &system.SysUser{ + Username: oaUserInfo.Data.Username, + NickName: oaUserInfo.Data.Username, + HeaderImg: "https://hn1.oss-cn-shenzhen.aliyuncs.com/w.jpg", + AuthorityId: system.NormalAuthorityId, + Authorities: []system.SysAuthority{{AuthorityId: system.NormalAuthorityId}}, + Enable: 1, + //Phone: r.Phone, // TODO 手机需要从ldap中获取用户详细数据 + Email: oaUserInfo.Data.Email, + Password: utils.RandomString(16), + } + var userReturn system.SysUser + userReturn, err = userService.Register(*sysUser, "") + if err != nil { + global.GVA_LOG.Error("注册失败!", zap.Error(err)) + response.FailWithDetailed(systemRes.SysUserResponse{User: userReturn}, "注册失败", c) + return + } else { + global.GVA_LOG.Info("注册成功!", zap.Any("username", userReturn.Username)) + } + sysUser = &userReturn + } + + var user *system.SysUser + user, err = userExtendService.OaLogin(sysUser) // 注意这个方法不检查密码 + if err != nil { + global.GVA_LOG.Error("登陆失败! 用户名不存在!", zap.Error(err)) + response.FailWithMessage("用户名不存在", c) + return + } + if sysUser.Enable != 1 { + global.GVA_LOG.Error("登陆失败! 用户被禁止登录!") + response.FailWithMessage("用户被禁止登录", c) + return + } + b.TokenNext(c, *user) + return + +} diff --git a/admin/server/config.docker.yaml b/admin/server/config.docker.yaml new file mode 100644 index 000000000..abd27d1d4 --- /dev/null +++ b/admin/server/config.docker.yaml @@ -0,0 +1,173 @@ +aliyun-oss: + endpoint: yourEndpoint + access-key-id: yourAccessKeyId + access-key-secret: yourAccessKeySecret + bucket-name: yourBucketName + bucket-url: yourBucketUrl + base-path: yourBasePath +autocode: + web: web/src + root: + server: server + module: github.com/flipped-aurora/gin-vue-admin/server + ai-path: "" +aws-s3: + bucket: xxxxx-10005608 + region: ap-shanghai + endpoint: "" + secret-id: your-secret-id + secret-key: your-secret-key + base-url: https://gin.vue.admin + path-prefix: github.com/flipped-aurora/gin-vue-admin/server + s3-force-path-style: false + disable-ssl: false +captcha: + key-long: 6 + img-width: 240 + img-height: 80 + open-captcha: 0 + open-captcha-timeout: 3600 +cloudflare-r2: + bucket: xxxx0bucket + base-url: https://gin.vue.admin.com + path: uploads + account-id: xxx_account_id + access-key-id: xxx_key_id + secret-access-key: xxx_secret_key +cors: + mode: strict-whitelist + whitelist: + - allow-origin: example1.com + allow-methods: POST, GET + allow-headers: Content-Type,AccessToken,X-CSRF-Token, Authorization, Token,X-Token,X-User-Id + expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type + allow-credentials: true + - allow-origin: example2.com + allow-methods: GET, POST + allow-headers: content-type + expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type + allow-credentials: true +db-list: + - type: "" + alias-name: "" + prefix: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + path: "" + engine: "" + log-mode: "" + max-idle-conns: 10 + max-open-conns: 100 + singular: false + log-zap: false + disable: true +disk-list: + - mount-point: / +email: + to: xxx@qq.com + from: xxx@163.com + host: smtp.163.com + secret: xxx + nickname: test + port: 465 + is-ssl: true +excel: + dir: ./resource/excel/ +gaia: + url: http://api:5001 + login_max_error_limit: 5 + SUPER_ADMIN_ACCOUNT_ID: + SUPER_ADMIN_TENANT_ID: +hua-wei-obs: + path: you-path + bucket: you-bucket + endpoint: you-endpoint + access-key: you-access-key + secret-key: you-secret-key +jwt: + signing-key: sk-9f73s3ljTXVcMT3Blb3ljTqtsKiGHXVcMT3BlbkFJLK7U + expires-time: 1d + buffer-time: 1d + issuer: CLOUD +local: + path: uploads/file + store-path: uploads/file +minio: + endpoint: yourEndpoint + access-key-id: yourAccessKeyId + access-key-secret: yourAccessKeySecret + bucket-name: yourBucketName + use-ssl: false + base-path: "" + bucket-url: http://host:9000/yourBucketName +oa-login: + url: + oauth2-client-id: + oauth2-client-secret: + get-user-info-api-path: + get-token-by-code-api-path: +pgsql: + prefix: "" + port: "5432" + config: sslmode=disable TimeZone=Asia/Shanghai + db-name: + username: + password: + path: + engine: "" + log-mode: error + max-idle-conns: 10 + max-open-conns: 100 + singular: false + log-zap: false +qiniu: + zone: ZoneHuaDong + bucket: "" + img-path: "" + access-key: "" + secret-key: "" + use-https: false + use-cdn-domains: false +redis: + name: "" + addr: redis:6379 + password: difyai123456 + db: 8 + useCluster: false + clusterAddrs: + - 172.21.0.3:7000 + - 172.21.0.4:7001 + - 172.21.0.2:7002 +system: + db-type: pgsql + oss-type: local + router-prefix: "" + addr: 8888 + iplimit-count: 15000 + iplimit-time: 3600 + use-multipoint: false + use-redis: true + use-mongo: false + use-strict-auth: false + user_default-group-id: "888" + docker-run: true +tencent-cos: + bucket: xxxxx-10005608 + region: ap-shanghai + secret-id: your-secret-id + secret-key: your-secret-key + base-url: https://gin.vue.admin + path-prefix: github.com/flipped-aurora/gin-vue-admin/server +zap: + level: info + prefix: '[dify_plus/admin_server]' + format: json + director: log + encode-level: LowercaseColorLevelEncoder + stacktrace-key: stacktrace + show-line: true + log-in-console: true + retention-day: -1 diff --git a/admin/server/config.yaml b/admin/server/config.yaml new file mode 100644 index 000000000..eed330e34 --- /dev/null +++ b/admin/server/config.yaml @@ -0,0 +1,71 @@ +gaia: + url: http://127.0.0.1:5001 + login_max_error_limit: 5 + SUPER_ADMIN_ACCOUNT_ID: + SUPER_ADMIN_TENANT_ID: +captcha: + key-long: 6 + img-width: 240 + img-height: 80 + open-captcha: 0 + open-captcha-timeout: 3600 +jwt: + signing-key: + expires-time: 1d + buffer-time: 1d + issuer: CLOUD +local: + path: uploads/file + store-path: uploads/file +oa-login: + url: + oauth2-client-id: + oauth2-client-secret: + get-user-info-api-path: + get-token-by-code-api-path: +pgsql: + prefix: "" + port: "5432" + config: sslmode=disable TimeZone=Asia/Shanghai + db-name: dify_plus + username: postgres + password: difyai123456 + path: 127.0.0.1 + engine: "" + log-mode: error + max-idle-conns: 10 + max-open-conns: 100 + singular: false + log-zap: false +redis: + name: "" + addr: 127.0.0.1:6379 + password: difyai123456 + db: 8 + useCluster: false + clusterAddrs: + - 172.21.0.3:7000 + - 172.21.0.4:7001 + - 172.21.0.2:7002 +system: + db-type: pgsql + oss-type: local + router-prefix: "" + addr: 8888 + iplimit-count: 15000 + iplimit-time: 3600 + use-multipoint: false + use-redis: true + use-mongo: false + use-strict-auth: false + user_default-group-id: "888" +zap: + level: info + prefix: '[gaia/server]' + format: json + director: log + encode-level: LowercaseColorLevelEncoder + stacktrace-key: stacktrace + show-line: true + log-in-console: true + retention-day: -1 diff --git a/admin/server/config/auto_code.go b/admin/server/config/auto_code.go new file mode 100644 index 000000000..ade79a023 --- /dev/null +++ b/admin/server/config/auto_code.go @@ -0,0 +1,22 @@ +package config + +import ( + "path/filepath" + "strings" +) + +type Autocode struct { + Web string `mapstructure:"web" json:"web" yaml:"web"` + Root string `mapstructure:"root" json:"root" yaml:"root"` + Server string `mapstructure:"server" json:"server" yaml:"server"` + Module string `mapstructure:"module" json:"module" yaml:"module"` + AiPath string `mapstructure:"ai-path" json:"ai-path" yaml:"ai-path"` +} + +func (a *Autocode) WebRoot() string { + webs := strings.Split(a.Web, "/") + if len(webs) == 0 { + webs = strings.Split(a.Web, "\\") + } + return filepath.Join(webs...) +} diff --git a/admin/server/config/captcha.go b/admin/server/config/captcha.go new file mode 100644 index 000000000..074a9bfad --- /dev/null +++ b/admin/server/config/captcha.go @@ -0,0 +1,9 @@ +package config + +type Captcha struct { + KeyLong int `mapstructure:"key-long" json:"key-long" yaml:"key-long"` // 验证码长度 + ImgWidth int `mapstructure:"img-width" json:"img-width" yaml:"img-width"` // 验证码宽度 + ImgHeight int `mapstructure:"img-height" json:"img-height" yaml:"img-height"` // 验证码高度 + OpenCaptcha int `mapstructure:"open-captcha" json:"open-captcha" yaml:"open-captcha"` // 防爆破验证码开启此数,0代表每次登录都需要验证码,其他数字代表错误密码此数,如3代表错误三次后出现验证码 + OpenCaptchaTimeOut int `mapstructure:"open-captcha-timeout" json:"open-captcha-timeout" yaml:"open-captcha-timeout"` // 防爆破验证码超时时间,单位:s(秒) +} diff --git a/admin/server/config/config.go b/admin/server/config/config.go new file mode 100644 index 000000000..09833b557 --- /dev/null +++ b/admin/server/config/config.go @@ -0,0 +1,44 @@ +package config + +type Server struct { + JWT JWT `mapstructure:"jwt" json:"jwt" yaml:"jwt"` + Zap Zap `mapstructure:"zap" json:"zap" yaml:"zap"` + Redis Redis `mapstructure:"redis" json:"redis" yaml:"redis"` + RedisList []Redis `mapstructure:"redis-list" json:"redis-list" yaml:"redis-list"` + DifyRedis Redis `mapstructure:"dify-redis" json:"dify-redis" yaml:"dify-redis"` // Extend: Global Code + Mongo Mongo `mapstructure:"mongo" json:"mongo" yaml:"mongo"` + Email Email `mapstructure:"email" json:"email" yaml:"email"` + System System `mapstructure:"system" json:"system" yaml:"system"` + Captcha Captcha `mapstructure:"captcha" json:"captcha" yaml:"captcha"` + // auto + AutoCode Autocode `mapstructure:"autocode" json:"autocode" yaml:"autocode"` + // gorm + Mysql Mysql `mapstructure:"mysql" json:"mysql" yaml:"mysql"` + Mssql Mssql `mapstructure:"mssql" json:"mssql" yaml:"mssql"` + Pgsql Pgsql `mapstructure:"pgsql" json:"pgsql" yaml:"pgsql"` + Oracle Oracle `mapstructure:"oracle" json:"oracle" yaml:"oracle"` + Sqlite Sqlite `mapstructure:"sqlite" json:"sqlite" yaml:"sqlite"` + DBList []SpecializedDB `mapstructure:"db-list" json:"db-list" yaml:"db-list"` + // oss + Local Local `mapstructure:"local" json:"local" yaml:"local"` + Qiniu Qiniu `mapstructure:"qiniu" json:"qiniu" yaml:"qiniu"` + AliyunOSS AliyunOSS `mapstructure:"aliyun-oss" json:"aliyun-oss" yaml:"aliyun-oss"` + HuaWeiObs HuaWeiObs `mapstructure:"hua-wei-obs" json:"hua-wei-obs" yaml:"hua-wei-obs"` + TencentCOS TencentCOS `mapstructure:"tencent-cos" json:"tencent-cos" yaml:"tencent-cos"` + AwsS3 AwsS3 `mapstructure:"aws-s3" json:"aws-s3" yaml:"aws-s3"` + CloudflareR2 CloudflareR2 `mapstructure:"cloudflare-r2" json:"cloudflare-r2" yaml:"cloudflare-r2"` + Minio Minio `mapstructure:"minio" json:"minio" yaml:"minio"` + + Excel Excel `mapstructure:"excel" json:"excel" yaml:"excel"` + + DiskList []DiskList `mapstructure:"disk-list" json:"disk-list" yaml:"disk-list"` + + // 跨域配置 + Cors CORS `mapstructure:"cors" json:"cors" yaml:"cors"` + + // 对接OA登录oauth2.0 + OaLogin OaLogin `mapstructure:"oa-login" json:"oa-login" yaml:"oa-login"` + + // 对接Gaia平台相关配置 + Gaia Gaia `mapstructure:"gaia" json:"gaia" yaml:"gaia"` +} diff --git a/admin/server/config/cors.go b/admin/server/config/cors.go new file mode 100644 index 000000000..7fba99346 --- /dev/null +++ b/admin/server/config/cors.go @@ -0,0 +1,14 @@ +package config + +type CORS struct { + Mode string `mapstructure:"mode" json:"mode" yaml:"mode"` + Whitelist []CORSWhitelist `mapstructure:"whitelist" json:"whitelist" yaml:"whitelist"` +} + +type CORSWhitelist struct { + AllowOrigin string `mapstructure:"allow-origin" json:"allow-origin" yaml:"allow-origin"` + AllowMethods string `mapstructure:"allow-methods" json:"allow-methods" yaml:"allow-methods"` + AllowHeaders string `mapstructure:"allow-headers" json:"allow-headers" yaml:"allow-headers"` + ExposeHeaders string `mapstructure:"expose-headers" json:"expose-headers" yaml:"expose-headers"` + AllowCredentials bool `mapstructure:"allow-credentials" json:"allow-credentials" yaml:"allow-credentials"` +} diff --git a/admin/server/config/db_list.go b/admin/server/config/db_list.go new file mode 100644 index 000000000..39767f53b --- /dev/null +++ b/admin/server/config/db_list.go @@ -0,0 +1,52 @@ +package config + +import ( + "gorm.io/gorm/logger" + "strings" +) + +type DsnProvider interface { + Dsn() string +} + +// Embeded 结构体可以压平到上一层,从而保持 config 文件的结构和原来一样 +// 见 playground: https://go.dev/play/p/KIcuhqEoxmY + +// GeneralDB 也被 Pgsql 和 Mysql 原样使用 +type GeneralDB struct { + Prefix string `mapstructure:"prefix" json:"prefix" yaml:"prefix"` // 数据库前缀 + Port string `mapstructure:"port" json:"port" yaml:"port"` // 数据库端口 + Config string `mapstructure:"config" json:"config" yaml:"config"` // 高级配置 + Dbname string `mapstructure:"db-name" json:"db-name" yaml:"db-name"` // 数据库名 + Username string `mapstructure:"username" json:"username" yaml:"username"` // 数据库账号 + Password string `mapstructure:"password" json:"password" yaml:"password"` // 数据库密码 + Path string `mapstructure:"path" json:"path" yaml:"path"` // 数据库地址 + Engine string `mapstructure:"engine" json:"engine" yaml:"engine" default:"InnoDB"` // 数据库引擎,默认InnoDB + LogMode string `mapstructure:"log-mode" json:"log-mode" yaml:"log-mode"` // 是否开启Gorm全局日志 + MaxIdleConns int `mapstructure:"max-idle-conns" json:"max-idle-conns" yaml:"max-idle-conns"` // 空闲中的最大连接数 + MaxOpenConns int `mapstructure:"max-open-conns" json:"max-open-conns" yaml:"max-open-conns"` // 打开到数据库的最大连接数 + Singular bool `mapstructure:"singular" json:"singular" yaml:"singular"` // 是否开启全局禁用复数,true表示开启 + LogZap bool `mapstructure:"log-zap" json:"log-zap" yaml:"log-zap"` // 是否通过zap写入日志文件 +} + +func (c GeneralDB) LogLevel() logger.LogLevel { + switch strings.ToLower(c.LogMode) { + case "silent", "Silent": + return logger.Silent + case "error", "Error": + return logger.Error + case "warn", "Warn": + return logger.Warn + case "info", "Info": + return logger.Info + default: + return logger.Info + } +} + +type SpecializedDB struct { + Type string `mapstructure:"type" json:"type" yaml:"type"` + AliasName string `mapstructure:"alias-name" json:"alias-name" yaml:"alias-name"` + GeneralDB `yaml:",inline" mapstructure:",squash"` + Disable bool `mapstructure:"disable" json:"disable" yaml:"disable"` +} diff --git a/admin/server/config/disk.go b/admin/server/config/disk.go new file mode 100644 index 000000000..59a633259 --- /dev/null +++ b/admin/server/config/disk.go @@ -0,0 +1,9 @@ +package config + +type Disk struct { + MountPoint string `mapstructure:"mount-point" json:"mount-point" yaml:"mount-point"` +} + +type DiskList struct { + Disk `yaml:",inline" mapstructure:",squash"` +} diff --git a/admin/server/config/email.go b/admin/server/config/email.go new file mode 100644 index 000000000..0984616b2 --- /dev/null +++ b/admin/server/config/email.go @@ -0,0 +1,11 @@ +package config + +type Email struct { + To string `mapstructure:"to" json:"to" yaml:"to"` // 收件人:多个以英文逗号分隔 例:a@qq.com b@qq.com 正式开发中请把此项目作为参数使用 + From string `mapstructure:"from" json:"from" yaml:"from"` // 发件人 你自己要发邮件的邮箱 + Host string `mapstructure:"host" json:"host" yaml:"host"` // 服务器地址 例如 smtp.qq.com 请前往QQ或者你要发邮件的邮箱查看其smtp协议 + Secret string `mapstructure:"secret" json:"secret" yaml:"secret"` // 密钥 用于登录的密钥 最好不要用邮箱密码 去邮箱smtp申请一个用于登录的密钥 + Nickname string `mapstructure:"nickname" json:"nickname" yaml:"nickname"` // 昵称 发件人昵称 通常为自己的邮箱 + Port int `mapstructure:"port" json:"port" yaml:"port"` // 端口 请前往QQ或者你要发邮件的邮箱查看其smtp协议 大多为 465 + IsSSL bool `mapstructure:"is-ssl" json:"is-ssl" yaml:"is-ssl"` // 是否SSL 是否开启SSL +} diff --git a/admin/server/config/excel.go b/admin/server/config/excel.go new file mode 100644 index 000000000..13caab7f5 --- /dev/null +++ b/admin/server/config/excel.go @@ -0,0 +1,5 @@ +package config + +type Excel struct { + Dir string `mapstructure:"dir" json:"dir" yaml:"dir"` +} diff --git a/admin/server/config/gaia.go b/admin/server/config/gaia.go new file mode 100644 index 000000000..8e014c51f --- /dev/null +++ b/admin/server/config/gaia.go @@ -0,0 +1,8 @@ +package config + +type Gaia struct { + Url string `mapstructure:"url" json:"url" yaml:"url"` + LoginMaxErrorLimit int `mapstructure:"login_max_error_limit" json:"login_max_error_limit" yaml:"login_max_error_limit"` + SuperAdminAccountId string `mapstructure:"SUPER_ADMIN_ACCOUNT_ID" json:"SUPER_ADMIN_ACCOUNT_ID" yaml:"SUPER_ADMIN_ACCOUNT_ID"` // 超级管理员账号 + SuperAdminTenantId string `mapstructure:"SUPER_ADMIN_TENANT_ID" json:"SUPER_ADMIN_TENANT_ID" yaml:"SUPER_ADMIN_TENANT_ID"` // 系统默认工作区 +} diff --git a/admin/server/config/gorm_mssql.go b/admin/server/config/gorm_mssql.go new file mode 100644 index 000000000..d18711948 --- /dev/null +++ b/admin/server/config/gorm_mssql.go @@ -0,0 +1,10 @@ +package config + +type Mssql struct { + GeneralDB `yaml:",inline" mapstructure:",squash"` +} + +// Dsn "sqlserver://gorm:LoremIpsum86@localhost:9930?database=gorm" +func (m *Mssql) Dsn() string { + return "sqlserver://" + m.Username + ":" + m.Password + "@" + m.Path + ":" + m.Port + "?database=" + m.Dbname + "&encrypt=disable" +} diff --git a/admin/server/config/gorm_mysql.go b/admin/server/config/gorm_mysql.go new file mode 100644 index 000000000..77e024539 --- /dev/null +++ b/admin/server/config/gorm_mysql.go @@ -0,0 +1,9 @@ +package config + +type Mysql struct { + GeneralDB `yaml:",inline" mapstructure:",squash"` +} + +func (m *Mysql) Dsn() string { + return m.Username + ":" + m.Password + "@tcp(" + m.Path + ":" + m.Port + ")/" + m.Dbname + "?" + m.Config +} diff --git a/admin/server/config/gorm_oracle.go b/admin/server/config/gorm_oracle.go new file mode 100644 index 000000000..1bbeb46ab --- /dev/null +++ b/admin/server/config/gorm_oracle.go @@ -0,0 +1,10 @@ +package config + +type Oracle struct { + GeneralDB `yaml:",inline" mapstructure:",squash"` +} + +func (m *Oracle) Dsn() string { + return "oracle://" + m.Username + ":" + m.Password + "@" + m.Path + ":" + m.Port + "/" + m.Dbname + "?" + m.Config + +} diff --git a/admin/server/config/gorm_pgsql.go b/admin/server/config/gorm_pgsql.go new file mode 100644 index 000000000..29fe03f43 --- /dev/null +++ b/admin/server/config/gorm_pgsql.go @@ -0,0 +1,17 @@ +package config + +type Pgsql struct { + GeneralDB `yaml:",inline" mapstructure:",squash"` +} + +// Dsn 基于配置文件获取 dsn +// Author [SliverHorn](https://github.com/SliverHorn) +func (p *Pgsql) Dsn() string { + return "host=" + p.Path + " user=" + p.Username + " password=" + p.Password + " dbname=" + p.Dbname + " port=" + p.Port + " " + p.Config +} + +// LinkDsn 根据 dbname 生成 dsn +// Author [SliverHorn](https://github.com/SliverHorn) +func (p *Pgsql) LinkDsn(dbname string) string { + return "host=" + p.Path + " user=" + p.Username + " password=" + p.Password + " dbname=" + dbname + " port=" + p.Port + " " + p.Config +} diff --git a/admin/server/config/gorm_sqlite.go b/admin/server/config/gorm_sqlite.go new file mode 100644 index 000000000..46f2e19a5 --- /dev/null +++ b/admin/server/config/gorm_sqlite.go @@ -0,0 +1,13 @@ +package config + +import ( + "path/filepath" +) + +type Sqlite struct { + GeneralDB `yaml:",inline" mapstructure:",squash"` +} + +func (s *Sqlite) Dsn() string { + return filepath.Join(s.Path, s.Dbname+".db") +} diff --git a/admin/server/config/jwt.go b/admin/server/config/jwt.go new file mode 100644 index 000000000..c95d30dc1 --- /dev/null +++ b/admin/server/config/jwt.go @@ -0,0 +1,8 @@ +package config + +type JWT struct { + SigningKey string `mapstructure:"signing-key" json:"signing-key" yaml:"signing-key"` // jwt签名 + ExpiresTime string `mapstructure:"expires-time" json:"expires-time" yaml:"expires-time"` // 过期时间 + BufferTime string `mapstructure:"buffer-time" json:"buffer-time" yaml:"buffer-time"` // 缓冲时间 + Issuer string `mapstructure:"issuer" json:"issuer" yaml:"issuer"` // 签发者 +} diff --git a/admin/server/config/mongo.go b/admin/server/config/mongo.go new file mode 100644 index 000000000..2034a3fb4 --- /dev/null +++ b/admin/server/config/mongo.go @@ -0,0 +1,41 @@ +package config + +import ( + "fmt" + "strings" +) + +type Mongo struct { + Coll string `json:"coll" yaml:"coll" mapstructure:"coll"` // collection name + Options string `json:"options" yaml:"options" mapstructure:"options"` // mongodb options + Database string `json:"database" yaml:"database" mapstructure:"database"` // database name + Username string `json:"username" yaml:"username" mapstructure:"username"` // 用户名 + Password string `json:"password" yaml:"password" mapstructure:"password"` // 密码 + AuthSource string `json:"auth-source" yaml:"auth-source" mapstructure:"auth-source"` // 验证数据库 + MinPoolSize uint64 `json:"min-pool-size" yaml:"min-pool-size" mapstructure:"min-pool-size"` // 最小连接池 + MaxPoolSize uint64 `json:"max-pool-size" yaml:"max-pool-size" mapstructure:"max-pool-size"` // 最大连接池 + SocketTimeoutMs int64 `json:"socket-timeout-ms" yaml:"socket-timeout-ms" mapstructure:"socket-timeout-ms"` // socket超时时间 + ConnectTimeoutMs int64 `json:"connect-timeout-ms" yaml:"connect-timeout-ms" mapstructure:"connect-timeout-ms"` // 连接超时时间 + IsZap bool `json:"is-zap" yaml:"is-zap" mapstructure:"is-zap"` // 是否开启zap日志 + Hosts []*MongoHost `json:"hosts" yaml:"hosts" mapstructure:"hosts"` // 主机列表 +} + +type MongoHost struct { + Host string `json:"host" yaml:"host" mapstructure:"host"` // ip地址 + Port string `json:"port" yaml:"port" mapstructure:"port"` // 端口 +} + +// Uri . +func (x *Mongo) Uri() string { + length := len(x.Hosts) + hosts := make([]string, 0, length) + for i := 0; i < length; i++ { + if x.Hosts[i].Host != "" && x.Hosts[i].Port != "" { + hosts = append(hosts, x.Hosts[i].Host+":"+x.Hosts[i].Port) + } + } + if x.Options != "" { + return fmt.Sprintf("mongodb://%s/%s?%s", strings.Join(hosts, ","), x.Database, x.Options) + } + return fmt.Sprintf("mongodb://%s/%s", strings.Join(hosts, ","), x.Database) +} diff --git a/admin/server/config/oa_login.go b/admin/server/config/oa_login.go new file mode 100644 index 000000000..037cb5abb --- /dev/null +++ b/admin/server/config/oa_login.go @@ -0,0 +1,9 @@ +package config + +type OaLogin struct { + Url string `mapstructure:"url" json:"url" yaml:"url"` + Oauth2ClientId string `mapstructure:"oauth2-client-id" json:"oauth2-client-id" yaml:"oauth2-client-id"` + Oauth2ClientSecret string `mapstructure:"oauth2-client-secret" json:"oauth2-client-secret" yaml:"oauth2-client-secret"` + GetUserApiPath string `mapstructure:"get-user-info-api-path" json:"get-user-info-api-path" yaml:"get-user-info-api-path"` + GetTokenByCodeApiPath string `mapstructure:"get-token-by-code-api-path" json:"get-token-by-code-api-path" yaml:"get-token-by-code-api-path"` +} diff --git a/admin/server/config/oss_aliyun.go b/admin/server/config/oss_aliyun.go new file mode 100644 index 000000000..934bd782a --- /dev/null +++ b/admin/server/config/oss_aliyun.go @@ -0,0 +1,10 @@ +package config + +type AliyunOSS struct { + Endpoint string `mapstructure:"endpoint" json:"endpoint" yaml:"endpoint"` + AccessKeyId string `mapstructure:"access-key-id" json:"access-key-id" yaml:"access-key-id"` + AccessKeySecret string `mapstructure:"access-key-secret" json:"access-key-secret" yaml:"access-key-secret"` + BucketName string `mapstructure:"bucket-name" json:"bucket-name" yaml:"bucket-name"` + BucketUrl string `mapstructure:"bucket-url" json:"bucket-url" yaml:"bucket-url"` + BasePath string `mapstructure:"base-path" json:"base-path" yaml:"base-path"` +} diff --git a/admin/server/config/oss_aws.go b/admin/server/config/oss_aws.go new file mode 100644 index 000000000..7ec6acc54 --- /dev/null +++ b/admin/server/config/oss_aws.go @@ -0,0 +1,13 @@ +package config + +type AwsS3 struct { + Bucket string `mapstructure:"bucket" json:"bucket" yaml:"bucket"` + Region string `mapstructure:"region" json:"region" yaml:"region"` + Endpoint string `mapstructure:"endpoint" json:"endpoint" yaml:"endpoint"` + SecretID string `mapstructure:"secret-id" json:"secret-id" yaml:"secret-id"` + SecretKey string `mapstructure:"secret-key" json:"secret-key" yaml:"secret-key"` + BaseURL string `mapstructure:"base-url" json:"base-url" yaml:"base-url"` + PathPrefix string `mapstructure:"path-prefix" json:"path-prefix" yaml:"path-prefix"` + S3ForcePathStyle bool `mapstructure:"s3-force-path-style" json:"s3-force-path-style" yaml:"s3-force-path-style"` + DisableSSL bool `mapstructure:"disable-ssl" json:"disable-ssl" yaml:"disable-ssl"` +} diff --git a/admin/server/config/oss_cloudflare.go b/admin/server/config/oss_cloudflare.go new file mode 100644 index 000000000..ab7a393dd --- /dev/null +++ b/admin/server/config/oss_cloudflare.go @@ -0,0 +1,10 @@ +package config + +type CloudflareR2 struct { + Bucket string `mapstructure:"bucket" json:"bucket" yaml:"bucket"` + BaseURL string `mapstructure:"base-url" json:"base-url" yaml:"base-url"` + Path string `mapstructure:"path" json:"path" yaml:"path"` + AccountID string `mapstructure:"account-id" json:"account-id" yaml:"account-id"` + AccessKeyID string `mapstructure:"access-key-id" json:"access-key-id" yaml:"access-key-id"` + SecretAccessKey string `mapstructure:"secret-access-key" json:"secret-access-key" yaml:"secret-access-key"` +} diff --git a/admin/server/config/oss_huawei.go b/admin/server/config/oss_huawei.go new file mode 100644 index 000000000..45dfbcdb0 --- /dev/null +++ b/admin/server/config/oss_huawei.go @@ -0,0 +1,9 @@ +package config + +type HuaWeiObs struct { + Path string `mapstructure:"path" json:"path" yaml:"path"` + Bucket string `mapstructure:"bucket" json:"bucket" yaml:"bucket"` + Endpoint string `mapstructure:"endpoint" json:"endpoint" yaml:"endpoint"` + AccessKey string `mapstructure:"access-key" json:"access-key" yaml:"access-key"` + SecretKey string `mapstructure:"secret-key" json:"secret-key" yaml:"secret-key"` +} diff --git a/admin/server/config/oss_local.go b/admin/server/config/oss_local.go new file mode 100644 index 000000000..7038d4ad9 --- /dev/null +++ b/admin/server/config/oss_local.go @@ -0,0 +1,6 @@ +package config + +type Local struct { + Path string `mapstructure:"path" json:"path" yaml:"path"` // 本地文件访问路径 + StorePath string `mapstructure:"store-path" json:"store-path" yaml:"store-path"` // 本地文件存储路径 +} diff --git a/admin/server/config/oss_minio.go b/admin/server/config/oss_minio.go new file mode 100644 index 000000000..a0faac74a --- /dev/null +++ b/admin/server/config/oss_minio.go @@ -0,0 +1,11 @@ +package config + +type Minio struct { + Endpoint string `mapstructure:"endpoint" json:"endpoint" yaml:"endpoint"` + AccessKeyId string `mapstructure:"access-key-id" json:"access-key-id" yaml:"access-key-id"` + AccessKeySecret string `mapstructure:"access-key-secret" json:"access-key-secret" yaml:"access-key-secret"` + BucketName string `mapstructure:"bucket-name" json:"bucket-name" yaml:"bucket-name"` + UseSSL bool `mapstructure:"use-ssl" json:"use-ssl" yaml:"use-ssl"` + BasePath string `mapstructure:"base-path" json:"base-path" yaml:"base-path"` + BucketUrl string `mapstructure:"bucket-url" json:"bucket-url" yaml:"bucket-url"` +} diff --git a/admin/server/config/oss_qiniu.go b/admin/server/config/oss_qiniu.go new file mode 100644 index 000000000..298fe2d3c --- /dev/null +++ b/admin/server/config/oss_qiniu.go @@ -0,0 +1,11 @@ +package config + +type Qiniu struct { + Zone string `mapstructure:"zone" json:"zone" yaml:"zone"` // 存储区域 + Bucket string `mapstructure:"bucket" json:"bucket" yaml:"bucket"` // 空间名称 + ImgPath string `mapstructure:"img-path" json:"img-path" yaml:"img-path"` // CDN加速域名 + AccessKey string `mapstructure:"access-key" json:"access-key" yaml:"access-key"` // 秘钥AK + SecretKey string `mapstructure:"secret-key" json:"secret-key" yaml:"secret-key"` // 秘钥SK + UseHTTPS bool `mapstructure:"use-https" json:"use-https" yaml:"use-https"` // 是否使用https + UseCdnDomains bool `mapstructure:"use-cdn-domains" json:"use-cdn-domains" yaml:"use-cdn-domains"` // 上传是否使用CDN上传加速 +} diff --git a/admin/server/config/oss_tencent.go b/admin/server/config/oss_tencent.go new file mode 100644 index 000000000..39a29d116 --- /dev/null +++ b/admin/server/config/oss_tencent.go @@ -0,0 +1,10 @@ +package config + +type TencentCOS struct { + Bucket string `mapstructure:"bucket" json:"bucket" yaml:"bucket"` + Region string `mapstructure:"region" json:"region" yaml:"region"` + SecretID string `mapstructure:"secret-id" json:"secret-id" yaml:"secret-id"` + SecretKey string `mapstructure:"secret-key" json:"secret-key" yaml:"secret-key"` + BaseURL string `mapstructure:"base-url" json:"base-url" yaml:"base-url"` + PathPrefix string `mapstructure:"path-prefix" json:"path-prefix" yaml:"path-prefix"` +} diff --git a/admin/server/config/redis.go b/admin/server/config/redis.go new file mode 100644 index 000000000..94b5bf6b5 --- /dev/null +++ b/admin/server/config/redis.go @@ -0,0 +1,10 @@ +package config + +type Redis struct { + Name string `mapstructure:"name" json:"name" yaml:"name"` // 代表当前实例的名字 + Addr string `mapstructure:"addr" json:"addr" yaml:"addr"` // 服务器地址:端口 + Password string `mapstructure:"password" json:"password" yaml:"password"` // 密码 + DB int `mapstructure:"db" json:"db" yaml:"db"` // 单实例模式下redis的哪个数据库 + UseCluster bool `mapstructure:"useCluster" json:"useCluster" yaml:"useCluster"` // 是否使用集群模式 + ClusterAddrs []string `mapstructure:"clusterAddrs" json:"clusterAddrs" yaml:"clusterAddrs"` // 集群模式下的节点地址列表 +} diff --git a/admin/server/config/system.go b/admin/server/config/system.go new file mode 100644 index 000000000..e30e12839 --- /dev/null +++ b/admin/server/config/system.go @@ -0,0 +1,18 @@ +package config + +type System struct { + DbType string `mapstructure:"db-type" json:"db-type" yaml:"db-type"` // 数据库类型:mysql(默认)|sqlite|sqlserver|postgresql + OssType string `mapstructure:"oss-type" json:"oss-type" yaml:"oss-type"` // Oss类型 + RouterPrefix string `mapstructure:"router-prefix" json:"router-prefix" yaml:"router-prefix"` + Addr int `mapstructure:"addr" json:"addr" yaml:"addr"` // 端口值 + LimitCountIP int `mapstructure:"iplimit-count" json:"iplimit-count" yaml:"iplimit-count"` + LimitTimeIP int `mapstructure:"iplimit-time" json:"iplimit-time" yaml:"iplimit-time"` + UseMultipoint bool `mapstructure:"use-multipoint" json:"use-multipoint" yaml:"use-multipoint"` // 多点登录拦截 + UseRedis bool `mapstructure:"use-redis" json:"use-redis" yaml:"use-redis"` // 使用redis + UseMongo bool `mapstructure:"use-mongo" json:"use-mongo" yaml:"use-mongo"` // 使用mongo + UseStrictAuth bool `mapstructure:"use-strict-auth" json:"use-strict-auth" yaml:"use-strict-auth"` // 使用树形角色分配模式 + // Extend: Start Custom Configuration + UserDefaultGroupID string `mapstructure:"user_default-group-id" default:"888" json:"user_default-group-id" yaml:"user_default-group-id"` // 用户默认群组id + DockerRun bool `mapstructure:"docker-run" default:false json:"docker-run" yaml:"docker-run"` // 是否在docker中运行,如果是的话,无需自动生成jwtkey,直接填sk-9f73s3ljTXVcMT3Blb3ljTqtsKiGHXVcMT3BlbkFJLK7U与dify保持一致 + // Extend: Stop Custom Configuration +} diff --git a/admin/server/config/zap.go b/admin/server/config/zap.go new file mode 100644 index 000000000..0e8ae2b29 --- /dev/null +++ b/admin/server/config/zap.go @@ -0,0 +1,71 @@ +package config + +import ( + "go.uber.org/zap/zapcore" + "time" +) + +type Zap struct { + Level string `mapstructure:"level" json:"level" yaml:"level"` // 级别 + Prefix string `mapstructure:"prefix" json:"prefix" yaml:"prefix"` // 日志前缀 + Format string `mapstructure:"format" json:"format" yaml:"format"` // 输出 + Director string `mapstructure:"director" json:"director" yaml:"director"` // 日志文件夹 + EncodeLevel string `mapstructure:"encode-level" json:"encode-level" yaml:"encode-level"` // 编码级 + StacktraceKey string `mapstructure:"stacktrace-key" json:"stacktrace-key" yaml:"stacktrace-key"` // 栈名 + ShowLine bool `mapstructure:"show-line" json:"show-line" yaml:"show-line"` // 显示行 + LogInConsole bool `mapstructure:"log-in-console" json:"log-in-console" yaml:"log-in-console"` // 输出控制台 + RetentionDay int `mapstructure:"retention-day" json:"retention-day" yaml:"retention-day"` // 日志保留天数 +} + +// Levels 根据字符串转化为 zapcore.Levels +func (c *Zap) Levels() []zapcore.Level { + levels := make([]zapcore.Level, 0, 7) + level, err := zapcore.ParseLevel(c.Level) + if err != nil { + level = zapcore.DebugLevel + } + for ; level <= zapcore.FatalLevel; level++ { + levels = append(levels, level) + } + return levels +} + +func (c *Zap) Encoder() zapcore.Encoder { + config := zapcore.EncoderConfig{ + TimeKey: "time", + NameKey: "name", + LevelKey: "level", + CallerKey: "caller", + MessageKey: "message", + StacktraceKey: c.StacktraceKey, + LineEnding: zapcore.DefaultLineEnding, + EncodeTime: func(t time.Time, encoder zapcore.PrimitiveArrayEncoder) { + encoder.AppendString(c.Prefix + t.Format("2006-01-02 15:04:05.000")) + }, + EncodeLevel: c.LevelEncoder(), + EncodeCaller: zapcore.FullCallerEncoder, + EncodeDuration: zapcore.SecondsDurationEncoder, + } + if c.Format == "json" { + return zapcore.NewJSONEncoder(config) + } + return zapcore.NewConsoleEncoder(config) + +} + +// LevelEncoder 根据 EncodeLevel 返回 zapcore.LevelEncoder +// Author [SliverHorn](https://github.com/SliverHorn) +func (c *Zap) LevelEncoder() zapcore.LevelEncoder { + switch { + case c.EncodeLevel == "LowercaseLevelEncoder": // 小写编码器(默认) + return zapcore.LowercaseLevelEncoder + case c.EncodeLevel == "LowercaseColorLevelEncoder": // 小写编码器带颜色 + return zapcore.LowercaseColorLevelEncoder + case c.EncodeLevel == "CapitalLevelEncoder": // 大写编码器 + return zapcore.CapitalLevelEncoder + case c.EncodeLevel == "CapitalColorLevelEncoder": // 大写编码器带颜色 + return zapcore.CapitalColorLevelEncoder + default: + return zapcore.LowercaseLevelEncoder + } +} diff --git a/admin/server/core/internal/constant.go b/admin/server/core/internal/constant.go new file mode 100644 index 000000000..b22362cc7 --- /dev/null +++ b/admin/server/core/internal/constant.go @@ -0,0 +1,9 @@ +package internal + +const ( + ConfigEnv = "GVA_CONFIG" + ConfigDefaultFile = "config.yaml" + ConfigTestFile = "config.test.yaml" + ConfigDebugFile = "config.debug.yaml" + ConfigReleaseFile = "config.release.yaml" +) diff --git a/admin/server/core/internal/cutter.go b/admin/server/core/internal/cutter.go new file mode 100644 index 000000000..e053af6e5 --- /dev/null +++ b/admin/server/core/internal/cutter.go @@ -0,0 +1,121 @@ +package internal + +import ( + "os" + "path/filepath" + "sync" + "time" +) + +// Cutter 实现 io.Writer 接口 +// 用于日志切割, strings.Join([]string{director,layout, formats..., level+".log"}, os.PathSeparator) +type Cutter struct { + level string // 日志级别(debug, info, warn, error, dpanic, panic, fatal) + layout string // 时间格式 2006-01-02 15:04:05 + formats []string // 自定义参数([]string{Director,"2006-01-02", "business"(此参数可不写), level+".log"} + director string // 日志文件夹 + retentionDay int //日志保留天数 + file *os.File // 文件句柄 + mutex *sync.RWMutex // 读写锁 +} + +type CutterOption func(*Cutter) + +// CutterWithLayout 时间格式 +func CutterWithLayout(layout string) CutterOption { + return func(c *Cutter) { + c.layout = layout + } +} + +// CutterWithFormats 格式化参数 +func CutterWithFormats(format ...string) CutterOption { + return func(c *Cutter) { + if len(format) > 0 { + c.formats = format + } + } +} + +func NewCutter(director string, level string, retentionDay int, options ...CutterOption) *Cutter { + rotate := &Cutter{ + level: level, + director: director, + retentionDay: retentionDay, + mutex: new(sync.RWMutex), + } + for i := 0; i < len(options); i++ { + options[i](rotate) + } + return rotate +} + +// Write satisfies the io.Writer interface. It writes to the +// appropriate file handle that is currently being used. +// If we have reached rotation time, the target file gets +// automatically rotated, and also purged if necessary. +func (c *Cutter) Write(bytes []byte) (n int, err error) { + c.mutex.Lock() + defer func() { + if c.file != nil { + _ = c.file.Close() + c.file = nil + } + c.mutex.Unlock() + }() + length := len(c.formats) + values := make([]string, 0, 3+length) + values = append(values, c.director) + if c.layout != "" { + values = append(values, time.Now().Format(c.layout)) + } + for i := 0; i < length; i++ { + values = append(values, c.formats[i]) + } + values = append(values, c.level+".log") + filename := filepath.Join(values...) + director := filepath.Dir(filename) + err = os.MkdirAll(director, os.ModePerm) + if err != nil { + return 0, err + } + err = removeNDaysFolders(c.director, c.retentionDay) + if err != nil { + return 0, err + } + c.file, err = os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + return 0, err + } + return c.file.Write(bytes) +} + +func (c *Cutter) Sync() error { + c.mutex.Lock() + defer c.mutex.Unlock() + + if c.file != nil { + return c.file.Sync() + } + return nil +} + +// 增加日志目录文件清理 小于等于零的值默认忽略不再处理 +func removeNDaysFolders(dir string, days int) error { + if days <= 0 { + return nil + } + cutoff := time.Now().AddDate(0, 0, -days) + return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() && info.ModTime().Before(cutoff) && path != dir { + err = os.RemoveAll(path) + if err != nil { + return err + } + } + return nil + }) +} diff --git a/admin/server/core/internal/zap_core.go b/admin/server/core/internal/zap_core.go new file mode 100644 index 000000000..4648e60cb --- /dev/null +++ b/admin/server/core/internal/zap_core.go @@ -0,0 +1,68 @@ +package internal + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "os" + "time" +) + +type ZapCore struct { + level zapcore.Level + zapcore.Core +} + +func NewZapCore(level zapcore.Level) *ZapCore { + entity := &ZapCore{level: level} + syncer := entity.WriteSyncer() + levelEnabler := zap.LevelEnablerFunc(func(l zapcore.Level) bool { + return l == level + }) + entity.Core = zapcore.NewCore(global.GVA_CONFIG.Zap.Encoder(), syncer, levelEnabler) + return entity +} + +func (z *ZapCore) WriteSyncer(formats ...string) zapcore.WriteSyncer { + cutter := NewCutter( + global.GVA_CONFIG.Zap.Director, + z.level.String(), + global.GVA_CONFIG.Zap.RetentionDay, + CutterWithLayout(time.DateOnly), + CutterWithFormats(formats...), + ) + if global.GVA_CONFIG.Zap.LogInConsole { + multiSyncer := zapcore.NewMultiWriteSyncer(os.Stdout, cutter) + return zapcore.AddSync(multiSyncer) + } + return zapcore.AddSync(cutter) +} + +func (z *ZapCore) Enabled(level zapcore.Level) bool { + return z.level == level +} + +func (z *ZapCore) With(fields []zapcore.Field) zapcore.Core { + return z.Core.With(fields) +} + +func (z *ZapCore) Check(entry zapcore.Entry, check *zapcore.CheckedEntry) *zapcore.CheckedEntry { + if z.Enabled(entry.Level) { + return check.AddCore(entry, z) + } + return check +} + +func (z *ZapCore) Write(entry zapcore.Entry, fields []zapcore.Field) error { + for i := 0; i < len(fields); i++ { + if fields[i].Key == "business" || fields[i].Key == "folder" || fields[i].Key == "directory" { + syncer := z.WriteSyncer(fields[i].String) + z.Core = zapcore.NewCore(global.GVA_CONFIG.Zap.Encoder(), syncer, z.level) + } + } + return z.Core.Write(entry, fields) +} + +func (z *ZapCore) Sync() error { + return z.Core.Sync() +} diff --git a/admin/server/core/server.go b/admin/server/core/server.go new file mode 100644 index 000000000..37778adc1 --- /dev/null +++ b/admin/server/core/server.go @@ -0,0 +1,44 @@ +package core + +import ( + "fmt" + cron "github.com/flipped-aurora/gin-vue-admin/server/corn" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/initialize" + "github.com/flipped-aurora/gin-vue-admin/server/service/system" + "go.uber.org/zap" +) + +type server interface { + ListenAndServe() error +} + +func RunWindowsServer() { + if global.GVA_CONFIG.System.UseMultipoint || global.GVA_CONFIG.System.UseRedis { + // 初始化redis服务 + initialize.Redis() + initialize.RedisList() + initialize.DifyRedis() // Extend: global code + } + + if global.GVA_CONFIG.System.UseMongo { + err := initialize.Mongo.Initialization() + if err != nil { + zap.L().Error(fmt.Sprintf("%+v", err)) + } + } + // 从db加载jwt数据 + if global.GVA_DB != nil { + system.LoadAll() + } + + Router := initialize.Routers() + cron.Corn() // Extend: corn job + address := fmt.Sprintf(":%d", global.GVA_CONFIG.System.Addr) + s := initServer(address, Router) + + global.GVA_LOG.Info("server run success on ", zap.String("address", address)) + + // + global.GVA_LOG.Error(s.ListenAndServe().Error()) +} diff --git a/admin/server/core/server_other.go b/admin/server/core/server_other.go new file mode 100644 index 000000000..83645fced --- /dev/null +++ b/admin/server/core/server_other.go @@ -0,0 +1,19 @@ +//go:build !windows +// +build !windows + +package core + +import ( + "time" + + "github.com/fvbock/endless" + "github.com/gin-gonic/gin" +) + +func initServer(address string, router *gin.Engine) server { + s := endless.NewServer(address, router) + s.ReadHeaderTimeout = 10 * time.Minute + s.WriteTimeout = 10 * time.Minute + s.MaxHeaderBytes = 1 << 20 + return s +} diff --git a/admin/server/core/server_win.go b/admin/server/core/server_win.go new file mode 100644 index 000000000..20cf44b9f --- /dev/null +++ b/admin/server/core/server_win.go @@ -0,0 +1,21 @@ +//go:build windows +// +build windows + +package core + +import ( + "net/http" + "time" + + "github.com/gin-gonic/gin" +) + +func initServer(address string, router *gin.Engine) server { + return &http.Server{ + Addr: address, + Handler: router, + ReadTimeout: 10 * time.Minute, + WriteTimeout: 10 * time.Minute, + MaxHeaderBytes: 1 << 20, + } +} diff --git a/admin/server/core/viper.go b/admin/server/core/viper.go new file mode 100644 index 000000000..cc119a3c9 --- /dev/null +++ b/admin/server/core/viper.go @@ -0,0 +1,72 @@ +package core + +import ( + "flag" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/core/internal" + "github.com/gin-gonic/gin" + "os" + "path/filepath" + + "github.com/fsnotify/fsnotify" + "github.com/spf13/viper" + + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +// Viper // +// 优先级: 命令行 > 环境变量 > 默认值 +// Author [SliverHorn](https://github.com/SliverHorn) +func Viper(path ...string) *viper.Viper { + var config string + + if len(path) == 0 { + flag.StringVar(&config, "c", "", "choose config file.") + flag.Parse() + if config == "" { // 判断命令行参数是否为空 + if configEnv := os.Getenv(internal.ConfigEnv); configEnv == "" { // 判断 internal.ConfigEnv 常量存储的环境变量是否为空 + switch gin.Mode() { + case gin.DebugMode: + config = internal.ConfigDefaultFile + case gin.ReleaseMode: + config = internal.ConfigReleaseFile + case gin.TestMode: + config = internal.ConfigTestFile + } + fmt.Printf("您正在使用gin模式的%s环境名称,config的路径为%s\n", gin.Mode(), config) + } else { // internal.ConfigEnv 常量存储的环境变量不为空 将值赋值于config + config = configEnv + fmt.Printf("您正在使用%s环境变量,config的路径为%s\n", internal.ConfigEnv, config) + } + } else { // 命令行参数不为空 将值赋值于config + fmt.Printf("您正在使用命令行的-c参数传递的值,config的路径为%s\n", config) + } + } else { // 函数传递的可变参数的第一个值赋值于config + config = path[0] + fmt.Printf("您正在使用func Viper()传递的值,config的路径为%s\n", config) + } + + v := viper.New() + v.SetConfigFile(config) + v.SetConfigType("yaml") + err := v.ReadInConfig() + if err != nil { + panic(fmt.Errorf("Fatal error config file: %s \n", err)) + } + v.WatchConfig() + + v.OnConfigChange(func(e fsnotify.Event) { + fmt.Println("config file changed:", e.Name) + if err = v.Unmarshal(&global.GVA_CONFIG); err != nil { + fmt.Println(err) + } + }) + if err = v.Unmarshal(&global.GVA_CONFIG); err != nil { + panic(err) + } + + // root 适配性 根据root位置去找到对应迁移位置,保证root路径有效 + global.GVA_CONFIG.AutoCode.Root, _ = filepath.Abs("..") + + return v +} diff --git a/admin/server/core/zap.go b/admin/server/core/zap.go new file mode 100644 index 000000000..d7e08a44a --- /dev/null +++ b/admin/server/core/zap.go @@ -0,0 +1,32 @@ +package core + +import ( + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/core/internal" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "os" +) + +// Zap 获取 zap.Logger +// Author [SliverHorn](https://github.com/SliverHorn) +func Zap() (logger *zap.Logger) { + if ok, _ := utils.PathExists(global.GVA_CONFIG.Zap.Director); !ok { // 判断是否有Director文件夹 + fmt.Printf("create %v directory\n", global.GVA_CONFIG.Zap.Director) + _ = os.Mkdir(global.GVA_CONFIG.Zap.Director, os.ModePerm) + } + levels := global.GVA_CONFIG.Zap.Levels() + length := len(levels) + cores := make([]zapcore.Core, 0, length) + for i := 0; i < length; i++ { + core := internal.NewZapCore(levels[i]) + cores = append(cores, core) + } + logger = zap.New(zapcore.NewTee(cores...)) + if global.GVA_CONFIG.Zap.ShowLine { + logger = logger.WithOptions(zap.AddCaller()) + } + return logger +} diff --git a/admin/server/corn/main.go b/admin/server/corn/main.go new file mode 100644 index 000000000..10d87d773 --- /dev/null +++ b/admin/server/corn/main.go @@ -0,0 +1,82 @@ +package cron + +import ( + "context" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + gaiaReq "github.com/flipped-aurora/gin-vue-admin/server/model/gaia/request" + "github.com/flipped-aurora/gin-vue-admin/server/service/gaia" + "github.com/flipped-aurora/gin-vue-admin/server/service/system" + "github.com/robfig/cron/v3" + "time" +) + +// 返回一个支持至 秒 级别的 cron +func newWithSeconds() *cron.Cron { + secondParser := cron.NewParser(cron.Second | cron.Minute | + cron.Hour | cron.Dom | cron.Month | cron.DowOptional | cron.Descriptor) + return cron.New(cron.WithParser(secondParser), cron.WithChain()) +} + +func Corn() { + var lock bool + c := newWithSeconds() + // 每分钟同步一次用户列表 + if _, err := c.AddFunc("0 */1 * * * *", func() { + if global.GVA_DB == nil { + global.GVA_LOG.Info("【定时任务-每1分钟执行1次】同步用户列表任务,数据库没有初始化,暂未开始同步") + return + } + + if lock { + return + } + lock = true + user := system.UserExtendService{} + user.SyncUser() + gaia.SyncUserStatus() + lock = false + }); err != nil { + global.GVA_LOG.Fatal("Start Cron Error:" + err.Error()) + time.Sleep(5) + return + } + global.GVA_LOG.Info("【定时任务-每1分钟执行1次】同步用户列表任务,已启动!") + + // 每10分钟同步一次【应用使用分析数据】 + if _, err := c.AddFunc("0 */10 * * * *", func() { + if global.GVA_DB == nil { + global.GVA_LOG.Info("【定时任务-每6分钟执行1次】同步应用使用分析数据任务,数据库没有初始化,暂未开始同步") + return + } + dashService := gaia.DashboardService{} + // 缓存前3页 + for i := 1; i <= 3; i++ { + req := gaiaReq.GetAppQuotaRankingDataReq{ + PageInfo: request.PageInfo{ + Page: i, + PageSize: 10, + }, + } + // 先删除缓存 + cacheKey := fmt.Sprintf("app_token_quota_ranking:%d:%d", i, 10) + global.GVA_REDIS.Del(context.Background(), cacheKey) + + // 再获取数据 + _, _, err := dashService.GetAppQuotaRankingData(req) + if err != nil { + global.GVA_LOG.Error("每10分钟同步一次应用使用分析 获取信息出错:" + err.Error()) + return + } + time.Sleep(time.Second * 10) + } + + }); err != nil { + global.GVA_LOG.Fatal("每10分钟同步一次应用使用分析 出错:" + err.Error()) + return + } + global.GVA_LOG.Info("【定时任务-每6分钟执行1次】同步应用使用分析数据任务,已启动!") + + c.Start() +} diff --git a/admin/server/docs/docs.go b/admin/server/docs/docs.go new file mode 100644 index 000000000..caf962a6b --- /dev/null +++ b/admin/server/docs/docs.go @@ -0,0 +1,8104 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/api/createApi": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "创建基础api", + "parameters": [ + { + "description": "api路径, api中文描述, api组, 方法", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysApi" + } + } + ], + "responses": { + "200": { + "description": "创建基础api", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/deleteApi": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "删除api", + "parameters": [ + { + "description": "ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysApi" + } + } + ], + "responses": { + "200": { + "description": "删除api", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/deleteApisByIds": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "删除选中Api", + "parameters": [ + { + "description": "ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.IdsReq" + } + } + ], + "responses": { + "200": { + "description": "删除选中Api", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/enterSyncApi": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "确认同步API", + "responses": { + "200": { + "description": "确认同步API", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/freshCasbin": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "刷新casbin缓存", + "responses": { + "200": { + "description": "刷新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/getAllApis": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "获取所有的Api 不分页", + "responses": { + "200": { + "description": "获取所有的Api 不分页,返回包括api列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysAPIListResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/getApiById": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "根据id获取api", + "parameters": [ + { + "description": "根据id获取api", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetById" + } + } + ], + "responses": { + "200": { + "description": "根据id获取api,返回包括api详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysAPIResponse" + } + } + } + ] + } + } + } + } + }, + "/api/getApiGroups": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "获取API分组", + "responses": { + "200": { + "description": "获取API分组", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/getApiList": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "分页获取API列表", + "parameters": [ + { + "description": "分页获取API列表", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.SearchApiParams" + } + } + ], + "responses": { + "200": { + "description": "分页获取API列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/ignoreApi": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "IgnoreApi" + ], + "summary": "忽略API", + "responses": { + "200": { + "description": "同步API", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/syncApi": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "同步API", + "responses": { + "200": { + "description": "同步API", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/updateApi": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "修改基础api", + "parameters": [ + { + "description": "api路径, api中文描述, api组, 方法", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysApi" + } + } + ], + "responses": { + "200": { + "description": "修改基础api", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/authority/copyAuthority": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authority" + ], + "summary": "拷贝角色", + "parameters": [ + { + "description": "旧角色id, 新权限id, 新权限名, 新父角色id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/response.SysAuthorityCopyResponse" + } + } + ], + "responses": { + "200": { + "description": "拷贝角色,返回包括系统角色详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysAuthorityResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/authority/createAuthority": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authority" + ], + "summary": "创建角色", + "parameters": [ + { + "description": "权限id, 权限名, 父角色id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysAuthority" + } + } + ], + "responses": { + "200": { + "description": "创建角色,返回包括系统角色详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysAuthorityResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/authority/deleteAuthority": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authority" + ], + "summary": "删除角色", + "parameters": [ + { + "description": "删除角色", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysAuthority" + } + } + ], + "responses": { + "200": { + "description": "删除角色", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/authority/getAuthorityList": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authority" + ], + "summary": "分页获取角色列表", + "parameters": [ + { + "description": "页码, 每页大小", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "分页获取角色列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/authority/setDataAuthority": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authority" + ], + "summary": "设置角色资源权限", + "parameters": [ + { + "description": "设置角色资源权限", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysAuthority" + } + } + ], + "responses": { + "200": { + "description": "设置角色资源权限", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/authority/updateAuthority": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authority" + ], + "summary": "更新角色信息", + "parameters": [ + { + "description": "权限id, 权限名, 父角色id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysAuthority" + } + } + ], + "responses": { + "200": { + "description": "更新角色信息,返回包括系统角色详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysAuthorityResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/authorityBtn/canRemoveAuthorityBtn": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AuthorityBtn" + ], + "summary": "设置权限按钮", + "responses": { + "200": { + "description": "删除成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/authorityBtn/getAuthorityBtn": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AuthorityBtn" + ], + "summary": "获取权限按钮", + "parameters": [ + { + "description": "菜单id, 角色id, 选中的按钮id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.SysAuthorityBtnReq" + } + } + ], + "responses": { + "200": { + "description": "返回列表成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysAuthorityBtnRes" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/authorityBtn/setAuthorityBtn": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AuthorityBtn" + ], + "summary": "设置权限按钮", + "parameters": [ + { + "description": "菜单id, 角色id, 选中的按钮id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.SysAuthorityBtnReq" + } + } + ], + "responses": { + "200": { + "description": "返回列表成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/addFunc": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AddFunc" + ], + "summary": "增加方法", + "parameters": [ + { + "description": "增加方法", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.AutoCode" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"创建成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/autoCode/createPackage": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCodePackage" + ], + "summary": "创建package", + "parameters": [ + { + "description": "创建package", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.SysAutoCodePackageCreate" + } + } + ], + "responses": { + "200": { + "description": "创建package成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/createTemp": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCodeTemplate" + ], + "summary": "自动代码模板", + "parameters": [ + { + "description": "创建自动代码", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.AutoCode" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"创建成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/autoCode/delPackage": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCode" + ], + "summary": "删除package", + "parameters": [ + { + "description": "创建package", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetById" + } + } + ], + "responses": { + "200": { + "description": "删除package成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/delSysHistory": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCode" + ], + "summary": "删除回滚记录", + "parameters": [ + { + "description": "请求参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetById" + } + } + ], + "responses": { + "200": { + "description": "删除回滚记录", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/getColumn": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCode" + ], + "summary": "获取当前表所有字段", + "responses": { + "200": { + "description": "获取当前表所有字段", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/getDatabase": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCode" + ], + "summary": "获取当前所有数据库", + "responses": { + "200": { + "description": "获取当前所有数据库", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/getMeta": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCode" + ], + "summary": "获取meta信息", + "parameters": [ + { + "description": "请求参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetById" + } + } + ], + "responses": { + "200": { + "description": "获取meta信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/getPackage": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCodePackage" + ], + "summary": "获取package", + "responses": { + "200": { + "description": "创建package成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/getSysHistory": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCode" + ], + "summary": "查询回滚记录", + "parameters": [ + { + "description": "请求参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "查询回滚记录,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/getTables": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCode" + ], + "summary": "获取当前数据库所有表", + "responses": { + "200": { + "description": "获取当前数据库所有表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/getTemplates": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCodePackage" + ], + "summary": "获取package", + "responses": { + "200": { + "description": "创建package成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/installPlugin": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCodePlugin" + ], + "summary": "安装插件", + "parameters": [ + { + "type": "file", + "description": "this is a test file", + "name": "plug", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "安装插件成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object" + } + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/preview": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCodeTemplate" + ], + "summary": "预览创建后的代码", + "parameters": [ + { + "description": "预览创建代码", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.AutoCode" + } + } + ], + "responses": { + "200": { + "description": "预览创建后的代码", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/pubPlug": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCodePlugin" + ], + "summary": "打包插件", + "parameters": [ + { + "type": "string", + "description": "插件名称", + "name": "plugName", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "打包插件成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/rollback": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCode" + ], + "summary": "回滚自动生成代码", + "parameters": [ + { + "description": "请求参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.SysAutoHistoryRollBack" + } + } + ], + "responses": { + "200": { + "description": "回滚自动生成代码", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/base/captcha": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Base" + ], + "summary": "生成验证码", + "responses": { + "200": { + "description": "生成验证码,返回包括随机数id,base64,验证码长度,是否开启验证码", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysCaptchaResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/base/login": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Base" + ], + "summary": "用户登录", + "parameters": [ + { + "description": "用户名, 密码, 验证码", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.Login" + } + } + ], + "responses": { + "200": { + "description": "返回包括用户信息,token,过期时间", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.LoginResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/casbin/UpdateCasbin": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Casbin" + ], + "summary": "更新角色api权限", + "parameters": [ + { + "description": "权限id, 权限模型列表", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.CasbinInReceive" + } + } + ], + "responses": { + "200": { + "description": "更新角色api权限", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/casbin/getPolicyPathByAuthorityId": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Casbin" + ], + "summary": "获取权限列表", + "parameters": [ + { + "description": "权限id, 权限模型列表", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.CasbinInReceive" + } + } + ], + "responses": { + "200": { + "description": "获取权限列表,返回包括casbin详情列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PolicyPathResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/customer/customer": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaCustomer" + ], + "summary": "获取单一客户信息", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "客户名", + "name": "customerName", + "in": "query" + }, + { + "type": "string", + "description": "客户手机号", + "name": "customerPhoneData", + "in": "query" + }, + { + "type": "integer", + "description": "管理角色ID", + "name": "sysUserAuthorityID", + "in": "query" + }, + { + "type": "integer", + "description": "管理ID", + "name": "sysUserId", + "in": "query" + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + } + ], + "responses": { + "200": { + "description": "获取单一客户信息,返回包括客户详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.ExaCustomerResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaCustomer" + ], + "summary": "更新客户信息", + "parameters": [ + { + "description": "客户ID, 客户信息", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/example.ExaCustomer" + } + } + ], + "responses": { + "200": { + "description": "更新客户信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaCustomer" + ], + "summary": "创建客户", + "parameters": [ + { + "description": "客户用户名, 客户手机号码", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/example.ExaCustomer" + } + } + ], + "responses": { + "200": { + "description": "创建客户", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + }, + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaCustomer" + ], + "summary": "删除客户", + "parameters": [ + { + "description": "客户ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/example.ExaCustomer" + } + } + ], + "responses": { + "200": { + "description": "删除客户", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/customer/customerList": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaCustomer" + ], + "summary": "分页获取权限客户列表", + "parameters": [ + { + "type": "string", + "description": "关键字", + "name": "keyword", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页大小", + "name": "pageSize", + "in": "query" + } + ], + "responses": { + "200": { + "description": "分页获取权限客户列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/email/emailTest": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "System" + ], + "summary": "发送测试邮件", + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"发送成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/email/sendEmail": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "System" + ], + "summary": "发送邮件", + "parameters": [ + { + "description": "发送邮件必须的参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/response.Email" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"发送成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/fileUploadAndDownload/breakpointContinue": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaFileUploadAndDownload" + ], + "summary": "断点续传到服务器", + "parameters": [ + { + "type": "file", + "description": "an example for breakpoint resume, 断点续传示例", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "断点续传到服务器", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/fileUploadAndDownload/deleteFile": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaFileUploadAndDownload" + ], + "summary": "删除文件", + "parameters": [ + { + "description": "传入文件里面id即可", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/example.ExaFileUploadAndDownload" + } + } + ], + "responses": { + "200": { + "description": "删除文件", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/fileUploadAndDownload/findFile": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaFileUploadAndDownload" + ], + "summary": "创建文件", + "parameters": [ + { + "type": "file", + "description": "上传文件完成", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "创建文件,返回包括文件路径", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.FilePathResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/fileUploadAndDownload/getFileList": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaFileUploadAndDownload" + ], + "summary": "分页文件列表", + "parameters": [ + { + "description": "页码, 每页大小", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "分页文件列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/fileUploadAndDownload/removeChunk": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaFileUploadAndDownload" + ], + "summary": "删除切片", + "parameters": [ + { + "type": "file", + "description": "删除缓存切片", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "删除切片", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/fileUploadAndDownload/upload": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaFileUploadAndDownload" + ], + "summary": "上传文件示例", + "parameters": [ + { + "type": "file", + "description": "上传文件示例", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "上传文件示例,返回包括文件详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.ExaFileResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/info/createInfo": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Info" + ], + "summary": "创建公告", + "parameters": [ + { + "description": "创建公告", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/model.Info" + } + } + ], + "responses": { + "200": { + "description": "创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/info/deleteInfo": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Info" + ], + "summary": "删除公告", + "parameters": [ + { + "description": "删除公告", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/model.Info" + } + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/info/deleteInfoByIds": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Info" + ], + "summary": "批量删除公告", + "responses": { + "200": { + "description": "批量删除成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/info/findInfo": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Info" + ], + "summary": "用id查询公告", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "内容", + "name": "content", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "标题", + "name": "title", + "in": "query" + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + }, + { + "type": "integer", + "description": "作者", + "name": "userID", + "in": "query" + } + ], + "responses": { + "200": { + "description": "查询成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.Info" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/info/getInfoDataSource": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Info" + ], + "summary": "获取Info的数据源", + "responses": { + "200": { + "description": "查询成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/info/getInfoList": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Info" + ], + "summary": "分页获取公告列表", + "parameters": [ + { + "type": "string", + "name": "endCreatedAt", + "in": "query" + }, + { + "type": "string", + "description": "关键字", + "name": "keyword", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页大小", + "name": "pageSize", + "in": "query" + }, + { + "type": "string", + "name": "startCreatedAt", + "in": "query" + } + ], + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/info/getInfoPublic": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Info" + ], + "summary": "不需要鉴权的公告接口", + "parameters": [ + { + "type": "string", + "name": "endCreatedAt", + "in": "query" + }, + { + "type": "string", + "description": "关键字", + "name": "keyword", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页大小", + "name": "pageSize", + "in": "query" + }, + { + "type": "string", + "name": "startCreatedAt", + "in": "query" + } + ], + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/info/updateInfo": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Info" + ], + "summary": "更新公告", + "parameters": [ + { + "description": "更新公告", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/model.Info" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/init/checkdb": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "CheckDB" + ], + "summary": "初始化用户数据库", + "responses": { + "200": { + "description": "初始化用户数据库", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/init/initdb": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "InitDB" + ], + "summary": "初始化用户数据库", + "parameters": [ + { + "description": "初始化数据库参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.InitDB" + } + } + ], + "responses": { + "200": { + "description": "初始化用户数据库", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/jwt/jsonInBlacklist": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Jwt" + ], + "summary": "jwt加入黑名单", + "responses": { + "200": { + "description": "jwt加入黑名单", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/addBaseMenu": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Menu" + ], + "summary": "新增菜单", + "parameters": [ + { + "description": "路由path, 父菜单ID, 路由name, 对应前端文件路径, 排序标记", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysBaseMenu" + } + } + ], + "responses": { + "200": { + "description": "新增菜单", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/addMenuAuthority": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AuthorityMenu" + ], + "summary": "增加menu和角色关联关系", + "parameters": [ + { + "description": "角色ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.AddMenuAuthorityInfo" + } + } + ], + "responses": { + "200": { + "description": "增加menu和角色关联关系", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/deleteBaseMenu": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Menu" + ], + "summary": "删除菜单", + "parameters": [ + { + "description": "菜单id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetById" + } + } + ], + "responses": { + "200": { + "description": "删除菜单", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/getBaseMenuById": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Menu" + ], + "summary": "根据id获取菜单", + "parameters": [ + { + "description": "菜单id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetById" + } + } + ], + "responses": { + "200": { + "description": "根据id获取菜单,返回包括系统菜单列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysBaseMenuResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/getBaseMenuTree": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "AuthorityMenu" + ], + "summary": "获取用户动态路由", + "parameters": [ + { + "description": "空", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.Empty" + } + } + ], + "responses": { + "200": { + "description": "获取用户动态路由,返回包括系统菜单列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysBaseMenusResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/getMenu": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "AuthorityMenu" + ], + "summary": "获取用户动态路由", + "parameters": [ + { + "description": "空", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.Empty" + } + } + ], + "responses": { + "200": { + "description": "获取用户动态路由,返回包括系统菜单详情列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysMenusResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/getMenuAuthority": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AuthorityMenu" + ], + "summary": "获取指定角色menu", + "parameters": [ + { + "description": "角色ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetAuthorityId" + } + } + ], + "responses": { + "200": { + "description": "获取指定角色menu", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/getMenuList": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Menu" + ], + "summary": "分页获取基础menu列表", + "parameters": [ + { + "description": "页码, 每页大小", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "分页获取基础menu列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/updateBaseMenu": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Menu" + ], + "summary": "更新菜单", + "parameters": [ + { + "description": "路由path, 父菜单ID, 路由name, 对应前端文件路径, 排序标记", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysBaseMenu" + } + } + ], + "responses": { + "200": { + "description": "更新菜单", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionary/createSysDictionary": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionary" + ], + "summary": "创建SysDictionary", + "parameters": [ + { + "description": "SysDictionary模型", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysDictionary" + } + } + ], + "responses": { + "200": { + "description": "创建SysDictionary", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionary/deleteSysDictionary": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionary" + ], + "summary": "删除SysDictionary", + "parameters": [ + { + "description": "SysDictionary模型", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysDictionary" + } + } + ], + "responses": { + "200": { + "description": "删除SysDictionary", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionary/findSysDictionary": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionary" + ], + "summary": "用id查询SysDictionary", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "描述", + "name": "desc", + "in": "query" + }, + { + "type": "string", + "description": "字典名(中)", + "name": "name", + "in": "query" + }, + { + "type": "boolean", + "description": "状态", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "字典名(英)", + "name": "type", + "in": "query" + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + } + ], + "responses": { + "200": { + "description": "用id查询SysDictionary", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionary/getSysDictionaryList": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionary" + ], + "summary": "分页获取SysDictionary列表", + "responses": { + "200": { + "description": "分页获取SysDictionary列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionary/updateSysDictionary": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionary" + ], + "summary": "更新SysDictionary", + "parameters": [ + { + "description": "SysDictionary模型", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysDictionary" + } + } + ], + "responses": { + "200": { + "description": "更新SysDictionary", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionaryDetail/createSysDictionaryDetail": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionaryDetail" + ], + "summary": "创建SysDictionaryDetail", + "parameters": [ + { + "description": "SysDictionaryDetail模型", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysDictionaryDetail" + } + } + ], + "responses": { + "200": { + "description": "创建SysDictionaryDetail", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionaryDetail/deleteSysDictionaryDetail": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionaryDetail" + ], + "summary": "删除SysDictionaryDetail", + "parameters": [ + { + "description": "SysDictionaryDetail模型", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysDictionaryDetail" + } + } + ], + "responses": { + "200": { + "description": "删除SysDictionaryDetail", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionaryDetail/findSysDictionaryDetail": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionaryDetail" + ], + "summary": "用id查询SysDictionaryDetail", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "扩展值", + "name": "extend", + "in": "query" + }, + { + "type": "string", + "description": "展示值", + "name": "label", + "in": "query" + }, + { + "type": "integer", + "description": "排序标记", + "name": "sort", + "in": "query" + }, + { + "type": "boolean", + "description": "启用状态", + "name": "status", + "in": "query" + }, + { + "type": "integer", + "description": "关联标记", + "name": "sysDictionaryID", + "in": "query" + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + }, + { + "type": "string", + "description": "字典值", + "name": "value", + "in": "query" + } + ], + "responses": { + "200": { + "description": "用id查询SysDictionaryDetail", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionaryDetail/getSysDictionaryDetailList": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionaryDetail" + ], + "summary": "分页获取SysDictionaryDetail列表", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "扩展值", + "name": "extend", + "in": "query" + }, + { + "type": "string", + "description": "关键字", + "name": "keyword", + "in": "query" + }, + { + "type": "string", + "description": "展示值", + "name": "label", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页大小", + "name": "pageSize", + "in": "query" + }, + { + "type": "integer", + "description": "排序标记", + "name": "sort", + "in": "query" + }, + { + "type": "boolean", + "description": "启用状态", + "name": "status", + "in": "query" + }, + { + "type": "integer", + "description": "关联标记", + "name": "sysDictionaryID", + "in": "query" + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + }, + { + "type": "string", + "description": "字典值", + "name": "value", + "in": "query" + } + ], + "responses": { + "200": { + "description": "分页获取SysDictionaryDetail列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionaryDetail/updateSysDictionaryDetail": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionaryDetail" + ], + "summary": "更新SysDictionaryDetail", + "parameters": [ + { + "description": "更新SysDictionaryDetail", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysDictionaryDetail" + } + } + ], + "responses": { + "200": { + "description": "更新SysDictionaryDetail", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysExportTemplate/createSysExportTemplate": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysExportTemplate" + ], + "summary": "创建导出模板", + "parameters": [ + { + "description": "创建导出模板", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysExportTemplate" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"创建成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/sysExportTemplate/deleteSysExportTemplate": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysExportTemplate" + ], + "summary": "删除导出模板", + "parameters": [ + { + "description": "删除导出模板", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysExportTemplate" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"删除成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/sysExportTemplate/deleteSysExportTemplateByIds": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysExportTemplate" + ], + "summary": "批量删除导出模板", + "parameters": [ + { + "description": "批量删除导出模板", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.IdsReq" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"批量删除成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/sysExportTemplate/exportExcel": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysExportTemplate" + ], + "summary": "导出表格模板", + "responses": {} + } + }, + "/sysExportTemplate/findSysExportTemplate": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysExportTemplate" + ], + "summary": "用id查询导出模板", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "数据库名称", + "name": "dbName", + "in": "query" + }, + { + "type": "integer", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "模板名称", + "name": "name", + "in": "query" + }, + { + "type": "string", + "name": "order", + "in": "query" + }, + { + "type": "string", + "description": "表名称", + "name": "tableName", + "in": "query" + }, + { + "type": "string", + "description": "模板标识", + "name": "templateID", + "in": "query" + }, + { + "type": "string", + "description": "模板信息", + "name": "templateInfo", + "in": "query" + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"查询成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/sysExportTemplate/getSysExportTemplateList": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysExportTemplate" + ], + "summary": "分页获取导出模板列表", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "数据库名称", + "name": "dbName", + "in": "query" + }, + { + "type": "string", + "name": "endCreatedAt", + "in": "query" + }, + { + "type": "string", + "description": "关键字", + "name": "keyword", + "in": "query" + }, + { + "type": "integer", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "模板名称", + "name": "name", + "in": "query" + }, + { + "type": "string", + "name": "order", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页大小", + "name": "pageSize", + "in": "query" + }, + { + "type": "string", + "name": "startCreatedAt", + "in": "query" + }, + { + "type": "string", + "description": "表名称", + "name": "tableName", + "in": "query" + }, + { + "type": "string", + "description": "模板标识", + "name": "templateID", + "in": "query" + }, + { + "type": "string", + "description": "模板信息", + "name": "templateInfo", + "in": "query" + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"获取成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/sysExportTemplate/importExcel": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysImportTemplate" + ], + "summary": "导入表格", + "responses": {} + } + }, + "/sysExportTemplate/updateSysExportTemplate": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysExportTemplate" + ], + "summary": "更新导出模板", + "parameters": [ + { + "description": "更新导出模板", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysExportTemplate" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"更新成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/sysOperationRecord/createSysOperationRecord": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysOperationRecord" + ], + "summary": "创建SysOperationRecord", + "parameters": [ + { + "description": "创建SysOperationRecord", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysOperationRecord" + } + } + ], + "responses": { + "200": { + "description": "创建SysOperationRecord", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysOperationRecord/deleteSysOperationRecord": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysOperationRecord" + ], + "summary": "删除SysOperationRecord", + "parameters": [ + { + "description": "SysOperationRecord模型", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysOperationRecord" + } + } + ], + "responses": { + "200": { + "description": "删除SysOperationRecord", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysOperationRecord/deleteSysOperationRecordByIds": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysOperationRecord" + ], + "summary": "批量删除SysOperationRecord", + "parameters": [ + { + "description": "批量删除SysOperationRecord", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.IdsReq" + } + } + ], + "responses": { + "200": { + "description": "批量删除SysOperationRecord", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysOperationRecord/findSysOperationRecord": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysOperationRecord" + ], + "summary": "用id查询SysOperationRecord", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "代理", + "name": "agent", + "in": "query" + }, + { + "type": "string", + "description": "请求Body", + "name": "body", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "错误信息", + "name": "error_message", + "in": "query" + }, + { + "type": "string", + "description": "请求ip", + "name": "ip", + "in": "query" + }, + { + "type": "string", + "description": "延迟", + "name": "latency", + "in": "query" + }, + { + "type": "string", + "description": "请求方法", + "name": "method", + "in": "query" + }, + { + "type": "string", + "description": "请求路径", + "name": "path", + "in": "query" + }, + { + "type": "string", + "description": "响应Body", + "name": "resp", + "in": "query" + }, + { + "type": "integer", + "description": "请求状态", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + }, + { + "type": "integer", + "description": "用户id", + "name": "user_id", + "in": "query" + } + ], + "responses": { + "200": { + "description": "用id查询SysOperationRecord", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysOperationRecord/getSysOperationRecordList": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysOperationRecord" + ], + "summary": "分页获取SysOperationRecord列表", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "代理", + "name": "agent", + "in": "query" + }, + { + "type": "string", + "description": "请求Body", + "name": "body", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "错误信息", + "name": "error_message", + "in": "query" + }, + { + "type": "string", + "description": "请求ip", + "name": "ip", + "in": "query" + }, + { + "type": "string", + "description": "关键字", + "name": "keyword", + "in": "query" + }, + { + "type": "string", + "description": "延迟", + "name": "latency", + "in": "query" + }, + { + "type": "string", + "description": "请求方法", + "name": "method", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页大小", + "name": "pageSize", + "in": "query" + }, + { + "type": "string", + "description": "请求路径", + "name": "path", + "in": "query" + }, + { + "type": "string", + "description": "响应Body", + "name": "resp", + "in": "query" + }, + { + "type": "integer", + "description": "请求状态", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + }, + { + "type": "integer", + "description": "用户id", + "name": "user_id", + "in": "query" + } + ], + "responses": { + "200": { + "description": "分页获取SysOperationRecord列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/system/getServerInfo": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "System" + ], + "summary": "获取服务器信息", + "responses": { + "200": { + "description": "获取服务器信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/system/getSystemConfig": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "System" + ], + "summary": "获取配置文件内容", + "responses": { + "200": { + "description": "获取配置文件内容,返回包括系统配置", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysConfigResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/system/reloadSystem": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "System" + ], + "summary": "重启系统", + "responses": { + "200": { + "description": "重启系统", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/system/setSystemConfig": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "System" + ], + "summary": "设置配置文件内容", + "parameters": [ + { + "description": "设置配置文件内容", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.System" + } + } + ], + "responses": { + "200": { + "description": "设置配置文件内容", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/SetSelfInfo": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "设置用户信息", + "parameters": [ + { + "description": "ID, 用户名, 昵称, 头像链接", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysUser" + } + } + ], + "responses": { + "200": { + "description": "设置用户信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/admin_register": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "用户注册账号", + "parameters": [ + { + "description": "用户名, 昵称, 密码, 角色ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.Register" + } + } + ], + "responses": { + "200": { + "description": "用户注册账号,返回包括用户信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysUserResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/changePassword": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "用户修改密码", + "parameters": [ + { + "description": "用户名, 原密码, 新密码", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.ChangePasswordReq" + } + } + ], + "responses": { + "200": { + "description": "用户修改密码", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/deleteUser": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "删除用户", + "parameters": [ + { + "description": "用户ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetById" + } + } + ], + "responses": { + "200": { + "description": "删除用户", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/getUserInfo": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "获取用户信息", + "responses": { + "200": { + "description": "获取用户信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/getUserList": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "分页获取用户列表", + "parameters": [ + { + "description": "页码, 每页大小", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "分页获取用户列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/resetPassword": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "重置用户密码", + "parameters": [ + { + "description": "ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysUser" + } + } + ], + "responses": { + "200": { + "description": "重置用户密码", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/setUserAuthorities": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "设置用户权限", + "parameters": [ + { + "description": "用户UUID, 角色ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.SetUserAuthorities" + } + } + ], + "responses": { + "200": { + "description": "设置用户权限", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/setUserAuthority": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "更改用户权限", + "parameters": [ + { + "description": "用户UUID, 角色ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.SetUserAuth" + } + } + ], + "responses": { + "200": { + "description": "设置用户权限", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/setUserInfo": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "设置用户信息", + "parameters": [ + { + "description": "ID, 用户名, 昵称, 头像链接", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysUser" + } + } + ], + "responses": { + "200": { + "description": "设置用户信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + } + }, + "definitions": { + "config.AliyunOSS": { + "type": "object", + "properties": { + "access-key-id": { + "type": "string" + }, + "access-key-secret": { + "type": "string" + }, + "base-path": { + "type": "string" + }, + "bucket-name": { + "type": "string" + }, + "bucket-url": { + "type": "string" + }, + "endpoint": { + "type": "string" + } + } + }, + "config.Autocode": { + "type": "object", + "properties": { + "ai-path": { + "type": "string" + }, + "module": { + "type": "string" + }, + "root": { + "type": "string" + }, + "server": { + "type": "string" + }, + "web": { + "type": "string" + } + } + }, + "config.AwsS3": { + "type": "object", + "properties": { + "base-url": { + "type": "string" + }, + "bucket": { + "type": "string" + }, + "disable-ssl": { + "type": "boolean" + }, + "endpoint": { + "type": "string" + }, + "path-prefix": { + "type": "string" + }, + "region": { + "type": "string" + }, + "s3-force-path-style": { + "type": "boolean" + }, + "secret-id": { + "type": "string" + }, + "secret-key": { + "type": "string" + } + } + }, + "config.CORS": { + "type": "object", + "properties": { + "mode": { + "type": "string" + }, + "whitelist": { + "type": "array", + "items": { + "$ref": "#/definitions/config.CORSWhitelist" + } + } + } + }, + "config.CORSWhitelist": { + "type": "object", + "properties": { + "allow-credentials": { + "type": "boolean" + }, + "allow-headers": { + "type": "string" + }, + "allow-methods": { + "type": "string" + }, + "allow-origin": { + "type": "string" + }, + "expose-headers": { + "type": "string" + } + } + }, + "config.Captcha": { + "type": "object", + "properties": { + "img-height": { + "description": "验证码高度", + "type": "integer" + }, + "img-width": { + "description": "验证码宽度", + "type": "integer" + }, + "key-long": { + "description": "验证码长度", + "type": "integer" + }, + "open-captcha": { + "description": "防爆破验证码开启此数,0代表每次登录都需要验证码,其他数字代表错误密码此数,如3代表错误三次后出现验证码", + "type": "integer" + }, + "open-captcha-timeout": { + "description": "防爆破验证码超时时间,单位:s(秒)", + "type": "integer" + } + } + }, + "config.CloudflareR2": { + "type": "object", + "properties": { + "access-key-id": { + "type": "string" + }, + "account-id": { + "type": "string" + }, + "base-url": { + "type": "string" + }, + "bucket": { + "type": "string" + }, + "path": { + "type": "string" + }, + "secret-access-key": { + "type": "string" + } + } + }, + "config.DiskList": { + "type": "object", + "properties": { + "mount-point": { + "type": "string" + } + } + }, + "config.Excel": { + "type": "object", + "properties": { + "dir": { + "type": "string" + } + } + }, + "config.HuaWeiObs": { + "type": "object", + "properties": { + "access-key": { + "type": "string" + }, + "bucket": { + "type": "string" + }, + "endpoint": { + "type": "string" + }, + "path": { + "type": "string" + }, + "secret-key": { + "type": "string" + } + } + }, + "config.JWT": { + "type": "object", + "properties": { + "buffer-time": { + "description": "缓冲时间", + "type": "string" + }, + "expires-time": { + "description": "过期时间", + "type": "string" + }, + "issuer": { + "description": "签发者", + "type": "string" + }, + "signing-key": { + "description": "jwt签名", + "type": "string" + } + } + }, + "config.Local": { + "type": "object", + "properties": { + "path": { + "description": "本地文件访问路径", + "type": "string" + }, + "store-path": { + "description": "本地文件存储路径", + "type": "string" + } + } + }, + "config.Mongo": { + "type": "object", + "properties": { + "auth-source": { + "description": "验证数据库", + "type": "string" + }, + "coll": { + "description": "collection name", + "type": "string" + }, + "connect-timeout-ms": { + "description": "连接超时时间", + "type": "integer" + }, + "database": { + "description": "database name", + "type": "string" + }, + "hosts": { + "description": "主机列表", + "type": "array", + "items": { + "$ref": "#/definitions/config.MongoHost" + } + }, + "is-zap": { + "description": "是否开启zap日志", + "type": "boolean" + }, + "max-pool-size": { + "description": "最大连接池", + "type": "integer" + }, + "min-pool-size": { + "description": "最小连接池", + "type": "integer" + }, + "options": { + "description": "mongodb options", + "type": "string" + }, + "password": { + "description": "密码", + "type": "string" + }, + "socket-timeout-ms": { + "description": "socket超时时间", + "type": "integer" + }, + "username": { + "description": "用户名", + "type": "string" + } + } + }, + "config.MongoHost": { + "type": "object", + "properties": { + "host": { + "description": "ip地址", + "type": "string" + }, + "port": { + "description": "端口", + "type": "string" + } + } + }, + "config.Mssql": { + "type": "object", + "properties": { + "config": { + "description": "高级配置", + "type": "string" + }, + "db-name": { + "description": "数据库名", + "type": "string" + }, + "engine": { + "description": "数据库引擎,默认InnoDB", + "type": "string", + "default": "InnoDB" + }, + "log-mode": { + "description": "是否开启Gorm全局日志", + "type": "string" + }, + "log-zap": { + "description": "是否通过zap写入日志文件", + "type": "boolean" + }, + "max-idle-conns": { + "description": "空闲中的最大连接数", + "type": "integer" + }, + "max-open-conns": { + "description": "打开到数据库的最大连接数", + "type": "integer" + }, + "password": { + "description": "数据库密码", + "type": "string" + }, + "path": { + "description": "数据库地址", + "type": "string" + }, + "port": { + "description": "数据库端口", + "type": "string" + }, + "prefix": { + "description": "数据库前缀", + "type": "string" + }, + "singular": { + "description": "是否开启全局禁用复数,true表示开启", + "type": "boolean" + }, + "username": { + "description": "数据库账号", + "type": "string" + } + } + }, + "config.Mysql": { + "type": "object", + "properties": { + "config": { + "description": "高级配置", + "type": "string" + }, + "db-name": { + "description": "数据库名", + "type": "string" + }, + "engine": { + "description": "数据库引擎,默认InnoDB", + "type": "string", + "default": "InnoDB" + }, + "log-mode": { + "description": "是否开启Gorm全局日志", + "type": "string" + }, + "log-zap": { + "description": "是否通过zap写入日志文件", + "type": "boolean" + }, + "max-idle-conns": { + "description": "空闲中的最大连接数", + "type": "integer" + }, + "max-open-conns": { + "description": "打开到数据库的最大连接数", + "type": "integer" + }, + "password": { + "description": "数据库密码", + "type": "string" + }, + "path": { + "description": "数据库地址", + "type": "string" + }, + "port": { + "description": "数据库端口", + "type": "string" + }, + "prefix": { + "description": "数据库前缀", + "type": "string" + }, + "singular": { + "description": "是否开启全局禁用复数,true表示开启", + "type": "boolean" + }, + "username": { + "description": "数据库账号", + "type": "string" + } + } + }, + "config.Oracle": { + "type": "object", + "properties": { + "config": { + "description": "高级配置", + "type": "string" + }, + "db-name": { + "description": "数据库名", + "type": "string" + }, + "engine": { + "description": "数据库引擎,默认InnoDB", + "type": "string", + "default": "InnoDB" + }, + "log-mode": { + "description": "是否开启Gorm全局日志", + "type": "string" + }, + "log-zap": { + "description": "是否通过zap写入日志文件", + "type": "boolean" + }, + "max-idle-conns": { + "description": "空闲中的最大连接数", + "type": "integer" + }, + "max-open-conns": { + "description": "打开到数据库的最大连接数", + "type": "integer" + }, + "password": { + "description": "数据库密码", + "type": "string" + }, + "path": { + "description": "数据库地址", + "type": "string" + }, + "port": { + "description": "数据库端口", + "type": "string" + }, + "prefix": { + "description": "数据库前缀", + "type": "string" + }, + "singular": { + "description": "是否开启全局禁用复数,true表示开启", + "type": "boolean" + }, + "username": { + "description": "数据库账号", + "type": "string" + } + } + }, + "config.Pgsql": { + "type": "object", + "properties": { + "config": { + "description": "高级配置", + "type": "string" + }, + "db-name": { + "description": "数据库名", + "type": "string" + }, + "engine": { + "description": "数据库引擎,默认InnoDB", + "type": "string", + "default": "InnoDB" + }, + "log-mode": { + "description": "是否开启Gorm全局日志", + "type": "string" + }, + "log-zap": { + "description": "是否通过zap写入日志文件", + "type": "boolean" + }, + "max-idle-conns": { + "description": "空闲中的最大连接数", + "type": "integer" + }, + "max-open-conns": { + "description": "打开到数据库的最大连接数", + "type": "integer" + }, + "password": { + "description": "数据库密码", + "type": "string" + }, + "path": { + "description": "数据库地址", + "type": "string" + }, + "port": { + "description": "数据库端口", + "type": "string" + }, + "prefix": { + "description": "数据库前缀", + "type": "string" + }, + "singular": { + "description": "是否开启全局禁用复数,true表示开启", + "type": "boolean" + }, + "username": { + "description": "数据库账号", + "type": "string" + } + } + }, + "config.Qiniu": { + "type": "object", + "properties": { + "access-key": { + "description": "秘钥AK", + "type": "string" + }, + "bucket": { + "description": "空间名称", + "type": "string" + }, + "img-path": { + "description": "CDN加速域名", + "type": "string" + }, + "secret-key": { + "description": "秘钥SK", + "type": "string" + }, + "use-cdn-domains": { + "description": "上传是否使用CDN上传加速", + "type": "boolean" + }, + "use-https": { + "description": "是否使用https", + "type": "boolean" + }, + "zone": { + "description": "存储区域", + "type": "string" + } + } + }, + "config.Redis": { + "type": "object", + "properties": { + "addr": { + "description": "服务器地址:端口", + "type": "string" + }, + "clusterAddrs": { + "description": "集群模式下的节点地址列表", + "type": "array", + "items": { + "type": "string" + } + }, + "db": { + "description": "单实例模式下redis的哪个数据库", + "type": "integer" + }, + "password": { + "description": "密码", + "type": "string" + }, + "useCluster": { + "description": "是否使用集群模式", + "type": "boolean" + } + } + }, + "config.Server": { + "type": "object", + "properties": { + "aliyun-oss": { + "$ref": "#/definitions/config.AliyunOSS" + }, + "autocode": { + "description": "auto", + "allOf": [ + { + "$ref": "#/definitions/config.Autocode" + } + ] + }, + "aws-s3": { + "$ref": "#/definitions/config.AwsS3" + }, + "captcha": { + "$ref": "#/definitions/config.Captcha" + }, + "cloudflare-r2": { + "$ref": "#/definitions/config.CloudflareR2" + }, + "cors": { + "description": "跨域配置", + "allOf": [ + { + "$ref": "#/definitions/config.CORS" + } + ] + }, + "db-list": { + "type": "array", + "items": { + "$ref": "#/definitions/config.SpecializedDB" + } + }, + "disk-list": { + "type": "array", + "items": { + "$ref": "#/definitions/config.DiskList" + } + }, + "email": { + "$ref": "#/definitions/github_com_flipped-aurora_gin-vue-admin_server_config.Email" + }, + "excel": { + "$ref": "#/definitions/config.Excel" + }, + "hua-wei-obs": { + "$ref": "#/definitions/config.HuaWeiObs" + }, + "jwt": { + "$ref": "#/definitions/config.JWT" + }, + "local": { + "description": "oss", + "allOf": [ + { + "$ref": "#/definitions/config.Local" + } + ] + }, + "mongo": { + "$ref": "#/definitions/config.Mongo" + }, + "mssql": { + "$ref": "#/definitions/config.Mssql" + }, + "mysql": { + "description": "gorm", + "allOf": [ + { + "$ref": "#/definitions/config.Mysql" + } + ] + }, + "oracle": { + "$ref": "#/definitions/config.Oracle" + }, + "pgsql": { + "$ref": "#/definitions/config.Pgsql" + }, + "qiniu": { + "$ref": "#/definitions/config.Qiniu" + }, + "redis": { + "$ref": "#/definitions/config.Redis" + }, + "sqlite": { + "$ref": "#/definitions/config.Sqlite" + }, + "system": { + "$ref": "#/definitions/config.System" + }, + "tencent-cos": { + "$ref": "#/definitions/config.TencentCOS" + }, + "zap": { + "$ref": "#/definitions/config.Zap" + } + } + }, + "config.SpecializedDB": { + "type": "object", + "properties": { + "alias-name": { + "type": "string" + }, + "config": { + "description": "高级配置", + "type": "string" + }, + "db-name": { + "description": "数据库名", + "type": "string" + }, + "disable": { + "type": "boolean" + }, + "engine": { + "description": "数据库引擎,默认InnoDB", + "type": "string", + "default": "InnoDB" + }, + "log-mode": { + "description": "是否开启Gorm全局日志", + "type": "string" + }, + "log-zap": { + "description": "是否通过zap写入日志文件", + "type": "boolean" + }, + "max-idle-conns": { + "description": "空闲中的最大连接数", + "type": "integer" + }, + "max-open-conns": { + "description": "打开到数据库的最大连接数", + "type": "integer" + }, + "password": { + "description": "数据库密码", + "type": "string" + }, + "path": { + "description": "数据库地址", + "type": "string" + }, + "port": { + "description": "数据库端口", + "type": "string" + }, + "prefix": { + "description": "数据库前缀", + "type": "string" + }, + "singular": { + "description": "是否开启全局禁用复数,true表示开启", + "type": "boolean" + }, + "type": { + "type": "string" + }, + "username": { + "description": "数据库账号", + "type": "string" + } + } + }, + "config.Sqlite": { + "type": "object", + "properties": { + "config": { + "description": "高级配置", + "type": "string" + }, + "db-name": { + "description": "数据库名", + "type": "string" + }, + "engine": { + "description": "数据库引擎,默认InnoDB", + "type": "string", + "default": "InnoDB" + }, + "log-mode": { + "description": "是否开启Gorm全局日志", + "type": "string" + }, + "log-zap": { + "description": "是否通过zap写入日志文件", + "type": "boolean" + }, + "max-idle-conns": { + "description": "空闲中的最大连接数", + "type": "integer" + }, + "max-open-conns": { + "description": "打开到数据库的最大连接数", + "type": "integer" + }, + "password": { + "description": "数据库密码", + "type": "string" + }, + "path": { + "description": "数据库地址", + "type": "string" + }, + "port": { + "description": "数据库端口", + "type": "string" + }, + "prefix": { + "description": "数据库前缀", + "type": "string" + }, + "singular": { + "description": "是否开启全局禁用复数,true表示开启", + "type": "boolean" + }, + "username": { + "description": "数据库账号", + "type": "string" + } + } + }, + "config.System": { + "type": "object", + "properties": { + "addr": { + "description": "端口值", + "type": "integer" + }, + "db-type": { + "description": "数据库类型:mysql(默认)|sqlite|sqlserver|postgresql", + "type": "string" + }, + "iplimit-count": { + "type": "integer" + }, + "iplimit-time": { + "type": "integer" + }, + "oss-type": { + "description": "Oss类型", + "type": "string" + }, + "router-prefix": { + "type": "string" + }, + "use-mongo": { + "description": "使用mongo", + "type": "boolean" + }, + "use-multipoint": { + "description": "多点登录拦截", + "type": "boolean" + }, + "use-redis": { + "description": "使用redis", + "type": "boolean" + } + } + }, + "config.TencentCOS": { + "type": "object", + "properties": { + "base-url": { + "type": "string" + }, + "bucket": { + "type": "string" + }, + "path-prefix": { + "type": "string" + }, + "region": { + "type": "string" + }, + "secret-id": { + "type": "string" + }, + "secret-key": { + "type": "string" + } + } + }, + "config.Zap": { + "type": "object", + "properties": { + "director": { + "description": "日志文件夹", + "type": "string" + }, + "encode-level": { + "description": "编码级", + "type": "string" + }, + "format": { + "description": "输出", + "type": "string" + }, + "level": { + "description": "级别", + "type": "string" + }, + "log-in-console": { + "description": "输出控制台", + "type": "boolean" + }, + "prefix": { + "description": "日志前缀", + "type": "string" + }, + "retention-day": { + "description": "日志保留天数", + "type": "integer" + }, + "show-line": { + "description": "显示行", + "type": "boolean" + }, + "stacktrace-key": { + "description": "栈名", + "type": "string" + } + } + }, + "example.ExaCustomer": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "customerName": { + "description": "客户名", + "type": "string" + }, + "customerPhoneData": { + "description": "客户手机号", + "type": "string" + }, + "sysUser": { + "description": "管理详情", + "allOf": [ + { + "$ref": "#/definitions/system.SysUser" + } + ] + }, + "sysUserAuthorityID": { + "description": "管理角色ID", + "type": "integer" + }, + "sysUserId": { + "description": "管理ID", + "type": "integer" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "example.ExaFile": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "chunkTotal": { + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "exaFileChunk": { + "type": "array", + "items": { + "$ref": "#/definitions/example.ExaFileChunk" + } + }, + "fileMd5": { + "type": "string" + }, + "fileName": { + "type": "string" + }, + "filePath": { + "type": "string" + }, + "isFinish": { + "type": "boolean" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "example.ExaFileChunk": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "exaFileID": { + "type": "integer" + }, + "fileChunkNumber": { + "type": "integer" + }, + "fileChunkPath": { + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "example.ExaFileUploadAndDownload": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "key": { + "description": "编号", + "type": "string" + }, + "name": { + "description": "文件名", + "type": "string" + }, + "tag": { + "description": "文件标签", + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + }, + "url": { + "description": "文件地址", + "type": "string" + } + } + }, + "github_com_flipped-aurora_gin-vue-admin_server_config.Email": { + "type": "object", + "properties": { + "from": { + "description": "发件人 你自己要发邮件的邮箱", + "type": "string" + }, + "host": { + "description": "服务器地址 例如 smtp.qq.com 请前往QQ或者你要发邮件的邮箱查看其smtp协议", + "type": "string" + }, + "is-ssl": { + "description": "是否SSL 是否开启SSL", + "type": "boolean" + }, + "nickname": { + "description": "昵称 发件人昵称 通常为自己的邮箱", + "type": "string" + }, + "port": { + "description": "端口 请前往QQ或者你要发邮件的邮箱查看其smtp协议 大多为 465", + "type": "integer" + }, + "secret": { + "description": "密钥 用于登录的密钥 最好不要用邮箱密码 去邮箱smtp申请一个用于登录的密钥", + "type": "string" + }, + "to": { + "description": "收件人:多个以英文逗号分隔 例:a@qq.com b@qq.com 正式开发中请把此项目作为参数使用", + "type": "string" + } + } + }, + "model.Info": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "attachments": { + "description": "附件", + "type": "array", + "items": { + "type": "object" + } + }, + "content": { + "description": "内容", + "type": "string" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "title": { + "description": "标题", + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + }, + "userID": { + "description": "作者", + "type": "integer" + } + } + }, + "request.AddMenuAuthorityInfo": { + "type": "object", + "properties": { + "authorityId": { + "description": "角色ID", + "type": "integer" + }, + "menus": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysBaseMenu" + } + } + } + }, + "request.AutoCode": { + "type": "object" + }, + "request.CasbinInReceive": { + "type": "object", + "properties": { + "authorityId": { + "description": "权限id", + "type": "integer" + }, + "casbinInfos": { + "type": "array", + "items": { + "$ref": "#/definitions/request.CasbinInfo" + } + } + } + }, + "request.CasbinInfo": { + "type": "object", + "properties": { + "method": { + "description": "方法", + "type": "string" + }, + "path": { + "description": "路径", + "type": "string" + } + } + }, + "request.ChangePasswordReq": { + "type": "object", + "properties": { + "newPassword": { + "description": "新密码", + "type": "string" + }, + "password": { + "description": "密码", + "type": "string" + } + } + }, + "request.Empty": { + "type": "object" + }, + "request.GetAuthorityId": { + "type": "object", + "properties": { + "authorityId": { + "description": "角色ID", + "type": "integer" + } + } + }, + "request.GetById": { + "type": "object", + "properties": { + "id": { + "description": "主键ID", + "type": "integer" + } + } + }, + "request.IdsReq": { + "type": "object", + "properties": { + "ids": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "request.InitDB": { + "type": "object", + "required": [ + "adminPassword", + "dbName" + ], + "properties": { + "adminPassword": { + "type": "string" + }, + "dbName": { + "description": "数据库名", + "type": "string" + }, + "dbPath": { + "description": "sqlite数据库文件路径", + "type": "string" + }, + "dbType": { + "description": "数据库类型", + "type": "string" + }, + "host": { + "description": "服务器地址", + "type": "string" + }, + "password": { + "description": "数据库密码", + "type": "string" + }, + "port": { + "description": "数据库连接端口", + "type": "string" + }, + "userName": { + "description": "数据库用户名", + "type": "string" + } + } + }, + "request.Login": { + "type": "object", + "properties": { + "captcha": { + "description": "验证码", + "type": "string" + }, + "captchaId": { + "description": "验证码ID", + "type": "string" + }, + "password": { + "description": "密码", + "type": "string" + }, + "username": { + "description": "用户名", + "type": "string" + } + } + }, + "request.PageInfo": { + "type": "object", + "properties": { + "keyword": { + "description": "关键字", + "type": "string" + }, + "page": { + "description": "页码", + "type": "integer" + }, + "pageSize": { + "description": "每页大小", + "type": "integer" + } + } + }, + "request.Register": { + "type": "object", + "properties": { + "authorityId": { + "type": "string", + "example": "int 角色id" + }, + "authorityIds": { + "type": "string", + "example": "[]uint 角色id" + }, + "email": { + "type": "string", + "example": "电子邮箱" + }, + "enable": { + "type": "string", + "example": "int 是否启用" + }, + "headerImg": { + "type": "string", + "example": "头像链接" + }, + "nickName": { + "type": "string", + "example": "昵称" + }, + "passWord": { + "type": "string", + "example": "密码" + }, + "phone": { + "type": "string", + "example": "电话号码" + }, + "userName": { + "type": "string", + "example": "用户名" + } + } + }, + "request.SearchApiParams": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "apiGroup": { + "description": "api组", + "type": "string" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "desc": { + "description": "排序方式:升序false(默认)|降序true", + "type": "boolean" + }, + "description": { + "description": "api中文描述", + "type": "string" + }, + "keyword": { + "description": "关键字", + "type": "string" + }, + "method": { + "description": "方法:创建POST(默认)|查看GET|更新PUT|删除DELETE", + "type": "string" + }, + "orderKey": { + "description": "排序", + "type": "string" + }, + "page": { + "description": "页码", + "type": "integer" + }, + "pageSize": { + "description": "每页大小", + "type": "integer" + }, + "path": { + "description": "api路径", + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "request.SetUserAuth": { + "type": "object", + "properties": { + "authorityId": { + "description": "角色ID", + "type": "integer" + } + } + }, + "request.SetUserAuthorities": { + "type": "object", + "properties": { + "authorityIds": { + "description": "角色ID", + "type": "array", + "items": { + "type": "integer" + } + }, + "id": { + "type": "integer" + } + } + }, + "request.SysAuthorityBtnReq": { + "type": "object", + "properties": { + "authorityId": { + "type": "integer" + }, + "menuID": { + "type": "integer" + }, + "selected": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "request.SysAutoCodePackageCreate": { + "type": "object", + "properties": { + "desc": { + "type": "string", + "example": "描述" + }, + "label": { + "type": "string", + "example": "展示名" + }, + "packageName": { + "type": "string", + "example": "包名" + }, + "template": { + "type": "string", + "example": "模版" + } + } + }, + "request.SysAutoHistoryRollBack": { + "type": "object", + "properties": { + "deleteApi": { + "description": "是否删除接口", + "type": "boolean" + }, + "deleteMenu": { + "description": "是否删除菜单", + "type": "boolean" + }, + "deleteTable": { + "description": "是否删除表", + "type": "boolean" + }, + "id": { + "description": "主键ID", + "type": "integer" + } + } + }, + "response.Email": { + "type": "object", + "properties": { + "body": { + "description": "邮件内容", + "type": "string" + }, + "subject": { + "description": "邮件标题", + "type": "string" + }, + "to": { + "description": "邮件发送给谁", + "type": "string" + } + } + }, + "response.ExaCustomerResponse": { + "type": "object", + "properties": { + "customer": { + "$ref": "#/definitions/example.ExaCustomer" + } + } + }, + "response.ExaFileResponse": { + "type": "object", + "properties": { + "file": { + "$ref": "#/definitions/example.ExaFileUploadAndDownload" + } + } + }, + "response.FilePathResponse": { + "type": "object", + "properties": { + "filePath": { + "type": "string" + } + } + }, + "response.FileResponse": { + "type": "object", + "properties": { + "file": { + "$ref": "#/definitions/example.ExaFile" + } + } + }, + "response.LoginResponse": { + "type": "object", + "properties": { + "expiresAt": { + "type": "integer" + }, + "token": { + "type": "string" + }, + "user": { + "$ref": "#/definitions/system.SysUser" + } + } + }, + "response.PageResult": { + "type": "object", + "properties": { + "list": {}, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "response.PolicyPathResponse": { + "type": "object", + "properties": { + "paths": { + "type": "array", + "items": { + "$ref": "#/definitions/request.CasbinInfo" + } + } + } + }, + "response.Response": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": {}, + "msg": { + "type": "string" + } + } + }, + "response.SysAPIListResponse": { + "type": "object", + "properties": { + "apis": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysApi" + } + } + } + }, + "response.SysAPIResponse": { + "type": "object", + "properties": { + "api": { + "$ref": "#/definitions/system.SysApi" + } + } + }, + "response.SysAuthorityBtnRes": { + "type": "object", + "properties": { + "selected": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "response.SysAuthorityCopyResponse": { + "type": "object", + "properties": { + "authority": { + "$ref": "#/definitions/system.SysAuthority" + }, + "oldAuthorityId": { + "description": "旧角色ID", + "type": "integer" + } + } + }, + "response.SysAuthorityResponse": { + "type": "object", + "properties": { + "authority": { + "$ref": "#/definitions/system.SysAuthority" + } + } + }, + "response.SysBaseMenuResponse": { + "type": "object", + "properties": { + "menu": { + "$ref": "#/definitions/system.SysBaseMenu" + } + } + }, + "response.SysBaseMenusResponse": { + "type": "object", + "properties": { + "menus": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysBaseMenu" + } + } + } + }, + "response.SysCaptchaResponse": { + "type": "object", + "properties": { + "captchaId": { + "type": "string" + }, + "captchaLength": { + "type": "integer" + }, + "openCaptcha": { + "type": "boolean" + }, + "picPath": { + "type": "string" + } + } + }, + "response.SysConfigResponse": { + "type": "object", + "properties": { + "config": { + "$ref": "#/definitions/config.Server" + } + } + }, + "response.SysMenusResponse": { + "type": "object", + "properties": { + "menus": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysMenu" + } + } + } + }, + "response.SysUserResponse": { + "type": "object", + "properties": { + "user": { + "$ref": "#/definitions/system.SysUser" + } + } + }, + "system.Condition": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "column": { + "type": "string" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "from": { + "type": "string" + }, + "operator": { + "type": "string" + }, + "templateID": { + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "system.JoinTemplate": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "joins": { + "type": "string" + }, + "on": { + "type": "string" + }, + "table": { + "type": "string" + }, + "templateID": { + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "system.Meta": { + "type": "object", + "properties": { + "activeName": { + "type": "string" + }, + "closeTab": { + "description": "自动关闭tab", + "type": "boolean" + }, + "defaultMenu": { + "description": "是否是基础路由(开发中)", + "type": "boolean" + }, + "icon": { + "description": "菜单图标", + "type": "string" + }, + "keepAlive": { + "description": "是否缓存", + "type": "boolean" + }, + "title": { + "description": "菜单名", + "type": "string" + } + } + }, + "system.SysApi": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "apiGroup": { + "description": "api组", + "type": "string" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "description": { + "description": "api中文描述", + "type": "string" + }, + "method": { + "description": "方法:创建POST(默认)|查看GET|更新PUT|删除DELETE", + "type": "string" + }, + "path": { + "description": "api路径", + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "system.SysAuthority": { + "type": "object", + "properties": { + "authorityId": { + "description": "角色ID", + "type": "integer" + }, + "authorityName": { + "description": "角色名", + "type": "string" + }, + "children": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysAuthority" + } + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "dataAuthorityId": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysAuthority" + } + }, + "defaultRouter": { + "description": "默认菜单(默认dashboard)", + "type": "string" + }, + "deletedAt": { + "type": "string" + }, + "menus": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysBaseMenu" + } + }, + "parentId": { + "description": "父角色ID", + "type": "integer" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "system.SysBaseMenu": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "authoritys": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysAuthority" + } + }, + "children": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysBaseMenu" + } + }, + "component": { + "description": "对应前端文件路径", + "type": "string" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "hidden": { + "description": "是否在列表隐藏", + "type": "boolean" + }, + "menuBtn": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysBaseMenuBtn" + } + }, + "meta": { + "description": "附加属性", + "allOf": [ + { + "$ref": "#/definitions/system.Meta" + } + ] + }, + "name": { + "description": "路由name", + "type": "string" + }, + "parameters": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysBaseMenuParameter" + } + }, + "parentId": { + "description": "父菜单ID", + "type": "integer" + }, + "path": { + "description": "路由path", + "type": "string" + }, + "sort": { + "description": "排序标记", + "type": "integer" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "system.SysBaseMenuBtn": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "desc": { + "type": "string" + }, + "name": { + "type": "string" + }, + "sysBaseMenuID": { + "type": "integer" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "system.SysBaseMenuParameter": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "key": { + "description": "地址栏携带参数的key", + "type": "string" + }, + "sysBaseMenuID": { + "type": "integer" + }, + "type": { + "description": "地址栏携带参数为params还是query", + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + }, + "value": { + "description": "地址栏携带参数的值", + "type": "string" + } + } + }, + "system.SysDictionary": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "desc": { + "description": "描述", + "type": "string" + }, + "name": { + "description": "字典名(中)", + "type": "string" + }, + "status": { + "description": "状态", + "type": "boolean" + }, + "sysDictionaryDetails": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysDictionaryDetail" + } + }, + "type": { + "description": "字典名(英)", + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "system.SysDictionaryDetail": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "extend": { + "description": "扩展值", + "type": "string" + }, + "label": { + "description": "展示值", + "type": "string" + }, + "sort": { + "description": "排序标记", + "type": "integer" + }, + "status": { + "description": "启用状态", + "type": "boolean" + }, + "sysDictionaryID": { + "description": "关联标记", + "type": "integer" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + }, + "value": { + "description": "字典值", + "type": "string" + } + } + }, + "system.SysExportTemplate": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "conditions": { + "type": "array", + "items": { + "$ref": "#/definitions/system.Condition" + } + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "dbName": { + "description": "数据库名称", + "type": "string" + }, + "joinTemplate": { + "type": "array", + "items": { + "$ref": "#/definitions/system.JoinTemplate" + } + }, + "limit": { + "type": "integer" + }, + "name": { + "description": "模板名称", + "type": "string" + }, + "order": { + "type": "string" + }, + "tableName": { + "description": "表名称", + "type": "string" + }, + "templateID": { + "description": "模板标识", + "type": "string" + }, + "templateInfo": { + "description": "模板信息", + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "system.SysMenu": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "authoritys": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysAuthority" + } + }, + "btns": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "children": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysMenu" + } + }, + "component": { + "description": "对应前端文件路径", + "type": "string" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "hidden": { + "description": "是否在列表隐藏", + "type": "boolean" + }, + "menuBtn": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysBaseMenuBtn" + } + }, + "menuId": { + "type": "integer" + }, + "meta": { + "description": "附加属性", + "allOf": [ + { + "$ref": "#/definitions/system.Meta" + } + ] + }, + "name": { + "description": "路由name", + "type": "string" + }, + "parameters": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysBaseMenuParameter" + } + }, + "parentId": { + "description": "父菜单ID", + "type": "integer" + }, + "path": { + "description": "路由path", + "type": "string" + }, + "sort": { + "description": "排序标记", + "type": "integer" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "system.SysOperationRecord": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "agent": { + "description": "代理", + "type": "string" + }, + "body": { + "description": "请求Body", + "type": "string" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "error_message": { + "description": "错误信息", + "type": "string" + }, + "ip": { + "description": "请求ip", + "type": "string" + }, + "latency": { + "description": "延迟", + "type": "string" + }, + "method": { + "description": "请求方法", + "type": "string" + }, + "path": { + "description": "请求路径", + "type": "string" + }, + "resp": { + "description": "响应Body", + "type": "string" + }, + "status": { + "description": "请求状态", + "type": "integer" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + }, + "user": { + "$ref": "#/definitions/system.SysUser" + }, + "user_id": { + "description": "用户id", + "type": "integer" + } + } + }, + "system.SysUser": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "authorities": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysAuthority" + } + }, + "authority": { + "$ref": "#/definitions/system.SysAuthority" + }, + "authorityId": { + "description": "用户角色ID", + "type": "integer" + }, + "baseColor": { + "description": "基础颜色", + "type": "string" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "email": { + "description": "用户邮箱", + "type": "string" + }, + "enable": { + "description": "用户是否被冻结 1正常 2冻结", + "type": "integer" + }, + "headerImg": { + "description": "用户头像", + "type": "string" + }, + "nickName": { + "description": "用户昵称", + "type": "string" + }, + "phone": { + "description": "用户手机号", + "type": "string" + }, + "sideMode": { + "description": "用户侧边主题", + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + }, + "userName": { + "description": "用户登录名", + "type": "string" + }, + "uuid": { + "description": "用户UUID", + "type": "string" + } + } + }, + "system.System": { + "type": "object", + "properties": { + "config": { + "$ref": "#/definitions/config.Server" + } + } + } + }, + "securityDefinitions": { + "ApiKeyAuth": { + "type": "apiKey", + "name": "x-token", + "in": "header" + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "v2.7.7", + Host: "", + BasePath: "", + Schemes: []string{}, + Title: "Gin-Vue-Admin Swagger API接口文档", + Description: "使用gin+vue进行极速开发的全栈开发基础平台", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/admin/server/docs/swagger.json b/admin/server/docs/swagger.json new file mode 100644 index 000000000..80aa541c9 --- /dev/null +++ b/admin/server/docs/swagger.json @@ -0,0 +1,8078 @@ +{ + "swagger": "2.0", + "info": { + "description": "使用gin+vue进行极速开发的全栈开发基础平台", + "title": "Gin-Vue-Admin Swagger API接口文档", + "contact": {}, + "version": "v2.7.7" + }, + "paths": { + "/api/createApi": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "创建基础api", + "parameters": [ + { + "description": "api路径, api中文描述, api组, 方法", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysApi" + } + } + ], + "responses": { + "200": { + "description": "创建基础api", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/deleteApi": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "删除api", + "parameters": [ + { + "description": "ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysApi" + } + } + ], + "responses": { + "200": { + "description": "删除api", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/deleteApisByIds": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "删除选中Api", + "parameters": [ + { + "description": "ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.IdsReq" + } + } + ], + "responses": { + "200": { + "description": "删除选中Api", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/enterSyncApi": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "确认同步API", + "responses": { + "200": { + "description": "确认同步API", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/freshCasbin": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "刷新casbin缓存", + "responses": { + "200": { + "description": "刷新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/getAllApis": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "获取所有的Api 不分页", + "responses": { + "200": { + "description": "获取所有的Api 不分页,返回包括api列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysAPIListResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/getApiById": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "根据id获取api", + "parameters": [ + { + "description": "根据id获取api", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetById" + } + } + ], + "responses": { + "200": { + "description": "根据id获取api,返回包括api详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysAPIResponse" + } + } + } + ] + } + } + } + } + }, + "/api/getApiGroups": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "获取API分组", + "responses": { + "200": { + "description": "获取API分组", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/getApiList": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "分页获取API列表", + "parameters": [ + { + "description": "分页获取API列表", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.SearchApiParams" + } + } + ], + "responses": { + "200": { + "description": "分页获取API列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/ignoreApi": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "IgnoreApi" + ], + "summary": "忽略API", + "responses": { + "200": { + "description": "同步API", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/syncApi": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "同步API", + "responses": { + "200": { + "description": "同步API", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/updateApi": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "修改基础api", + "parameters": [ + { + "description": "api路径, api中文描述, api组, 方法", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysApi" + } + } + ], + "responses": { + "200": { + "description": "修改基础api", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/authority/copyAuthority": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authority" + ], + "summary": "拷贝角色", + "parameters": [ + { + "description": "旧角色id, 新权限id, 新权限名, 新父角色id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/response.SysAuthorityCopyResponse" + } + } + ], + "responses": { + "200": { + "description": "拷贝角色,返回包括系统角色详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysAuthorityResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/authority/createAuthority": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authority" + ], + "summary": "创建角色", + "parameters": [ + { + "description": "权限id, 权限名, 父角色id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysAuthority" + } + } + ], + "responses": { + "200": { + "description": "创建角色,返回包括系统角色详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysAuthorityResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/authority/deleteAuthority": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authority" + ], + "summary": "删除角色", + "parameters": [ + { + "description": "删除角色", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysAuthority" + } + } + ], + "responses": { + "200": { + "description": "删除角色", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/authority/getAuthorityList": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authority" + ], + "summary": "分页获取角色列表", + "parameters": [ + { + "description": "页码, 每页大小", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "分页获取角色列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/authority/setDataAuthority": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authority" + ], + "summary": "设置角色资源权限", + "parameters": [ + { + "description": "设置角色资源权限", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysAuthority" + } + } + ], + "responses": { + "200": { + "description": "设置角色资源权限", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/authority/updateAuthority": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authority" + ], + "summary": "更新角色信息", + "parameters": [ + { + "description": "权限id, 权限名, 父角色id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysAuthority" + } + } + ], + "responses": { + "200": { + "description": "更新角色信息,返回包括系统角色详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysAuthorityResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/authorityBtn/canRemoveAuthorityBtn": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AuthorityBtn" + ], + "summary": "设置权限按钮", + "responses": { + "200": { + "description": "删除成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/authorityBtn/getAuthorityBtn": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AuthorityBtn" + ], + "summary": "获取权限按钮", + "parameters": [ + { + "description": "菜单id, 角色id, 选中的按钮id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.SysAuthorityBtnReq" + } + } + ], + "responses": { + "200": { + "description": "返回列表成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysAuthorityBtnRes" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/authorityBtn/setAuthorityBtn": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AuthorityBtn" + ], + "summary": "设置权限按钮", + "parameters": [ + { + "description": "菜单id, 角色id, 选中的按钮id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.SysAuthorityBtnReq" + } + } + ], + "responses": { + "200": { + "description": "返回列表成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/addFunc": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AddFunc" + ], + "summary": "增加方法", + "parameters": [ + { + "description": "增加方法", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.AutoCode" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"创建成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/autoCode/createPackage": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCodePackage" + ], + "summary": "创建package", + "parameters": [ + { + "description": "创建package", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.SysAutoCodePackageCreate" + } + } + ], + "responses": { + "200": { + "description": "创建package成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/createTemp": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCodeTemplate" + ], + "summary": "自动代码模板", + "parameters": [ + { + "description": "创建自动代码", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.AutoCode" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"创建成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/autoCode/delPackage": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCode" + ], + "summary": "删除package", + "parameters": [ + { + "description": "创建package", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetById" + } + } + ], + "responses": { + "200": { + "description": "删除package成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/delSysHistory": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCode" + ], + "summary": "删除回滚记录", + "parameters": [ + { + "description": "请求参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetById" + } + } + ], + "responses": { + "200": { + "description": "删除回滚记录", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/getColumn": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCode" + ], + "summary": "获取当前表所有字段", + "responses": { + "200": { + "description": "获取当前表所有字段", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/getDatabase": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCode" + ], + "summary": "获取当前所有数据库", + "responses": { + "200": { + "description": "获取当前所有数据库", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/getMeta": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCode" + ], + "summary": "获取meta信息", + "parameters": [ + { + "description": "请求参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetById" + } + } + ], + "responses": { + "200": { + "description": "获取meta信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/getPackage": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCodePackage" + ], + "summary": "获取package", + "responses": { + "200": { + "description": "创建package成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/getSysHistory": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCode" + ], + "summary": "查询回滚记录", + "parameters": [ + { + "description": "请求参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "查询回滚记录,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/getTables": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCode" + ], + "summary": "获取当前数据库所有表", + "responses": { + "200": { + "description": "获取当前数据库所有表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/getTemplates": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCodePackage" + ], + "summary": "获取package", + "responses": { + "200": { + "description": "创建package成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/installPlugin": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCodePlugin" + ], + "summary": "安装插件", + "parameters": [ + { + "type": "file", + "description": "this is a test file", + "name": "plug", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "安装插件成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object" + } + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/preview": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCodeTemplate" + ], + "summary": "预览创建后的代码", + "parameters": [ + { + "description": "预览创建代码", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.AutoCode" + } + } + ], + "responses": { + "200": { + "description": "预览创建后的代码", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/pubPlug": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCodePlugin" + ], + "summary": "打包插件", + "parameters": [ + { + "type": "string", + "description": "插件名称", + "name": "plugName", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "打包插件成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/rollback": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCode" + ], + "summary": "回滚自动生成代码", + "parameters": [ + { + "description": "请求参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.SysAutoHistoryRollBack" + } + } + ], + "responses": { + "200": { + "description": "回滚自动生成代码", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/base/captcha": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Base" + ], + "summary": "生成验证码", + "responses": { + "200": { + "description": "生成验证码,返回包括随机数id,base64,验证码长度,是否开启验证码", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysCaptchaResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/base/login": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Base" + ], + "summary": "用户登录", + "parameters": [ + { + "description": "用户名, 密码, 验证码", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.Login" + } + } + ], + "responses": { + "200": { + "description": "返回包括用户信息,token,过期时间", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.LoginResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/casbin/UpdateCasbin": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Casbin" + ], + "summary": "更新角色api权限", + "parameters": [ + { + "description": "权限id, 权限模型列表", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.CasbinInReceive" + } + } + ], + "responses": { + "200": { + "description": "更新角色api权限", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/casbin/getPolicyPathByAuthorityId": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Casbin" + ], + "summary": "获取权限列表", + "parameters": [ + { + "description": "权限id, 权限模型列表", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.CasbinInReceive" + } + } + ], + "responses": { + "200": { + "description": "获取权限列表,返回包括casbin详情列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PolicyPathResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/customer/customer": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaCustomer" + ], + "summary": "获取单一客户信息", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "客户名", + "name": "customerName", + "in": "query" + }, + { + "type": "string", + "description": "客户手机号", + "name": "customerPhoneData", + "in": "query" + }, + { + "type": "integer", + "description": "管理角色ID", + "name": "sysUserAuthorityID", + "in": "query" + }, + { + "type": "integer", + "description": "管理ID", + "name": "sysUserId", + "in": "query" + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + } + ], + "responses": { + "200": { + "description": "获取单一客户信息,返回包括客户详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.ExaCustomerResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaCustomer" + ], + "summary": "更新客户信息", + "parameters": [ + { + "description": "客户ID, 客户信息", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/example.ExaCustomer" + } + } + ], + "responses": { + "200": { + "description": "更新客户信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaCustomer" + ], + "summary": "创建客户", + "parameters": [ + { + "description": "客户用户名, 客户手机号码", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/example.ExaCustomer" + } + } + ], + "responses": { + "200": { + "description": "创建客户", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + }, + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaCustomer" + ], + "summary": "删除客户", + "parameters": [ + { + "description": "客户ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/example.ExaCustomer" + } + } + ], + "responses": { + "200": { + "description": "删除客户", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/customer/customerList": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaCustomer" + ], + "summary": "分页获取权限客户列表", + "parameters": [ + { + "type": "string", + "description": "关键字", + "name": "keyword", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页大小", + "name": "pageSize", + "in": "query" + } + ], + "responses": { + "200": { + "description": "分页获取权限客户列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/email/emailTest": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "System" + ], + "summary": "发送测试邮件", + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"发送成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/email/sendEmail": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "System" + ], + "summary": "发送邮件", + "parameters": [ + { + "description": "发送邮件必须的参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/response.Email" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"发送成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/fileUploadAndDownload/breakpointContinue": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaFileUploadAndDownload" + ], + "summary": "断点续传到服务器", + "parameters": [ + { + "type": "file", + "description": "an example for breakpoint resume, 断点续传示例", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "断点续传到服务器", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/fileUploadAndDownload/deleteFile": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaFileUploadAndDownload" + ], + "summary": "删除文件", + "parameters": [ + { + "description": "传入文件里面id即可", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/example.ExaFileUploadAndDownload" + } + } + ], + "responses": { + "200": { + "description": "删除文件", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/fileUploadAndDownload/findFile": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaFileUploadAndDownload" + ], + "summary": "创建文件", + "parameters": [ + { + "type": "file", + "description": "上传文件完成", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "创建文件,返回包括文件路径", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.FilePathResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/fileUploadAndDownload/getFileList": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaFileUploadAndDownload" + ], + "summary": "分页文件列表", + "parameters": [ + { + "description": "页码, 每页大小", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "分页文件列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/fileUploadAndDownload/removeChunk": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaFileUploadAndDownload" + ], + "summary": "删除切片", + "parameters": [ + { + "type": "file", + "description": "删除缓存切片", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "删除切片", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/fileUploadAndDownload/upload": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaFileUploadAndDownload" + ], + "summary": "上传文件示例", + "parameters": [ + { + "type": "file", + "description": "上传文件示例", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "上传文件示例,返回包括文件详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.ExaFileResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/info/createInfo": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Info" + ], + "summary": "创建公告", + "parameters": [ + { + "description": "创建公告", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/model.Info" + } + } + ], + "responses": { + "200": { + "description": "创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/info/deleteInfo": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Info" + ], + "summary": "删除公告", + "parameters": [ + { + "description": "删除公告", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/model.Info" + } + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/info/deleteInfoByIds": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Info" + ], + "summary": "批量删除公告", + "responses": { + "200": { + "description": "批量删除成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/info/findInfo": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Info" + ], + "summary": "用id查询公告", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "内容", + "name": "content", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "标题", + "name": "title", + "in": "query" + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + }, + { + "type": "integer", + "description": "作者", + "name": "userID", + "in": "query" + } + ], + "responses": { + "200": { + "description": "查询成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.Info" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/info/getInfoDataSource": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Info" + ], + "summary": "获取Info的数据源", + "responses": { + "200": { + "description": "查询成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/info/getInfoList": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Info" + ], + "summary": "分页获取公告列表", + "parameters": [ + { + "type": "string", + "name": "endCreatedAt", + "in": "query" + }, + { + "type": "string", + "description": "关键字", + "name": "keyword", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页大小", + "name": "pageSize", + "in": "query" + }, + { + "type": "string", + "name": "startCreatedAt", + "in": "query" + } + ], + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/info/getInfoPublic": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Info" + ], + "summary": "不需要鉴权的公告接口", + "parameters": [ + { + "type": "string", + "name": "endCreatedAt", + "in": "query" + }, + { + "type": "string", + "description": "关键字", + "name": "keyword", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页大小", + "name": "pageSize", + "in": "query" + }, + { + "type": "string", + "name": "startCreatedAt", + "in": "query" + } + ], + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/info/updateInfo": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Info" + ], + "summary": "更新公告", + "parameters": [ + { + "description": "更新公告", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/model.Info" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/init/checkdb": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "CheckDB" + ], + "summary": "初始化用户数据库", + "responses": { + "200": { + "description": "初始化用户数据库", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/init/initdb": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "InitDB" + ], + "summary": "初始化用户数据库", + "parameters": [ + { + "description": "初始化数据库参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.InitDB" + } + } + ], + "responses": { + "200": { + "description": "初始化用户数据库", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/jwt/jsonInBlacklist": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Jwt" + ], + "summary": "jwt加入黑名单", + "responses": { + "200": { + "description": "jwt加入黑名单", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/addBaseMenu": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Menu" + ], + "summary": "新增菜单", + "parameters": [ + { + "description": "路由path, 父菜单ID, 路由name, 对应前端文件路径, 排序标记", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysBaseMenu" + } + } + ], + "responses": { + "200": { + "description": "新增菜单", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/addMenuAuthority": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AuthorityMenu" + ], + "summary": "增加menu和角色关联关系", + "parameters": [ + { + "description": "角色ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.AddMenuAuthorityInfo" + } + } + ], + "responses": { + "200": { + "description": "增加menu和角色关联关系", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/deleteBaseMenu": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Menu" + ], + "summary": "删除菜单", + "parameters": [ + { + "description": "菜单id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetById" + } + } + ], + "responses": { + "200": { + "description": "删除菜单", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/getBaseMenuById": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Menu" + ], + "summary": "根据id获取菜单", + "parameters": [ + { + "description": "菜单id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetById" + } + } + ], + "responses": { + "200": { + "description": "根据id获取菜单,返回包括系统菜单列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysBaseMenuResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/getBaseMenuTree": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "AuthorityMenu" + ], + "summary": "获取用户动态路由", + "parameters": [ + { + "description": "空", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.Empty" + } + } + ], + "responses": { + "200": { + "description": "获取用户动态路由,返回包括系统菜单列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysBaseMenusResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/getMenu": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "AuthorityMenu" + ], + "summary": "获取用户动态路由", + "parameters": [ + { + "description": "空", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.Empty" + } + } + ], + "responses": { + "200": { + "description": "获取用户动态路由,返回包括系统菜单详情列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysMenusResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/getMenuAuthority": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AuthorityMenu" + ], + "summary": "获取指定角色menu", + "parameters": [ + { + "description": "角色ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetAuthorityId" + } + } + ], + "responses": { + "200": { + "description": "获取指定角色menu", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/getMenuList": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Menu" + ], + "summary": "分页获取基础menu列表", + "parameters": [ + { + "description": "页码, 每页大小", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "分页获取基础menu列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/updateBaseMenu": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Menu" + ], + "summary": "更新菜单", + "parameters": [ + { + "description": "路由path, 父菜单ID, 路由name, 对应前端文件路径, 排序标记", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysBaseMenu" + } + } + ], + "responses": { + "200": { + "description": "更新菜单", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionary/createSysDictionary": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionary" + ], + "summary": "创建SysDictionary", + "parameters": [ + { + "description": "SysDictionary模型", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysDictionary" + } + } + ], + "responses": { + "200": { + "description": "创建SysDictionary", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionary/deleteSysDictionary": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionary" + ], + "summary": "删除SysDictionary", + "parameters": [ + { + "description": "SysDictionary模型", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysDictionary" + } + } + ], + "responses": { + "200": { + "description": "删除SysDictionary", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionary/findSysDictionary": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionary" + ], + "summary": "用id查询SysDictionary", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "描述", + "name": "desc", + "in": "query" + }, + { + "type": "string", + "description": "字典名(中)", + "name": "name", + "in": "query" + }, + { + "type": "boolean", + "description": "状态", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "字典名(英)", + "name": "type", + "in": "query" + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + } + ], + "responses": { + "200": { + "description": "用id查询SysDictionary", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionary/getSysDictionaryList": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionary" + ], + "summary": "分页获取SysDictionary列表", + "responses": { + "200": { + "description": "分页获取SysDictionary列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionary/updateSysDictionary": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionary" + ], + "summary": "更新SysDictionary", + "parameters": [ + { + "description": "SysDictionary模型", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysDictionary" + } + } + ], + "responses": { + "200": { + "description": "更新SysDictionary", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionaryDetail/createSysDictionaryDetail": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionaryDetail" + ], + "summary": "创建SysDictionaryDetail", + "parameters": [ + { + "description": "SysDictionaryDetail模型", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysDictionaryDetail" + } + } + ], + "responses": { + "200": { + "description": "创建SysDictionaryDetail", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionaryDetail/deleteSysDictionaryDetail": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionaryDetail" + ], + "summary": "删除SysDictionaryDetail", + "parameters": [ + { + "description": "SysDictionaryDetail模型", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysDictionaryDetail" + } + } + ], + "responses": { + "200": { + "description": "删除SysDictionaryDetail", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionaryDetail/findSysDictionaryDetail": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionaryDetail" + ], + "summary": "用id查询SysDictionaryDetail", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "扩展值", + "name": "extend", + "in": "query" + }, + { + "type": "string", + "description": "展示值", + "name": "label", + "in": "query" + }, + { + "type": "integer", + "description": "排序标记", + "name": "sort", + "in": "query" + }, + { + "type": "boolean", + "description": "启用状态", + "name": "status", + "in": "query" + }, + { + "type": "integer", + "description": "关联标记", + "name": "sysDictionaryID", + "in": "query" + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + }, + { + "type": "string", + "description": "字典值", + "name": "value", + "in": "query" + } + ], + "responses": { + "200": { + "description": "用id查询SysDictionaryDetail", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionaryDetail/getSysDictionaryDetailList": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionaryDetail" + ], + "summary": "分页获取SysDictionaryDetail列表", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "扩展值", + "name": "extend", + "in": "query" + }, + { + "type": "string", + "description": "关键字", + "name": "keyword", + "in": "query" + }, + { + "type": "string", + "description": "展示值", + "name": "label", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页大小", + "name": "pageSize", + "in": "query" + }, + { + "type": "integer", + "description": "排序标记", + "name": "sort", + "in": "query" + }, + { + "type": "boolean", + "description": "启用状态", + "name": "status", + "in": "query" + }, + { + "type": "integer", + "description": "关联标记", + "name": "sysDictionaryID", + "in": "query" + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + }, + { + "type": "string", + "description": "字典值", + "name": "value", + "in": "query" + } + ], + "responses": { + "200": { + "description": "分页获取SysDictionaryDetail列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionaryDetail/updateSysDictionaryDetail": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionaryDetail" + ], + "summary": "更新SysDictionaryDetail", + "parameters": [ + { + "description": "更新SysDictionaryDetail", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysDictionaryDetail" + } + } + ], + "responses": { + "200": { + "description": "更新SysDictionaryDetail", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysExportTemplate/createSysExportTemplate": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysExportTemplate" + ], + "summary": "创建导出模板", + "parameters": [ + { + "description": "创建导出模板", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysExportTemplate" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"创建成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/sysExportTemplate/deleteSysExportTemplate": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysExportTemplate" + ], + "summary": "删除导出模板", + "parameters": [ + { + "description": "删除导出模板", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysExportTemplate" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"删除成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/sysExportTemplate/deleteSysExportTemplateByIds": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysExportTemplate" + ], + "summary": "批量删除导出模板", + "parameters": [ + { + "description": "批量删除导出模板", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.IdsReq" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"批量删除成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/sysExportTemplate/exportExcel": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysExportTemplate" + ], + "summary": "导出表格模板", + "responses": {} + } + }, + "/sysExportTemplate/findSysExportTemplate": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysExportTemplate" + ], + "summary": "用id查询导出模板", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "数据库名称", + "name": "dbName", + "in": "query" + }, + { + "type": "integer", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "模板名称", + "name": "name", + "in": "query" + }, + { + "type": "string", + "name": "order", + "in": "query" + }, + { + "type": "string", + "description": "表名称", + "name": "tableName", + "in": "query" + }, + { + "type": "string", + "description": "模板标识", + "name": "templateID", + "in": "query" + }, + { + "type": "string", + "description": "模板信息", + "name": "templateInfo", + "in": "query" + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"查询成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/sysExportTemplate/getSysExportTemplateList": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysExportTemplate" + ], + "summary": "分页获取导出模板列表", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "数据库名称", + "name": "dbName", + "in": "query" + }, + { + "type": "string", + "name": "endCreatedAt", + "in": "query" + }, + { + "type": "string", + "description": "关键字", + "name": "keyword", + "in": "query" + }, + { + "type": "integer", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "模板名称", + "name": "name", + "in": "query" + }, + { + "type": "string", + "name": "order", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页大小", + "name": "pageSize", + "in": "query" + }, + { + "type": "string", + "name": "startCreatedAt", + "in": "query" + }, + { + "type": "string", + "description": "表名称", + "name": "tableName", + "in": "query" + }, + { + "type": "string", + "description": "模板标识", + "name": "templateID", + "in": "query" + }, + { + "type": "string", + "description": "模板信息", + "name": "templateInfo", + "in": "query" + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"获取成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/sysExportTemplate/importExcel": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysImportTemplate" + ], + "summary": "导入表格", + "responses": {} + } + }, + "/sysExportTemplate/updateSysExportTemplate": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysExportTemplate" + ], + "summary": "更新导出模板", + "parameters": [ + { + "description": "更新导出模板", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysExportTemplate" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"更新成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/sysOperationRecord/createSysOperationRecord": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysOperationRecord" + ], + "summary": "创建SysOperationRecord", + "parameters": [ + { + "description": "创建SysOperationRecord", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysOperationRecord" + } + } + ], + "responses": { + "200": { + "description": "创建SysOperationRecord", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysOperationRecord/deleteSysOperationRecord": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysOperationRecord" + ], + "summary": "删除SysOperationRecord", + "parameters": [ + { + "description": "SysOperationRecord模型", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysOperationRecord" + } + } + ], + "responses": { + "200": { + "description": "删除SysOperationRecord", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysOperationRecord/deleteSysOperationRecordByIds": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysOperationRecord" + ], + "summary": "批量删除SysOperationRecord", + "parameters": [ + { + "description": "批量删除SysOperationRecord", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.IdsReq" + } + } + ], + "responses": { + "200": { + "description": "批量删除SysOperationRecord", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysOperationRecord/findSysOperationRecord": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysOperationRecord" + ], + "summary": "用id查询SysOperationRecord", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "代理", + "name": "agent", + "in": "query" + }, + { + "type": "string", + "description": "请求Body", + "name": "body", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "错误信息", + "name": "error_message", + "in": "query" + }, + { + "type": "string", + "description": "请求ip", + "name": "ip", + "in": "query" + }, + { + "type": "string", + "description": "延迟", + "name": "latency", + "in": "query" + }, + { + "type": "string", + "description": "请求方法", + "name": "method", + "in": "query" + }, + { + "type": "string", + "description": "请求路径", + "name": "path", + "in": "query" + }, + { + "type": "string", + "description": "响应Body", + "name": "resp", + "in": "query" + }, + { + "type": "integer", + "description": "请求状态", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + }, + { + "type": "integer", + "description": "用户id", + "name": "user_id", + "in": "query" + } + ], + "responses": { + "200": { + "description": "用id查询SysOperationRecord", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysOperationRecord/getSysOperationRecordList": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysOperationRecord" + ], + "summary": "分页获取SysOperationRecord列表", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "代理", + "name": "agent", + "in": "query" + }, + { + "type": "string", + "description": "请求Body", + "name": "body", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "错误信息", + "name": "error_message", + "in": "query" + }, + { + "type": "string", + "description": "请求ip", + "name": "ip", + "in": "query" + }, + { + "type": "string", + "description": "关键字", + "name": "keyword", + "in": "query" + }, + { + "type": "string", + "description": "延迟", + "name": "latency", + "in": "query" + }, + { + "type": "string", + "description": "请求方法", + "name": "method", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页大小", + "name": "pageSize", + "in": "query" + }, + { + "type": "string", + "description": "请求路径", + "name": "path", + "in": "query" + }, + { + "type": "string", + "description": "响应Body", + "name": "resp", + "in": "query" + }, + { + "type": "integer", + "description": "请求状态", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + }, + { + "type": "integer", + "description": "用户id", + "name": "user_id", + "in": "query" + } + ], + "responses": { + "200": { + "description": "分页获取SysOperationRecord列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/system/getServerInfo": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "System" + ], + "summary": "获取服务器信息", + "responses": { + "200": { + "description": "获取服务器信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/system/getSystemConfig": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "System" + ], + "summary": "获取配置文件内容", + "responses": { + "200": { + "description": "获取配置文件内容,返回包括系统配置", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysConfigResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/system/reloadSystem": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "System" + ], + "summary": "重启系统", + "responses": { + "200": { + "description": "重启系统", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/system/setSystemConfig": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "System" + ], + "summary": "设置配置文件内容", + "parameters": [ + { + "description": "设置配置文件内容", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.System" + } + } + ], + "responses": { + "200": { + "description": "设置配置文件内容", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/SetSelfInfo": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "设置用户信息", + "parameters": [ + { + "description": "ID, 用户名, 昵称, 头像链接", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysUser" + } + } + ], + "responses": { + "200": { + "description": "设置用户信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/admin_register": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "用户注册账号", + "parameters": [ + { + "description": "用户名, 昵称, 密码, 角色ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.Register" + } + } + ], + "responses": { + "200": { + "description": "用户注册账号,返回包括用户信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysUserResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/changePassword": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "用户修改密码", + "parameters": [ + { + "description": "用户名, 原密码, 新密码", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.ChangePasswordReq" + } + } + ], + "responses": { + "200": { + "description": "用户修改密码", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/deleteUser": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "删除用户", + "parameters": [ + { + "description": "用户ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetById" + } + } + ], + "responses": { + "200": { + "description": "删除用户", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/getUserInfo": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "获取用户信息", + "responses": { + "200": { + "description": "获取用户信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/getUserList": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "分页获取用户列表", + "parameters": [ + { + "description": "页码, 每页大小", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "分页获取用户列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/resetPassword": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "重置用户密码", + "parameters": [ + { + "description": "ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysUser" + } + } + ], + "responses": { + "200": { + "description": "重置用户密码", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/setUserAuthorities": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "设置用户权限", + "parameters": [ + { + "description": "用户UUID, 角色ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.SetUserAuthorities" + } + } + ], + "responses": { + "200": { + "description": "设置用户权限", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/setUserAuthority": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "更改用户权限", + "parameters": [ + { + "description": "用户UUID, 角色ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.SetUserAuth" + } + } + ], + "responses": { + "200": { + "description": "设置用户权限", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/setUserInfo": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "设置用户信息", + "parameters": [ + { + "description": "ID, 用户名, 昵称, 头像链接", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysUser" + } + } + ], + "responses": { + "200": { + "description": "设置用户信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + } + }, + "definitions": { + "config.AliyunOSS": { + "type": "object", + "properties": { + "access-key-id": { + "type": "string" + }, + "access-key-secret": { + "type": "string" + }, + "base-path": { + "type": "string" + }, + "bucket-name": { + "type": "string" + }, + "bucket-url": { + "type": "string" + }, + "endpoint": { + "type": "string" + } + } + }, + "config.Autocode": { + "type": "object", + "properties": { + "ai-path": { + "type": "string" + }, + "module": { + "type": "string" + }, + "root": { + "type": "string" + }, + "server": { + "type": "string" + }, + "web": { + "type": "string" + } + } + }, + "config.AwsS3": { + "type": "object", + "properties": { + "base-url": { + "type": "string" + }, + "bucket": { + "type": "string" + }, + "disable-ssl": { + "type": "boolean" + }, + "endpoint": { + "type": "string" + }, + "path-prefix": { + "type": "string" + }, + "region": { + "type": "string" + }, + "s3-force-path-style": { + "type": "boolean" + }, + "secret-id": { + "type": "string" + }, + "secret-key": { + "type": "string" + } + } + }, + "config.CORS": { + "type": "object", + "properties": { + "mode": { + "type": "string" + }, + "whitelist": { + "type": "array", + "items": { + "$ref": "#/definitions/config.CORSWhitelist" + } + } + } + }, + "config.CORSWhitelist": { + "type": "object", + "properties": { + "allow-credentials": { + "type": "boolean" + }, + "allow-headers": { + "type": "string" + }, + "allow-methods": { + "type": "string" + }, + "allow-origin": { + "type": "string" + }, + "expose-headers": { + "type": "string" + } + } + }, + "config.Captcha": { + "type": "object", + "properties": { + "img-height": { + "description": "验证码高度", + "type": "integer" + }, + "img-width": { + "description": "验证码宽度", + "type": "integer" + }, + "key-long": { + "description": "验证码长度", + "type": "integer" + }, + "open-captcha": { + "description": "防爆破验证码开启此数,0代表每次登录都需要验证码,其他数字代表错误密码此数,如3代表错误三次后出现验证码", + "type": "integer" + }, + "open-captcha-timeout": { + "description": "防爆破验证码超时时间,单位:s(秒)", + "type": "integer" + } + } + }, + "config.CloudflareR2": { + "type": "object", + "properties": { + "access-key-id": { + "type": "string" + }, + "account-id": { + "type": "string" + }, + "base-url": { + "type": "string" + }, + "bucket": { + "type": "string" + }, + "path": { + "type": "string" + }, + "secret-access-key": { + "type": "string" + } + } + }, + "config.DiskList": { + "type": "object", + "properties": { + "mount-point": { + "type": "string" + } + } + }, + "config.Excel": { + "type": "object", + "properties": { + "dir": { + "type": "string" + } + } + }, + "config.HuaWeiObs": { + "type": "object", + "properties": { + "access-key": { + "type": "string" + }, + "bucket": { + "type": "string" + }, + "endpoint": { + "type": "string" + }, + "path": { + "type": "string" + }, + "secret-key": { + "type": "string" + } + } + }, + "config.JWT": { + "type": "object", + "properties": { + "buffer-time": { + "description": "缓冲时间", + "type": "string" + }, + "expires-time": { + "description": "过期时间", + "type": "string" + }, + "issuer": { + "description": "签发者", + "type": "string" + }, + "signing-key": { + "description": "jwt签名", + "type": "string" + } + } + }, + "config.Local": { + "type": "object", + "properties": { + "path": { + "description": "本地文件访问路径", + "type": "string" + }, + "store-path": { + "description": "本地文件存储路径", + "type": "string" + } + } + }, + "config.Mongo": { + "type": "object", + "properties": { + "auth-source": { + "description": "验证数据库", + "type": "string" + }, + "coll": { + "description": "collection name", + "type": "string" + }, + "connect-timeout-ms": { + "description": "连接超时时间", + "type": "integer" + }, + "database": { + "description": "database name", + "type": "string" + }, + "hosts": { + "description": "主机列表", + "type": "array", + "items": { + "$ref": "#/definitions/config.MongoHost" + } + }, + "is-zap": { + "description": "是否开启zap日志", + "type": "boolean" + }, + "max-pool-size": { + "description": "最大连接池", + "type": "integer" + }, + "min-pool-size": { + "description": "最小连接池", + "type": "integer" + }, + "options": { + "description": "mongodb options", + "type": "string" + }, + "password": { + "description": "密码", + "type": "string" + }, + "socket-timeout-ms": { + "description": "socket超时时间", + "type": "integer" + }, + "username": { + "description": "用户名", + "type": "string" + } + } + }, + "config.MongoHost": { + "type": "object", + "properties": { + "host": { + "description": "ip地址", + "type": "string" + }, + "port": { + "description": "端口", + "type": "string" + } + } + }, + "config.Mssql": { + "type": "object", + "properties": { + "config": { + "description": "高级配置", + "type": "string" + }, + "db-name": { + "description": "数据库名", + "type": "string" + }, + "engine": { + "description": "数据库引擎,默认InnoDB", + "type": "string", + "default": "InnoDB" + }, + "log-mode": { + "description": "是否开启Gorm全局日志", + "type": "string" + }, + "log-zap": { + "description": "是否通过zap写入日志文件", + "type": "boolean" + }, + "max-idle-conns": { + "description": "空闲中的最大连接数", + "type": "integer" + }, + "max-open-conns": { + "description": "打开到数据库的最大连接数", + "type": "integer" + }, + "password": { + "description": "数据库密码", + "type": "string" + }, + "path": { + "description": "数据库地址", + "type": "string" + }, + "port": { + "description": "数据库端口", + "type": "string" + }, + "prefix": { + "description": "数据库前缀", + "type": "string" + }, + "singular": { + "description": "是否开启全局禁用复数,true表示开启", + "type": "boolean" + }, + "username": { + "description": "数据库账号", + "type": "string" + } + } + }, + "config.Mysql": { + "type": "object", + "properties": { + "config": { + "description": "高级配置", + "type": "string" + }, + "db-name": { + "description": "数据库名", + "type": "string" + }, + "engine": { + "description": "数据库引擎,默认InnoDB", + "type": "string", + "default": "InnoDB" + }, + "log-mode": { + "description": "是否开启Gorm全局日志", + "type": "string" + }, + "log-zap": { + "description": "是否通过zap写入日志文件", + "type": "boolean" + }, + "max-idle-conns": { + "description": "空闲中的最大连接数", + "type": "integer" + }, + "max-open-conns": { + "description": "打开到数据库的最大连接数", + "type": "integer" + }, + "password": { + "description": "数据库密码", + "type": "string" + }, + "path": { + "description": "数据库地址", + "type": "string" + }, + "port": { + "description": "数据库端口", + "type": "string" + }, + "prefix": { + "description": "数据库前缀", + "type": "string" + }, + "singular": { + "description": "是否开启全局禁用复数,true表示开启", + "type": "boolean" + }, + "username": { + "description": "数据库账号", + "type": "string" + } + } + }, + "config.Oracle": { + "type": "object", + "properties": { + "config": { + "description": "高级配置", + "type": "string" + }, + "db-name": { + "description": "数据库名", + "type": "string" + }, + "engine": { + "description": "数据库引擎,默认InnoDB", + "type": "string", + "default": "InnoDB" + }, + "log-mode": { + "description": "是否开启Gorm全局日志", + "type": "string" + }, + "log-zap": { + "description": "是否通过zap写入日志文件", + "type": "boolean" + }, + "max-idle-conns": { + "description": "空闲中的最大连接数", + "type": "integer" + }, + "max-open-conns": { + "description": "打开到数据库的最大连接数", + "type": "integer" + }, + "password": { + "description": "数据库密码", + "type": "string" + }, + "path": { + "description": "数据库地址", + "type": "string" + }, + "port": { + "description": "数据库端口", + "type": "string" + }, + "prefix": { + "description": "数据库前缀", + "type": "string" + }, + "singular": { + "description": "是否开启全局禁用复数,true表示开启", + "type": "boolean" + }, + "username": { + "description": "数据库账号", + "type": "string" + } + } + }, + "config.Pgsql": { + "type": "object", + "properties": { + "config": { + "description": "高级配置", + "type": "string" + }, + "db-name": { + "description": "数据库名", + "type": "string" + }, + "engine": { + "description": "数据库引擎,默认InnoDB", + "type": "string", + "default": "InnoDB" + }, + "log-mode": { + "description": "是否开启Gorm全局日志", + "type": "string" + }, + "log-zap": { + "description": "是否通过zap写入日志文件", + "type": "boolean" + }, + "max-idle-conns": { + "description": "空闲中的最大连接数", + "type": "integer" + }, + "max-open-conns": { + "description": "打开到数据库的最大连接数", + "type": "integer" + }, + "password": { + "description": "数据库密码", + "type": "string" + }, + "path": { + "description": "数据库地址", + "type": "string" + }, + "port": { + "description": "数据库端口", + "type": "string" + }, + "prefix": { + "description": "数据库前缀", + "type": "string" + }, + "singular": { + "description": "是否开启全局禁用复数,true表示开启", + "type": "boolean" + }, + "username": { + "description": "数据库账号", + "type": "string" + } + } + }, + "config.Qiniu": { + "type": "object", + "properties": { + "access-key": { + "description": "秘钥AK", + "type": "string" + }, + "bucket": { + "description": "空间名称", + "type": "string" + }, + "img-path": { + "description": "CDN加速域名", + "type": "string" + }, + "secret-key": { + "description": "秘钥SK", + "type": "string" + }, + "use-cdn-domains": { + "description": "上传是否使用CDN上传加速", + "type": "boolean" + }, + "use-https": { + "description": "是否使用https", + "type": "boolean" + }, + "zone": { + "description": "存储区域", + "type": "string" + } + } + }, + "config.Redis": { + "type": "object", + "properties": { + "addr": { + "description": "服务器地址:端口", + "type": "string" + }, + "clusterAddrs": { + "description": "集群模式下的节点地址列表", + "type": "array", + "items": { + "type": "string" + } + }, + "db": { + "description": "单实例模式下redis的哪个数据库", + "type": "integer" + }, + "password": { + "description": "密码", + "type": "string" + }, + "useCluster": { + "description": "是否使用集群模式", + "type": "boolean" + } + } + }, + "config.Server": { + "type": "object", + "properties": { + "aliyun-oss": { + "$ref": "#/definitions/config.AliyunOSS" + }, + "autocode": { + "description": "auto", + "allOf": [ + { + "$ref": "#/definitions/config.Autocode" + } + ] + }, + "aws-s3": { + "$ref": "#/definitions/config.AwsS3" + }, + "captcha": { + "$ref": "#/definitions/config.Captcha" + }, + "cloudflare-r2": { + "$ref": "#/definitions/config.CloudflareR2" + }, + "cors": { + "description": "跨域配置", + "allOf": [ + { + "$ref": "#/definitions/config.CORS" + } + ] + }, + "db-list": { + "type": "array", + "items": { + "$ref": "#/definitions/config.SpecializedDB" + } + }, + "disk-list": { + "type": "array", + "items": { + "$ref": "#/definitions/config.DiskList" + } + }, + "email": { + "$ref": "#/definitions/github_com_flipped-aurora_gin-vue-admin_server_config.Email" + }, + "excel": { + "$ref": "#/definitions/config.Excel" + }, + "hua-wei-obs": { + "$ref": "#/definitions/config.HuaWeiObs" + }, + "jwt": { + "$ref": "#/definitions/config.JWT" + }, + "local": { + "description": "oss", + "allOf": [ + { + "$ref": "#/definitions/config.Local" + } + ] + }, + "mongo": { + "$ref": "#/definitions/config.Mongo" + }, + "mssql": { + "$ref": "#/definitions/config.Mssql" + }, + "mysql": { + "description": "gorm", + "allOf": [ + { + "$ref": "#/definitions/config.Mysql" + } + ] + }, + "oracle": { + "$ref": "#/definitions/config.Oracle" + }, + "pgsql": { + "$ref": "#/definitions/config.Pgsql" + }, + "qiniu": { + "$ref": "#/definitions/config.Qiniu" + }, + "redis": { + "$ref": "#/definitions/config.Redis" + }, + "sqlite": { + "$ref": "#/definitions/config.Sqlite" + }, + "system": { + "$ref": "#/definitions/config.System" + }, + "tencent-cos": { + "$ref": "#/definitions/config.TencentCOS" + }, + "zap": { + "$ref": "#/definitions/config.Zap" + } + } + }, + "config.SpecializedDB": { + "type": "object", + "properties": { + "alias-name": { + "type": "string" + }, + "config": { + "description": "高级配置", + "type": "string" + }, + "db-name": { + "description": "数据库名", + "type": "string" + }, + "disable": { + "type": "boolean" + }, + "engine": { + "description": "数据库引擎,默认InnoDB", + "type": "string", + "default": "InnoDB" + }, + "log-mode": { + "description": "是否开启Gorm全局日志", + "type": "string" + }, + "log-zap": { + "description": "是否通过zap写入日志文件", + "type": "boolean" + }, + "max-idle-conns": { + "description": "空闲中的最大连接数", + "type": "integer" + }, + "max-open-conns": { + "description": "打开到数据库的最大连接数", + "type": "integer" + }, + "password": { + "description": "数据库密码", + "type": "string" + }, + "path": { + "description": "数据库地址", + "type": "string" + }, + "port": { + "description": "数据库端口", + "type": "string" + }, + "prefix": { + "description": "数据库前缀", + "type": "string" + }, + "singular": { + "description": "是否开启全局禁用复数,true表示开启", + "type": "boolean" + }, + "type": { + "type": "string" + }, + "username": { + "description": "数据库账号", + "type": "string" + } + } + }, + "config.Sqlite": { + "type": "object", + "properties": { + "config": { + "description": "高级配置", + "type": "string" + }, + "db-name": { + "description": "数据库名", + "type": "string" + }, + "engine": { + "description": "数据库引擎,默认InnoDB", + "type": "string", + "default": "InnoDB" + }, + "log-mode": { + "description": "是否开启Gorm全局日志", + "type": "string" + }, + "log-zap": { + "description": "是否通过zap写入日志文件", + "type": "boolean" + }, + "max-idle-conns": { + "description": "空闲中的最大连接数", + "type": "integer" + }, + "max-open-conns": { + "description": "打开到数据库的最大连接数", + "type": "integer" + }, + "password": { + "description": "数据库密码", + "type": "string" + }, + "path": { + "description": "数据库地址", + "type": "string" + }, + "port": { + "description": "数据库端口", + "type": "string" + }, + "prefix": { + "description": "数据库前缀", + "type": "string" + }, + "singular": { + "description": "是否开启全局禁用复数,true表示开启", + "type": "boolean" + }, + "username": { + "description": "数据库账号", + "type": "string" + } + } + }, + "config.System": { + "type": "object", + "properties": { + "addr": { + "description": "端口值", + "type": "integer" + }, + "db-type": { + "description": "数据库类型:mysql(默认)|sqlite|sqlserver|postgresql", + "type": "string" + }, + "iplimit-count": { + "type": "integer" + }, + "iplimit-time": { + "type": "integer" + }, + "oss-type": { + "description": "Oss类型", + "type": "string" + }, + "router-prefix": { + "type": "string" + }, + "use-mongo": { + "description": "使用mongo", + "type": "boolean" + }, + "use-multipoint": { + "description": "多点登录拦截", + "type": "boolean" + }, + "use-redis": { + "description": "使用redis", + "type": "boolean" + } + } + }, + "config.TencentCOS": { + "type": "object", + "properties": { + "base-url": { + "type": "string" + }, + "bucket": { + "type": "string" + }, + "path-prefix": { + "type": "string" + }, + "region": { + "type": "string" + }, + "secret-id": { + "type": "string" + }, + "secret-key": { + "type": "string" + } + } + }, + "config.Zap": { + "type": "object", + "properties": { + "director": { + "description": "日志文件夹", + "type": "string" + }, + "encode-level": { + "description": "编码级", + "type": "string" + }, + "format": { + "description": "输出", + "type": "string" + }, + "level": { + "description": "级别", + "type": "string" + }, + "log-in-console": { + "description": "输出控制台", + "type": "boolean" + }, + "prefix": { + "description": "日志前缀", + "type": "string" + }, + "retention-day": { + "description": "日志保留天数", + "type": "integer" + }, + "show-line": { + "description": "显示行", + "type": "boolean" + }, + "stacktrace-key": { + "description": "栈名", + "type": "string" + } + } + }, + "example.ExaCustomer": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "customerName": { + "description": "客户名", + "type": "string" + }, + "customerPhoneData": { + "description": "客户手机号", + "type": "string" + }, + "sysUser": { + "description": "管理详情", + "allOf": [ + { + "$ref": "#/definitions/system.SysUser" + } + ] + }, + "sysUserAuthorityID": { + "description": "管理角色ID", + "type": "integer" + }, + "sysUserId": { + "description": "管理ID", + "type": "integer" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "example.ExaFile": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "chunkTotal": { + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "exaFileChunk": { + "type": "array", + "items": { + "$ref": "#/definitions/example.ExaFileChunk" + } + }, + "fileMd5": { + "type": "string" + }, + "fileName": { + "type": "string" + }, + "filePath": { + "type": "string" + }, + "isFinish": { + "type": "boolean" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "example.ExaFileChunk": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "exaFileID": { + "type": "integer" + }, + "fileChunkNumber": { + "type": "integer" + }, + "fileChunkPath": { + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "example.ExaFileUploadAndDownload": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "key": { + "description": "编号", + "type": "string" + }, + "name": { + "description": "文件名", + "type": "string" + }, + "tag": { + "description": "文件标签", + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + }, + "url": { + "description": "文件地址", + "type": "string" + } + } + }, + "github_com_flipped-aurora_gin-vue-admin_server_config.Email": { + "type": "object", + "properties": { + "from": { + "description": "发件人 你自己要发邮件的邮箱", + "type": "string" + }, + "host": { + "description": "服务器地址 例如 smtp.qq.com 请前往QQ或者你要发邮件的邮箱查看其smtp协议", + "type": "string" + }, + "is-ssl": { + "description": "是否SSL 是否开启SSL", + "type": "boolean" + }, + "nickname": { + "description": "昵称 发件人昵称 通常为自己的邮箱", + "type": "string" + }, + "port": { + "description": "端口 请前往QQ或者你要发邮件的邮箱查看其smtp协议 大多为 465", + "type": "integer" + }, + "secret": { + "description": "密钥 用于登录的密钥 最好不要用邮箱密码 去邮箱smtp申请一个用于登录的密钥", + "type": "string" + }, + "to": { + "description": "收件人:多个以英文逗号分隔 例:a@qq.com b@qq.com 正式开发中请把此项目作为参数使用", + "type": "string" + } + } + }, + "model.Info": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "attachments": { + "description": "附件", + "type": "array", + "items": { + "type": "object" + } + }, + "content": { + "description": "内容", + "type": "string" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "title": { + "description": "标题", + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + }, + "userID": { + "description": "作者", + "type": "integer" + } + } + }, + "request.AddMenuAuthorityInfo": { + "type": "object", + "properties": { + "authorityId": { + "description": "角色ID", + "type": "integer" + }, + "menus": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysBaseMenu" + } + } + } + }, + "request.AutoCode": { + "type": "object" + }, + "request.CasbinInReceive": { + "type": "object", + "properties": { + "authorityId": { + "description": "权限id", + "type": "integer" + }, + "casbinInfos": { + "type": "array", + "items": { + "$ref": "#/definitions/request.CasbinInfo" + } + } + } + }, + "request.CasbinInfo": { + "type": "object", + "properties": { + "method": { + "description": "方法", + "type": "string" + }, + "path": { + "description": "路径", + "type": "string" + } + } + }, + "request.ChangePasswordReq": { + "type": "object", + "properties": { + "newPassword": { + "description": "新密码", + "type": "string" + }, + "password": { + "description": "密码", + "type": "string" + } + } + }, + "request.Empty": { + "type": "object" + }, + "request.GetAuthorityId": { + "type": "object", + "properties": { + "authorityId": { + "description": "角色ID", + "type": "integer" + } + } + }, + "request.GetById": { + "type": "object", + "properties": { + "id": { + "description": "主键ID", + "type": "integer" + } + } + }, + "request.IdsReq": { + "type": "object", + "properties": { + "ids": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "request.InitDB": { + "type": "object", + "required": [ + "adminPassword", + "dbName" + ], + "properties": { + "adminPassword": { + "type": "string" + }, + "dbName": { + "description": "数据库名", + "type": "string" + }, + "dbPath": { + "description": "sqlite数据库文件路径", + "type": "string" + }, + "dbType": { + "description": "数据库类型", + "type": "string" + }, + "host": { + "description": "服务器地址", + "type": "string" + }, + "password": { + "description": "数据库密码", + "type": "string" + }, + "port": { + "description": "数据库连接端口", + "type": "string" + }, + "userName": { + "description": "数据库用户名", + "type": "string" + } + } + }, + "request.Login": { + "type": "object", + "properties": { + "captcha": { + "description": "验证码", + "type": "string" + }, + "captchaId": { + "description": "验证码ID", + "type": "string" + }, + "password": { + "description": "密码", + "type": "string" + }, + "username": { + "description": "用户名", + "type": "string" + } + } + }, + "request.PageInfo": { + "type": "object", + "properties": { + "keyword": { + "description": "关键字", + "type": "string" + }, + "page": { + "description": "页码", + "type": "integer" + }, + "pageSize": { + "description": "每页大小", + "type": "integer" + } + } + }, + "request.Register": { + "type": "object", + "properties": { + "authorityId": { + "type": "string", + "example": "int 角色id" + }, + "authorityIds": { + "type": "string", + "example": "[]uint 角色id" + }, + "email": { + "type": "string", + "example": "电子邮箱" + }, + "enable": { + "type": "string", + "example": "int 是否启用" + }, + "headerImg": { + "type": "string", + "example": "头像链接" + }, + "nickName": { + "type": "string", + "example": "昵称" + }, + "passWord": { + "type": "string", + "example": "密码" + }, + "phone": { + "type": "string", + "example": "电话号码" + }, + "userName": { + "type": "string", + "example": "用户名" + } + } + }, + "request.SearchApiParams": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "apiGroup": { + "description": "api组", + "type": "string" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "desc": { + "description": "排序方式:升序false(默认)|降序true", + "type": "boolean" + }, + "description": { + "description": "api中文描述", + "type": "string" + }, + "keyword": { + "description": "关键字", + "type": "string" + }, + "method": { + "description": "方法:创建POST(默认)|查看GET|更新PUT|删除DELETE", + "type": "string" + }, + "orderKey": { + "description": "排序", + "type": "string" + }, + "page": { + "description": "页码", + "type": "integer" + }, + "pageSize": { + "description": "每页大小", + "type": "integer" + }, + "path": { + "description": "api路径", + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "request.SetUserAuth": { + "type": "object", + "properties": { + "authorityId": { + "description": "角色ID", + "type": "integer" + } + } + }, + "request.SetUserAuthorities": { + "type": "object", + "properties": { + "authorityIds": { + "description": "角色ID", + "type": "array", + "items": { + "type": "integer" + } + }, + "id": { + "type": "integer" + } + } + }, + "request.SysAuthorityBtnReq": { + "type": "object", + "properties": { + "authorityId": { + "type": "integer" + }, + "menuID": { + "type": "integer" + }, + "selected": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "request.SysAutoCodePackageCreate": { + "type": "object", + "properties": { + "desc": { + "type": "string", + "example": "描述" + }, + "label": { + "type": "string", + "example": "展示名" + }, + "packageName": { + "type": "string", + "example": "包名" + }, + "template": { + "type": "string", + "example": "模版" + } + } + }, + "request.SysAutoHistoryRollBack": { + "type": "object", + "properties": { + "deleteApi": { + "description": "是否删除接口", + "type": "boolean" + }, + "deleteMenu": { + "description": "是否删除菜单", + "type": "boolean" + }, + "deleteTable": { + "description": "是否删除表", + "type": "boolean" + }, + "id": { + "description": "主键ID", + "type": "integer" + } + } + }, + "response.Email": { + "type": "object", + "properties": { + "body": { + "description": "邮件内容", + "type": "string" + }, + "subject": { + "description": "邮件标题", + "type": "string" + }, + "to": { + "description": "邮件发送给谁", + "type": "string" + } + } + }, + "response.ExaCustomerResponse": { + "type": "object", + "properties": { + "customer": { + "$ref": "#/definitions/example.ExaCustomer" + } + } + }, + "response.ExaFileResponse": { + "type": "object", + "properties": { + "file": { + "$ref": "#/definitions/example.ExaFileUploadAndDownload" + } + } + }, + "response.FilePathResponse": { + "type": "object", + "properties": { + "filePath": { + "type": "string" + } + } + }, + "response.FileResponse": { + "type": "object", + "properties": { + "file": { + "$ref": "#/definitions/example.ExaFile" + } + } + }, + "response.LoginResponse": { + "type": "object", + "properties": { + "expiresAt": { + "type": "integer" + }, + "token": { + "type": "string" + }, + "user": { + "$ref": "#/definitions/system.SysUser" + } + } + }, + "response.PageResult": { + "type": "object", + "properties": { + "list": {}, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "response.PolicyPathResponse": { + "type": "object", + "properties": { + "paths": { + "type": "array", + "items": { + "$ref": "#/definitions/request.CasbinInfo" + } + } + } + }, + "response.Response": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": {}, + "msg": { + "type": "string" + } + } + }, + "response.SysAPIListResponse": { + "type": "object", + "properties": { + "apis": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysApi" + } + } + } + }, + "response.SysAPIResponse": { + "type": "object", + "properties": { + "api": { + "$ref": "#/definitions/system.SysApi" + } + } + }, + "response.SysAuthorityBtnRes": { + "type": "object", + "properties": { + "selected": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "response.SysAuthorityCopyResponse": { + "type": "object", + "properties": { + "authority": { + "$ref": "#/definitions/system.SysAuthority" + }, + "oldAuthorityId": { + "description": "旧角色ID", + "type": "integer" + } + } + }, + "response.SysAuthorityResponse": { + "type": "object", + "properties": { + "authority": { + "$ref": "#/definitions/system.SysAuthority" + } + } + }, + "response.SysBaseMenuResponse": { + "type": "object", + "properties": { + "menu": { + "$ref": "#/definitions/system.SysBaseMenu" + } + } + }, + "response.SysBaseMenusResponse": { + "type": "object", + "properties": { + "menus": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysBaseMenu" + } + } + } + }, + "response.SysCaptchaResponse": { + "type": "object", + "properties": { + "captchaId": { + "type": "string" + }, + "captchaLength": { + "type": "integer" + }, + "openCaptcha": { + "type": "boolean" + }, + "picPath": { + "type": "string" + } + } + }, + "response.SysConfigResponse": { + "type": "object", + "properties": { + "config": { + "$ref": "#/definitions/config.Server" + } + } + }, + "response.SysMenusResponse": { + "type": "object", + "properties": { + "menus": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysMenu" + } + } + } + }, + "response.SysUserResponse": { + "type": "object", + "properties": { + "user": { + "$ref": "#/definitions/system.SysUser" + } + } + }, + "system.Condition": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "column": { + "type": "string" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "from": { + "type": "string" + }, + "operator": { + "type": "string" + }, + "templateID": { + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "system.JoinTemplate": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "joins": { + "type": "string" + }, + "on": { + "type": "string" + }, + "table": { + "type": "string" + }, + "templateID": { + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "system.Meta": { + "type": "object", + "properties": { + "activeName": { + "type": "string" + }, + "closeTab": { + "description": "自动关闭tab", + "type": "boolean" + }, + "defaultMenu": { + "description": "是否是基础路由(开发中)", + "type": "boolean" + }, + "icon": { + "description": "菜单图标", + "type": "string" + }, + "keepAlive": { + "description": "是否缓存", + "type": "boolean" + }, + "title": { + "description": "菜单名", + "type": "string" + } + } + }, + "system.SysApi": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "apiGroup": { + "description": "api组", + "type": "string" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "description": { + "description": "api中文描述", + "type": "string" + }, + "method": { + "description": "方法:创建POST(默认)|查看GET|更新PUT|删除DELETE", + "type": "string" + }, + "path": { + "description": "api路径", + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "system.SysAuthority": { + "type": "object", + "properties": { + "authorityId": { + "description": "角色ID", + "type": "integer" + }, + "authorityName": { + "description": "角色名", + "type": "string" + }, + "children": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysAuthority" + } + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "dataAuthorityId": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysAuthority" + } + }, + "defaultRouter": { + "description": "默认菜单(默认dashboard)", + "type": "string" + }, + "deletedAt": { + "type": "string" + }, + "menus": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysBaseMenu" + } + }, + "parentId": { + "description": "父角色ID", + "type": "integer" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "system.SysBaseMenu": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "authoritys": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysAuthority" + } + }, + "children": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysBaseMenu" + } + }, + "component": { + "description": "对应前端文件路径", + "type": "string" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "hidden": { + "description": "是否在列表隐藏", + "type": "boolean" + }, + "menuBtn": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysBaseMenuBtn" + } + }, + "meta": { + "description": "附加属性", + "allOf": [ + { + "$ref": "#/definitions/system.Meta" + } + ] + }, + "name": { + "description": "路由name", + "type": "string" + }, + "parameters": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysBaseMenuParameter" + } + }, + "parentId": { + "description": "父菜单ID", + "type": "integer" + }, + "path": { + "description": "路由path", + "type": "string" + }, + "sort": { + "description": "排序标记", + "type": "integer" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "system.SysBaseMenuBtn": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "desc": { + "type": "string" + }, + "name": { + "type": "string" + }, + "sysBaseMenuID": { + "type": "integer" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "system.SysBaseMenuParameter": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "key": { + "description": "地址栏携带参数的key", + "type": "string" + }, + "sysBaseMenuID": { + "type": "integer" + }, + "type": { + "description": "地址栏携带参数为params还是query", + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + }, + "value": { + "description": "地址栏携带参数的值", + "type": "string" + } + } + }, + "system.SysDictionary": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "desc": { + "description": "描述", + "type": "string" + }, + "name": { + "description": "字典名(中)", + "type": "string" + }, + "status": { + "description": "状态", + "type": "boolean" + }, + "sysDictionaryDetails": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysDictionaryDetail" + } + }, + "type": { + "description": "字典名(英)", + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "system.SysDictionaryDetail": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "extend": { + "description": "扩展值", + "type": "string" + }, + "label": { + "description": "展示值", + "type": "string" + }, + "sort": { + "description": "排序标记", + "type": "integer" + }, + "status": { + "description": "启用状态", + "type": "boolean" + }, + "sysDictionaryID": { + "description": "关联标记", + "type": "integer" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + }, + "value": { + "description": "字典值", + "type": "string" + } + } + }, + "system.SysExportTemplate": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "conditions": { + "type": "array", + "items": { + "$ref": "#/definitions/system.Condition" + } + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "dbName": { + "description": "数据库名称", + "type": "string" + }, + "joinTemplate": { + "type": "array", + "items": { + "$ref": "#/definitions/system.JoinTemplate" + } + }, + "limit": { + "type": "integer" + }, + "name": { + "description": "模板名称", + "type": "string" + }, + "order": { + "type": "string" + }, + "tableName": { + "description": "表名称", + "type": "string" + }, + "templateID": { + "description": "模板标识", + "type": "string" + }, + "templateInfo": { + "description": "模板信息", + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "system.SysMenu": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "authoritys": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysAuthority" + } + }, + "btns": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "children": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysMenu" + } + }, + "component": { + "description": "对应前端文件路径", + "type": "string" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "hidden": { + "description": "是否在列表隐藏", + "type": "boolean" + }, + "menuBtn": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysBaseMenuBtn" + } + }, + "menuId": { + "type": "integer" + }, + "meta": { + "description": "附加属性", + "allOf": [ + { + "$ref": "#/definitions/system.Meta" + } + ] + }, + "name": { + "description": "路由name", + "type": "string" + }, + "parameters": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysBaseMenuParameter" + } + }, + "parentId": { + "description": "父菜单ID", + "type": "integer" + }, + "path": { + "description": "路由path", + "type": "string" + }, + "sort": { + "description": "排序标记", + "type": "integer" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "system.SysOperationRecord": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "agent": { + "description": "代理", + "type": "string" + }, + "body": { + "description": "请求Body", + "type": "string" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "error_message": { + "description": "错误信息", + "type": "string" + }, + "ip": { + "description": "请求ip", + "type": "string" + }, + "latency": { + "description": "延迟", + "type": "string" + }, + "method": { + "description": "请求方法", + "type": "string" + }, + "path": { + "description": "请求路径", + "type": "string" + }, + "resp": { + "description": "响应Body", + "type": "string" + }, + "status": { + "description": "请求状态", + "type": "integer" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + }, + "user": { + "$ref": "#/definitions/system.SysUser" + }, + "user_id": { + "description": "用户id", + "type": "integer" + } + } + }, + "system.SysUser": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "authorities": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysAuthority" + } + }, + "authority": { + "$ref": "#/definitions/system.SysAuthority" + }, + "authorityId": { + "description": "用户角色ID", + "type": "integer" + }, + "baseColor": { + "description": "基础颜色", + "type": "string" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "email": { + "description": "用户邮箱", + "type": "string" + }, + "enable": { + "description": "用户是否被冻结 1正常 2冻结", + "type": "integer" + }, + "headerImg": { + "description": "用户头像", + "type": "string" + }, + "nickName": { + "description": "用户昵称", + "type": "string" + }, + "phone": { + "description": "用户手机号", + "type": "string" + }, + "sideMode": { + "description": "用户侧边主题", + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + }, + "userName": { + "description": "用户登录名", + "type": "string" + }, + "uuid": { + "description": "用户UUID", + "type": "string" + } + } + }, + "system.System": { + "type": "object", + "properties": { + "config": { + "$ref": "#/definitions/config.Server" + } + } + } + }, + "securityDefinitions": { + "ApiKeyAuth": { + "type": "apiKey", + "name": "x-token", + "in": "header" + } + } +} \ No newline at end of file diff --git a/admin/server/docs/swagger.yaml b/admin/server/docs/swagger.yaml new file mode 100644 index 000000000..430abdb26 --- /dev/null +++ b/admin/server/docs/swagger.yaml @@ -0,0 +1,4927 @@ +definitions: + config.AliyunOSS: + properties: + access-key-id: + type: string + access-key-secret: + type: string + base-path: + type: string + bucket-name: + type: string + bucket-url: + type: string + endpoint: + type: string + type: object + config.Autocode: + properties: + ai-path: + type: string + module: + type: string + root: + type: string + server: + type: string + web: + type: string + type: object + config.AwsS3: + properties: + base-url: + type: string + bucket: + type: string + disable-ssl: + type: boolean + endpoint: + type: string + path-prefix: + type: string + region: + type: string + s3-force-path-style: + type: boolean + secret-id: + type: string + secret-key: + type: string + type: object + config.CORS: + properties: + mode: + type: string + whitelist: + items: + $ref: '#/definitions/config.CORSWhitelist' + type: array + type: object + config.CORSWhitelist: + properties: + allow-credentials: + type: boolean + allow-headers: + type: string + allow-methods: + type: string + allow-origin: + type: string + expose-headers: + type: string + type: object + config.Captcha: + properties: + img-height: + description: 验证码高度 + type: integer + img-width: + description: 验证码宽度 + type: integer + key-long: + description: 验证码长度 + type: integer + open-captcha: + description: 防爆破验证码开启此数,0代表每次登录都需要验证码,其他数字代表错误密码此数,如3代表错误三次后出现验证码 + type: integer + open-captcha-timeout: + description: 防爆破验证码超时时间,单位:s(秒) + type: integer + type: object + config.CloudflareR2: + properties: + access-key-id: + type: string + account-id: + type: string + base-url: + type: string + bucket: + type: string + path: + type: string + secret-access-key: + type: string + type: object + config.DiskList: + properties: + mount-point: + type: string + type: object + config.Excel: + properties: + dir: + type: string + type: object + config.HuaWeiObs: + properties: + access-key: + type: string + bucket: + type: string + endpoint: + type: string + path: + type: string + secret-key: + type: string + type: object + config.JWT: + properties: + buffer-time: + description: 缓冲时间 + type: string + expires-time: + description: 过期时间 + type: string + issuer: + description: 签发者 + type: string + signing-key: + description: jwt签名 + type: string + type: object + config.Local: + properties: + path: + description: 本地文件访问路径 + type: string + store-path: + description: 本地文件存储路径 + type: string + type: object + config.Mongo: + properties: + auth-source: + description: 验证数据库 + type: string + coll: + description: collection name + type: string + connect-timeout-ms: + description: 连接超时时间 + type: integer + database: + description: database name + type: string + hosts: + description: 主机列表 + items: + $ref: '#/definitions/config.MongoHost' + type: array + is-zap: + description: 是否开启zap日志 + type: boolean + max-pool-size: + description: 最大连接池 + type: integer + min-pool-size: + description: 最小连接池 + type: integer + options: + description: mongodb options + type: string + password: + description: 密码 + type: string + socket-timeout-ms: + description: socket超时时间 + type: integer + username: + description: 用户名 + type: string + type: object + config.MongoHost: + properties: + host: + description: ip地址 + type: string + port: + description: 端口 + type: string + type: object + config.Mssql: + properties: + config: + description: 高级配置 + type: string + db-name: + description: 数据库名 + type: string + engine: + default: InnoDB + description: 数据库引擎,默认InnoDB + type: string + log-mode: + description: 是否开启Gorm全局日志 + type: string + log-zap: + description: 是否通过zap写入日志文件 + type: boolean + max-idle-conns: + description: 空闲中的最大连接数 + type: integer + max-open-conns: + description: 打开到数据库的最大连接数 + type: integer + password: + description: 数据库密码 + type: string + path: + description: 数据库地址 + type: string + port: + description: 数据库端口 + type: string + prefix: + description: 数据库前缀 + type: string + singular: + description: 是否开启全局禁用复数,true表示开启 + type: boolean + username: + description: 数据库账号 + type: string + type: object + config.Mysql: + properties: + config: + description: 高级配置 + type: string + db-name: + description: 数据库名 + type: string + engine: + default: InnoDB + description: 数据库引擎,默认InnoDB + type: string + log-mode: + description: 是否开启Gorm全局日志 + type: string + log-zap: + description: 是否通过zap写入日志文件 + type: boolean + max-idle-conns: + description: 空闲中的最大连接数 + type: integer + max-open-conns: + description: 打开到数据库的最大连接数 + type: integer + password: + description: 数据库密码 + type: string + path: + description: 数据库地址 + type: string + port: + description: 数据库端口 + type: string + prefix: + description: 数据库前缀 + type: string + singular: + description: 是否开启全局禁用复数,true表示开启 + type: boolean + username: + description: 数据库账号 + type: string + type: object + config.Oracle: + properties: + config: + description: 高级配置 + type: string + db-name: + description: 数据库名 + type: string + engine: + default: InnoDB + description: 数据库引擎,默认InnoDB + type: string + log-mode: + description: 是否开启Gorm全局日志 + type: string + log-zap: + description: 是否通过zap写入日志文件 + type: boolean + max-idle-conns: + description: 空闲中的最大连接数 + type: integer + max-open-conns: + description: 打开到数据库的最大连接数 + type: integer + password: + description: 数据库密码 + type: string + path: + description: 数据库地址 + type: string + port: + description: 数据库端口 + type: string + prefix: + description: 数据库前缀 + type: string + singular: + description: 是否开启全局禁用复数,true表示开启 + type: boolean + username: + description: 数据库账号 + type: string + type: object + config.Pgsql: + properties: + config: + description: 高级配置 + type: string + db-name: + description: 数据库名 + type: string + engine: + default: InnoDB + description: 数据库引擎,默认InnoDB + type: string + log-mode: + description: 是否开启Gorm全局日志 + type: string + log-zap: + description: 是否通过zap写入日志文件 + type: boolean + max-idle-conns: + description: 空闲中的最大连接数 + type: integer + max-open-conns: + description: 打开到数据库的最大连接数 + type: integer + password: + description: 数据库密码 + type: string + path: + description: 数据库地址 + type: string + port: + description: 数据库端口 + type: string + prefix: + description: 数据库前缀 + type: string + singular: + description: 是否开启全局禁用复数,true表示开启 + type: boolean + username: + description: 数据库账号 + type: string + type: object + config.Qiniu: + properties: + access-key: + description: 秘钥AK + type: string + bucket: + description: 空间名称 + type: string + img-path: + description: CDN加速域名 + type: string + secret-key: + description: 秘钥SK + type: string + use-cdn-domains: + description: 上传是否使用CDN上传加速 + type: boolean + use-https: + description: 是否使用https + type: boolean + zone: + description: 存储区域 + type: string + type: object + config.Redis: + properties: + addr: + description: 服务器地址:端口 + type: string + clusterAddrs: + description: 集群模式下的节点地址列表 + items: + type: string + type: array + db: + description: 单实例模式下redis的哪个数据库 + type: integer + password: + description: 密码 + type: string + useCluster: + description: 是否使用集群模式 + type: boolean + type: object + config.Server: + properties: + aliyun-oss: + $ref: '#/definitions/config.AliyunOSS' + autocode: + allOf: + - $ref: '#/definitions/config.Autocode' + description: auto + aws-s3: + $ref: '#/definitions/config.AwsS3' + captcha: + $ref: '#/definitions/config.Captcha' + cloudflare-r2: + $ref: '#/definitions/config.CloudflareR2' + cors: + allOf: + - $ref: '#/definitions/config.CORS' + description: 跨域配置 + db-list: + items: + $ref: '#/definitions/config.SpecializedDB' + type: array + disk-list: + items: + $ref: '#/definitions/config.DiskList' + type: array + email: + $ref: '#/definitions/github_com_flipped-aurora_gin-vue-admin_server_config.Email' + excel: + $ref: '#/definitions/config.Excel' + hua-wei-obs: + $ref: '#/definitions/config.HuaWeiObs' + jwt: + $ref: '#/definitions/config.JWT' + local: + allOf: + - $ref: '#/definitions/config.Local' + description: oss + mongo: + $ref: '#/definitions/config.Mongo' + mssql: + $ref: '#/definitions/config.Mssql' + mysql: + allOf: + - $ref: '#/definitions/config.Mysql' + description: gorm + oracle: + $ref: '#/definitions/config.Oracle' + pgsql: + $ref: '#/definitions/config.Pgsql' + qiniu: + $ref: '#/definitions/config.Qiniu' + redis: + $ref: '#/definitions/config.Redis' + sqlite: + $ref: '#/definitions/config.Sqlite' + system: + $ref: '#/definitions/config.System' + tencent-cos: + $ref: '#/definitions/config.TencentCOS' + zap: + $ref: '#/definitions/config.Zap' + type: object + config.SpecializedDB: + properties: + alias-name: + type: string + config: + description: 高级配置 + type: string + db-name: + description: 数据库名 + type: string + disable: + type: boolean + engine: + default: InnoDB + description: 数据库引擎,默认InnoDB + type: string + log-mode: + description: 是否开启Gorm全局日志 + type: string + log-zap: + description: 是否通过zap写入日志文件 + type: boolean + max-idle-conns: + description: 空闲中的最大连接数 + type: integer + max-open-conns: + description: 打开到数据库的最大连接数 + type: integer + password: + description: 数据库密码 + type: string + path: + description: 数据库地址 + type: string + port: + description: 数据库端口 + type: string + prefix: + description: 数据库前缀 + type: string + singular: + description: 是否开启全局禁用复数,true表示开启 + type: boolean + type: + type: string + username: + description: 数据库账号 + type: string + type: object + config.Sqlite: + properties: + config: + description: 高级配置 + type: string + db-name: + description: 数据库名 + type: string + engine: + default: InnoDB + description: 数据库引擎,默认InnoDB + type: string + log-mode: + description: 是否开启Gorm全局日志 + type: string + log-zap: + description: 是否通过zap写入日志文件 + type: boolean + max-idle-conns: + description: 空闲中的最大连接数 + type: integer + max-open-conns: + description: 打开到数据库的最大连接数 + type: integer + password: + description: 数据库密码 + type: string + path: + description: 数据库地址 + type: string + port: + description: 数据库端口 + type: string + prefix: + description: 数据库前缀 + type: string + singular: + description: 是否开启全局禁用复数,true表示开启 + type: boolean + username: + description: 数据库账号 + type: string + type: object + config.System: + properties: + addr: + description: 端口值 + type: integer + db-type: + description: 数据库类型:mysql(默认)|sqlite|sqlserver|postgresql + type: string + iplimit-count: + type: integer + iplimit-time: + type: integer + oss-type: + description: Oss类型 + type: string + router-prefix: + type: string + use-mongo: + description: 使用mongo + type: boolean + use-multipoint: + description: 多点登录拦截 + type: boolean + use-redis: + description: 使用redis + type: boolean + type: object + config.TencentCOS: + properties: + base-url: + type: string + bucket: + type: string + path-prefix: + type: string + region: + type: string + secret-id: + type: string + secret-key: + type: string + type: object + config.Zap: + properties: + director: + description: 日志文件夹 + type: string + encode-level: + description: 编码级 + type: string + format: + description: 输出 + type: string + level: + description: 级别 + type: string + log-in-console: + description: 输出控制台 + type: boolean + prefix: + description: 日志前缀 + type: string + retention-day: + description: 日志保留天数 + type: integer + show-line: + description: 显示行 + type: boolean + stacktrace-key: + description: 栈名 + type: string + type: object + example.ExaCustomer: + properties: + ID: + description: 主键ID + type: integer + createdAt: + description: 创建时间 + type: string + customerName: + description: 客户名 + type: string + customerPhoneData: + description: 客户手机号 + type: string + sysUser: + allOf: + - $ref: '#/definitions/system.SysUser' + description: 管理详情 + sysUserAuthorityID: + description: 管理角色ID + type: integer + sysUserId: + description: 管理ID + type: integer + updatedAt: + description: 更新时间 + type: string + type: object + example.ExaFile: + properties: + ID: + description: 主键ID + type: integer + chunkTotal: + type: integer + createdAt: + description: 创建时间 + type: string + exaFileChunk: + items: + $ref: '#/definitions/example.ExaFileChunk' + type: array + fileMd5: + type: string + fileName: + type: string + filePath: + type: string + isFinish: + type: boolean + updatedAt: + description: 更新时间 + type: string + type: object + example.ExaFileChunk: + properties: + ID: + description: 主键ID + type: integer + createdAt: + description: 创建时间 + type: string + exaFileID: + type: integer + fileChunkNumber: + type: integer + fileChunkPath: + type: string + updatedAt: + description: 更新时间 + type: string + type: object + example.ExaFileUploadAndDownload: + properties: + ID: + description: 主键ID + type: integer + createdAt: + description: 创建时间 + type: string + key: + description: 编号 + type: string + name: + description: 文件名 + type: string + tag: + description: 文件标签 + type: string + updatedAt: + description: 更新时间 + type: string + url: + description: 文件地址 + type: string + type: object + github_com_flipped-aurora_gin-vue-admin_server_config.Email: + properties: + from: + description: 发件人 你自己要发邮件的邮箱 + type: string + host: + description: 服务器地址 例如 smtp.qq.com 请前往QQ或者你要发邮件的邮箱查看其smtp协议 + type: string + is-ssl: + description: 是否SSL 是否开启SSL + type: boolean + nickname: + description: 昵称 发件人昵称 通常为自己的邮箱 + type: string + port: + description: 端口 请前往QQ或者你要发邮件的邮箱查看其smtp协议 大多为 465 + type: integer + secret: + description: 密钥 用于登录的密钥 最好不要用邮箱密码 去邮箱smtp申请一个用于登录的密钥 + type: string + to: + description: 收件人:多个以英文逗号分隔 例:a@qq.com b@qq.com 正式开发中请把此项目作为参数使用 + type: string + type: object + model.Info: + properties: + ID: + description: 主键ID + type: integer + attachments: + description: 附件 + items: + type: object + type: array + content: + description: 内容 + type: string + createdAt: + description: 创建时间 + type: string + title: + description: 标题 + type: string + updatedAt: + description: 更新时间 + type: string + userID: + description: 作者 + type: integer + type: object + request.AddMenuAuthorityInfo: + properties: + authorityId: + description: 角色ID + type: integer + menus: + items: + $ref: '#/definitions/system.SysBaseMenu' + type: array + type: object + request.AutoCode: + type: object + request.CasbinInReceive: + properties: + authorityId: + description: 权限id + type: integer + casbinInfos: + items: + $ref: '#/definitions/request.CasbinInfo' + type: array + type: object + request.CasbinInfo: + properties: + method: + description: 方法 + type: string + path: + description: 路径 + type: string + type: object + request.ChangePasswordReq: + properties: + newPassword: + description: 新密码 + type: string + password: + description: 密码 + type: string + type: object + request.Empty: + type: object + request.GetAuthorityId: + properties: + authorityId: + description: 角色ID + type: integer + type: object + request.GetById: + properties: + id: + description: 主键ID + type: integer + type: object + request.IdsReq: + properties: + ids: + items: + type: integer + type: array + type: object + request.InitDB: + properties: + adminPassword: + type: string + dbName: + description: 数据库名 + type: string + dbPath: + description: sqlite数据库文件路径 + type: string + dbType: + description: 数据库类型 + type: string + host: + description: 服务器地址 + type: string + password: + description: 数据库密码 + type: string + port: + description: 数据库连接端口 + type: string + userName: + description: 数据库用户名 + type: string + required: + - adminPassword + - dbName + type: object + request.Login: + properties: + captcha: + description: 验证码 + type: string + captchaId: + description: 验证码ID + type: string + password: + description: 密码 + type: string + username: + description: 用户名 + type: string + type: object + request.PageInfo: + properties: + keyword: + description: 关键字 + type: string + page: + description: 页码 + type: integer + pageSize: + description: 每页大小 + type: integer + type: object + request.Register: + properties: + authorityId: + example: int 角色id + type: string + authorityIds: + example: '[]uint 角色id' + type: string + email: + example: 电子邮箱 + type: string + enable: + example: int 是否启用 + type: string + headerImg: + example: 头像链接 + type: string + nickName: + example: 昵称 + type: string + passWord: + example: 密码 + type: string + phone: + example: 电话号码 + type: string + userName: + example: 用户名 + type: string + type: object + request.SearchApiParams: + properties: + ID: + description: 主键ID + type: integer + apiGroup: + description: api组 + type: string + createdAt: + description: 创建时间 + type: string + desc: + description: 排序方式:升序false(默认)|降序true + type: boolean + description: + description: api中文描述 + type: string + keyword: + description: 关键字 + type: string + method: + description: 方法:创建POST(默认)|查看GET|更新PUT|删除DELETE + type: string + orderKey: + description: 排序 + type: string + page: + description: 页码 + type: integer + pageSize: + description: 每页大小 + type: integer + path: + description: api路径 + type: string + updatedAt: + description: 更新时间 + type: string + type: object + request.SetUserAuth: + properties: + authorityId: + description: 角色ID + type: integer + type: object + request.SetUserAuthorities: + properties: + authorityIds: + description: 角色ID + items: + type: integer + type: array + id: + type: integer + type: object + request.SysAuthorityBtnReq: + properties: + authorityId: + type: integer + menuID: + type: integer + selected: + items: + type: integer + type: array + type: object + request.SysAutoCodePackageCreate: + properties: + desc: + example: 描述 + type: string + label: + example: 展示名 + type: string + packageName: + example: 包名 + type: string + template: + example: 模版 + type: string + type: object + request.SysAutoHistoryRollBack: + properties: + deleteApi: + description: 是否删除接口 + type: boolean + deleteMenu: + description: 是否删除菜单 + type: boolean + deleteTable: + description: 是否删除表 + type: boolean + id: + description: 主键ID + type: integer + type: object + response.Email: + properties: + body: + description: 邮件内容 + type: string + subject: + description: 邮件标题 + type: string + to: + description: 邮件发送给谁 + type: string + type: object + response.ExaCustomerResponse: + properties: + customer: + $ref: '#/definitions/example.ExaCustomer' + type: object + response.ExaFileResponse: + properties: + file: + $ref: '#/definitions/example.ExaFileUploadAndDownload' + type: object + response.FilePathResponse: + properties: + filePath: + type: string + type: object + response.FileResponse: + properties: + file: + $ref: '#/definitions/example.ExaFile' + type: object + response.LoginResponse: + properties: + expiresAt: + type: integer + token: + type: string + user: + $ref: '#/definitions/system.SysUser' + type: object + response.PageResult: + properties: + list: {} + page: + type: integer + pageSize: + type: integer + total: + type: integer + type: object + response.PolicyPathResponse: + properties: + paths: + items: + $ref: '#/definitions/request.CasbinInfo' + type: array + type: object + response.Response: + properties: + code: + type: integer + data: {} + msg: + type: string + type: object + response.SysAPIListResponse: + properties: + apis: + items: + $ref: '#/definitions/system.SysApi' + type: array + type: object + response.SysAPIResponse: + properties: + api: + $ref: '#/definitions/system.SysApi' + type: object + response.SysAuthorityBtnRes: + properties: + selected: + items: + type: integer + type: array + type: object + response.SysAuthorityCopyResponse: + properties: + authority: + $ref: '#/definitions/system.SysAuthority' + oldAuthorityId: + description: 旧角色ID + type: integer + type: object + response.SysAuthorityResponse: + properties: + authority: + $ref: '#/definitions/system.SysAuthority' + type: object + response.SysBaseMenuResponse: + properties: + menu: + $ref: '#/definitions/system.SysBaseMenu' + type: object + response.SysBaseMenusResponse: + properties: + menus: + items: + $ref: '#/definitions/system.SysBaseMenu' + type: array + type: object + response.SysCaptchaResponse: + properties: + captchaId: + type: string + captchaLength: + type: integer + openCaptcha: + type: boolean + picPath: + type: string + type: object + response.SysConfigResponse: + properties: + config: + $ref: '#/definitions/config.Server' + type: object + response.SysMenusResponse: + properties: + menus: + items: + $ref: '#/definitions/system.SysMenu' + type: array + type: object + response.SysUserResponse: + properties: + user: + $ref: '#/definitions/system.SysUser' + type: object + system.Condition: + properties: + ID: + description: 主键ID + type: integer + column: + type: string + createdAt: + description: 创建时间 + type: string + from: + type: string + operator: + type: string + templateID: + type: string + updatedAt: + description: 更新时间 + type: string + type: object + system.JoinTemplate: + properties: + ID: + description: 主键ID + type: integer + createdAt: + description: 创建时间 + type: string + joins: + type: string + "on": + type: string + table: + type: string + templateID: + type: string + updatedAt: + description: 更新时间 + type: string + type: object + system.Meta: + properties: + activeName: + type: string + closeTab: + description: 自动关闭tab + type: boolean + defaultMenu: + description: 是否是基础路由(开发中) + type: boolean + icon: + description: 菜单图标 + type: string + keepAlive: + description: 是否缓存 + type: boolean + title: + description: 菜单名 + type: string + type: object + system.SysApi: + properties: + ID: + description: 主键ID + type: integer + apiGroup: + description: api组 + type: string + createdAt: + description: 创建时间 + type: string + description: + description: api中文描述 + type: string + method: + description: 方法:创建POST(默认)|查看GET|更新PUT|删除DELETE + type: string + path: + description: api路径 + type: string + updatedAt: + description: 更新时间 + type: string + type: object + system.SysAuthority: + properties: + authorityId: + description: 角色ID + type: integer + authorityName: + description: 角色名 + type: string + children: + items: + $ref: '#/definitions/system.SysAuthority' + type: array + createdAt: + description: 创建时间 + type: string + dataAuthorityId: + items: + $ref: '#/definitions/system.SysAuthority' + type: array + defaultRouter: + description: 默认菜单(默认dashboard) + type: string + deletedAt: + type: string + menus: + items: + $ref: '#/definitions/system.SysBaseMenu' + type: array + parentId: + description: 父角色ID + type: integer + updatedAt: + description: 更新时间 + type: string + type: object + system.SysBaseMenu: + properties: + ID: + description: 主键ID + type: integer + authoritys: + items: + $ref: '#/definitions/system.SysAuthority' + type: array + children: + items: + $ref: '#/definitions/system.SysBaseMenu' + type: array + component: + description: 对应前端文件路径 + type: string + createdAt: + description: 创建时间 + type: string + hidden: + description: 是否在列表隐藏 + type: boolean + menuBtn: + items: + $ref: '#/definitions/system.SysBaseMenuBtn' + type: array + meta: + allOf: + - $ref: '#/definitions/system.Meta' + description: 附加属性 + name: + description: 路由name + type: string + parameters: + items: + $ref: '#/definitions/system.SysBaseMenuParameter' + type: array + parentId: + description: 父菜单ID + type: integer + path: + description: 路由path + type: string + sort: + description: 排序标记 + type: integer + updatedAt: + description: 更新时间 + type: string + type: object + system.SysBaseMenuBtn: + properties: + ID: + description: 主键ID + type: integer + createdAt: + description: 创建时间 + type: string + desc: + type: string + name: + type: string + sysBaseMenuID: + type: integer + updatedAt: + description: 更新时间 + type: string + type: object + system.SysBaseMenuParameter: + properties: + ID: + description: 主键ID + type: integer + createdAt: + description: 创建时间 + type: string + key: + description: 地址栏携带参数的key + type: string + sysBaseMenuID: + type: integer + type: + description: 地址栏携带参数为params还是query + type: string + updatedAt: + description: 更新时间 + type: string + value: + description: 地址栏携带参数的值 + type: string + type: object + system.SysDictionary: + properties: + ID: + description: 主键ID + type: integer + createdAt: + description: 创建时间 + type: string + desc: + description: 描述 + type: string + name: + description: 字典名(中) + type: string + status: + description: 状态 + type: boolean + sysDictionaryDetails: + items: + $ref: '#/definitions/system.SysDictionaryDetail' + type: array + type: + description: 字典名(英) + type: string + updatedAt: + description: 更新时间 + type: string + type: object + system.SysDictionaryDetail: + properties: + ID: + description: 主键ID + type: integer + createdAt: + description: 创建时间 + type: string + extend: + description: 扩展值 + type: string + label: + description: 展示值 + type: string + sort: + description: 排序标记 + type: integer + status: + description: 启用状态 + type: boolean + sysDictionaryID: + description: 关联标记 + type: integer + updatedAt: + description: 更新时间 + type: string + value: + description: 字典值 + type: string + type: object + system.SysExportTemplate: + properties: + ID: + description: 主键ID + type: integer + conditions: + items: + $ref: '#/definitions/system.Condition' + type: array + createdAt: + description: 创建时间 + type: string + dbName: + description: 数据库名称 + type: string + joinTemplate: + items: + $ref: '#/definitions/system.JoinTemplate' + type: array + limit: + type: integer + name: + description: 模板名称 + type: string + order: + type: string + tableName: + description: 表名称 + type: string + templateID: + description: 模板标识 + type: string + templateInfo: + description: 模板信息 + type: string + updatedAt: + description: 更新时间 + type: string + type: object + system.SysMenu: + properties: + ID: + description: 主键ID + type: integer + authoritys: + items: + $ref: '#/definitions/system.SysAuthority' + type: array + btns: + additionalProperties: + type: integer + type: object + children: + items: + $ref: '#/definitions/system.SysMenu' + type: array + component: + description: 对应前端文件路径 + type: string + createdAt: + description: 创建时间 + type: string + hidden: + description: 是否在列表隐藏 + type: boolean + menuBtn: + items: + $ref: '#/definitions/system.SysBaseMenuBtn' + type: array + menuId: + type: integer + meta: + allOf: + - $ref: '#/definitions/system.Meta' + description: 附加属性 + name: + description: 路由name + type: string + parameters: + items: + $ref: '#/definitions/system.SysBaseMenuParameter' + type: array + parentId: + description: 父菜单ID + type: integer + path: + description: 路由path + type: string + sort: + description: 排序标记 + type: integer + updatedAt: + description: 更新时间 + type: string + type: object + system.SysOperationRecord: + properties: + ID: + description: 主键ID + type: integer + agent: + description: 代理 + type: string + body: + description: 请求Body + type: string + createdAt: + description: 创建时间 + type: string + error_message: + description: 错误信息 + type: string + ip: + description: 请求ip + type: string + latency: + description: 延迟 + type: string + method: + description: 请求方法 + type: string + path: + description: 请求路径 + type: string + resp: + description: 响应Body + type: string + status: + description: 请求状态 + type: integer + updatedAt: + description: 更新时间 + type: string + user: + $ref: '#/definitions/system.SysUser' + user_id: + description: 用户id + type: integer + type: object + system.SysUser: + properties: + ID: + description: 主键ID + type: integer + authorities: + items: + $ref: '#/definitions/system.SysAuthority' + type: array + authority: + $ref: '#/definitions/system.SysAuthority' + authorityId: + description: 用户角色ID + type: integer + baseColor: + description: 基础颜色 + type: string + createdAt: + description: 创建时间 + type: string + email: + description: 用户邮箱 + type: string + enable: + description: 用户是否被冻结 1正常 2冻结 + type: integer + headerImg: + description: 用户头像 + type: string + nickName: + description: 用户昵称 + type: string + phone: + description: 用户手机号 + type: string + sideMode: + description: 用户侧边主题 + type: string + updatedAt: + description: 更新时间 + type: string + userName: + description: 用户登录名 + type: string + uuid: + description: 用户UUID + type: string + type: object + system.System: + properties: + config: + $ref: '#/definitions/config.Server' + type: object +info: + contact: {} + description: 使用gin+vue进行极速开发的全栈开发基础平台 + title: Gin-Vue-Admin Swagger API接口文档 + version: v2.7.7 +paths: + /api/createApi: + post: + consumes: + - application/json + parameters: + - description: api路径, api中文描述, api组, 方法 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysApi' + produces: + - application/json + responses: + "200": + description: 创建基础api + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 创建基础api + tags: + - SysApi + /api/deleteApi: + post: + consumes: + - application/json + parameters: + - description: ID + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysApi' + produces: + - application/json + responses: + "200": + description: 删除api + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 删除api + tags: + - SysApi + /api/deleteApisByIds: + delete: + consumes: + - application/json + parameters: + - description: ID + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.IdsReq' + produces: + - application/json + responses: + "200": + description: 删除选中Api + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 删除选中Api + tags: + - SysApi + /api/enterSyncApi: + post: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 确认同步API + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 确认同步API + tags: + - SysApi + /api/freshCasbin: + get: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 刷新成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + summary: 刷新casbin缓存 + tags: + - SysApi + /api/getAllApis: + post: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 获取所有的Api 不分页,返回包括api列表 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.SysAPIListResponse' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取所有的Api 不分页 + tags: + - SysApi + /api/getApiById: + post: + consumes: + - application/json + parameters: + - description: 根据id获取api + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.GetById' + produces: + - application/json + responses: + "200": + description: 根据id获取api,返回包括api详情 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.SysAPIResponse' + type: object + security: + - ApiKeyAuth: [] + summary: 根据id获取api + tags: + - SysApi + /api/getApiGroups: + post: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 获取API分组 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取API分组 + tags: + - SysApi + /api/getApiList: + post: + consumes: + - application/json + parameters: + - description: 分页获取API列表 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.SearchApiParams' + produces: + - application/json + responses: + "200": + description: 分页获取API列表,返回包括列表,总数,页码,每页数量 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.PageResult' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 分页获取API列表 + tags: + - SysApi + /api/ignoreApi: + post: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 同步API + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 忽略API + tags: + - IgnoreApi + /api/syncApi: + get: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 同步API + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 同步API + tags: + - SysApi + /api/updateApi: + post: + consumes: + - application/json + parameters: + - description: api路径, api中文描述, api组, 方法 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysApi' + produces: + - application/json + responses: + "200": + description: 修改基础api + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 修改基础api + tags: + - SysApi + /authority/copyAuthority: + post: + consumes: + - application/json + parameters: + - description: 旧角色id, 新权限id, 新权限名, 新父角色id + in: body + name: data + required: true + schema: + $ref: '#/definitions/response.SysAuthorityCopyResponse' + produces: + - application/json + responses: + "200": + description: 拷贝角色,返回包括系统角色详情 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.SysAuthorityResponse' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 拷贝角色 + tags: + - Authority + /authority/createAuthority: + post: + consumes: + - application/json + parameters: + - description: 权限id, 权限名, 父角色id + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysAuthority' + produces: + - application/json + responses: + "200": + description: 创建角色,返回包括系统角色详情 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.SysAuthorityResponse' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 创建角色 + tags: + - Authority + /authority/deleteAuthority: + post: + consumes: + - application/json + parameters: + - description: 删除角色 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysAuthority' + produces: + - application/json + responses: + "200": + description: 删除角色 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 删除角色 + tags: + - Authority + /authority/getAuthorityList: + post: + consumes: + - application/json + parameters: + - description: 页码, 每页大小 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.PageInfo' + produces: + - application/json + responses: + "200": + description: 分页获取角色列表,返回包括列表,总数,页码,每页数量 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.PageResult' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 分页获取角色列表 + tags: + - Authority + /authority/setDataAuthority: + post: + consumes: + - application/json + parameters: + - description: 设置角色资源权限 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysAuthority' + produces: + - application/json + responses: + "200": + description: 设置角色资源权限 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 设置角色资源权限 + tags: + - Authority + /authority/updateAuthority: + post: + consumes: + - application/json + parameters: + - description: 权限id, 权限名, 父角色id + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysAuthority' + produces: + - application/json + responses: + "200": + description: 更新角色信息,返回包括系统角色详情 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.SysAuthorityResponse' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 更新角色信息 + tags: + - Authority + /authorityBtn/canRemoveAuthorityBtn: + post: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 删除成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 设置权限按钮 + tags: + - AuthorityBtn + /authorityBtn/getAuthorityBtn: + post: + consumes: + - application/json + parameters: + - description: 菜单id, 角色id, 选中的按钮id + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.SysAuthorityBtnReq' + produces: + - application/json + responses: + "200": + description: 返回列表成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.SysAuthorityBtnRes' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取权限按钮 + tags: + - AuthorityBtn + /authorityBtn/setAuthorityBtn: + post: + consumes: + - application/json + parameters: + - description: 菜单id, 角色id, 选中的按钮id + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.SysAuthorityBtnReq' + produces: + - application/json + responses: + "200": + description: 返回列表成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 设置权限按钮 + tags: + - AuthorityBtn + /autoCode/addFunc: + post: + consumes: + - application/json + parameters: + - description: 增加方法 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.AutoCode' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"创建成功"}' + schema: + type: string + security: + - ApiKeyAuth: [] + summary: 增加方法 + tags: + - AddFunc + /autoCode/createPackage: + post: + consumes: + - application/json + parameters: + - description: 创建package + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.SysAutoCodePackageCreate' + produces: + - application/json + responses: + "200": + description: 创建package成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 创建package + tags: + - AutoCodePackage + /autoCode/createTemp: + post: + consumes: + - application/json + parameters: + - description: 创建自动代码 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.AutoCode' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"创建成功"}' + schema: + type: string + security: + - ApiKeyAuth: [] + summary: 自动代码模板 + tags: + - AutoCodeTemplate + /autoCode/delPackage: + post: + consumes: + - application/json + parameters: + - description: 创建package + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.GetById' + produces: + - application/json + responses: + "200": + description: 删除package成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 删除package + tags: + - AutoCode + /autoCode/delSysHistory: + post: + consumes: + - application/json + parameters: + - description: 请求参数 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.GetById' + produces: + - application/json + responses: + "200": + description: 删除回滚记录 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 删除回滚记录 + tags: + - AutoCode + /autoCode/getColumn: + get: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 获取当前表所有字段 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取当前表所有字段 + tags: + - AutoCode + /autoCode/getDatabase: + get: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 获取当前所有数据库 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取当前所有数据库 + tags: + - AutoCode + /autoCode/getMeta: + post: + consumes: + - application/json + parameters: + - description: 请求参数 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.GetById' + produces: + - application/json + responses: + "200": + description: 获取meta信息 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取meta信息 + tags: + - AutoCode + /autoCode/getPackage: + post: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 创建package成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取package + tags: + - AutoCodePackage + /autoCode/getSysHistory: + post: + consumes: + - application/json + parameters: + - description: 请求参数 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.PageInfo' + produces: + - application/json + responses: + "200": + description: 查询回滚记录,返回包括列表,总数,页码,每页数量 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.PageResult' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 查询回滚记录 + tags: + - AutoCode + /autoCode/getTables: + get: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 获取当前数据库所有表 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取当前数据库所有表 + tags: + - AutoCode + /autoCode/getTemplates: + get: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 创建package成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取package + tags: + - AutoCodePackage + /autoCode/installPlugin: + post: + consumes: + - multipart/form-data + parameters: + - description: this is a test file + in: formData + name: plug + required: true + type: file + produces: + - application/json + responses: + "200": + description: 安装插件成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + items: + type: object + type: array + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 安装插件 + tags: + - AutoCodePlugin + /autoCode/preview: + post: + consumes: + - application/json + parameters: + - description: 预览创建代码 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.AutoCode' + produces: + - application/json + responses: + "200": + description: 预览创建后的代码 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 预览创建后的代码 + tags: + - AutoCodeTemplate + /autoCode/pubPlug: + get: + consumes: + - application/json + parameters: + - description: 插件名称 + in: query + name: plugName + required: true + type: string + produces: + - application/json + responses: + "200": + description: 打包插件成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 打包插件 + tags: + - AutoCodePlugin + /autoCode/rollback: + post: + consumes: + - application/json + parameters: + - description: 请求参数 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.SysAutoHistoryRollBack' + produces: + - application/json + responses: + "200": + description: 回滚自动生成代码 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 回滚自动生成代码 + tags: + - AutoCode + /base/captcha: + post: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 生成验证码,返回包括随机数id,base64,验证码长度,是否开启验证码 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.SysCaptchaResponse' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 生成验证码 + tags: + - Base + /base/login: + post: + parameters: + - description: 用户名, 密码, 验证码 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.Login' + produces: + - application/json + responses: + "200": + description: 返回包括用户信息,token,过期时间 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.LoginResponse' + msg: + type: string + type: object + summary: 用户登录 + tags: + - Base + /casbin/UpdateCasbin: + post: + consumes: + - application/json + parameters: + - description: 权限id, 权限模型列表 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.CasbinInReceive' + produces: + - application/json + responses: + "200": + description: 更新角色api权限 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 更新角色api权限 + tags: + - Casbin + /casbin/getPolicyPathByAuthorityId: + post: + consumes: + - application/json + parameters: + - description: 权限id, 权限模型列表 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.CasbinInReceive' + produces: + - application/json + responses: + "200": + description: 获取权限列表,返回包括casbin详情列表 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.PolicyPathResponse' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取权限列表 + tags: + - Casbin + /customer/customer: + delete: + consumes: + - application/json + parameters: + - description: 客户ID + in: body + name: data + required: true + schema: + $ref: '#/definitions/example.ExaCustomer' + produces: + - application/json + responses: + "200": + description: 删除客户 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 删除客户 + tags: + - ExaCustomer + get: + consumes: + - application/json + parameters: + - description: 主键ID + in: query + name: ID + type: integer + - description: 创建时间 + in: query + name: createdAt + type: string + - description: 客户名 + in: query + name: customerName + type: string + - description: 客户手机号 + in: query + name: customerPhoneData + type: string + - description: 管理角色ID + in: query + name: sysUserAuthorityID + type: integer + - description: 管理ID + in: query + name: sysUserId + type: integer + - description: 更新时间 + in: query + name: updatedAt + type: string + produces: + - application/json + responses: + "200": + description: 获取单一客户信息,返回包括客户详情 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.ExaCustomerResponse' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取单一客户信息 + tags: + - ExaCustomer + post: + consumes: + - application/json + parameters: + - description: 客户用户名, 客户手机号码 + in: body + name: data + required: true + schema: + $ref: '#/definitions/example.ExaCustomer' + produces: + - application/json + responses: + "200": + description: 创建客户 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 创建客户 + tags: + - ExaCustomer + put: + consumes: + - application/json + parameters: + - description: 客户ID, 客户信息 + in: body + name: data + required: true + schema: + $ref: '#/definitions/example.ExaCustomer' + produces: + - application/json + responses: + "200": + description: 更新客户信息 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 更新客户信息 + tags: + - ExaCustomer + /customer/customerList: + get: + consumes: + - application/json + parameters: + - description: 关键字 + in: query + name: keyword + type: string + - description: 页码 + in: query + name: page + type: integer + - description: 每页大小 + in: query + name: pageSize + type: integer + produces: + - application/json + responses: + "200": + description: 分页获取权限客户列表,返回包括列表,总数,页码,每页数量 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.PageResult' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 分页获取权限客户列表 + tags: + - ExaCustomer + /email/emailTest: + post: + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"发送成功"}' + schema: + type: string + security: + - ApiKeyAuth: [] + summary: 发送测试邮件 + tags: + - System + /email/sendEmail: + post: + parameters: + - description: 发送邮件必须的参数 + in: body + name: data + required: true + schema: + $ref: '#/definitions/response.Email' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"发送成功"}' + schema: + type: string + security: + - ApiKeyAuth: [] + summary: 发送邮件 + tags: + - System + /fileUploadAndDownload/breakpointContinue: + post: + consumes: + - multipart/form-data + parameters: + - description: an example for breakpoint resume, 断点续传示例 + in: formData + name: file + required: true + type: file + produces: + - application/json + responses: + "200": + description: 断点续传到服务器 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 断点续传到服务器 + tags: + - ExaFileUploadAndDownload + /fileUploadAndDownload/deleteFile: + post: + parameters: + - description: 传入文件里面id即可 + in: body + name: data + required: true + schema: + $ref: '#/definitions/example.ExaFileUploadAndDownload' + produces: + - application/json + responses: + "200": + description: 删除文件 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 删除文件 + tags: + - ExaFileUploadAndDownload + /fileUploadAndDownload/findFile: + post: + consumes: + - multipart/form-data + parameters: + - description: 上传文件完成 + in: formData + name: file + required: true + type: file + produces: + - application/json + responses: + "200": + description: 创建文件,返回包括文件路径 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.FilePathResponse' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 创建文件 + tags: + - ExaFileUploadAndDownload + /fileUploadAndDownload/getFileList: + post: + consumes: + - application/json + parameters: + - description: 页码, 每页大小 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.PageInfo' + produces: + - application/json + responses: + "200": + description: 分页文件列表,返回包括列表,总数,页码,每页数量 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.PageResult' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 分页文件列表 + tags: + - ExaFileUploadAndDownload + /fileUploadAndDownload/removeChunk: + post: + consumes: + - multipart/form-data + parameters: + - description: 删除缓存切片 + in: formData + name: file + required: true + type: file + produces: + - application/json + responses: + "200": + description: 删除切片 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 删除切片 + tags: + - ExaFileUploadAndDownload + /fileUploadAndDownload/upload: + post: + consumes: + - multipart/form-data + parameters: + - description: 上传文件示例 + in: formData + name: file + required: true + type: file + produces: + - application/json + responses: + "200": + description: 上传文件示例,返回包括文件详情 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.ExaFileResponse' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 上传文件示例 + tags: + - ExaFileUploadAndDownload + /info/createInfo: + post: + consumes: + - application/json + parameters: + - description: 创建公告 + in: body + name: data + required: true + schema: + $ref: '#/definitions/model.Info' + produces: + - application/json + responses: + "200": + description: 创建成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 创建公告 + tags: + - Info + /info/deleteInfo: + delete: + consumes: + - application/json + parameters: + - description: 删除公告 + in: body + name: data + required: true + schema: + $ref: '#/definitions/model.Info' + produces: + - application/json + responses: + "200": + description: 删除成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 删除公告 + tags: + - Info + /info/deleteInfoByIds: + delete: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 批量删除成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 批量删除公告 + tags: + - Info + /info/findInfo: + get: + consumes: + - application/json + parameters: + - description: 主键ID + in: query + name: ID + type: integer + - description: 内容 + in: query + name: content + type: string + - description: 创建时间 + in: query + name: createdAt + type: string + - description: 标题 + in: query + name: title + type: string + - description: 更新时间 + in: query + name: updatedAt + type: string + - description: 作者 + in: query + name: userID + type: integer + produces: + - application/json + responses: + "200": + description: 查询成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/model.Info' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 用id查询公告 + tags: + - Info + /info/getInfoDataSource: + get: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 查询成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + type: object + msg: + type: string + type: object + summary: 获取Info的数据源 + tags: + - Info + /info/getInfoList: + get: + consumes: + - application/json + parameters: + - in: query + name: endCreatedAt + type: string + - description: 关键字 + in: query + name: keyword + type: string + - description: 页码 + in: query + name: page + type: integer + - description: 每页大小 + in: query + name: pageSize + type: integer + - in: query + name: startCreatedAt + type: string + produces: + - application/json + responses: + "200": + description: 获取成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.PageResult' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 分页获取公告列表 + tags: + - Info + /info/getInfoPublic: + get: + consumes: + - application/json + parameters: + - in: query + name: endCreatedAt + type: string + - description: 关键字 + in: query + name: keyword + type: string + - description: 页码 + in: query + name: page + type: integer + - description: 每页大小 + in: query + name: pageSize + type: integer + - in: query + name: startCreatedAt + type: string + produces: + - application/json + responses: + "200": + description: 获取成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + type: object + msg: + type: string + type: object + summary: 不需要鉴权的公告接口 + tags: + - Info + /info/updateInfo: + put: + consumes: + - application/json + parameters: + - description: 更新公告 + in: body + name: data + required: true + schema: + $ref: '#/definitions/model.Info' + produces: + - application/json + responses: + "200": + description: 更新成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 更新公告 + tags: + - Info + /init/checkdb: + post: + produces: + - application/json + responses: + "200": + description: 初始化用户数据库 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + summary: 初始化用户数据库 + tags: + - CheckDB + /init/initdb: + post: + parameters: + - description: 初始化数据库参数 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.InitDB' + produces: + - application/json + responses: + "200": + description: 初始化用户数据库 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + type: string + type: object + summary: 初始化用户数据库 + tags: + - InitDB + /jwt/jsonInBlacklist: + post: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: jwt加入黑名单 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: jwt加入黑名单 + tags: + - Jwt + /menu/addBaseMenu: + post: + consumes: + - application/json + parameters: + - description: 路由path, 父菜单ID, 路由name, 对应前端文件路径, 排序标记 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysBaseMenu' + produces: + - application/json + responses: + "200": + description: 新增菜单 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 新增菜单 + tags: + - Menu + /menu/addMenuAuthority: + post: + consumes: + - application/json + parameters: + - description: 角色ID + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.AddMenuAuthorityInfo' + produces: + - application/json + responses: + "200": + description: 增加menu和角色关联关系 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 增加menu和角色关联关系 + tags: + - AuthorityMenu + /menu/deleteBaseMenu: + post: + consumes: + - application/json + parameters: + - description: 菜单id + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.GetById' + produces: + - application/json + responses: + "200": + description: 删除菜单 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 删除菜单 + tags: + - Menu + /menu/getBaseMenuById: + post: + consumes: + - application/json + parameters: + - description: 菜单id + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.GetById' + produces: + - application/json + responses: + "200": + description: 根据id获取菜单,返回包括系统菜单列表 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.SysBaseMenuResponse' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 根据id获取菜单 + tags: + - Menu + /menu/getBaseMenuTree: + post: + parameters: + - description: 空 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.Empty' + produces: + - application/json + responses: + "200": + description: 获取用户动态路由,返回包括系统菜单列表 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.SysBaseMenusResponse' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取用户动态路由 + tags: + - AuthorityMenu + /menu/getMenu: + post: + parameters: + - description: 空 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.Empty' + produces: + - application/json + responses: + "200": + description: 获取用户动态路由,返回包括系统菜单详情列表 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.SysMenusResponse' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取用户动态路由 + tags: + - AuthorityMenu + /menu/getMenuAuthority: + post: + consumes: + - application/json + parameters: + - description: 角色ID + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.GetAuthorityId' + produces: + - application/json + responses: + "200": + description: 获取指定角色menu + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取指定角色menu + tags: + - AuthorityMenu + /menu/getMenuList: + post: + consumes: + - application/json + parameters: + - description: 页码, 每页大小 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.PageInfo' + produces: + - application/json + responses: + "200": + description: 分页获取基础menu列表,返回包括列表,总数,页码,每页数量 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.PageResult' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 分页获取基础menu列表 + tags: + - Menu + /menu/updateBaseMenu: + post: + consumes: + - application/json + parameters: + - description: 路由path, 父菜单ID, 路由name, 对应前端文件路径, 排序标记 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysBaseMenu' + produces: + - application/json + responses: + "200": + description: 更新菜单 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 更新菜单 + tags: + - Menu + /sysDictionary/createSysDictionary: + post: + consumes: + - application/json + parameters: + - description: SysDictionary模型 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysDictionary' + produces: + - application/json + responses: + "200": + description: 创建SysDictionary + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 创建SysDictionary + tags: + - SysDictionary + /sysDictionary/deleteSysDictionary: + delete: + consumes: + - application/json + parameters: + - description: SysDictionary模型 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysDictionary' + produces: + - application/json + responses: + "200": + description: 删除SysDictionary + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 删除SysDictionary + tags: + - SysDictionary + /sysDictionary/findSysDictionary: + get: + consumes: + - application/json + parameters: + - description: 主键ID + in: query + name: ID + type: integer + - description: 创建时间 + in: query + name: createdAt + type: string + - description: 描述 + in: query + name: desc + type: string + - description: 字典名(中) + in: query + name: name + type: string + - description: 状态 + in: query + name: status + type: boolean + - description: 字典名(英) + in: query + name: type + type: string + - description: 更新时间 + in: query + name: updatedAt + type: string + produces: + - application/json + responses: + "200": + description: 用id查询SysDictionary + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 用id查询SysDictionary + tags: + - SysDictionary + /sysDictionary/getSysDictionaryList: + get: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 分页获取SysDictionary列表,返回包括列表,总数,页码,每页数量 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.PageResult' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 分页获取SysDictionary列表 + tags: + - SysDictionary + /sysDictionary/updateSysDictionary: + put: + consumes: + - application/json + parameters: + - description: SysDictionary模型 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysDictionary' + produces: + - application/json + responses: + "200": + description: 更新SysDictionary + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 更新SysDictionary + tags: + - SysDictionary + /sysDictionaryDetail/createSysDictionaryDetail: + post: + consumes: + - application/json + parameters: + - description: SysDictionaryDetail模型 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysDictionaryDetail' + produces: + - application/json + responses: + "200": + description: 创建SysDictionaryDetail + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 创建SysDictionaryDetail + tags: + - SysDictionaryDetail + /sysDictionaryDetail/deleteSysDictionaryDetail: + delete: + consumes: + - application/json + parameters: + - description: SysDictionaryDetail模型 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysDictionaryDetail' + produces: + - application/json + responses: + "200": + description: 删除SysDictionaryDetail + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 删除SysDictionaryDetail + tags: + - SysDictionaryDetail + /sysDictionaryDetail/findSysDictionaryDetail: + get: + consumes: + - application/json + parameters: + - description: 主键ID + in: query + name: ID + type: integer + - description: 创建时间 + in: query + name: createdAt + type: string + - description: 扩展值 + in: query + name: extend + type: string + - description: 展示值 + in: query + name: label + type: string + - description: 排序标记 + in: query + name: sort + type: integer + - description: 启用状态 + in: query + name: status + type: boolean + - description: 关联标记 + in: query + name: sysDictionaryID + type: integer + - description: 更新时间 + in: query + name: updatedAt + type: string + - description: 字典值 + in: query + name: value + type: string + produces: + - application/json + responses: + "200": + description: 用id查询SysDictionaryDetail + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 用id查询SysDictionaryDetail + tags: + - SysDictionaryDetail + /sysDictionaryDetail/getSysDictionaryDetailList: + get: + consumes: + - application/json + parameters: + - description: 主键ID + in: query + name: ID + type: integer + - description: 创建时间 + in: query + name: createdAt + type: string + - description: 扩展值 + in: query + name: extend + type: string + - description: 关键字 + in: query + name: keyword + type: string + - description: 展示值 + in: query + name: label + type: string + - description: 页码 + in: query + name: page + type: integer + - description: 每页大小 + in: query + name: pageSize + type: integer + - description: 排序标记 + in: query + name: sort + type: integer + - description: 启用状态 + in: query + name: status + type: boolean + - description: 关联标记 + in: query + name: sysDictionaryID + type: integer + - description: 更新时间 + in: query + name: updatedAt + type: string + - description: 字典值 + in: query + name: value + type: string + produces: + - application/json + responses: + "200": + description: 分页获取SysDictionaryDetail列表,返回包括列表,总数,页码,每页数量 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.PageResult' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 分页获取SysDictionaryDetail列表 + tags: + - SysDictionaryDetail + /sysDictionaryDetail/updateSysDictionaryDetail: + put: + consumes: + - application/json + parameters: + - description: 更新SysDictionaryDetail + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysDictionaryDetail' + produces: + - application/json + responses: + "200": + description: 更新SysDictionaryDetail + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 更新SysDictionaryDetail + tags: + - SysDictionaryDetail + /sysExportTemplate/createSysExportTemplate: + post: + consumes: + - application/json + parameters: + - description: 创建导出模板 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysExportTemplate' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"创建成功"}' + schema: + type: string + security: + - ApiKeyAuth: [] + summary: 创建导出模板 + tags: + - SysExportTemplate + /sysExportTemplate/deleteSysExportTemplate: + delete: + consumes: + - application/json + parameters: + - description: 删除导出模板 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysExportTemplate' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"删除成功"}' + schema: + type: string + security: + - ApiKeyAuth: [] + summary: 删除导出模板 + tags: + - SysExportTemplate + /sysExportTemplate/deleteSysExportTemplateByIds: + delete: + consumes: + - application/json + parameters: + - description: 批量删除导出模板 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.IdsReq' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"批量删除成功"}' + schema: + type: string + security: + - ApiKeyAuth: [] + summary: 批量删除导出模板 + tags: + - SysExportTemplate + /sysExportTemplate/exportExcel: + get: + consumes: + - application/json + produces: + - application/json + responses: {} + security: + - ApiKeyAuth: [] + summary: 导出表格模板 + tags: + - SysExportTemplate + /sysExportTemplate/findSysExportTemplate: + get: + consumes: + - application/json + parameters: + - description: 主键ID + in: query + name: ID + type: integer + - description: 创建时间 + in: query + name: createdAt + type: string + - description: 数据库名称 + in: query + name: dbName + type: string + - in: query + name: limit + type: integer + - description: 模板名称 + in: query + name: name + type: string + - in: query + name: order + type: string + - description: 表名称 + in: query + name: tableName + type: string + - description: 模板标识 + in: query + name: templateID + type: string + - description: 模板信息 + in: query + name: templateInfo + type: string + - description: 更新时间 + in: query + name: updatedAt + type: string + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"查询成功"}' + schema: + type: string + security: + - ApiKeyAuth: [] + summary: 用id查询导出模板 + tags: + - SysExportTemplate + /sysExportTemplate/getSysExportTemplateList: + get: + consumes: + - application/json + parameters: + - description: 主键ID + in: query + name: ID + type: integer + - description: 创建时间 + in: query + name: createdAt + type: string + - description: 数据库名称 + in: query + name: dbName + type: string + - in: query + name: endCreatedAt + type: string + - description: 关键字 + in: query + name: keyword + type: string + - in: query + name: limit + type: integer + - description: 模板名称 + in: query + name: name + type: string + - in: query + name: order + type: string + - description: 页码 + in: query + name: page + type: integer + - description: 每页大小 + in: query + name: pageSize + type: integer + - in: query + name: startCreatedAt + type: string + - description: 表名称 + in: query + name: tableName + type: string + - description: 模板标识 + in: query + name: templateID + type: string + - description: 模板信息 + in: query + name: templateInfo + type: string + - description: 更新时间 + in: query + name: updatedAt + type: string + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"获取成功"}' + schema: + type: string + security: + - ApiKeyAuth: [] + summary: 分页获取导出模板列表 + tags: + - SysExportTemplate + /sysExportTemplate/importExcel: + post: + consumes: + - application/json + produces: + - application/json + responses: {} + security: + - ApiKeyAuth: [] + summary: 导入表格 + tags: + - SysImportTemplate + /sysExportTemplate/updateSysExportTemplate: + put: + consumes: + - application/json + parameters: + - description: 更新导出模板 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysExportTemplate' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"更新成功"}' + schema: + type: string + security: + - ApiKeyAuth: [] + summary: 更新导出模板 + tags: + - SysExportTemplate + /sysOperationRecord/createSysOperationRecord: + post: + consumes: + - application/json + parameters: + - description: 创建SysOperationRecord + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysOperationRecord' + produces: + - application/json + responses: + "200": + description: 创建SysOperationRecord + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 创建SysOperationRecord + tags: + - SysOperationRecord + /sysOperationRecord/deleteSysOperationRecord: + delete: + consumes: + - application/json + parameters: + - description: SysOperationRecord模型 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysOperationRecord' + produces: + - application/json + responses: + "200": + description: 删除SysOperationRecord + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 删除SysOperationRecord + tags: + - SysOperationRecord + /sysOperationRecord/deleteSysOperationRecordByIds: + delete: + consumes: + - application/json + parameters: + - description: 批量删除SysOperationRecord + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.IdsReq' + produces: + - application/json + responses: + "200": + description: 批量删除SysOperationRecord + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 批量删除SysOperationRecord + tags: + - SysOperationRecord + /sysOperationRecord/findSysOperationRecord: + get: + consumes: + - application/json + parameters: + - description: 主键ID + in: query + name: ID + type: integer + - description: 代理 + in: query + name: agent + type: string + - description: 请求Body + in: query + name: body + type: string + - description: 创建时间 + in: query + name: createdAt + type: string + - description: 错误信息 + in: query + name: error_message + type: string + - description: 请求ip + in: query + name: ip + type: string + - description: 延迟 + in: query + name: latency + type: string + - description: 请求方法 + in: query + name: method + type: string + - description: 请求路径 + in: query + name: path + type: string + - description: 响应Body + in: query + name: resp + type: string + - description: 请求状态 + in: query + name: status + type: integer + - description: 更新时间 + in: query + name: updatedAt + type: string + - description: 用户id + in: query + name: user_id + type: integer + produces: + - application/json + responses: + "200": + description: 用id查询SysOperationRecord + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 用id查询SysOperationRecord + tags: + - SysOperationRecord + /sysOperationRecord/getSysOperationRecordList: + get: + consumes: + - application/json + parameters: + - description: 主键ID + in: query + name: ID + type: integer + - description: 代理 + in: query + name: agent + type: string + - description: 请求Body + in: query + name: body + type: string + - description: 创建时间 + in: query + name: createdAt + type: string + - description: 错误信息 + in: query + name: error_message + type: string + - description: 请求ip + in: query + name: ip + type: string + - description: 关键字 + in: query + name: keyword + type: string + - description: 延迟 + in: query + name: latency + type: string + - description: 请求方法 + in: query + name: method + type: string + - description: 页码 + in: query + name: page + type: integer + - description: 每页大小 + in: query + name: pageSize + type: integer + - description: 请求路径 + in: query + name: path + type: string + - description: 响应Body + in: query + name: resp + type: string + - description: 请求状态 + in: query + name: status + type: integer + - description: 更新时间 + in: query + name: updatedAt + type: string + - description: 用户id + in: query + name: user_id + type: integer + produces: + - application/json + responses: + "200": + description: 分页获取SysOperationRecord列表,返回包括列表,总数,页码,每页数量 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.PageResult' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 分页获取SysOperationRecord列表 + tags: + - SysOperationRecord + /system/getServerInfo: + post: + produces: + - application/json + responses: + "200": + description: 获取服务器信息 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取服务器信息 + tags: + - System + /system/getSystemConfig: + post: + produces: + - application/json + responses: + "200": + description: 获取配置文件内容,返回包括系统配置 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.SysConfigResponse' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取配置文件内容 + tags: + - System + /system/reloadSystem: + post: + produces: + - application/json + responses: + "200": + description: 重启系统 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 重启系统 + tags: + - System + /system/setSystemConfig: + post: + parameters: + - description: 设置配置文件内容 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.System' + produces: + - application/json + responses: + "200": + description: 设置配置文件内容 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 设置配置文件内容 + tags: + - System + /user/SetSelfInfo: + put: + consumes: + - application/json + parameters: + - description: ID, 用户名, 昵称, 头像链接 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysUser' + produces: + - application/json + responses: + "200": + description: 设置用户信息 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 设置用户信息 + tags: + - SysUser + /user/admin_register: + post: + parameters: + - description: 用户名, 昵称, 密码, 角色ID + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.Register' + produces: + - application/json + responses: + "200": + description: 用户注册账号,返回包括用户信息 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.SysUserResponse' + msg: + type: string + type: object + summary: 用户注册账号 + tags: + - SysUser + /user/changePassword: + post: + parameters: + - description: 用户名, 原密码, 新密码 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.ChangePasswordReq' + produces: + - application/json + responses: + "200": + description: 用户修改密码 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 用户修改密码 + tags: + - SysUser + /user/deleteUser: + delete: + consumes: + - application/json + parameters: + - description: 用户ID + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.GetById' + produces: + - application/json + responses: + "200": + description: 删除用户 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 删除用户 + tags: + - SysUser + /user/getUserInfo: + get: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 获取用户信息 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取用户信息 + tags: + - SysUser + /user/getUserList: + post: + consumes: + - application/json + parameters: + - description: 页码, 每页大小 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.PageInfo' + produces: + - application/json + responses: + "200": + description: 分页获取用户列表,返回包括列表,总数,页码,每页数量 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.PageResult' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 分页获取用户列表 + tags: + - SysUser + /user/resetPassword: + post: + parameters: + - description: ID + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysUser' + produces: + - application/json + responses: + "200": + description: 重置用户密码 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 重置用户密码 + tags: + - SysUser + /user/setUserAuthorities: + post: + consumes: + - application/json + parameters: + - description: 用户UUID, 角色ID + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.SetUserAuthorities' + produces: + - application/json + responses: + "200": + description: 设置用户权限 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 设置用户权限 + tags: + - SysUser + /user/setUserAuthority: + post: + consumes: + - application/json + parameters: + - description: 用户UUID, 角色ID + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.SetUserAuth' + produces: + - application/json + responses: + "200": + description: 设置用户权限 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 更改用户权限 + tags: + - SysUser + /user/setUserInfo: + put: + consumes: + - application/json + parameters: + - description: ID, 用户名, 昵称, 头像链接 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysUser' + produces: + - application/json + responses: + "200": + description: 设置用户信息 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 设置用户信息 + tags: + - SysUser +securityDefinitions: + ApiKeyAuth: + in: header + name: x-token + type: apiKey +swagger: "2.0" diff --git a/admin/server/global/global.go b/admin/server/global/global.go new file mode 100644 index 000000000..6e9b79519 --- /dev/null +++ b/admin/server/global/global.go @@ -0,0 +1,67 @@ +package global + +import ( + "fmt" + "sync" + + "github.com/gin-gonic/gin" + "github.com/qiniu/qmgo" + + "github.com/flipped-aurora/gin-vue-admin/server/utils/timer" + "github.com/songzhibin97/gkit/cache/local_cache" + + "golang.org/x/sync/singleflight" + + "go.uber.org/zap" + + "github.com/flipped-aurora/gin-vue-admin/server/config" + + "github.com/redis/go-redis/v9" + "github.com/spf13/viper" + "gorm.io/gorm" +) + +var ( + GVA_DB *gorm.DB + GVA_DBList map[string]*gorm.DB + GVA_REDIS redis.UniversalClient + GVA_REDISList map[string]redis.UniversalClient + GVA_Dify_REDIS redis.UniversalClient // Extend: global code + GVA_MONGO *qmgo.QmgoClient + GVA_CONFIG config.Server + GVA_VP *viper.Viper + // GVA_LOG *oplogging.Logger + GVA_LOG *zap.Logger + GVA_Timer timer.Timer = timer.NewTimerTask() + GVA_Concurrency_Control = &singleflight.Group{} + GVA_ROUTERS gin.RoutesInfo + GVA_ACTIVE_DBNAME *string + BlackCache local_cache.Cache + lock sync.RWMutex +) + +// GetGlobalDBByDBName 通过名称获取db list中的db +func GetGlobalDBByDBName(dbname string) *gorm.DB { + lock.RLock() + defer lock.RUnlock() + return GVA_DBList[dbname] +} + +// MustGetGlobalDBByDBName 通过名称获取db 如果不存在则panic +func MustGetGlobalDBByDBName(dbname string) *gorm.DB { + lock.RLock() + defer lock.RUnlock() + db, ok := GVA_DBList[dbname] + if !ok || db == nil { + panic("db no init") + } + return db +} + +func GetRedis(name string) redis.UniversalClient { + redis, ok := GVA_REDISList[name] + if !ok || redis == nil { + panic(fmt.Sprintf("redis `%s` no init", name)) + } + return redis +} diff --git a/admin/server/global/model.go b/admin/server/global/model.go new file mode 100644 index 000000000..9772eb312 --- /dev/null +++ b/admin/server/global/model.go @@ -0,0 +1,14 @@ +package global + +import ( + "time" + + "gorm.io/gorm" +) + +type GVA_MODEL struct { + ID uint `gorm:"primarykey" json:"ID"` // 主键ID + CreatedAt time.Time // 创建时间 + UpdatedAt time.Time // 更新时间 + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` // 删除时间 +} diff --git a/admin/server/go.mod b/admin/server/go.mod new file mode 100644 index 000000000..f9903e434 --- /dev/null +++ b/admin/server/go.mod @@ -0,0 +1,185 @@ +module github.com/flipped-aurora/gin-vue-admin/server + +go 1.22.0 + +toolchain go1.22.2 + +require ( + github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible + github.com/aws/aws-sdk-go v1.55.5 + github.com/casbin/casbin/v2 v2.100.0 + github.com/casbin/gorm-adapter/v3 v3.28.0 + github.com/fsnotify/fsnotify v1.7.0 + github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6 + github.com/gin-gonic/gin v1.10.0 + github.com/glebarez/sqlite v1.11.0 + github.com/go-resty/resty/v2 v2.15.3 + github.com/go-sql-driver/mysql v1.8.1 + github.com/goccy/go-json v0.10.3 + github.com/gofrs/uuid/v5 v5.3.0 + github.com/golang-jwt/jwt/v4 v4.5.0 + github.com/google/uuid v1.6.0 + github.com/gookit/color v1.5.4 + github.com/huaweicloud/huaweicloud-sdk-go-obs v3.24.9+incompatible + github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible + github.com/mholt/archiver/v4 v4.0.0-alpha.8 + github.com/minio/minio-go/v7 v7.0.78 + github.com/mojocn/base64Captcha v1.3.6 + github.com/otiai10/copy v1.14.0 + github.com/pkg/errors v0.9.1 + github.com/qiniu/go-sdk/v7 v7.23.0 + github.com/qiniu/qmgo v1.1.8 + github.com/redis/go-redis/v9 v9.6.2 + github.com/richardlehane/msoleps v1.0.4 + github.com/robfig/cron/v3 v3.0.1 + github.com/shirou/gopsutil/v3 v3.24.5 + github.com/songzhibin97/gkit v1.2.13 + github.com/spf13/viper v1.19.0 + github.com/stretchr/testify v1.9.0 + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.0 + github.com/swaggo/swag v1.16.3 + github.com/tencentyun/cos-go-sdk-v5 v0.7.55 + github.com/unrolled/secure v1.16.0 + github.com/xuri/excelize/v2 v2.9.0 + go.mongodb.org/mongo-driver v1.17.1 + go.uber.org/automaxprocs v1.6.0 + go.uber.org/zap v1.27.0 + golang.org/x/crypto v0.28.0 + golang.org/x/sync v0.8.0 + golang.org/x/text v0.19.0 + gorm.io/datatypes v1.2.3 + gorm.io/driver/mysql v1.5.7 + gorm.io/driver/postgres v1.5.9 + gorm.io/driver/sqlserver v1.5.3 + gorm.io/gen v0.3.26 + gorm.io/gorm v1.25.12 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/BurntSushi/toml v1.4.0 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 // indirect + github.com/andybalholm/brotli v1.1.1 // indirect + github.com/bmatcuk/doublestar/v4 v4.7.1 // indirect + github.com/bodgit/plumbing v1.3.0 // indirect + github.com/bodgit/sevenzip v1.5.2 // indirect + github.com/bodgit/windows v1.0.1 // indirect + github.com/bytedance/sonic v1.12.3 // indirect + github.com/bytedance/sonic/loader v0.2.0 // indirect + github.com/casbin/govaluate v1.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/clbanning/mxj v1.8.4 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/dsnet/compress v0.0.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/elastic/go-sysinfo v1.14.2 // indirect + github.com/elastic/go-windows v1.0.2 // indirect + github.com/faabiosr/cachego v0.15.0 // indirect + github.com/fastwego/dingding v1.0.0-beta.4 // indirect + github.com/gabriel-vasile/mimetype v1.4.6 // indirect + github.com/gammazero/toposort v0.1.1 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/glebarez/go-sqlite v1.22.0 // indirect + github.com/go-ini/ini v1.67.0 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/spec v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.22.1 // indirect + github.com/gofrs/flock v0.12.1 // indirect + github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect + github.com/golang-sql/sqlexp v0.1.0 // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.1 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/klauspost/cpuid/v2 v2.2.8 // indirect + github.com/klauspost/pgzip v1.2.6 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/microsoft/go-mssqldb v1.7.2 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect + github.com/mozillazg/go-httpheader v0.4.0 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/nwaples/rardecode/v2 v2.0.0-beta.3 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pierrec/lz4/v4 v4.1.21 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/richardlehane/mscfb v1.0.4 // indirect + github.com/rs/xid v1.6.0 // indirect + github.com/sagikazarmark/locafero v0.6.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.7.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/therootcompany/xz v1.0.1 // indirect + github.com/tklauser/go-sysconf v0.3.14 // indirect + github.com/tklauser/numcpus v0.9.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + github.com/ulikunitz/xz v0.5.12 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect + github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.uber.org/multierr v1.11.0 // indirect + go4.org v0.0.0-20230225012048-214862532bf5 // indirect + golang.org/x/arch v0.11.0 // indirect + golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect + golang.org/x/image v0.21.0 // indirect + golang.org/x/mod v0.21.0 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/time v0.7.0 // indirect + golang.org/x/tools v0.26.0 // indirect + google.golang.org/protobuf v1.35.1 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/hints v1.1.2 // indirect + gorm.io/plugin/dbresolver v1.5.3 // indirect + howett.net/plist v1.0.1 // indirect + modernc.org/fileutil v1.3.0 // indirect + modernc.org/libc v1.61.0 // indirect + modernc.org/mathutil v1.6.0 // indirect + modernc.org/memory v1.8.0 // indirect + modernc.org/sqlite v1.33.1 // indirect +) diff --git a/admin/server/go.sum b/admin/server/go.sum new file mode 100644 index 000000000..3c27f7940 --- /dev/null +++ b/admin/server/go.sum @@ -0,0 +1,859 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0/go.mod h1:ON4tFdPTwRcgWEaVDrN3584Ef+b7GgSJaXxe5fW9t4M= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 h1:lGlwhPtrX6EVml1hO0ivjkUxsSyl4dsiw9qcA1k/3IQ= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 h1:sO0/P7g68FrryJzljemN+6GTssUXdANk6aJ7T1ZxnsQ= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.2.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 h1:6oNBlSdi1QqM1PNW7FPA6xOGA5UNsXnkaYZz9vdPGhA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0/go.mod h1:Q28U+75mpCaSCDowNEmhIo/rmgdkqmkmzI7N6TGR4UY= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0/go.mod h1:cw4zVQgBby0Z5f2v0itn6se2dDP17nTjbZFXW5uPyHA= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o= +github.com/AzureAD/microsoft-authentication-library-for-go v1.1.0/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 h1:DzHpqpoJVaCgOUdVHxE8QB52S6NiVdDQvGlny1qvPqA= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409/go.mod h1:1pk82RBxDY/JZnPQrtqHlUFfCctgdorsd9M06fMynOM= +github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 h1:7dONQ3WNZ1zy960TmkxJPuwoolZwL7xKtpcM04MBnt4= +github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82/go.mod h1:nLnM0KdK1CmygvjpDUO6m1TjSsiQtL61juhNsvV/JVI= +github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g= +github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU= +github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bmatcuk/doublestar/v4 v4.7.1 h1:fdDeAqgT47acgwd9bd9HxJRDmc9UAmPpc+2m0CXv75Q= +github.com/bmatcuk/doublestar/v4 v4.7.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU= +github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs= +github.com/bodgit/sevenzip v1.5.2 h1:acMIYRaqoHAdeu9LhEGGjL9UzBD4RNf9z7+kWDNignI= +github.com/bodgit/sevenzip v1.5.2/go.mod h1:gTGzXA67Yko6/HLSD0iK4kWaWzPlPmLfDO73jTjSRqc= +github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= +github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= +github.com/bradfitz/gomemcache v0.0.0-20170208213004-1952afaa557d/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/bytedance/sonic v1.12.3 h1:W2MGa7RCU1QTeYRTPE3+88mVC0yXmsRQRChiyVocVjU= +github.com/bytedance/sonic v1.12.3/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM= +github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/casbin/casbin/v2 v2.100.0 h1:aeugSNjjHfCrgA22nHkVvw2xsscboHv5r0a13ljQKGQ= +github.com/casbin/casbin/v2 v2.100.0/go.mod h1:LO7YPez4dX3LgoTCqSQAleQDo0S0BeZBDxYnPUl95Ng= +github.com/casbin/gorm-adapter/v3 v3.28.0 h1:ORF8prF6SfaipdgT1fud+r1Tp5J0uul8QaKJHqCPY/o= +github.com/casbin/gorm-adapter/v3 v3.28.0/go.mod h1:aftWi0cla0CC1bHQVrSFzBcX/98IFK28AvuPppCQgTs= +github.com/casbin/govaluate v1.2.0 h1:wXCXFmqyY+1RwiKfYo3jMKyrtZmOL3kHwaqDyCPOYak= +github.com/casbin/govaluate v1.2.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I= +github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/dave/jennifer v1.6.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q= +github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo= +github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/elastic/go-sysinfo v1.0.2/go.mod h1:O/D5m1VpYLwGjCYzEt63g3Z1uO3jXfwyzzjiW90t8cY= +github.com/elastic/go-sysinfo v1.14.2 h1:DeIy+pVfdRsd08Nx2Xjh+dUS+jrEEI7LGc29U/BKVWo= +github.com/elastic/go-sysinfo v1.14.2/go.mod h1:jPSuTgXG+dhhh0GKIyI2Cso+w5lPJ5PvVqKlL8LV/Hk= +github.com/elastic/go-windows v1.0.0/go.mod h1:TsU0Nrp7/y3+VwE82FoZF8gC/XFg/Elz6CcloAxnPgU= +github.com/elastic/go-windows v1.0.2 h1:yoLLsAsV5cfg9FLhZ9EXZ2n2sQFKeDYrHenkcivY4vI= +github.com/elastic/go-windows v1.0.2/go.mod h1:bGcDpBzXgYSqM0Gx3DM4+UxFj300SZLixie9u9ixLM8= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/faabiosr/cachego v0.15.0 h1:IqcDhvzMbL4a1c9Dek88DIWJYQ5HG//L0PKCReneOA4= +github.com/faabiosr/cachego v0.15.0/go.mod h1:L2EomlU3/rUWjzFavY9Fwm8B4zZmX2X6u8kTMkETrwI= +github.com/fastwego/dingding v1.0.0-beta.4 h1:lhO7OyCIvhFQb/N8OAwi/KDqtq94tb06FfnDGfv5PKM= +github.com/fastwego/dingding v1.0.0-beta.4/go.mod h1:EphZlfmXhp/2PSkQt1sy9Q1P2Co/5ioFZUXPyyy4wY4= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6 h1:6VSn3hB5U5GeA6kQw4TwWIWbOhtvR2hmbBJnTOtqTWc= +github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6/go.mod h1:YxOVT5+yHzKvwhsiSIWmbAYM3Dr9AEEbER2dVayfBkg= +github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc= +github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc= +github.com/gammazero/toposort v0.1.1 h1:OivGxsWxF3U3+U80VoLJ+f50HcPU1MIqE1JlKzoJ2Eg= +github.com/gammazero/toposort v0.1.1/go.mod h1:H2cozTnNpMw0hg2VHAYsAxmkHXBYroNangj2NTBQDvw= +github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= +github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= +github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= +github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= +github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= +github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= +github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-playground/validator/v10 v10.7.0/go.mod h1:xm76BBt941f7yWdGnI2DVPFFg1UK3YY04qifoXU3lOk= +github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= +github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-resty/resty/v2 v2.15.3 h1:bqff+hcqAflpiF591hhJzNdkRsFhlB96CYfBwSFvql8= +github.com/go-resty/resty/v2 v2.15.3/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gofrs/uuid/v5 v5.3.0 h1:m0mUMr+oVYUdxpMLgSYCZiXe7PuVPnI94+OMeVBNedk= +github.com/gofrs/uuid/v5 v5.3.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= +github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= +github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huaweicloud/huaweicloud-sdk-go-obs v3.24.9+incompatible h1:XQVXdk+WAJ4fSNB6mMRuYNvFWou7BZs6SZB925hPrnk= +github.com/huaweicloud/huaweicloud-sdk-go-obs v3.24.9+incompatible/go.mod h1:l7VUhRbTKCzdOacdT4oWCwATKyvZqUOlOqr0Ous3k4s= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= +github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak= +github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA= +github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= +github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= +github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0= +github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/matishsiao/goInfo v0.0.0-20210923090445-da2e3fa8d45f/go.mod h1:aEt7p9Rvh67BYApmZwNDPpgircTO2kgdmDUoF/1QmwA= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mholt/archiver/v4 v4.0.0-alpha.8 h1:tRGQuDVPh66WCOelqe6LIGh0gwmfwxUrSSDunscGsRM= +github.com/mholt/archiver/v4 v4.0.0-alpha.8/go.mod h1:5f7FUYGXdJWUjESffJaYR4R60VhnHxb2X3T1teMyv5A= +github.com/microsoft/go-mssqldb v1.6.0/go.mod h1:00mDtPbeQCRGC1HwOOR5K/gr30P1NcEG0vx6Kbv2aJU= +github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA= +github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.78 h1:LqW2zy52fxnI4gg8C2oZviTaKHcBV36scS+RzJnxUFs= +github.com/minio/minio-go/v7 v7.0.78/go.mod h1:84gmIilaX4zcvAWWzJ5Z1WI5axN+hAbM5w25xf8xvC0= +github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/mojocn/base64Captcha v1.3.6 h1:gZEKu1nsKpttuIAQgWHO+4Mhhls8cAKyiV2Ew03H+Tw= +github.com/mojocn/base64Captcha v1.3.6/go.mod h1:i5CtHvm+oMbj1UzEPXaA8IH/xHFZ3DGY3Wh3dBpZ28E= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/mozillazg/go-httpheader v0.2.1/go.mod h1:jJ8xECTlalr6ValeXYdOF8fFUISeBAdw6E61aqQma60= +github.com/mozillazg/go-httpheader v0.4.0 h1:aBn6aRXtFzyDLZ4VIRLsZbbJloagQfMnCiYgOq6hK4w= +github.com/mozillazg/go-httpheader v0.4.0/go.mod h1:PuT8h0pw6efvp8ZeUec1Rs7dwjK08bt6gKSReGMqtdA= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nwaples/rardecode/v2 v2.0.0-beta.3 h1:evQTW0IjM2GAL5AaPHiQrT+laWohkt5zHKA3yCsGQGU= +github.com/nwaples/rardecode/v2 v2.0.0-beta.3/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= +github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= +github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks= +github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= +github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/procfs v0.0.0-20190425082905-87a4384529e0/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdkk= +github.com/qiniu/go-sdk/v7 v7.23.0 h1:4wYB4EGE6MBhvjtE/FZH/mIUt/VH6WjzBucU3VfPwhg= +github.com/qiniu/go-sdk/v7 v7.23.0/go.mod h1:OXsAVU5YrLLtVi4iPFpP80jzb3SRBAczrGkcqQmWhcY= +github.com/qiniu/qmgo v1.1.8 h1:E64M+P59aqQpXKI24ClVtluYkLaJLkkeD2hTVhrdMks= +github.com/qiniu/qmgo v1.1.8/go.mod h1:QvZkzWNEv0buWPx0kdZsSs6URhESVubacxFPlITmvB8= +github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs= +github.com/redis/go-redis/v9 v9.6.2 h1:w0uvkRbc9KpgD98zcvo5IrVUsn0lXpRMuhNgiHDJzdk= +github.com/redis/go-redis/v9 v9.6.2/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= +github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= +github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00= +github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= +github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= +github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= +github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/songzhibin97/gkit v1.2.13 h1:paY0XJkdRuy9/8k9nTnbdrzo8pC22jIIFldUkOQv5nU= +github.com/songzhibin97/gkit v1.2.13/go.mod h1:38CreNR27eTGaG1UMGihrXqI4xc3nGfYxLVKKVx6Ngg= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= +github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= +github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= +github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.563/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/kms v1.0.563/go.mod h1:uom4Nvi9W+Qkom0exYiJ9VWJjXwyxtPYTkKkaLMlfE0= +github.com/tencentyun/cos-go-sdk-v5 v0.7.55 h1:9DfH3umWUd0I2jdqcUxrU1kLfUPOydULNy4T9qN5PF8= +github.com/tencentyun/cos-go-sdk-v5 v0.7.55/go.mod h1:8+hG+mQMuRP/OIS9d83syAvXvrMj9HhkND6Q1fLghw0= +github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= +github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= +github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= +github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo= +github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= +github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= +github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/unrolled/secure v1.16.0 h1:XgdAsS/Zl50ZfZPRJK6WpicFttfrsFYFd0+ONDBJubU= +github.com/unrolled/secure v1.16.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY= +github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE= +github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE= +github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= +github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.mongodb.org/mongo-driver v1.11.6/go.mod h1:G9TgswdsWjX4tmDA5zfs2+6AEPpYJwqblyjsfuh8oXY= +go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM= +go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc= +go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU= +golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4= +golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk= +golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s= +golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190425145619-16072639606e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/bsm/ratelimit.v1 v1.0.0-20160220154919-db14e161995a/go.mod h1:KF9sEfUPAXdG8Oev9e99iLGnl2uJMjc5B+4y3O7x610= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/mgo.v2 v2.0.0-20160818020120-3f83fa500528/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/redis.v4 v4.2.4/go.mod h1:8KREHdypkCEojGKQcjMqAODMICIVwZAONWq8RowTITA= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/datatypes v1.2.3 h1:95ucr9ip9dZMPhB3Tc9zbcoAi62hxYAgHicu7SLjK4g= +gorm.io/datatypes v1.2.3/go.mod h1:f4BsLcFAX67szSv8svwLRjklArSHAvHLeE3pXAS5DZI= +gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= +gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= +gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8= +gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= +gorm.io/driver/sqlite v1.5.0 h1:zKYbzRCpBrT1bNijRnxLDJWPjVfImGEn0lSnUY5gZ+c= +gorm.io/driver/sqlite v1.5.0/go.mod h1:kDMDfntV9u/vuMmz8APHtHF0b4nyBB7sfCieC6G8k8I= +gorm.io/driver/sqlserver v1.5.3 h1:rjupPS4PVw+rjJkfvr8jn2lJ8BMhT4UW5FwuJY0P3Z0= +gorm.io/driver/sqlserver v1.5.3/go.mod h1:B+CZ0/7oFJ6tAlefsKoyxdgDCXJKSgwS2bMOQZT0I00= +gorm.io/gen v0.3.26 h1:sFf1j7vNStimPRRAtH4zz5NiHM+1dr6eA9aaRdplyhY= +gorm.io/gen v0.3.26/go.mod h1:a5lq5y3w4g5LMxBcw0wnO6tYUCdNutWODq5LrIt75LE= +gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +gorm.io/hints v1.1.2 h1:b5j0kwk5p4+3BtDtYqqfY+ATSxjj+6ptPgVveuynn9o= +gorm.io/hints v1.1.2/go.mod h1:/ARdpUHAtyEMCh5NNi3tI7FsGh+Cj/MIUlvNxCNCFWg= +gorm.io/plugin/dbresolver v1.5.3 h1:wFwINGZZmttuu9h7XpvbDHd8Lf9bb8GNzp/NpAMV2wU= +gorm.io/plugin/dbresolver v1.5.3/go.mod h1:TSrVhaUg2DZAWP3PrHlDlITEJmNOkL0tFTjvTEsQ4XE= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= +howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM= +howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= +modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= +modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= +modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4= +modernc.org/ccgo/v4 v4.21.0/go.mod h1:h6kt6H/A2+ew/3MW/p6KEoQmrq/i3pr0J/SiwiaF/g0= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M= +modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= +modernc.org/libc v1.61.0 h1:eGFcvWpqlnoGwzZeZe3PWJkkKbM/3SUGyk1DVZQ0TpE= +modernc.org/libc v1.61.0/go.mod h1:DvxVX89wtGTu+r72MLGhygpfi3aUGgZRdAYGCAVVud0= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= +modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= +modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM= +modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/admin/server/initialize/db_list.go b/admin/server/initialize/db_list.go new file mode 100644 index 000000000..90eef9ea1 --- /dev/null +++ b/admin/server/initialize/db_list.go @@ -0,0 +1,36 @@ +package initialize + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/config" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "gorm.io/gorm" +) + +const sys = "system" + +func DBList() { + dbMap := make(map[string]*gorm.DB) + for _, info := range global.GVA_CONFIG.DBList { + if info.Disable { + continue + } + switch info.Type { + case "mysql": + dbMap[info.AliasName] = GormMysqlByConfig(config.Mysql{GeneralDB: info.GeneralDB}) + case "mssql": + dbMap[info.AliasName] = GormMssqlByConfig(config.Mssql{GeneralDB: info.GeneralDB}) + case "pgsql": + dbMap[info.AliasName] = GormPgSqlByConfig(config.Pgsql{GeneralDB: info.GeneralDB}) + case "oracle": + dbMap[info.AliasName] = GormOracleByConfig(config.Oracle{GeneralDB: info.GeneralDB}) + default: + continue + } + } + // 做特殊判断,是否有迁移 + // 适配低版本迁移多数据库版本 + if sysDB, ok := dbMap[sys]; ok { + global.GVA_DB = sysDB + } + global.GVA_DBList = dbMap +} diff --git a/admin/server/initialize/ensure_tables.go b/admin/server/initialize/ensure_tables.go new file mode 100644 index 000000000..f4f19990f --- /dev/null +++ b/admin/server/initialize/ensure_tables.go @@ -0,0 +1,119 @@ +package initialize + +import ( + "context" + adapter "github.com/casbin/gorm-adapter/v3" + "github.com/flipped-aurora/gin-vue-admin/server/model/example" + "github.com/flipped-aurora/gin-vue-admin/server/model/gaia" + sysModel "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/service/system" + "gorm.io/gorm" +) + +const initOrderEnsureTables = system.InitOrderExternal - 1 + +type ensureTables struct{} + +// auto run +func init() { + system.RegisterInit(initOrderEnsureTables, &ensureTables{}) +} + +func (ensureTables) InitializerName() string { + return "ensure_tables_created" +} +func (e *ensureTables) InitializeData(ctx context.Context) (next context.Context, err error) { + return ctx, nil +} + +func (e *ensureTables) DataInserted(ctx context.Context) bool { + return true +} + +func (e *ensureTables) MigrateTable(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + tables := []interface{}{ + sysModel.SysApi{}, + sysModel.SysUser{}, + sysModel.SysBaseMenu{}, + sysModel.SysAuthority{}, + sysModel.JwtBlacklist{}, + sysModel.SysDictionary{}, + sysModel.SysAutoCodeHistory{}, + sysModel.SysOperationRecord{}, + sysModel.SysDictionaryDetail{}, + sysModel.SysBaseMenuParameter{}, + sysModel.SysBaseMenuBtn{}, + sysModel.SysAuthorityBtn{}, + sysModel.SysAutoCodePackage{}, + sysModel.SysExportTemplate{}, + sysModel.Condition{}, + sysModel.JoinTemplate{}, + sysModel.SysParams{}, + + adapter.CasbinRule{}, + + example.ExaFile{}, + example.ExaCustomer{}, + example.ExaFileChunk{}, + example.ExaFileUploadAndDownload{}, + + // Extend gaia model + gaia.AccountDingTalkExtend{}, + gaia.AppRequestTestBatch{}, + gaia.AppRequestTest{}, + // Extend gaia model + } + for _, t := range tables { + _ = db.AutoMigrate(&t) + // 视图 authority_menu 会被当成表来创建,引发冲突错误(更新版本的gorm似乎不会) + // 由于 AutoMigrate() 基本无需考虑错误,因此显式忽略 + } + return ctx, nil +} + +func (e *ensureTables) TableCreated(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + tables := []interface{}{ + sysModel.SysApi{}, + sysModel.SysUser{}, + sysModel.SysBaseMenu{}, + sysModel.SysAuthority{}, + sysModel.JwtBlacklist{}, + sysModel.SysDictionary{}, + sysModel.SysAutoCodeHistory{}, + sysModel.SysOperationRecord{}, + sysModel.SysDictionaryDetail{}, + sysModel.SysBaseMenuParameter{}, + sysModel.SysBaseMenuBtn{}, + sysModel.SysAuthorityBtn{}, + sysModel.SysAutoCodePackage{}, + sysModel.SysExportTemplate{}, + sysModel.Condition{}, + sysModel.JoinTemplate{}, + + adapter.CasbinRule{}, + + example.ExaFile{}, + example.ExaCustomer{}, + example.ExaFileChunk{}, + example.ExaFileUploadAndDownload{}, + + // Extend gaia model + gaia.AccountDingTalkExtend{}, + gaia.AppRequestTestBatch{}, + gaia.AppRequestTest{}, + // Extend gaia model + } + yes := true + for _, t := range tables { + yes = yes && db.Migrator().HasTable(t) + } + return yes +} diff --git a/admin/server/initialize/gorm.go b/admin/server/initialize/gorm.go new file mode 100644 index 000000000..c29c33fec --- /dev/null +++ b/admin/server/initialize/gorm.go @@ -0,0 +1,85 @@ +package initialize + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/example" + "github.com/flipped-aurora/gin-vue-admin/server/model/gaia" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "os" + + "go.uber.org/zap" + "gorm.io/gorm" +) + +func Gorm() *gorm.DB { + switch global.GVA_CONFIG.System.DbType { + case "mysql": + global.GVA_ACTIVE_DBNAME = &global.GVA_CONFIG.Mysql.Dbname + return GormMysql() + case "pgsql": + global.GVA_ACTIVE_DBNAME = &global.GVA_CONFIG.Pgsql.Dbname + return GormPgSql() + case "oracle": + global.GVA_ACTIVE_DBNAME = &global.GVA_CONFIG.Oracle.Dbname + return GormOracle() + case "mssql": + global.GVA_ACTIVE_DBNAME = &global.GVA_CONFIG.Mssql.Dbname + return GormMssql() + case "sqlite": + global.GVA_ACTIVE_DBNAME = &global.GVA_CONFIG.Sqlite.Dbname + return GormSqlite() + default: + global.GVA_ACTIVE_DBNAME = &global.GVA_CONFIG.Mysql.Dbname + return GormMysql() + } +} + +func RegisterTables() { + db := global.GVA_DB + err := db.AutoMigrate( + + system.SysApi{}, + system.SysIgnoreApi{}, + system.SysUser{}, + system.SysBaseMenu{}, + system.JwtBlacklist{}, + system.SysAuthority{}, + system.SysDictionary{}, + system.SysOperationRecord{}, + system.SysAutoCodeHistory{}, + system.SysDictionaryDetail{}, + system.SysBaseMenuParameter{}, + system.SysBaseMenuBtn{}, + system.SysAuthorityBtn{}, + system.SysAutoCodePackage{}, + system.SysExportTemplate{}, + system.Condition{}, + system.JoinTemplate{}, + system.SysParams{}, + + example.ExaFile{}, + example.ExaCustomer{}, + example.ExaFileChunk{}, + example.ExaFileUploadAndDownload{}, + + // Extend gaia model + gaia.AccountDingTalkExtend{}, + gaia.AppRequestTestBatch{}, + gaia.AppRequestTest{}, + // Extend gaia model + gaia.SystemIntegration{}, // Extend System Integration + system.SysUserGlobalCode{}, // Extend Global Code + ) + if err != nil { + global.GVA_LOG.Error("register table failed", zap.Error(err)) + os.Exit(0) + } + + err = bizModel() + + if err != nil { + global.GVA_LOG.Error("register biz_table failed", zap.Error(err)) + os.Exit(0) + } + global.GVA_LOG.Info("register table success") +} diff --git a/admin/server/initialize/gorm_biz.go b/admin/server/initialize/gorm_biz.go new file mode 100644 index 000000000..9316ccc88 --- /dev/null +++ b/admin/server/initialize/gorm_biz.go @@ -0,0 +1,14 @@ +package initialize + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +func bizModel() error { + db := global.GVA_DB + err := db.AutoMigrate() + if err != nil { + return err + } + return nil +} diff --git a/admin/server/initialize/gorm_mssql.go b/admin/server/initialize/gorm_mssql.go new file mode 100644 index 000000000..0ec25a7f5 --- /dev/null +++ b/admin/server/initialize/gorm_mssql.go @@ -0,0 +1,59 @@ +/* + * @Author: 逆光飞翔 191180776@qq.com + * @Date: 2022-12-08 17:25:49 + * @LastEditors: 逆光飞翔 191180776@qq.com + * @LastEditTime: 2022-12-08 18:00:00 + * @FilePath: \server\initialize\gorm_mssql.go + * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE + */ +package initialize + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/config" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/initialize/internal" + "gorm.io/driver/sqlserver" + "gorm.io/gorm" +) + +// GormMssql 初始化Mssql数据库 +// Author [LouisZhang](191180776@qq.com) +func GormMssql() *gorm.DB { + m := global.GVA_CONFIG.Mssql + if m.Dbname == "" { + return nil + } + mssqlConfig := sqlserver.Config{ + DSN: m.Dsn(), // DSN data source name + DefaultStringSize: 191, // string 类型字段的默认长度 + } + if db, err := gorm.Open(sqlserver.New(mssqlConfig), internal.Gorm.Config(m.Prefix, m.Singular)); err != nil { + return nil + } else { + db.InstanceSet("gorm:table_options", "ENGINE="+m.Engine) + sqlDB, _ := db.DB() + sqlDB.SetMaxIdleConns(m.MaxIdleConns) + sqlDB.SetMaxOpenConns(m.MaxOpenConns) + return db + } +} + +// GormMssqlByConfig 初始化Mysql数据库用过传入配置 +func GormMssqlByConfig(m config.Mssql) *gorm.DB { + if m.Dbname == "" { + return nil + } + mssqlConfig := sqlserver.Config{ + DSN: m.Dsn(), // DSN data source name + DefaultStringSize: 191, // string 类型字段的默认长度 + } + if db, err := gorm.Open(sqlserver.New(mssqlConfig), internal.Gorm.Config(m.Prefix, m.Singular)); err != nil { + panic(err) + } else { + db.InstanceSet("gorm:table_options", "ENGINE=InnoDB") + sqlDB, _ := db.DB() + sqlDB.SetMaxIdleConns(m.MaxIdleConns) + sqlDB.SetMaxOpenConns(m.MaxOpenConns) + return db + } +} diff --git a/admin/server/initialize/gorm_mysql.go b/admin/server/initialize/gorm_mysql.go new file mode 100644 index 000000000..6e496a4d3 --- /dev/null +++ b/admin/server/initialize/gorm_mysql.go @@ -0,0 +1,55 @@ +package initialize + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/config" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/initialize/internal" + _ "github.com/go-sql-driver/mysql" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +// GormMysql 初始化Mysql数据库 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func GormMysql() *gorm.DB { + m := global.GVA_CONFIG.Mysql + if m.Dbname == "" { + return nil + } + mysqlConfig := mysql.Config{ + DSN: m.Dsn(), // DSN data source name + DefaultStringSize: 191, // string 类型字段的默认长度 + SkipInitializeWithVersion: false, // 根据版本自动配置 + } + if db, err := gorm.Open(mysql.New(mysqlConfig), internal.Gorm.Config(m.Prefix, m.Singular)); err != nil { + return nil + } else { + db.InstanceSet("gorm:table_options", "ENGINE="+m.Engine) + sqlDB, _ := db.DB() + sqlDB.SetMaxIdleConns(m.MaxIdleConns) + sqlDB.SetMaxOpenConns(m.MaxOpenConns) + return db + } +} + +// GormMysqlByConfig 初始化Mysql数据库用过传入配置 +func GormMysqlByConfig(m config.Mysql) *gorm.DB { + if m.Dbname == "" { + return nil + } + mysqlConfig := mysql.Config{ + DSN: m.Dsn(), // DSN data source name + DefaultStringSize: 191, // string 类型字段的默认长度 + SkipInitializeWithVersion: false, // 根据版本自动配置 + } + if db, err := gorm.Open(mysql.New(mysqlConfig), internal.Gorm.Config(m.Prefix, m.Singular)); err != nil { + panic(err) + } else { + db.InstanceSet("gorm:table_options", "ENGINE=InnoDB") + sqlDB, _ := db.DB() + sqlDB.SetMaxIdleConns(m.MaxIdleConns) + sqlDB.SetMaxOpenConns(m.MaxOpenConns) + return db + } +} diff --git a/admin/server/initialize/gorm_oracle.go b/admin/server/initialize/gorm_oracle.go new file mode 100644 index 000000000..4d18c8a84 --- /dev/null +++ b/admin/server/initialize/gorm_oracle.go @@ -0,0 +1,52 @@ +package initialize + +import ( + //"github.com/dzwvip/oracle" + "github.com/flipped-aurora/gin-vue-admin/server/config" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/initialize/internal" + + //_ "github.com/godror/godror" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +// GormOracle 初始化oracle数据库 +// 如果需要Oracle库 放开import里的注释 把下方 mysql.Config 改为 oracle.Config ; mysql.New 改为 oracle.New +func GormOracle() *gorm.DB { + m := global.GVA_CONFIG.Oracle + if m.Dbname == "" { + return nil + } + oracleConfig := mysql.Config{ + DSN: m.Dsn(), // DSN data source name + DefaultStringSize: 191, // string 类型字段的默认长度 + } + if db, err := gorm.Open(mysql.New(oracleConfig), internal.Gorm.Config(m.Prefix, m.Singular)); err != nil { + panic(err) + } else { + sqlDB, _ := db.DB() + sqlDB.SetMaxIdleConns(m.MaxIdleConns) + sqlDB.SetMaxOpenConns(m.MaxOpenConns) + return db + } +} + +// GormOracleByConfig 初始化Oracle数据库用过传入配置 +func GormOracleByConfig(m config.Oracle) *gorm.DB { + if m.Dbname == "" { + return nil + } + oracleConfig := mysql.Config{ + DSN: m.Dsn(), // DSN data source name + DefaultStringSize: 191, // string 类型字段的默认长度 + } + if db, err := gorm.Open(mysql.New(oracleConfig), internal.Gorm.Config(m.Prefix, m.Singular)); err != nil { + panic(err) + } else { + sqlDB, _ := db.DB() + sqlDB.SetMaxIdleConns(m.MaxIdleConns) + sqlDB.SetMaxOpenConns(m.MaxOpenConns) + return db + } +} diff --git a/admin/server/initialize/gorm_pgsql.go b/admin/server/initialize/gorm_pgsql.go new file mode 100644 index 000000000..625c87385 --- /dev/null +++ b/admin/server/initialize/gorm_pgsql.go @@ -0,0 +1,50 @@ +package initialize + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/config" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/initialize/internal" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +// GormPgSql 初始化 Postgresql 数据库 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func GormPgSql() *gorm.DB { + p := global.GVA_CONFIG.Pgsql + if p.Dbname == "" { + return nil + } + pgsqlConfig := postgres.Config{ + DSN: p.Dsn(), // DSN data source name + PreferSimpleProtocol: false, + } + if db, err := gorm.Open(postgres.New(pgsqlConfig), internal.Gorm.Config(p.Prefix, p.Singular)); err != nil { + return nil + } else { + sqlDB, _ := db.DB() + sqlDB.SetMaxIdleConns(p.MaxIdleConns) + sqlDB.SetMaxOpenConns(p.MaxOpenConns) + return db + } +} + +// GormPgSqlByConfig 初始化 Postgresql 数据库 通过参数 +func GormPgSqlByConfig(p config.Pgsql) *gorm.DB { + if p.Dbname == "" { + return nil + } + pgsqlConfig := postgres.Config{ + DSN: p.Dsn(), // DSN data source name + PreferSimpleProtocol: false, + } + if db, err := gorm.Open(postgres.New(pgsqlConfig), internal.Gorm.Config(p.Prefix, p.Singular)); err != nil { + panic(err) + } else { + sqlDB, _ := db.DB() + sqlDB.SetMaxIdleConns(p.MaxIdleConns) + sqlDB.SetMaxOpenConns(p.MaxOpenConns) + return db + } +} diff --git a/admin/server/initialize/gorm_sqlite.go b/admin/server/initialize/gorm_sqlite.go new file mode 100644 index 000000000..041264107 --- /dev/null +++ b/admin/server/initialize/gorm_sqlite.go @@ -0,0 +1,42 @@ +package initialize + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/config" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/initialize/internal" + "github.com/glebarez/sqlite" + "gorm.io/gorm" +) + +// GormSqlite 初始化Sqlite数据库 +func GormSqlite() *gorm.DB { + s := global.GVA_CONFIG.Sqlite + if s.Dbname == "" { + return nil + } + + if db, err := gorm.Open(sqlite.Open(s.Dsn()), internal.Gorm.Config(s.Prefix, s.Singular)); err != nil { + panic(err) + } else { + sqlDB, _ := db.DB() + sqlDB.SetMaxIdleConns(s.MaxIdleConns) + sqlDB.SetMaxOpenConns(s.MaxOpenConns) + return db + } +} + +// GormSqliteByConfig 初始化Sqlite数据库用过传入配置 +func GormSqliteByConfig(s config.Sqlite) *gorm.DB { + if s.Dbname == "" { + return nil + } + + if db, err := gorm.Open(sqlite.Open(s.Dsn()), internal.Gorm.Config(s.Prefix, s.Singular)); err != nil { + panic(err) + } else { + sqlDB, _ := db.DB() + sqlDB.SetMaxIdleConns(s.MaxIdleConns) + sqlDB.SetMaxOpenConns(s.MaxOpenConns) + return db + } +} diff --git a/admin/server/initialize/internal/gorm.go b/admin/server/initialize/internal/gorm.go new file mode 100644 index 000000000..dcf388bea --- /dev/null +++ b/admin/server/initialize/internal/gorm.go @@ -0,0 +1,48 @@ +package internal + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/config" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "gorm.io/gorm" + "gorm.io/gorm/logger" + "gorm.io/gorm/schema" + "log" + "os" + "time" +) + +var Gorm = new(_gorm) + +type _gorm struct{} + +// Config gorm 自定义配置 +// Author [SliverHorn](https://github.com/SliverHorn) +func (g *_gorm) Config(prefix string, singular bool) *gorm.Config { + var general config.GeneralDB + switch global.GVA_CONFIG.System.DbType { + case "mysql": + general = global.GVA_CONFIG.Mysql.GeneralDB + case "pgsql": + general = global.GVA_CONFIG.Pgsql.GeneralDB + case "oracle": + general = global.GVA_CONFIG.Oracle.GeneralDB + case "sqlite": + general = global.GVA_CONFIG.Sqlite.GeneralDB + case "mssql": + general = global.GVA_CONFIG.Mssql.GeneralDB + default: + general = global.GVA_CONFIG.Mysql.GeneralDB + } + return &gorm.Config{ + Logger: logger.New(NewWriter(general, log.New(os.Stdout, "\r\n", log.LstdFlags)), logger.Config{ + SlowThreshold: 200 * time.Millisecond, + LogLevel: general.LogLevel(), + Colorful: true, + }), + NamingStrategy: schema.NamingStrategy{ + TablePrefix: prefix, + SingularTable: singular, + }, + DisableForeignKeyConstraintWhenMigrating: true, + } +} diff --git a/admin/server/initialize/internal/gorm_logger_writer.go b/admin/server/initialize/internal/gorm_logger_writer.go new file mode 100644 index 000000000..955503d87 --- /dev/null +++ b/admin/server/initialize/internal/gorm_logger_writer.go @@ -0,0 +1,37 @@ +package internal + +import ( + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/config" + "go.uber.org/zap" + "gorm.io/gorm/logger" +) + +type Writer struct { + config config.GeneralDB + writer logger.Writer +} + +func NewWriter(config config.GeneralDB, writer logger.Writer) *Writer { + return &Writer{config: config, writer: writer} +} + +// Printf 格式化打印日志 +func (c *Writer) Printf(message string, data ...any) { + if c.config.LogZap { + switch c.config.LogLevel() { + case logger.Silent: + zap.L().Debug(fmt.Sprintf(message, data...)) + case logger.Error: + zap.L().Error(fmt.Sprintf(message, data...)) + case logger.Warn: + zap.L().Warn(fmt.Sprintf(message, data...)) + case logger.Info: + zap.L().Info(fmt.Sprintf(message, data...)) + default: + zap.L().Info(fmt.Sprintf(message, data...)) + } + return + } + c.writer.Printf(message, data...) +} diff --git a/admin/server/initialize/internal/mongo.go b/admin/server/initialize/internal/mongo.go new file mode 100644 index 000000000..c4992d712 --- /dev/null +++ b/admin/server/initialize/internal/mongo.go @@ -0,0 +1,29 @@ +package internal + +import ( + "context" + "fmt" + "github.com/qiniu/qmgo/options" + "go.mongodb.org/mongo-driver/event" + opt "go.mongodb.org/mongo-driver/mongo/options" + "go.uber.org/zap" +) + +var Mongo = new(mongo) + +type mongo struct{} + +func (m *mongo) GetClientOptions() []options.ClientOptions { + cmdMonitor := &event.CommandMonitor{ + Started: func(ctx context.Context, event *event.CommandStartedEvent) { + zap.L().Info(fmt.Sprintf("[MongoDB][RequestID:%d][database:%s] %s\n", event.RequestID, event.DatabaseName, event.Command), zap.String("business", "mongo")) + }, + Succeeded: func(ctx context.Context, event *event.CommandSucceededEvent) { + zap.L().Info(fmt.Sprintf("[MongoDB][RequestID:%d] [%s] %s\n", event.RequestID, event.Duration.String(), event.Reply), zap.String("business", "mongo")) + }, + Failed: func(ctx context.Context, event *event.CommandFailedEvent) { + zap.L().Error(fmt.Sprintf("[MongoDB][RequestID:%d] [%s] %s\n", event.RequestID, event.Duration.String(), event.Failure), zap.String("business", "mongo")) + }, + } + return []options.ClientOptions{{ClientOptions: &opt.ClientOptions{Monitor: cmdMonitor}}} +} diff --git a/admin/server/initialize/mongo.go b/admin/server/initialize/mongo.go new file mode 100644 index 000000000..d88afaa74 --- /dev/null +++ b/admin/server/initialize/mongo.go @@ -0,0 +1,151 @@ +package initialize + +import ( + "context" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/initialize/internal" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/pkg/errors" + "github.com/qiniu/qmgo" + "github.com/qiniu/qmgo/options" + "go.mongodb.org/mongo-driver/bson" + option "go.mongodb.org/mongo-driver/mongo/options" + "sort" + "strings" +) + +var Mongo = new(mongo) + +type ( + mongo struct{} + Index struct { + V any `bson:"v"` + Ns any `bson:"ns"` + Key []bson.E `bson:"key"` + Name string `bson:"name"` + } +) + +func (m *mongo) Indexes(ctx context.Context) error { + // 表名:索引列表 列: "表名": [][]string{{"index1", "index2"}} + indexMap := map[string][][]string{} + for collection, indexes := range indexMap { + err := m.CreateIndexes(ctx, collection, indexes) + if err != nil { + return err + } + } + return nil +} + +func (m *mongo) Initialization() error { + var opts []options.ClientOptions + if global.GVA_CONFIG.Mongo.IsZap { + opts = internal.Mongo.GetClientOptions() + } + ctx := context.Background() + client, err := qmgo.Open(ctx, &qmgo.Config{ + Uri: global.GVA_CONFIG.Mongo.Uri(), + Coll: global.GVA_CONFIG.Mongo.Coll, + Database: global.GVA_CONFIG.Mongo.Database, + MinPoolSize: &global.GVA_CONFIG.Mongo.MinPoolSize, + MaxPoolSize: &global.GVA_CONFIG.Mongo.MaxPoolSize, + SocketTimeoutMS: &global.GVA_CONFIG.Mongo.SocketTimeoutMs, + ConnectTimeoutMS: &global.GVA_CONFIG.Mongo.ConnectTimeoutMs, + Auth: &qmgo.Credential{ + Username: global.GVA_CONFIG.Mongo.Username, + Password: global.GVA_CONFIG.Mongo.Password, + AuthSource: global.GVA_CONFIG.Mongo.AuthSource, + }, + }, opts...) + if err != nil { + return errors.Wrap(err, "链接mongodb数据库失败!") + } + global.GVA_MONGO = client + err = m.Indexes(ctx) + if err != nil { + return err + } + return nil +} + +func (m *mongo) CreateIndexes(ctx context.Context, name string, indexes [][]string) error { + collection, err := global.GVA_MONGO.Database.Collection(name).CloneCollection() + if err != nil { + return errors.Wrapf(err, "获取[%s]的表对象失败!", name) + } + list, err := collection.Indexes().List(ctx) + if err != nil { + return errors.Wrapf(err, "获取[%s]的索引对象失败!", name) + } + var entities []Index + err = list.All(ctx, &entities) + if err != nil { + return errors.Wrapf(err, "获取[%s]的索引列表失败!", name) + } + length := len(indexes) + indexMap1 := make(map[string][]string, length) + for i := 0; i < length; i++ { + sort.Strings(indexes[i]) // 对索引key进行排序, 在使用bson.M搜索时, bson会自动按照key的字母顺序进行排序 + length1 := len(indexes[i]) + keys := make([]string, 0, length1) + for j := 0; j < length1; j++ { + if indexes[i][i][0] == '-' { + keys = append(keys, indexes[i][j], "-1") + continue + } + keys = append(keys, indexes[i][j], "1") + } + key := strings.Join(keys, "_") + _, o1 := indexMap1[key] + if o1 { + return errors.Errorf("索引[%s]重复!", key) + } + indexMap1[key] = indexes[i] + } + length = len(entities) + indexMap2 := make(map[string]map[string]string, length) + for i := 0; i < length; i++ { + v1, o1 := indexMap2[entities[i].Name] + if !o1 { + keyLength := len(entities[i].Key) + v1 = make(map[string]string, keyLength) + for j := 0; j < keyLength; j++ { + v2, o2 := v1[entities[i].Key[j].Key] + if !o2 { + v1 = make(map[string]string) + } + v2 = entities[i].Key[j].Key + v1[entities[i].Key[j].Key] = v2 + indexMap2[entities[i].Name] = v1 + } + } + } + for k1, v1 := range indexMap1 { + _, o2 := indexMap2[k1] + if o2 { + continue + } // 索引存在 + if len(fmt.Sprintf("%s.%s.$%s", collection.Name(), name, v1)) > 127 { + err = global.GVA_MONGO.Database.Collection(name).CreateOneIndex(ctx, options.IndexModel{ + Key: v1, + IndexOptions: option.Index().SetName(utils.MD5V([]byte(k1))), + // IndexOptions: option.Index().SetName(utils.MD5V([]byte(k1))).SetExpireAfterSeconds(86400), // SetExpireAfterSeconds(86400) 设置索引过期时间, 86400 = 1天 + }) + if err != nil { + return errors.Wrapf(err, "创建索引[%s]失败!", k1) + } + return nil + } + err = global.GVA_MONGO.Database.Collection(name).CreateOneIndex(ctx, options.IndexModel{ + Key: v1, + IndexOptions: option.Index().SetExpireAfterSeconds(86400), + // IndexOptions: option.Index().SetName(utils.MD5V([]byte(k1))).SetExpireAfterSeconds(86400), // SetExpireAfterSeconds(86400) 设置索引过期时间(秒), 86400 = 1天 + }) + if err != nil { + return errors.Wrapf(err, "创建索引[%s]失败!", k1) + } + } + return nil +} diff --git a/admin/server/initialize/other.go b/admin/server/initialize/other.go new file mode 100644 index 000000000..f272a812a --- /dev/null +++ b/admin/server/initialize/other.go @@ -0,0 +1,32 @@ +package initialize + +import ( + "bufio" + "github.com/songzhibin97/gkit/cache/local_cache" + "os" + "strings" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/utils" +) + +func OtherInit() { + dr, err := utils.ParseDuration(global.GVA_CONFIG.JWT.ExpiresTime) + if err != nil { + panic(err) + } + _, err = utils.ParseDuration(global.GVA_CONFIG.JWT.BufferTime) + if err != nil { + panic(err) + } + + global.BlackCache = local_cache.NewCache( + local_cache.SetDefaultExpire(dr), + ) + file, err := os.Open("go.mod") + if err == nil && global.GVA_CONFIG.AutoCode.Module == "" { + scanner := bufio.NewScanner(file) + scanner.Scan() + global.GVA_CONFIG.AutoCode.Module = strings.TrimPrefix(scanner.Text(), "module ") + } +} diff --git a/admin/server/initialize/plugin.go b/admin/server/initialize/plugin.go new file mode 100644 index 000000000..16913b18f --- /dev/null +++ b/admin/server/initialize/plugin.go @@ -0,0 +1,15 @@ +package initialize + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/gin-gonic/gin" +) + +func InstallPlugin(PrivateGroup *gin.RouterGroup, PublicRouter *gin.RouterGroup, engine *gin.Engine) { + if global.GVA_DB == nil { + global.GVA_LOG.Info("项目暂未初始化,无法安装插件,初始化后重启项目即可完成插件安装") + return + } + bizPluginV1(PrivateGroup, PublicRouter) + bizPluginV2(engine) +} diff --git a/admin/server/initialize/plugin_biz_v1.go b/admin/server/initialize/plugin_biz_v1.go new file mode 100644 index 000000000..7366c65d9 --- /dev/null +++ b/admin/server/initialize/plugin_biz_v1.go @@ -0,0 +1,34 @@ +package initialize + +import ( + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/email" + "github.com/flipped-aurora/gin-vue-admin/server/utils/plugin" + "github.com/gin-gonic/gin" +) + +func PluginInit(group *gin.RouterGroup, Plugin ...plugin.Plugin) { + for i := range Plugin { + fmt.Println(Plugin[i].RouterPath(), "注册开始!") + PluginGroup := group.Group(Plugin[i].RouterPath()) + Plugin[i].Register(PluginGroup) + fmt.Println(Plugin[i].RouterPath(), "注册成功!") + } +} + +func bizPluginV1(group ...*gin.RouterGroup) { + private := group[0] + public := group[1] + // 添加跟角色挂钩权限的插件 示例 本地示例模式于在线仓库模式注意上方的import 可以自行切换 效果相同 + PluginInit(private, email.CreateEmailPlug( + global.GVA_CONFIG.Email.To, + global.GVA_CONFIG.Email.From, + global.GVA_CONFIG.Email.Host, + global.GVA_CONFIG.Email.Secret, + global.GVA_CONFIG.Email.Nickname, + global.GVA_CONFIG.Email.Port, + global.GVA_CONFIG.Email.IsSSL, + )) + holder(public, private) +} diff --git a/admin/server/initialize/plugin_biz_v2.go b/admin/server/initialize/plugin_biz_v2.go new file mode 100644 index 000000000..9d13bbe02 --- /dev/null +++ b/admin/server/initialize/plugin_biz_v2.go @@ -0,0 +1,16 @@ +package initialize + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement" + "github.com/flipped-aurora/gin-vue-admin/server/utils/plugin/v2" + "github.com/gin-gonic/gin" +) + +func PluginInitV2(group *gin.Engine, plugins ...plugin.Plugin) { + for i := 0; i < len(plugins); i++ { + plugins[i].Register(group) + } +} +func bizPluginV2(engine *gin.Engine) { + PluginInitV2(engine, announcement.Plugin) +} diff --git a/admin/server/initialize/redis.go b/admin/server/initialize/redis.go new file mode 100644 index 000000000..90f61bd8d --- /dev/null +++ b/admin/server/initialize/redis.go @@ -0,0 +1,71 @@ +package initialize + +import ( + "context" + + "github.com/flipped-aurora/gin-vue-admin/server/config" + "github.com/flipped-aurora/gin-vue-admin/server/global" + + "github.com/redis/go-redis/v9" + "go.uber.org/zap" +) + +func initRedisClient(redisCfg config.Redis) (redis.UniversalClient, error) { + var client redis.UniversalClient + // 使用集群模式 + if redisCfg.UseCluster { + client = redis.NewClusterClient(&redis.ClusterOptions{ + Addrs: redisCfg.ClusterAddrs, + Password: redisCfg.Password, + }) + } else { + // 使用单例模式 + client = redis.NewClient(&redis.Options{ + Addr: redisCfg.Addr, + Password: redisCfg.Password, + DB: redisCfg.DB, + }) + } + pong, err := client.Ping(context.Background()).Result() + if err != nil { + global.GVA_LOG.Error("redis connect ping failed, err:", zap.String("name", redisCfg.Name), zap.Error(err)) + return nil, err + } + + global.GVA_LOG.Info("redis connect ping response:", zap.String("name", redisCfg.Name), zap.String("pong", pong)) + return client, nil +} + +func Redis() { + redisClient, err := initRedisClient(global.GVA_CONFIG.Redis) + if err != nil { + panic(err) + } + global.GVA_REDIS = redisClient +} + +// Extend Start: global code + +func DifyRedis() { + redisClient, err := initRedisClient(global.GVA_CONFIG.DifyRedis) + if err != nil { + panic(err) + } + global.GVA_Dify_REDIS = redisClient +} + +// Extend Stop: global code + +func RedisList() { + redisMap := make(map[string]redis.UniversalClient) + + for _, redisCfg := range global.GVA_CONFIG.RedisList { + client, err := initRedisClient(redisCfg) + if err != nil { + panic(err) + } + redisMap[redisCfg.Name] = client + } + + global.GVA_REDISList = redisMap +} diff --git a/admin/server/initialize/register_init.go b/admin/server/initialize/register_init.go new file mode 100644 index 000000000..a2496612b --- /dev/null +++ b/admin/server/initialize/register_init.go @@ -0,0 +1,10 @@ +package initialize + +import ( + _ "github.com/flipped-aurora/gin-vue-admin/server/source/example" + _ "github.com/flipped-aurora/gin-vue-admin/server/source/system" +) + +func init() { + // do nothing,only import source package so that inits can be registered +} diff --git a/admin/server/initialize/router.go b/admin/server/initialize/router.go new file mode 100644 index 000000000..499f87d8b --- /dev/null +++ b/admin/server/initialize/router.go @@ -0,0 +1,110 @@ +package initialize + +import ( + "net/http" + "os" + + "github.com/flipped-aurora/gin-vue-admin/server/docs" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/flipped-aurora/gin-vue-admin/server/router" + "github.com/gin-gonic/gin" + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" +) + +type justFilesFilesystem struct { + fs http.FileSystem +} + +func (fs justFilesFilesystem) Open(name string) (http.File, error) { + f, err := fs.fs.Open(name) + if err != nil { + return nil, err + } + + stat, err := f.Stat() + if stat.IsDir() { + return nil, os.ErrPermission + } + + return f, nil +} + +// 初始化总路由 + +func Routers() *gin.Engine { + Router := gin.New() + Router.Use(gin.Recovery()) + if gin.Mode() == gin.DebugMode { + Router.Use(gin.Logger()) + } + + systemRouter := router.RouterGroupApp.System + exampleRouter := router.RouterGroupApp.Example + // 如果想要不使用nginx代理前端网页,可以修改 web/.env.production 下的 + // VUE_APP_BASE_API = / + // VUE_APP_BASE_PATH = http://localhost + // 然后执行打包命令 npm run build。在打开下面3行注释 + // Router.Static("/favicon.ico", "./dist/favicon.ico") + // Router.Static("/assets", "./dist/assets") // dist里面的静态资源 + // Router.StaticFile("/", "./dist/index.html") // 前端网页入口页面 + + Router.StaticFS(global.GVA_CONFIG.Local.StorePath, justFilesFilesystem{http.Dir(global.GVA_CONFIG.Local.StorePath)}) // Router.Use(middleware.LoadTls()) // 如果需要使用https 请打开此中间件 然后前往 core/server.go 将启动模式 更变为 Router.RunTLS("端口","你的cre/pem文件","你的key文件") + // 跨域,如需跨域可以打开下面的注释 + // Router.Use(middleware.Cors()) // 直接放行全部跨域请求 + // Router.Use(middleware.CorsByRules()) // 按照配置的规则放行跨域请求 + // global.GVA_LOG.Info("use middleware cors") + docs.SwaggerInfo.BasePath = global.GVA_CONFIG.System.RouterPrefix + Router.GET(global.GVA_CONFIG.System.RouterPrefix+"/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + global.GVA_LOG.Info("register swagger handler") + // 方便统一添加路由组前缀 多服务器上线使用 + + PublicGroup := Router.Group(global.GVA_CONFIG.System.RouterPrefix) + PrivateGroup := Router.Group(global.GVA_CONFIG.System.RouterPrefix) + + PrivateGroup.Use(middleware.JWTAuth()).Use(middleware.CasbinHandler()) + + { + // 健康监测 + PublicGroup.GET("/health", func(c *gin.Context) { + c.JSON(http.StatusOK, "ok") + }) + } + { + systemRouter.InitBaseRouter(PublicGroup) // 注册基础功能路由 不做鉴权 + systemRouter.InitInitRouter(PublicGroup) // 自动初始化相关 + } + + { + systemRouter.InitApiRouter(PrivateGroup, PublicGroup) // 注册功能api路由 + systemRouter.InitJwtRouter(PrivateGroup) // jwt相关路由 + systemRouter.InitUserRouter(PrivateGroup) // 注册用户路由 + systemRouter.InitMenuRouter(PrivateGroup) // 注册menu路由 + systemRouter.InitSystemRouter(PrivateGroup) // system相关路由 + systemRouter.InitCasbinRouter(PrivateGroup) // 权限相关路由 + systemRouter.InitAutoCodeRouter(PrivateGroup, PublicGroup) // 创建自动化代码 + systemRouter.InitAuthorityRouter(PrivateGroup) // 注册角色路由 + systemRouter.InitSysDictionaryRouter(PrivateGroup) // 字典管理 + systemRouter.InitAutoCodeHistoryRouter(PrivateGroup) // 自动化代码历史 + systemRouter.InitSysOperationRecordRouter(PrivateGroup) // 操作记录 + systemRouter.InitSysDictionaryDetailRouter(PrivateGroup) // 字典详情管理 + systemRouter.InitAuthorityBtnRouterRouter(PrivateGroup) // 按钮权限管理 + systemRouter.InitSysExportTemplateRouter(PrivateGroup) // 导出模板 + systemRouter.InitSysParamsRouter(PrivateGroup, PublicGroup) // 参数管理 + exampleRouter.InitCustomerRouter(PrivateGroup) // 客户路由 + exampleRouter.InitFileUploadAndDownloadRouter(PrivateGroup) // 文件上传下载功能路由 + + } + + //插件路由安装 + InstallPlugin(PrivateGroup, PublicGroup, Router) + + // 注册业务路由 + initBizRouter(PrivateGroup, PublicGroup) + + global.GVA_ROUTERS = Router.Routes() + + global.GVA_LOG.Info("router register success") + return Router +} diff --git a/admin/server/initialize/router_biz.go b/admin/server/initialize/router_biz.go new file mode 100644 index 000000000..9b08faeaf --- /dev/null +++ b/admin/server/initialize/router_biz.go @@ -0,0 +1,24 @@ +package initialize + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/router" + "github.com/gin-gonic/gin" +) + +func holder(routers ...*gin.RouterGroup) { + _ = routers + _ = router.RouterGroupApp +} +func initBizRouter(routers ...*gin.RouterGroup) { + privateGroup := routers[0] + publicGroup := routers[1] + holder(publicGroup, privateGroup) + { + gaiaRouter := router.RouterGroupApp.Gaia + gaiaRouter.InitDashboardRouter(privateGroup, publicGroup) + gaiaRouter.InitQuotaRouter(privateGroup, publicGroup) + gaiaRouter.InitTenantsRouter(privateGroup, publicGroup) + gaiaRouter.InitTestRouter(privateGroup, publicGroup) + gaiaRouter.InitSystemRouter(privateGroup) + } +} diff --git a/admin/server/initialize/timer.go b/admin/server/initialize/timer.go new file mode 100644 index 000000000..ab5a7a5a4 --- /dev/null +++ b/admin/server/initialize/timer.go @@ -0,0 +1,37 @@ +package initialize + +import ( + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/task" + + "github.com/robfig/cron/v3" + + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +func Timer() { + go func() { + var option []cron.Option + option = append(option, cron.WithSeconds()) + // 清理DB定时任务 + _, err := global.GVA_Timer.AddTaskByFunc("ClearDB", "@daily", func() { + err := task.ClearTable(global.GVA_DB) // 定时任务方法定在task文件包中 + if err != nil { + fmt.Println("timer error:", err) + } + }, "定时清理数据库【日志,黑名单】内容", option...) + if err != nil { + fmt.Println("add timer error:", err) + } + + // 其他定时任务定在这里 参考上方使用方法 + + //_, err := global.GVA_Timer.AddTaskByFunc("定时任务标识", "corn表达式", func() { + // 具体执行内容... + // ...... + //}, option...) + //if err != nil { + // fmt.Println("add timer error:", err) + //} + }() +} diff --git a/admin/server/initialize/validator.go b/admin/server/initialize/validator.go new file mode 100644 index 000000000..79aea6693 --- /dev/null +++ b/admin/server/initialize/validator.go @@ -0,0 +1,22 @@ +package initialize + +import "github.com/flipped-aurora/gin-vue-admin/server/utils" + +func init() { + _ = utils.RegisterRule("PageVerify", + utils.Rules{ + "Page": {utils.NotEmpty()}, + "PageSize": {utils.NotEmpty()}, + }, + ) + _ = utils.RegisterRule("IdVerify", + utils.Rules{ + "Id": {utils.NotEmpty()}, + }, + ) + _ = utils.RegisterRule("AuthorityIdVerify", + utils.Rules{ + "AuthorityId": {utils.NotEmpty()}, + }, + ) +} diff --git a/admin/server/main.go b/admin/server/main.go new file mode 100644 index 000000000..6f4444892 --- /dev/null +++ b/admin/server/main.go @@ -0,0 +1,38 @@ +package main + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/core" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/initialize" + _ "go.uber.org/automaxprocs" + "go.uber.org/zap" +) + +//go:generate go env -w GO111MODULE=on +//go:generate go env -w GOPROXY=https://goproxy.cn,direct +//go:generate go mod tidy +//go:generate go mod download + +// @title Gin-Vue-Admin Swagger API接口文档 +// @version v2.7.7 +// @description 使用gin+vue进行极速开发的全栈开发基础平台 +// @securityDefinitions.apikey ApiKeyAuth +// @in header +// @name x-token +// @BasePath / +func main() { + global.GVA_VP = core.Viper() // 初始化Viper + initialize.OtherInit() + global.GVA_LOG = core.Zap() // 初始化zap日志库 + zap.ReplaceGlobals(global.GVA_LOG) + global.GVA_DB = initialize.Gorm() // gorm连接数据库 + initialize.Timer() + initialize.DBList() + if global.GVA_DB != nil { + initialize.RegisterTables() // 初始化表 + // 程序结束前关闭数据库链接 + db, _ := global.GVA_DB.DB() + defer db.Close() + } + core.RunWindowsServer() +} diff --git a/admin/server/middleware/casbin_rbac.go b/admin/server/middleware/casbin_rbac.go new file mode 100644 index 000000000..a1ca4c2b7 --- /dev/null +++ b/admin/server/middleware/casbin_rbac.go @@ -0,0 +1,36 @@ +package middleware + +import ( + "strconv" + "strings" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/service" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/gin-gonic/gin" +) + +var casbinService = service.ServiceGroupApp.SystemServiceGroup.CasbinService + +// CasbinHandler 拦截器 +func CasbinHandler() gin.HandlerFunc { + return func(c *gin.Context) { + waitUse, _ := utils.GetClaims(c) + //获取请求的PATH + path := c.Request.URL.Path + obj := strings.TrimPrefix(path, global.GVA_CONFIG.System.RouterPrefix) + // 获取请求方法 + act := c.Request.Method + // 获取用户的角色 + sub := strconv.Itoa(int(waitUse.AuthorityId)) + e := casbinService.Casbin() // 判断策略中是否存在 + success, _ := e.Enforce(sub, obj, act) + if !success { + response.FailWithDetailed(gin.H{}, "权限不足", c) + c.Abort() + return + } + c.Next() + } +} diff --git a/admin/server/middleware/cors.go b/admin/server/middleware/cors.go new file mode 100644 index 000000000..d7e3ccd4c --- /dev/null +++ b/admin/server/middleware/cors.go @@ -0,0 +1,73 @@ +package middleware + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/config" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/gin-gonic/gin" + "net/http" +) + +// Cors 直接放行所有跨域请求并放行所有 OPTIONS 方法 +func Cors() gin.HandlerFunc { + return func(c *gin.Context) { + method := c.Request.Method + origin := c.Request.Header.Get("Origin") + c.Header("Access-Control-Allow-Origin", origin) + c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token,X-Token,X-User-Id") + c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS,DELETE,PUT") + c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type, New-Token, New-Expires-At") + c.Header("Access-Control-Allow-Credentials", "true") + + // 放行所有OPTIONS方法 + if method == "OPTIONS" { + c.AbortWithStatus(http.StatusNoContent) + } + // 处理请求 + c.Next() + } +} + +// CorsByRules 按照配置处理跨域请求 +func CorsByRules() gin.HandlerFunc { + // 放行全部 + if global.GVA_CONFIG.Cors.Mode == "allow-all" { + return Cors() + } + return func(c *gin.Context) { + whitelist := checkCors(c.GetHeader("origin")) + + // 通过检查, 添加请求头 + if whitelist != nil { + c.Header("Access-Control-Allow-Origin", whitelist.AllowOrigin) + c.Header("Access-Control-Allow-Headers", whitelist.AllowHeaders) + c.Header("Access-Control-Allow-Methods", whitelist.AllowMethods) + c.Header("Access-Control-Expose-Headers", whitelist.ExposeHeaders) + if whitelist.AllowCredentials { + c.Header("Access-Control-Allow-Credentials", "true") + } + } + + // 严格白名单模式且未通过检查,直接拒绝处理请求 + if whitelist == nil && global.GVA_CONFIG.Cors.Mode == "strict-whitelist" && !(c.Request.Method == "GET" && c.Request.URL.Path == "/health") { + c.AbortWithStatus(http.StatusForbidden) + } else { + // 非严格白名单模式,无论是否通过检查均放行所有 OPTIONS 方法 + if c.Request.Method == http.MethodOptions { + c.AbortWithStatus(http.StatusNoContent) + } + } + + // 处理请求 + c.Next() + } +} + +func checkCors(currentOrigin string) *config.CORSWhitelist { + for _, whitelist := range global.GVA_CONFIG.Cors.Whitelist { + // 遍历配置中的跨域头,寻找匹配项 + if currentOrigin == whitelist.AllowOrigin { + return &whitelist + } + } + return nil +} diff --git a/admin/server/middleware/email.go b/admin/server/middleware/email.go new file mode 100644 index 000000000..4a07561c9 --- /dev/null +++ b/admin/server/middleware/email.go @@ -0,0 +1,60 @@ +package middleware + +import ( + "bytes" + "io" + "strconv" + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/plugin/email/utils" + utils2 "github.com/flipped-aurora/gin-vue-admin/server/utils" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/service" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +var userService = service.ServiceGroupApp.SystemServiceGroup.UserService + +func ErrorToEmail() gin.HandlerFunc { + return func(c *gin.Context) { + var username string + claims, _ := utils2.GetClaims(c) + if claims.Username != "" { + username = claims.Username + } else { + id, _ := strconv.Atoi(c.Request.Header.Get("x-user-id")) + user, err := userService.FindUserById(id) + if err != nil { + username = "Unknown" + } + username = user.Username + } + body, _ := io.ReadAll(c.Request.Body) + // 再重新写回请求体body中,ioutil.ReadAll会清空c.Request.Body中的数据 + c.Request.Body = io.NopCloser(bytes.NewBuffer(body)) + record := system.SysOperationRecord{ + Ip: c.ClientIP(), + Method: c.Request.Method, + Path: c.Request.URL.Path, + Agent: c.Request.UserAgent(), + Body: string(body), + } + now := time.Now() + + c.Next() + + latency := time.Since(now) + status := c.Writer.Status() + record.ErrorMessage = c.Errors.ByType(gin.ErrorTypePrivate).String() + str := "接收到的请求为" + record.Body + "\n" + "请求方式为" + record.Method + "\n" + "报错信息如下" + record.ErrorMessage + "\n" + "耗时" + latency.String() + "\n" + if status != 200 { + subject := username + "" + record.Ip + "调用了" + record.Path + "报错了" + if err := utils.ErrorToEmail(subject, str); err != nil { + global.GVA_LOG.Error("ErrorToEmail Failed, err:", zap.Error(err)) + } + } + } +} diff --git a/admin/server/middleware/error.go b/admin/server/middleware/error.go new file mode 100644 index 000000000..f68b7a562 --- /dev/null +++ b/admin/server/middleware/error.go @@ -0,0 +1,61 @@ +package middleware + +import ( + "net" + "net/http" + "net/http/httputil" + "os" + "runtime/debug" + "strings" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +// GinRecovery recover掉项目可能出现的panic,并使用zap记录相关日志 +func GinRecovery(stack bool) gin.HandlerFunc { + return func(c *gin.Context) { + defer func() { + if err := recover(); err != nil { + // Check for a broken connection, as it is not really a + // condition that warrants a panic stack trace. + var brokenPipe bool + if ne, ok := err.(*net.OpError); ok { + if se, ok := ne.Err.(*os.SyscallError); ok { + if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") { + brokenPipe = true + } + } + } + + httpRequest, _ := httputil.DumpRequest(c.Request, false) + if brokenPipe { + global.GVA_LOG.Error(c.Request.URL.Path, + zap.Any("error", err), + zap.String("request", string(httpRequest)), + ) + // If the connection is dead, we can't write a status to it. + _ = c.Error(err.(error)) // nolint: errcheck + c.Abort() + return + } + + if stack { + global.GVA_LOG.Error("[Recovery from panic]", + zap.Any("error", err), + zap.String("request", string(httpRequest)), + zap.String("stack", string(debug.Stack())), + ) + } else { + global.GVA_LOG.Error("[Recovery from panic]", + zap.Any("error", err), + zap.String("request", string(httpRequest)), + ) + } + c.AbortWithStatus(http.StatusInternalServerError) + } + }() + c.Next() + } +} diff --git a/admin/server/middleware/jwt.go b/admin/server/middleware/jwt.go new file mode 100644 index 000000000..0d7c016aa --- /dev/null +++ b/admin/server/middleware/jwt.go @@ -0,0 +1,95 @@ +package middleware + +import ( + "context" + "errors" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/golang-jwt/jwt/v4" + "strconv" + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/service" + + "github.com/gin-gonic/gin" +) + +var jwtService = service.ServiceGroupApp.SystemServiceGroup.JwtService + +func JWTAuth() gin.HandlerFunc { + return func(c *gin.Context) { + // 我们这里jwt鉴权取头部信息 x-token 登录时回返回token信息 这里前端需要把token存储到cookie或者本地localStorage中 不过需要跟后端协商过期时间 可以约定刷新令牌或者重新登录 + token := utils.GetToken(c) + if token == "" { + response.NoAuth("未登录或非法访问", c) + c.Abort() + return + } + if jwtService.IsBlacklist(token) { + response.NoAuth("您的帐户异地登陆或令牌失效", c) + utils.ClearToken(c) + c.Abort() + return + } + j := utils.NewJWT() + // parseToken 解析token包含的信息 + claims, err := j.ParseToken(token) + if err != nil { + if errors.Is(err, utils.TokenExpired) { + response.NoAuth("授权已过期", c) + utils.ClearToken(c) + c.Abort() + return + } + response.NoAuth(err.Error(), c) + utils.ClearToken(c) + c.Abort() + return + } + + // 已登录用户被管理员禁用 需要使该用户的jwt失效 此处比较消耗性能 如果需要 请自行打开 + // 用户被删除的逻辑 需要优化 此处比较消耗性能 如果需要 请自行打开 + + //if user, err := userService.FindUserByUuid(claims.UUID.String()); err != nil || user.Enable == 2 { + // _ = jwtService.JsonInBlacklist(system.JwtBlacklist{Jwt: token}) + // response.FailWithDetailed(gin.H{"reload": true}, err.Error(), c) + // c.Abort() + //} + // Extend Start: Gaia 的黑名单 + if global.GVA_CONFIG.System.UseMultipoint || global.GVA_CONFIG.System.UseRedis { + // 查询是否有黑名单 + // Check if the user is disabled + if count, iErr := global.GVA_REDIS.Get(context.Background(), fmt.Sprintf( + "login_error_rate_limit:%s", claims.Email)).Int(); iErr == nil && count >= global.GVA_CONFIG.Gaia.LoginMaxErrorLimit { + response.FailWithDetailed(gin.H{"reload": true}, err.Error(), c) + c.Abort() + } + } + // Extend Stop: Gaia 的黑名单 + c.Set("claims", claims) + if claims.Exp-time.Now().Unix() < claims.BufferTime { + dr, _ := utils.ParseDuration(global.GVA_CONFIG.JWT.ExpiresTime) + claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(dr)) + claims.Exp = time.Now().Add(dr).Unix() + newToken, _ := j.CreateTokenByOldToken(token, *claims) + newClaims, _ := j.ParseToken(newToken) + c.Header("new-token", newToken) + c.Header("new-expires-at", strconv.FormatInt(newClaims.Exp, 10)) + utils.SetToken(c, newToken, int(dr.Seconds())) + if global.GVA_CONFIG.System.UseMultipoint { + // 记录新的活跃jwt + _ = jwtService.SetRedisJWT(newToken, newClaims.Username) + } + } + c.Next() + + if newToken, exists := c.Get("new-token"); exists { + c.Header("new-token", newToken.(string)) + } + if newExpiresAt, exists := c.Get("new-expires-at"); exists { + c.Header("new-expires-at", newExpiresAt.(string)) + } + } +} diff --git a/admin/server/middleware/limit_ip.go b/admin/server/middleware/limit_ip.go new file mode 100644 index 000000000..315010b22 --- /dev/null +++ b/admin/server/middleware/limit_ip.go @@ -0,0 +1,92 @@ +package middleware + +import ( + "context" + "errors" + "net/http" + "time" + + "go.uber.org/zap" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/gin-gonic/gin" +) + +type LimitConfig struct { + // GenerationKey 根据业务生成key 下面CheckOrMark查询生成 + GenerationKey func(c *gin.Context) string + // 检查函数,用户可修改具体逻辑,更加灵活 + CheckOrMark func(key string, expire int, limit int) error + // Expire key 过期时间 + Expire int + // Limit 周期时间 + Limit int +} + +func (l LimitConfig) LimitWithTime() gin.HandlerFunc { + return func(c *gin.Context) { + if err := l.CheckOrMark(l.GenerationKey(c), l.Expire, l.Limit); err != nil { + c.JSON(http.StatusOK, gin.H{"code": response.ERROR, "msg": err}) + c.Abort() + return + } else { + c.Next() + } + } +} + +// DefaultGenerationKey 默认生成key +func DefaultGenerationKey(c *gin.Context) string { + return "GVA_Limit" + c.ClientIP() +} + +func DefaultCheckOrMark(key string, expire int, limit int) (err error) { + // 判断是否开启redis + if global.GVA_REDIS == nil { + return err + } + if err = SetLimitWithTime(key, limit, time.Duration(expire)*time.Second); err != nil { + global.GVA_LOG.Error("limit", zap.Error(err)) + } + return err +} + +func DefaultLimit() gin.HandlerFunc { + return LimitConfig{ + GenerationKey: DefaultGenerationKey, + CheckOrMark: DefaultCheckOrMark, + Expire: global.GVA_CONFIG.System.LimitTimeIP, + Limit: global.GVA_CONFIG.System.LimitCountIP, + }.LimitWithTime() +} + +// SetLimitWithTime 设置访问次数 +func SetLimitWithTime(key string, limit int, expiration time.Duration) error { + count, err := global.GVA_REDIS.Exists(context.Background(), key).Result() + if err != nil { + return err + } + if count == 0 { + pipe := global.GVA_REDIS.TxPipeline() + pipe.Incr(context.Background(), key) + pipe.Expire(context.Background(), key, expiration) + _, err = pipe.Exec(context.Background()) + return err + } else { + // 次数 + if times, err := global.GVA_REDIS.Get(context.Background(), key).Int(); err != nil { + return err + } else { + if times >= limit { + if t, err := global.GVA_REDIS.PTTL(context.Background(), key).Result(); err != nil { + return errors.New("请求太过频繁,请稍后再试") + } else { + return errors.New("请求太过频繁, 请 " + t.String() + " 秒后尝试") + } + } else { + return global.GVA_REDIS.Incr(context.Background(), key).Err() + } + } + } +} diff --git a/admin/server/middleware/loadtls.go b/admin/server/middleware/loadtls.go new file mode 100644 index 000000000..a17cf653b --- /dev/null +++ b/admin/server/middleware/loadtls.go @@ -0,0 +1,27 @@ +package middleware + +import ( + "fmt" + + "github.com/gin-gonic/gin" + "github.com/unrolled/secure" +) + +// 用https把这个中间件在router里面use一下就好 + +func LoadTls() gin.HandlerFunc { + return func(c *gin.Context) { + middleware := secure.New(secure.Options{ + SSLRedirect: true, + SSLHost: "localhost:443", + }) + err := middleware.Process(c.Writer, c.Request) + if err != nil { + // 如果出现错误,请不要继续 + fmt.Println(err) + return + } + // 继续往下处理 + c.Next() + } +} diff --git a/admin/server/middleware/logger.go b/admin/server/middleware/logger.go new file mode 100644 index 000000000..fabc33497 --- /dev/null +++ b/admin/server/middleware/logger.go @@ -0,0 +1,89 @@ +package middleware + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "strings" + "time" + + "github.com/gin-gonic/gin" +) + +// LogLayout 日志layout +type LogLayout struct { + Time time.Time + Metadata map[string]interface{} // 存储自定义原数据 + Path string // 访问路径 + Query string // 携带query + Body string // 携带body数据 + IP string // ip地址 + UserAgent string // 代理 + Error string // 错误 + Cost time.Duration // 花费时间 + Source string // 来源 +} + +type Logger struct { + // Filter 用户自定义过滤 + Filter func(c *gin.Context) bool + // FilterKeyword 关键字过滤(key) + FilterKeyword func(layout *LogLayout) bool + // AuthProcess 鉴权处理 + AuthProcess func(c *gin.Context, layout *LogLayout) + // 日志处理 + Print func(LogLayout) + // Source 服务唯一标识 + Source string +} + +func (l Logger) SetLoggerMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + start := time.Now() + path := c.Request.URL.Path + query := c.Request.URL.RawQuery + var body []byte + if l.Filter != nil && !l.Filter(c) { + body, _ = c.GetRawData() + // 将原body塞回去 + c.Request.Body = io.NopCloser(bytes.NewBuffer(body)) + } + c.Next() + cost := time.Since(start) + layout := LogLayout{ + Time: time.Now(), + Path: path, + Query: query, + IP: c.ClientIP(), + UserAgent: c.Request.UserAgent(), + Error: strings.TrimRight(c.Errors.ByType(gin.ErrorTypePrivate).String(), "\n"), + Cost: cost, + Source: l.Source, + } + if l.Filter != nil && !l.Filter(c) { + layout.Body = string(body) + } + if l.AuthProcess != nil { + // 处理鉴权需要的信息 + l.AuthProcess(c, &layout) + } + if l.FilterKeyword != nil { + // 自行判断key/value 脱敏等 + l.FilterKeyword(&layout) + } + // 自行处理日志 + l.Print(layout) + } +} + +func DefaultLogger() gin.HandlerFunc { + return Logger{ + Print: func(layout LogLayout) { + // 标准输出,k8s做收集 + v, _ := json.Marshal(layout) + fmt.Println(string(v)) + }, + Source: "GVA", + }.SetLoggerMiddleware() +} diff --git a/admin/server/middleware/operation.go b/admin/server/middleware/operation.go new file mode 100644 index 000000000..f34cf68ee --- /dev/null +++ b/admin/server/middleware/operation.go @@ -0,0 +1,133 @@ +package middleware + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "net/url" + "strconv" + "strings" + "sync" + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/utils" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/service" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +var operationRecordService = service.ServiceGroupApp.SystemServiceGroup.OperationRecordService + +var respPool sync.Pool +var bufferSize = 1024 + +func init() { + respPool.New = func() interface{} { + return make([]byte, bufferSize) + } +} + +func OperationRecord() gin.HandlerFunc { + return func(c *gin.Context) { + var body []byte + var userId int + if c.Request.Method != http.MethodGet { + var err error + body, err = io.ReadAll(c.Request.Body) + if err != nil { + global.GVA_LOG.Error("read body from request error:", zap.Error(err)) + } else { + c.Request.Body = io.NopCloser(bytes.NewBuffer(body)) + } + } else { + query := c.Request.URL.RawQuery + query, _ = url.QueryUnescape(query) + split := strings.Split(query, "&") + m := make(map[string]string) + for _, v := range split { + kv := strings.Split(v, "=") + if len(kv) == 2 { + m[kv[0]] = kv[1] + } + } + body, _ = json.Marshal(&m) + } + claims, _ := utils.GetClaims(c) + if claims != nil && claims.BaseClaims.ID != 0 { + userId = int(claims.BaseClaims.ID) + } else { + id, err := strconv.Atoi(c.Request.Header.Get("x-user-id")) + if err != nil { + userId = 0 + } + userId = id + } + record := system.SysOperationRecord{ + Ip: c.ClientIP(), + Method: c.Request.Method, + Path: c.Request.URL.Path, + Agent: c.Request.UserAgent(), + Body: "", + UserID: userId, + } + + // 上传文件时候 中间件日志进行裁断操作 + if strings.Contains(c.GetHeader("Content-Type"), "multipart/form-data") { + record.Body = "[文件]" + } else { + if len(body) > bufferSize { + record.Body = "[超出记录长度]" + } else { + record.Body = string(body) + } + } + + writer := responseBodyWriter{ + ResponseWriter: c.Writer, + body: &bytes.Buffer{}, + } + c.Writer = writer + now := time.Now() + + c.Next() + + latency := time.Since(now) + record.ErrorMessage = c.Errors.ByType(gin.ErrorTypePrivate).String() + record.Status = c.Writer.Status() + record.Latency = latency + record.Resp = writer.body.String() + + if strings.Contains(c.Writer.Header().Get("Pragma"), "public") || + strings.Contains(c.Writer.Header().Get("Expires"), "0") || + strings.Contains(c.Writer.Header().Get("Cache-Control"), "must-revalidate, post-check=0, pre-check=0") || + strings.Contains(c.Writer.Header().Get("Content-Type"), "application/force-download") || + strings.Contains(c.Writer.Header().Get("Content-Type"), "application/octet-stream") || + strings.Contains(c.Writer.Header().Get("Content-Type"), "application/vnd.ms-excel") || + strings.Contains(c.Writer.Header().Get("Content-Type"), "application/download") || + strings.Contains(c.Writer.Header().Get("Content-Disposition"), "attachment") || + strings.Contains(c.Writer.Header().Get("Content-Transfer-Encoding"), "binary") { + if len(record.Resp) > bufferSize { + // 截断 + record.Body = "超出记录长度" + } + } + + if err := operationRecordService.CreateSysOperationRecord(record); err != nil { + global.GVA_LOG.Error("create operation record error:", zap.Error(err)) + } + } +} + +type responseBodyWriter struct { + gin.ResponseWriter + body *bytes.Buffer +} + +func (r responseBodyWriter) Write(b []byte) (int, error) { + r.body.Write(b) + return r.ResponseWriter.Write(b) +} diff --git a/admin/server/model/common/basetypes.go b/admin/server/model/common/basetypes.go new file mode 100644 index 000000000..870d975c6 --- /dev/null +++ b/admin/server/model/common/basetypes.go @@ -0,0 +1,36 @@ +package common + +import ( + "database/sql/driver" + "encoding/json" + "errors" +) + +type JSONMap map[string]interface{} + +func (m JSONMap) Value() (driver.Value, error) { + if m == nil { + return nil, nil + } + return json.Marshal(m) +} + +func (m *JSONMap) Scan(value interface{}) error { + if value == nil { + *m = make(map[string]interface{}) + return nil + } + var err error + switch value.(type) { + case []byte: + err = json.Unmarshal(value.([]byte), m) + case string: + err = json.Unmarshal([]byte(value.(string)), m) + default: + err = errors.New("basetypes.JSONMap.Scan: invalid value type") + } + if err != nil { + return err + } + return nil +} diff --git a/admin/server/model/common/clearDB.go b/admin/server/model/common/clearDB.go new file mode 100644 index 000000000..e7fc75795 --- /dev/null +++ b/admin/server/model/common/clearDB.go @@ -0,0 +1,7 @@ +package common + +type ClearDB struct { + TableName string + CompareField string + Interval string +} diff --git a/admin/server/model/common/request/common.go b/admin/server/model/common/request/common.go new file mode 100644 index 000000000..c729f3db2 --- /dev/null +++ b/admin/server/model/common/request/common.go @@ -0,0 +1,48 @@ +package request + +import ( + "gorm.io/gorm" +) + +// PageInfo Paging common input parameter structure +type PageInfo struct { + Page int `json:"page" form:"page"` // 页码 + PageSize int `json:"pageSize" form:"pageSize"` // 每页大小 + Keyword string `json:"keyword" form:"keyword"` // 关键字 +} + +func (r *PageInfo) Paginate() func(db *gorm.DB) *gorm.DB { + return func(db *gorm.DB) *gorm.DB { + if r.Page <= 0 { + r.Page = 1 + } + switch { + case r.PageSize > 100: + r.PageSize = 100 + case r.PageSize <= 0: + r.PageSize = 10 + } + offset := (r.Page - 1) * r.PageSize + return db.Offset(offset).Limit(r.PageSize) + } +} + +// GetById Find by id structure +type GetById struct { + ID int `json:"id" form:"id"` // 主键ID +} + +func (r *GetById) Uint() uint { + return uint(r.ID) +} + +type IdsReq struct { + Ids []int `json:"ids" form:"ids"` +} + +// GetAuthorityId Get role by id structure +type GetAuthorityId struct { + AuthorityId uint `json:"authorityId" form:"authorityId"` // 角色ID +} + +type Empty struct{} diff --git a/admin/server/model/common/response/common.go b/admin/server/model/common/response/common.go new file mode 100644 index 000000000..74610965b --- /dev/null +++ b/admin/server/model/common/response/common.go @@ -0,0 +1,8 @@ +package response + +type PageResult struct { + List interface{} `json:"list"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"pageSize"` +} diff --git a/admin/server/model/common/response/response.go b/admin/server/model/common/response/response.go new file mode 100644 index 000000000..a429b12e1 --- /dev/null +++ b/admin/server/model/common/response/response.go @@ -0,0 +1,63 @@ +package response + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +type Response struct { + Code int `json:"code"` + Data interface{} `json:"data"` + Msg string `json:"msg"` +} + +const ( + ERROR = 7 + SUCCESS = 0 +) + +func Result(code int, data interface{}, msg string, c *gin.Context) { + // 开始时间 + c.JSON(http.StatusOK, Response{ + code, + data, + msg, + }) +} + +func Ok(c *gin.Context) { + Result(SUCCESS, map[string]interface{}{}, "操作成功", c) +} + +func OkWithMessage(message string, c *gin.Context) { + Result(SUCCESS, map[string]interface{}{}, message, c) +} + +func OkWithData(data interface{}, c *gin.Context) { + Result(SUCCESS, data, "成功", c) +} + +func OkWithDetailed(data interface{}, message string, c *gin.Context) { + Result(SUCCESS, data, message, c) +} + +func Fail(c *gin.Context) { + Result(ERROR, map[string]interface{}{}, "操作失败", c) +} + +func FailWithMessage(message string, c *gin.Context) { + Result(ERROR, map[string]interface{}{}, message, c) +} + +func NoAuth(message string, c *gin.Context) { + c.JSON(http.StatusUnauthorized, Response{ + 7, + nil, + message, + }) +} + +func FailWithDetailed(data interface{}, message string, c *gin.Context) { + Result(ERROR, data, message, c) +} diff --git a/admin/server/model/example/exa_breakpoint_continue.go b/admin/server/model/example/exa_breakpoint_continue.go new file mode 100644 index 000000000..3c2924bdb --- /dev/null +++ b/admin/server/model/example/exa_breakpoint_continue.go @@ -0,0 +1,24 @@ +package example + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +// file struct, 文件结构体 +type ExaFile struct { + global.GVA_MODEL + FileName string + FileMd5 string + FilePath string + ExaFileChunk []ExaFileChunk + ChunkTotal int + IsFinish bool +} + +// file chunk struct, 切片结构体 +type ExaFileChunk struct { + global.GVA_MODEL + ExaFileID uint + FileChunkNumber int + FileChunkPath string +} diff --git a/admin/server/model/example/exa_customer.go b/admin/server/model/example/exa_customer.go new file mode 100644 index 000000000..e78dd093f --- /dev/null +++ b/admin/server/model/example/exa_customer.go @@ -0,0 +1,15 @@ +package example + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" +) + +type ExaCustomer struct { + global.GVA_MODEL + CustomerName string `json:"customerName" form:"customerName" gorm:"comment:客户名"` // 客户名 + CustomerPhoneData string `json:"customerPhoneData" form:"customerPhoneData" gorm:"comment:客户手机号"` // 客户手机号 + SysUserID uint `json:"sysUserId" form:"sysUserId" gorm:"comment:管理ID"` // 管理ID + SysUserAuthorityID uint `json:"sysUserAuthorityID" form:"sysUserAuthorityID" gorm:"comment:管理角色ID"` // 管理角色ID + SysUser system.SysUser `json:"sysUser" form:"sysUser" gorm:"comment:管理详情"` // 管理详情 +} diff --git a/admin/server/model/example/exa_file_upload_download.go b/admin/server/model/example/exa_file_upload_download.go new file mode 100644 index 000000000..bf4c7df7e --- /dev/null +++ b/admin/server/model/example/exa_file_upload_download.go @@ -0,0 +1,17 @@ +package example + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +type ExaFileUploadAndDownload struct { + global.GVA_MODEL + Name string `json:"name" gorm:"comment:文件名"` // 文件名 + Url string `json:"url" gorm:"comment:文件地址"` // 文件地址 + Tag string `json:"tag" gorm:"comment:文件标签"` // 文件标签 + Key string `json:"key" gorm:"comment:编号"` // 编号 +} + +func (ExaFileUploadAndDownload) TableName() string { + return "exa_file_upload_and_downloads" +} diff --git a/admin/server/model/example/response/exa_breakpoint_continue.go b/admin/server/model/example/response/exa_breakpoint_continue.go new file mode 100644 index 000000000..54aa3516b --- /dev/null +++ b/admin/server/model/example/response/exa_breakpoint_continue.go @@ -0,0 +1,11 @@ +package response + +import "github.com/flipped-aurora/gin-vue-admin/server/model/example" + +type FilePathResponse struct { + FilePath string `json:"filePath"` +} + +type FileResponse struct { + File example.ExaFile `json:"file"` +} diff --git a/admin/server/model/example/response/exa_customer.go b/admin/server/model/example/response/exa_customer.go new file mode 100644 index 000000000..7fd26f9d6 --- /dev/null +++ b/admin/server/model/example/response/exa_customer.go @@ -0,0 +1,7 @@ +package response + +import "github.com/flipped-aurora/gin-vue-admin/server/model/example" + +type ExaCustomerResponse struct { + Customer example.ExaCustomer `json:"customer"` +} diff --git a/admin/server/model/example/response/exa_file_upload_download.go b/admin/server/model/example/response/exa_file_upload_download.go new file mode 100644 index 000000000..c1b7931a0 --- /dev/null +++ b/admin/server/model/example/response/exa_file_upload_download.go @@ -0,0 +1,7 @@ +package response + +import "github.com/flipped-aurora/gin-vue-admin/server/model/example" + +type ExaFileResponse struct { + File example.ExaFileUploadAndDownload `json:"file"` +} diff --git a/admin/server/model/gaia/ForwardingExtend.go b/admin/server/model/gaia/ForwardingExtend.go new file mode 100644 index 000000000..34b01cf77 --- /dev/null +++ b/admin/server/model/gaia/ForwardingExtend.go @@ -0,0 +1,48 @@ +package gaia + +import ( + "github.com/goccy/go-json" + "github.com/gofrs/uuid/v5" + "github.com/richardlehane/msoleps/types" + "time" +) + +type ForwardingExtend struct { + ID uuid.UUID `gorm:"column:id;type:uuid;primary_key;default:uuid_generate_v4()" json:"id"` // 唯一标识符 + Path string `gorm:"column:path;type:varchar(255);not null" json:"path"` // 转发路径 + Address string `gorm:"column:address;type:varchar(255);not null" json:"address"` // 转发地址 + Header string `gorm:"column:header;type:text;not null;default:'[]'::text" json:"header"` // 请求头信息 + Description string `gorm:"column:description;type:text;not null;default:''::character varying" json:"description"` // 描述信息 +} + +func (ForwardingExtend) TableName() string { + return "forwarding_extend" +} + +type ForwardingAddressExtend struct { + ID uuid.UUID `gorm:"column:id;primary_key;type:uuid;default:uuid_generate_v4()" json:"id"` // 唯一标识符 + ForwardingID uuid.UUID `gorm:"column:forwarding_id;type:uuid;not null" json:"forwarding_id"` // 转发ID + Path string `gorm:"column:path;type:varchar(255);not null" json:"path"` // 路径 + Models string `gorm:"column:models;type:varchar(255);not null" json:"models"` // 模型 + Description string `gorm:"column:description;type:text;default:''" json:"description"` // 描述 + ContentType int `gorm:"column:content_type;type:integer;not null;default:0" json:"content_type"` // 内容类型 + Billing string `gorm:"column:billing;type:text;default:'[]'" json:"billing"` // 计费信息 + Status bool `gorm:"column:status;type:boolean;default:true" json:"status"` // 状态 +} + +func (ForwardingAddressExtend) TableName() string { + return "public.forwarding_address_extend" +} + +type AccountLayoverRecordExtend struct { + ID uuid.UUID `gorm:"column:id;type:uuid;primary_key;default:uuid_generate_v4()" json:"id"` // 记录ID + AccountID uuid.UUID `gorm:"column:account_id;type:uuid;not null" json:"account_id"` // 账户ID + ForwardingID uuid.UUID `gorm:"column:forwarding_id;type:uuid;not null" json:"forwarding_id"` // 转发ID + Money types.Decimal `gorm:"column:money;type:numeric(16,7)" json:"money"` // 金额 + Info json.RawMessage `gorm:"column:info;type:json" json:"info"` // 附加信息 + CreatedAt time.Time `gorm:"column:created_at;type:timestamp(6);not null;default:CURRENT_TIMESTAMP" json:"created_at"` // 创建时间 +} + +func (AccountLayoverRecordExtend) TableName() string { + return "account_layover_record_extend" +} diff --git a/admin/server/model/gaia/account.go b/admin/server/model/gaia/account.go new file mode 100644 index 000000000..97315f5fd --- /dev/null +++ b/admin/server/model/gaia/account.go @@ -0,0 +1,82 @@ +package gaia + +import ( + "errors" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/gofrs/uuid/v5" + "time" +) + +const UserActive = "active" // 用户状态: 活跃 +const UserPending = "pending" // 用户状态: 待办的 +const UserUninitialized = "uninitialized" // 用户状态: 未初始化 +const UserBanned = "banned" // 用户状态: 禁止 +const UserClosed = "closed" // 用户状态: 关闭 +const DefaultProviderType = "oauth2" // 默认提供者类型: oauth2 + +// Account gaia 用户表 +type Account struct { + ID uuid.UUID `json:"id" gorm:"primaryKey;comment:账户唯一标识符"` + Name string `json:"name" gorm:"not null;comment:账户名称"` + Email string `json:"email" gorm:"not null;index:account_email_idx;comment:账户邮箱"` + Password string `json:"password" gorm:"comment:账户密码"` + PasswordSalt string `json:"password_salt" gorm:"comment:加密密码的盐值"` + Avatar string `json:"avatar" gorm:"comment:头像URL"` + InterfaceLanguage string `json:"interface_language" gorm:"comment:用户界面语言"` + InterfaceTheme string `json:"interface_theme" gorm:"comment:用户界面主题"` + Timezone string `json:"timezone" gorm:"comment:用户时区"` + LastLoginAt time.Time `json:"last_login_at" gorm:"comment:最后登录时间"` + LastLoginIP string `json:"last_login_ip" gorm:"comment:最后登录的IP地址"` + Status string `json:"status" gorm:"default:'active';not null;comment:账户状态"` + InitializedAt time.Time `json:"initialized_at" gorm:"comment:账户初始化时间"` + CreatedAt time.Time `json:"created_at" gorm:"not null;default:CURRENT_TIMESTAMP;comment:账户创建时间"` + UpdatedAt time.Time `json:"updated_at" gorm:"not null;default:CURRENT_TIMESTAMP;comment:账户更新时间"` + LastActiveAt time.Time `json:"last_active_at" gorm:"not null;default:CURRENT_TIMESTAMP;comment:最后活跃时间"` +} + +// AccountIntegrate gaia 用户提供者关联表 +type AccountIntegrate struct { + ID uuid.UUID `json:"id" gorm:"index;comment:唯一标识"` + AccountID uuid.UUID `json:"account_id" gorm:"not null;comment:账户ID"` + Provider string `json:"provider" gorm:"not null;comment:提供者类型"` + OpenID string `json:"open_id" gorm:"not null;comment:开放ID"` + EncryptedToken string `json:"encrypted_token" gorm:"not null;comment:加密令牌"` + CreatedAt time.Time `json:"created_at" gorm:"not null;comment:创建时间"` + UpdatedAt time.Time `json:"updated_at" gorm:"not null;comment:更新时间"` +} + +// TenantAccountJoin gaia 用户和命名空间关联表 +type TenantAccountJoin struct { + ID string `json:"id" gorm:"primary_key;comment:租户账户连接ID"` + TenantID string `json:"tenant_id" gorm:"not null;comment:租户ID"` + AccountID string `json:"account_id" gorm:"not null;comment:账户ID"` + Role string `json:"role" gorm:"not null;default:normal;comment:角色"` + InvitedBy *string `json:"invited_by" gorm:"comment:邀请人ID"` + CreatedAt time.Time `json:"created_at" gorm:"not null;default:CURRENT_TIMESTAMP;comment:创建时间"` + UpdatedAt time.Time `json:"updated_at" gorm:"not null;default:CURRENT_TIMESTAMP;comment:更新时间"` + Current bool `json:"current" gorm:"not null;default:false;comment:当前状态"` +} + +// AccountDingTalkExtend gaia钉钉关联表 +type AccountDingTalkExtend struct { + ID uuid.UUID `json:"id" gorm:"primaryKey;comment:账户唯一标识符"` + DingTalk string `json:"ding_talk" gorm:"index:account_ding_talk_idx;comment:关联钉钉id"` +} + +func (Account) TableName() string { return "accounts" } +func (AccountIntegrate) TableName() string { return "account_integrates" } +func (TenantAccountJoin) TableName() string { return "tenant_account_joins" } +func (AccountDingTalkExtend) TableName() string { return "account_ding_talk_extend" } + +// GetAccount +// @description: Get user information through the user provider relationship table +// @return account, err error +func (i Account) GetAccount(username string) (integrate AccountIntegrate, err error) { + // init + if err = global.GVA_DB.Where("account_id IN (?) AND provider=?", + []string{i.ID.String(), username}, DefaultProviderType).First(&integrate).Error; err != nil { + return integrate, errors.New("the query for the user-provider relationship could not be found") + } + // return + return integrate, nil +} diff --git a/admin/server/model/gaia/account_money_extend.go b/admin/server/model/gaia/account_money_extend.go new file mode 100644 index 000000000..0a5232079 --- /dev/null +++ b/admin/server/model/gaia/account_money_extend.go @@ -0,0 +1,22 @@ +// 自动生成模板AccountMoneyExtend +package gaia + +import ( + "github.com/gofrs/uuid/v5" + "time" +) + +// accountMoneyExtend表 结构体 AccountMoneyExtend +type AccountMoneyExtend struct { + Id *string `json:"id" form:"id" gorm:"primarykey;column:id;comment:;"` //id字段 + AccountId uuid.UUID `json:"accountId" form:"accountId" gorm:"uniqueIndex;column:account_id;comment:;"` //accountId字段 + TotalQuota float64 `json:"totalQuota" form:"totalQuota" gorm:"column:total_quota;comment:;"` //totalQuota字段 + UsedQuota float64 `json:"usedQuota" form:"usedQuota" gorm:"column:used_quota;comment:;"` //usedQuota字段 + CreatedAt *time.Time `json:"createdAt" form:"createdAt" gorm:"column:created_at;comment:;size:6;"` //createdAt字段 + UpdatedAt *time.Time `json:"updatedAt" form:"updatedAt" gorm:"column:updated_at;comment:;size:6;"` //updatedAt字段 +} + +// TableName accountMoneyExtend表 AccountMoneyExtend自定义表名 account_money_extend +func (AccountMoneyExtend) TableName() string { + return "account_money_extend" +} diff --git a/admin/server/model/gaia/api_tokens.go b/admin/server/model/gaia/api_tokens.go new file mode 100644 index 000000000..22e61b7a9 --- /dev/null +++ b/admin/server/model/gaia/api_tokens.go @@ -0,0 +1,71 @@ +package gaia + +import ( + "github.com/gofrs/uuid/v5" + "time" +) + +type ApiTokens struct { + ID uuid.UUID `gorm:"column:id;type:uuid;primary_key;default:uuid_generate_v4()" json:"id"` // 主键ID + AppID uuid.UUID `gorm:"column:app_id;type:uuid" json:"app_id"` // 应用ID + Type string `gorm:"column:type;type:varchar(16);not null" json:"type"` // 令牌类型 + Token string `gorm:"column:token;type:varchar(255);not null" json:"token"` // 令牌值 + LastUsedAt *time.Time `gorm:"column:last_used_at;type:timestamp(6)" json:"last_used_at"` // 最后使用时间 + CreatedAt time.Time `gorm:"column:created_at;type:timestamp(6);not null;default:CURRENT_TIMESTAMP" json:"created_at"` // 创建时间 + TenantID uuid.UUID `gorm:"column:tenant_id;type:uuid" json:"tenant_id"` // 工作空间ID +} + +func (*ApiTokens) TableName() string { + return "api_tokens" +} +func (a *ApiTokens) GenerateToken() string { + return a.Token[:3] + "..." + a.Token[len(a.Token)-23:] +} + +type ApiTokenMoneyDailyStatExtend struct { + ID uuid.UUID `gorm:"column:id;type:uuid;primary_key;default:uuid_generate_v4()" json:"id"` // 主键ID + AppTokenID uuid.UUID `gorm:"column:app_token_id;type:uuid;not null" json:"app_token_id"` // 应用令牌ID + AccumulatedQuota float64 `gorm:"column:accumulated_quota;type:numeric(16,7)" json:"accumulated_quota"` // 累计配额 + DayUsedQuota float64 `gorm:"column:day_used_quota;type:numeric(16,7)" json:"day_used_quota"` // 日使用配额 + DayLimitQuota float64 `gorm:"column:day_limit_quota;type:numeric(16,7)" json:"day_limit_quota"` // 日限额配额 + StatAt time.Time `gorm:"column:stat_at;type:timestamp(6);not null" json:"stat_at"` // 统计时间 + CreatedAt time.Time `gorm:"column:created_at;type:timestamp(6);not null;default:CURRENT_TIMESTAMP" json:"created_at"` // 创建时间 + UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp(6);not null;default:CURRENT_TIMESTAMP" json:"updated_at"` // 更新时间 +} + +func (*ApiTokenMoneyDailyStatExtend) TableName() string { + return "api_token_money_daily_stat_extend" +} + +type ApiTokenMoneyMonthlyStatExtend struct { + ID uuid.UUID `gorm:"column:id;type:uuid;primary_key;default:uuid_generate_v4()" json:"id"` // 主键ID + AppTokenID uuid.UUID `gorm:"column:app_token_id;type:uuid;not null" json:"app_token_id"` // 应用令牌ID + AccumulatedQuota float64 `gorm:"column:accumulated_quota;type:numeric(16,7)" json:"accumulated_quota"` // 累计配额 + MonthUsedQuota float64 `gorm:"column:month_used_quota;type:numeric(16,7)" json:"month_used_quota"` // 月使用配额 + MonthLimitQuota float64 `gorm:"column:month_limit_quota;type:numeric(16,7)" json:"month_limit_quota"` // 月限额配额 + StatAt time.Time `gorm:"column:stat_at;type:timestamp(6);not null" json:"stat_at"` // 统计时间 + CreatedAt time.Time `gorm:"column:created_at;type:timestamp(6);not null;default:CURRENT_TIMESTAMP" json:"created_at"` // 创建时间 + UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp(6);not null;default:CURRENT_TIMESTAMP" json:"updated_at"` // 更新时间 +} + +func (*ApiTokenMoneyMonthlyStatExtend) TableName() string { + return "api_token_money_monthly_stat_extend" +} + +type ApiTokenMoneyExtend struct { + ID uuid.UUID `gorm:"column:id;type:uuid;primary_key;default:uuid_generate_v4()" json:"id"` // 主键ID + AppTokenID uuid.UUID `gorm:"column:app_token_id;type:uuid" json:"app_token_id"` // 应用令牌ID + AccumulatedQuota float64 `gorm:"column:accumulated_quota;type:numeric(16,7)" json:"accumulated_quota"` // 累计配额 + DayUsedQuota float64 `gorm:"column:day_used_quota;type:numeric(16,7)" json:"day_used_quota"` // 日使用配额 + MonthUsedQuota float64 `gorm:"column:month_used_quota;type:numeric(16,7)" json:"month_used_quota"` // 月使用配额 + DayLimitQuota float64 `gorm:"column:day_limit_quota;type:numeric(16,7)" json:"day_limit_quota"` // 日限额配额 + MonthLimitQuota float64 `gorm:"column:month_limit_quota;type:numeric(16,7)" json:"month_limit_quota"` // 月限额配额 + IsDeleted bool `gorm:"column:is_deleted;type:bool;not null;default:false" json:"is_deleted"` // 是否删除 + UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp(6);not null;default:CURRENT_TIMESTAMP" json:"updated_at"` // 更新时间 + CreatedAt time.Time `gorm:"column:created_at;type:timestamp(6);not null;default:CURRENT_TIMESTAMP" json:"created_at"` // 创建时间 + Description string `gorm:"column:description;type:varchar(50)" json:"description"` // 描述 +} + +func (*ApiTokenMoneyExtend) TableName() string { + return "api_token_money_extend" +} diff --git a/admin/server/model/gaia/apps.go b/admin/server/model/gaia/apps.go new file mode 100644 index 000000000..98393032b --- /dev/null +++ b/admin/server/model/gaia/apps.go @@ -0,0 +1,48 @@ +package gaia + +import ( + "github.com/gofrs/uuid/v5" + "time" +) + +type Apps struct { + ID uuid.UUID `gorm:"column:id;type:uuid;primary_key;default:uuid_generate_v4()" json:"id"` // 应用ID + TenantID uuid.UUID `gorm:"column:tenant_id;type:uuid;not null" json:"tenant_id"` // 工作空间ID + Name string `gorm:"column:name;type:varchar(255);not null" json:"name"` // 应用名称 + Mode string `gorm:"column:mode;type:varchar(255);not null" json:"mode"` // 应用模式 + Icon string `gorm:"column:icon;type:varchar(255)" json:"icon"` // 应用图标 + IconBackground string `gorm:"column:icon_background;type:varchar(255)" json:"icon_background"` // 应用图标背景 + AppModelConfigID uuid.UUID `gorm:"column:app_model_config_id;type:uuid" json:"app_model_config_id"` // 应用模型配置ID + Status string `gorm:"column:status;type:varchar(255);not null;default:normal" json:"status"` // 应用状态 + EnableSite bool `gorm:"column:enable_site;not null" json:"enable_site"` // 是否启用站点 + EnableAPI bool `gorm:"column:enable_api;not null" json:"enable_api"` // 是否启用API + APIRPM int `gorm:"column:api_rpm;type:int4;not null;default:0" json:"api_rpm"` // API每分钟请求限制 + APIRPH int `gorm:"column:api_rph;type:int4;not null;default:0" json:"api_rph"` // API每小时请求限制 + IsDemo bool `gorm:"column:is_demo;not null;default:false" json:"is_demo"` // 是否为演示应用 + IsPublic bool `gorm:"column:is_public;not null;default:false" json:"is_public"` // 是否为公开应用 + CreatedAt time.Time `gorm:"column:created_at;type:timestamp(6);not null;default:CURRENT_TIMESTAMP(0)" json:"created_at"` // 创建时间 + UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp(6);not null;default:CURRENT_TIMESTAMP(0)" json:"updated_at"` // 更新时间 + IsUniversal bool `gorm:"column:is_universal;not null;default:false" json:"is_universal"` // 是否为通用应用 + WorkflowID uuid.UUID `gorm:"column:workflow_id;type:uuid" json:"workflow_id"` // 工作流ID + Description string `gorm:"column:description;type:text;not null;default:''" json:"description"` // 应用描述 + Tracing string `gorm:"column:tracing;type:text" json:"tracing"` // 追踪信息 + MaxActiveRequests int `gorm:"column:max_active_requests;type:int4" json:"max_active_requests"` // 最大活跃请求数 + IconType string `gorm:"column:icon_type;type:varchar(255)" json:"icon_type"` // 图标类型 + CreatedBy uuid.UUID `gorm:"column:created_by;type:uuid" json:"created_by"` // 创建者ID + UpdatedBy uuid.UUID `gorm:"column:updated_by;type:uuid" json:"updated_by"` // 更新者ID + UseIconAsAnswerIcon bool `gorm:"column:use_icon_as_answer_icon;not null;default:false" json:"use_icon_as_answer_icon"` // 是否使用图标作为回答图标 +} + +func (Apps) TableName() string { + return "apps" +} + +type AppStatisticsExtend struct { + ID uuid.UUID `gorm:"column:id;type:uuid;primary_key;default:uuid_generate_v4()" json:"id"` // 唯一标识符 + AppID uuid.UUID `gorm:"column:app_id;type:uuid;not null" json:"app_id"` // 应用ID + Number int `gorm:"column:number;type:integer;not null" json:"number"` // 统计数量 +} + +func (AppStatisticsExtend) TableName() string { + return "app_statistics_extend" +} diff --git a/admin/server/model/gaia/end_user.go b/admin/server/model/gaia/end_user.go new file mode 100644 index 000000000..5ba44613a --- /dev/null +++ b/admin/server/model/gaia/end_user.go @@ -0,0 +1,22 @@ +package gaia + +import "time" + +const IndirectAccessUser = "end_user" +const UserUsingApiRequest = "service_api" +const UsernameUsingApiRequest = "gaia_test_api_user" + +type EndUser struct { + ID string `json:"id" gorm:"primary_key;comment:用户唯一标识"` + TenantID string `json:"tenant_id" gorm:"comment:租户唯一标识"` + AppID string `json:"app_id" gorm:"comment:应用唯一标识"` // 使用指针允许该字段为NULL + Type string `json:"type" gorm:"comment:用户类型"` + ExternalUserID string `json:"external_user_id" gorm:"comment:外部用户唯一标识"` // 使用指针允许该字段为NULL + Name string `json:"name" gorm:"comment:用户名称"` // 使用指针允许该字段为NULL + IsAnonymous bool `json:"is_anonymous" gorm:"comment:是否匿名"` + SessionID string `json:"session_id" gorm:"comment:会话唯一标识"` + CreatedAt time.Time `json:"created_at" gorm:"comment:创建时间"` + UpdatedAt time.Time `json:"updated_at" gorm:"comment:更新时间"` +} + +func (EndUser) TableName() string { return "end_users" } diff --git a/admin/server/model/gaia/messages.go b/admin/server/model/gaia/messages.go new file mode 100644 index 000000000..43b3723d0 --- /dev/null +++ b/admin/server/model/gaia/messages.go @@ -0,0 +1,46 @@ +package gaia + +import ( + "github.com/gofrs/uuid/v5" + "time" +) + +const ChatRequestTypeApi = "api" // 聊天请求类型为api +const MessagesSucceeded = "normal" // 聊天状态成功 + +type Messages struct { + ID uuid.UUID `gorm:"column:id;primary_key;default:uuid_generate_v4()" json:"id"` // 消息ID + AppID uuid.UUID `gorm:"column:app_id;not null" json:"app_id"` // 应用ID + ModelProvider string `gorm:"column:model_provider" json:"model_provider"` // 模型提供商 + ModelID string `gorm:"column:model_id" json:"model_id"` // 模型ID + OverrideModelConfigs string `gorm:"column:override_model_configs" json:"override_model_configs"` // 覆盖模型配置 + ConversationID uuid.UUID `gorm:"column:conversation_id;not null" json:"conversation_id"` // 对话ID + Inputs string `gorm:"column:inputs" json:"inputs"` // 输入数据 + Query string `gorm:"column:query;not null" json:"query"` // 查询内容 + Message string `gorm:"column:message;not null" json:"message"` // 消息内容 + MessageTokens int `gorm:"column:message_tokens;not null;default:0" json:"message_tokens"` // 消息令牌数 + MessageUnitPrice float64 `gorm:"column:message_unit_price;not null" json:"message_unit_price"` // 消息单价 + Answer string `gorm:"column:answer;not null" json:"answer"` // 回答内容 + AnswerTokens int `gorm:"column:answer_tokens;not null;default:0" json:"answer_tokens"` // 回答令牌数 + AnswerUnitPrice float64 `gorm:"column:answer_unit_price;not null" json:"answer_unit_price"` // 回答单价 + ProviderResponseLatency float64 `gorm:"column:provider_response_latency;not null;default:0" json:"provider_response_latency"` // 提供商响应延迟 + TotalPrice float64 `gorm:"column:total_price" json:"total_price"` // 总价格 + Currency string `gorm:"column:currency;not null" json:"currency"` // 货币 + FromSource string `gorm:"column:from_source;not null" json:"from_source"` // 来源 + FromEndUserID uuid.UUID `gorm:"column:from_end_user_id" json:"from_end_user_id"` // 来自最终用户ID + FromAccountID uuid.UUID `gorm:"column:from_account_id" json:"from_account_id"` // 来自账户ID + CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP" json:"created_at"` // 创建时间 + UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP" json:"updated_at"` // 更新时间 + AgentBased bool `gorm:"column:agent_based;not null;default:false" json:"agent_based"` // 是否基于代理 + MessagePriceUnit float64 `gorm:"column:message_price_unit;not null;default:0.001" json:"message_price_unit"` // 消息价格单位 + AnswerPriceUnit float64 `gorm:"column:answer_price_unit;not null;default:0.001" json:"answer_price_unit"` // 回答价格单位 + WorkflowRunID uuid.UUID `gorm:"column:workflow_run_id" json:"workflow_run_id"` // 工作流运行ID + Status string `gorm:"column:status;not null;default:normal" json:"status"` // 状态 + Error string `gorm:"column:error" json:"error"` // 错误信息 + MessageMetadata string `gorm:"column:message_metadata" json:"message_metadata"` // 消息元数据 + InvokeFrom string `gorm:"column:invoke_from" json:"invoke_from"` // 调用来源 +} + +func (Messages) TableName() string { + return "messages" +} diff --git a/admin/server/model/gaia/request/dashboard.go b/admin/server/model/gaia/request/dashboard.go new file mode 100644 index 000000000..7a0349ca0 --- /dev/null +++ b/admin/server/model/gaia/request/dashboard.go @@ -0,0 +1,36 @@ +package request + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "time" +) + +type DashboardSearch struct { + request.PageInfo +} + +type GetAccountQuotaRankingDataReq struct { + request.PageInfo +} + +// GetAppQuotaRankingDataReq 获取应用配额排名数据 +type GetAppQuotaRankingDataReq struct { + request.PageInfo +} + +// GetAppTokenQuotaRankingDataReq 获取应用配额排名数据 +type GetAppTokenQuotaRankingDataReq struct { + request.PageInfo +} + +type GetAppTokenDailyQuotaDataReq struct { + request.PageInfo + AppId string `json:"app_id" form:"app_id"` // 应用ID + StatAt time.Time `json:"stat_at" form:"stat_at"` // 统计时间 +} + +// GetAiImageQuotaRankingDataReq 获取AI图片使用量排名数据 +type GetAiImageQuotaRankingDataReq struct { + request.PageInfo + StatAt time.Time `json:"stat_at" form:"stat_at"` // 统计时间 +} diff --git a/admin/server/model/gaia/request/quota.go b/admin/server/model/gaia/request/quota.go new file mode 100644 index 000000000..c16def082 --- /dev/null +++ b/admin/server/model/gaia/request/quota.go @@ -0,0 +1,6 @@ +package request + +type SetUserQuotaRequest struct { + Uid string `json:"uid" form:"uid"` // 用户id + Quota float64 `json:"quota" form:"quota"` // 额度 +} diff --git a/admin/server/model/gaia/request/tenants.go b/admin/server/model/gaia/request/tenants.go new file mode 100644 index 000000000..02e4e599b --- /dev/null +++ b/admin/server/model/gaia/request/tenants.go @@ -0,0 +1,11 @@ + +package request + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + +) + +type TenantsSearch struct{ + request.PageInfo +} diff --git a/admin/server/model/gaia/request/test.go b/admin/server/model/gaia/request/test.go new file mode 100644 index 000000000..739dd9e05 --- /dev/null +++ b/admin/server/model/gaia/request/test.go @@ -0,0 +1,39 @@ +package request + +import "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + +const GetAppRequestFilterSuccess = 1 // 筛选成功 +const GetAppRequestFilterFailure = 2 // 筛选失败 +const PostgreSQLDataLimit = 1000 // 查询数据限制 +const PostgreSQLDataTypeUUID = "uuid" // uuid类型 +const PostgreSQLDataTypeCharacterVarying = "character varying" // 可变字符类型 +const PostgreSQLDataTypeText = "text" // 文本类型 +const PostgreSQLDataTypeJSON = "json" // JSON类型 +const PostgreSQLDataTypeInteger = "integer" // 整数类型 +const PostgreSQLDataTypeDoublePrecision = "double precision" // 双精度浮点数类型 +const PostgreSQLDataTypeNumeric = "numeric" // 数值类型 +const PostgreSQLDataTypeTimestampWithoutTZ = "timestamp without time zone" // 不带时区的时间戳类型 +const PostgreSQLDataTypeBoolean = "boolean" // 布尔类型 +const PostgreSQLDefaultSchema = "public" // 默认环境 + +// SyncDatabaseTableData 同步数据库表数据 +type SyncDatabaseTableData struct { + LogTable string `json:"log_table" form:"log_table" gorm:"comment:旧表"` + NewTable string `json:"new_table" form:"new_table" gorm:"comment:要同步数据到的旧表"` + KeyName string `json:"key_name" form:"key_name" gorm:"comment:表主键名"` + OrderName string `json:"order_name" form:"order_name" gorm:"comment:排序索引名"` + GroupName string `json:"group_name" form:"group_name" gorm:"comment:表分组名(可为空)"` +} + +type GetAppRequestTestRequest struct { + request.PageInfo + Apps []string `json:"apps[]" form:"apps[]" gorm:"comment:检索app"` + Status uint `json:"status" form:"status" gorm:"index;comment:状态"` + BatchId uint `json:"batch_id" form:"batch_id" gorm:"index;comment:批次ID"` +} + +type DatabaseTableColumn struct { + ColumnName string `json:"column_name" form:"column_name" gorm:"comment:列名"` + DataType string `json:"data_type" form:"data_type" gorm:"comment:数据类型"` + IsNullable bool `json:"is_nullable" form:"is_nullable" gorm:"comment:是否为空"` +} diff --git a/admin/server/model/gaia/response/dashboard.go b/admin/server/model/gaia/response/dashboard.go new file mode 100644 index 000000000..6023740f2 --- /dev/null +++ b/admin/server/model/gaia/response/dashboard.go @@ -0,0 +1,51 @@ +package response + +// GetAccountQuotaRankingDataRes 获取账户配额排名数据的响应结构 +type GetAccountQuotaRankingDataRes struct { + Ranking int `json:"ranking"` // 排名 + Name string `json:"name"` // 姓名 + UsedQuota float64 `json:"used_quota"` // 已使用配额 + TotalQuota float64 `json:"total_quota"` // 总配额 +} + +// GetAppQuotaRankingDataRes 获取应用配额排名数据的响应结构 +type GetAppQuotaRankingDataRes struct { + Ranking int `json:"ranking"` // 排名 + Name string `json:"name"` // 应用名称 + Mode string `json:"mode"` // 应用类型 + AccountName string `json:"account_name"` // 账号名称 + UsedQuota float64 `json:"used_quota"` // 已使用配额 + AppID string `json:"app_id"` + TotalCost float64 `json:"total_cost"` + MessageCost float64 `json:"message_cost"` + WorkflowCost float64 `json:"workflow_cost"` + RecordNum float64 `json:"record_num"` + UseNum int `json:"use_num"` +} + +// GetAppTokenQuotaRankingDataRes 获取应用密钥配额排名数据的响应结构 +type GetAppTokenQuotaRankingDataRes struct { + Ranking int `json:"ranking"` // 排名 + Name string `json:"name"` // 对应应用名称 + AppToken string `json:"app_token"` // 密钥(需要加密显示) + AccumulatedQuota float64 `json:"accumulated_quota"` // 累计使用 + DayUsedQuota float64 `json:"day_used_quota"` // 日使用 + MonthUsedQuota float64 `json:"month_used_quota"` // 月使用 + DayLimitQuota float64 `json:"day_limit_quota"` // 日限额 + MonthLimitQuota float64 `json:"month_limit_quota"` // 月限额 +} + +type GetAppTokenDailyQuotaDataRes struct { + StatDate string `json:"stat_date"` + TotalUsed float64 `json:"total_used"` +} + +// GetAiImageQuotaRankingRes 获取AI图片配额排名数据的响应结构 +type GetAiImageQuotaRankingRes struct { + Ranking int `json:"ranking"` // 排名 + Address string `json:"address"` // 域名 + Path string `json:"path"` // 路径 + Model string `json:"model"` // 模型 + TotalCost float64 `json:"total_cost"` // 总花费 + RecordNum int `json:"record_num"` // 调用次数 +} diff --git a/admin/server/model/gaia/response/quota.go b/admin/server/model/gaia/response/quota.go new file mode 100644 index 000000000..cdf0fcfff --- /dev/null +++ b/admin/server/model/gaia/response/quota.go @@ -0,0 +1,9 @@ +package response + +type GetQuotaManagementDataResponse struct { + Uid string `json:"uid"` // 用户id + Ranking int `json:"ranking"` // 排名 + Name string `json:"name"` // 姓名 + UsedQuota float64 `json:"used_quota"` // 已使用配额 + TotalQuota float64 `json:"total_quota"` // 总配额 +} diff --git a/admin/server/model/gaia/response/test.go b/admin/server/model/gaia/response/test.go new file mode 100644 index 000000000..58b1cb071 --- /dev/null +++ b/admin/server/model/gaia/response/test.go @@ -0,0 +1,12 @@ +package response + +type GetAppRequestTestDataResponse struct { + Name string `json:"name" gorm:"comment:应用名"` + Status bool `json:"status" gorm:"comment:状态"` + Inputs string `json:"inputs" gorm:"comment:输入"` + Outputs string `json:"outputs" gorm:"comment:输出"` + Error string `json:"error" gorm:"comment:错误信息"` + Comparison string `json:"comparison" gorm:"comment:历史对照"` + LogTime float64 `json:"log_time" gorm:"not null;default:0;comment:旧耗时"` + ElapsedTime float64 `json:"elapsed_time" gorm:"not null;default:0;comment:耗时"` +} diff --git a/admin/server/model/gaia/system_integration.go b/admin/server/model/gaia/system_integration.go new file mode 100644 index 000000000..df41ef4b3 --- /dev/null +++ b/admin/server/model/gaia/system_integration.go @@ -0,0 +1,23 @@ +package gaia + +const SystemIntegrationDingTalk = uint(1) // 钉钉集成 +const SystemIntegrationWeiXin = uint(2) // 微信集成 +const SystemIntegrationFeiShu = uint(3) // 飞书集成 + +// SystemIntegration 系统集成表 +type SystemIntegration struct { + Id uint `json:"id" form:"id" gorm:"primarykey;column:id;comment:id;"` + Classify uint `json:"classify" gorm:"column:classify;default:1;comment:集成类型"` + Status bool `json:"status" gorm:"column:status;default:f;comment:配置启用状态"` + CorpID string `json:"corp_id" gorm:"default:;comment:企业id"` + AgentID string `json:"agent_id" gorm:"default:;comment:代理Id"` + AppID string `json:"app_id" gorm:"default:;comment:应用ID"` + AppKey string `json:"app_key" gorm:"default:;comment:加密key"` + AppSecret string `json:"app_secret" gorm:"default:;comment:加密密钥"` + Test bool `json:"test" gorm:"default:0;comment:是否测试链接联通性"` +} + +// TableName system_integration_extend表 SystemIntegration自定义表名 system_integration_extend +func (SystemIntegration) TableName() string { + return "system_integration_extend" +} diff --git a/admin/server/model/gaia/tenants.go b/admin/server/model/gaia/tenants.go new file mode 100644 index 000000000..6cbbcd462 --- /dev/null +++ b/admin/server/model/gaia/tenants.go @@ -0,0 +1,48 @@ +// 自动生成模板Tenants +package gaia + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/gofrs/uuid/v5" + "time" +) + +// tenants表 结构体 Tenants +type Tenants struct { + Id string `gorm:"column:id;type:uuid;primary_key;default:uuid_generate_v4()" json:"id"` // 工作空间唯一标识 + Name string `gorm:"column:name;type:varchar(255);not null" json:"name"` // 工作空间名称 + EncryptPublicKey string `gorm:"column:encrypt_public_key;type:text" json:"encrypt_public_key"` // 加密公钥 + Plan string `gorm:"column:plan;type:varchar(255);not null;default:basic" json:"plan"` // 套餐类型 + Status string `gorm:"column:status;type:varchar(255);not null;default:normal" json:"status"` // 工作空间状态 + CreatedAt time.Time `gorm:"column:created_at;type:timestamp(6);not null;default:CURRENT_TIMESTAMP" json:"created_at"` // 创建时间 + UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp(6);not null;default:CURRENT_TIMESTAMP" json:"updated_at"` // 更新时间 + CustomConfig string `gorm:"column:custom_config;type:text" json:"custom_config"` // 自定义配置 +} + +func (*Tenants) TableName() string { + return "tenants" +} + +func (t *Tenants) GetSuperAdminTenantId() string { + err := global.GVA_DB.Order("created_at ASC").First(&t).Error + if err != nil { + global.GVA_LOG.Error("GetSuperAdminTenantId gaia表查询失败,原因: " + err.Error()) + return "" + } + return t.Id +} + +type TenantAccountJoins struct { + ID uuid.UUID `gorm:"column:id;type:uuid;primary_key;default:uuid_generate_v4()" json:"id"` // 唯一标识符 + TenantID uuid.UUID `gorm:"column:tenant_id;type:uuid;not null" json:"tenant_id"` // 工作空间ID + AccountID uuid.UUID `gorm:"column:account_id;type:uuid;not null" json:"account_id"` // 账户ID + Role string `gorm:"column:role;type:varchar(16);not null;default:normal" json:"role"` // 角色 + InvitedBy uuid.UUID `gorm:"column:invited_by;type:uuid" json:"invited_by"` // 邀请人ID + CreatedAt time.Time `gorm:"column:created_at;type:timestamp(6);not null;default:CURRENT_TIMESTAMP" json:"created_at"` // 创建时间 + UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp(6);not null;default:CURRENT_TIMESTAMP" json:"updated_at"` // 更新时间 + Current bool `gorm:"column:current;type:bool;not null;default:false" json:"current"` // 是否当前 +} + +func (TenantAccountJoins) TableName() string { + return "tenant_account_joins" +} diff --git a/admin/server/model/gaia/test.go b/admin/server/model/gaia/test.go new file mode 100644 index 000000000..e9f868663 --- /dev/null +++ b/admin/server/model/gaia/test.go @@ -0,0 +1,34 @@ +package gaia + +const TestDefaultNumber = 2 // 默认测试执行次数 +const BatchStatusInProgress = 1 // 批次状态:执行中 +const BatchStatusCompleted = 2 // 批次状态:已结束 + +// AppRequestTest APP请求测试表 +type AppRequestTest struct { + ID uint `json:"id" gorm:"primarykey;comment:主键"` + AppID string `json:"app_id" gorm:"index;comment:应用ID"` + BatchId uint `json:"batch_id" gorm:"index;comment:批次ID"` + Status string `json:"status" gorm:"index;comment:状态"` + Inputs string `json:"inputs" gorm:"comment:输入"` + Outputs string `json:"outputs" gorm:"comment:输出"` + Error string `json:"error" gorm:"comment:错误信息"` + Comparison string `json:"comparison" gorm:"comment:历史对照"` + LogTime float64 `json:"log_time" gorm:"not null;default:0;comment:旧耗时"` + ElapsedTime float64 `json:"elapsed_time" gorm:"not null;default:0;comment:耗时"` +} + +// AppRequestTestBatch APP请求测试批次表 +type AppRequestTestBatch struct { + ID uint `json:"id" gorm:"primarykey;comment:主键"` + Status uint `json:"status" gorm:"index;comment:状态"` + App uint `json:"app" gorm:"comment:app测试数"` + Sum uint `json:"sum" gorm:"comment:累计测试数"` + CreateTime int64 `json:"create_time" gorm:"comment:创建时间"` + EndTime int64 `json:"end_time" gorm:"comment:结束时间"` + SuccessCount uint `json:"success_count" gorm:"comment:成功数"` + FailureCount uint `json:"failure_count" gorm:"comment:失败数"` +} + +func (AppRequestTest) TableName() string { return "app_request_tests_extend" } +func (AppRequestTestBatch) TableName() string { return "app_request_test_batches_extend" } diff --git a/admin/server/model/gaia/workflow_node_executions.go b/admin/server/model/gaia/workflow_node_executions.go new file mode 100644 index 000000000..fe09c4e81 --- /dev/null +++ b/admin/server/model/gaia/workflow_node_executions.go @@ -0,0 +1,32 @@ +package gaia + +import ( + "github.com/gofrs/uuid/v5" + "time" +) + +type WorkflowNodeExecutions struct { + ID uuid.UUID `gorm:"column:id;primaryKey" json:"id"` // 唯一标识 + TenantID uuid.UUID `gorm:"column:tenant_id" json:"tenant_id"` // 工作空间ID + AppID uuid.UUID `gorm:"column:app_id" json:"app_id"` // 应用ID + WorkflowID uuid.UUID `gorm:"column:workflow_id" json:"workflow_id"` // 工作流ID + TriggeredFrom string `gorm:"column:triggered_from" json:"triggered_from"` // 触发来源 + WorkflowRunID *uuid.UUID `gorm:"column:workflow_run_id" json:"workflow_run_id"` // 工作流运行ID + Index int `gorm:"column:index" json:"index"` // 索引 + PredecessorNodeID *string `gorm:"column:predecessor_node_id" json:"predecessor_node_id"` // 前置节点ID + NodeID string `gorm:"column:node_id" json:"node_id"` // 节点ID + NodeType string `gorm:"column:node_type" json:"node_type"` // 节点类型 + Title string `gorm:"column:title" json:"title"` // 标题 + Inputs *string `gorm:"column:inputs" json:"inputs"` // 输入数据 + ProcessData *string `gorm:"column:process_data" json:"process_data"` // 处理数据 + Outputs *string `gorm:"column:outputs" json:"outputs"` // 输出数据 + Status string `gorm:"column:status" json:"status"` // 状态 + Error *string `gorm:"column:error" json:"error"` // 错误信息 + ElapsedTime float64 `gorm:"column:elapsed_time" json:"elapsed_time"` // 执行耗时 + ExecutionMetadata *string `gorm:"column:execution_metadata" json:"execution_metadata"` // 执行元数据 + CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` // 创建时间 + CreatedByRole string `gorm:"column:created_by_role" json:"created_by_role"` // 创建者角色 + CreatedBy uuid.UUID `gorm:"column:created_by" json:"created_by"` // 创建者ID + FinishedAt *time.Time `gorm:"column:finished_at" json:"finished_at"` // 完成时间 + NodeExecutionID *string `gorm:"column:node_execution_id" json:"node_execution_id"` // 节点执行ID +} diff --git a/admin/server/model/gaia/workflow_runs.go b/admin/server/model/gaia/workflow_runs.go new file mode 100644 index 000000000..fe4678614 --- /dev/null +++ b/admin/server/model/gaia/workflow_runs.go @@ -0,0 +1,32 @@ +package gaia + +import "time" + +const WorkflowSucceeded = "succeeded" // 工作流状态成功 +const WorkflowRunning = "running" // 工作流状态运行中 + +// WorkflowRun 工作流运行 +type WorkflowRun struct { + ID string `json:"id" gorm:"index;comment:工作流运行ID"` + TenantID string `json:"tenant_id" gorm:"not null;comment:租户ID"` + AppID string `json:"app_id" gorm:"not null;comment:应用ID"` + SequenceNumber int `json:"sequence_number" gorm:"not null;comment:序列号"` + WorkflowID string `json:"workflow_id" gorm:"not null;comment:工作流ID"` + Type string `json:"type" gorm:"not null;comment:类型"` + TriggeredFrom string `json:"triggered_from" gorm:"not null;comment:触发来源"` + Version string `json:"version" gorm:"not null;comment:版本"` + Graph string `json:"graph" gorm:"comment:图形表示"` + Inputs string `json:"inputs" gorm:"comment:输入"` + Status string `json:"status" gorm:"not null;comment:状态"` + Outputs string `json:"outputs" gorm:"comment:输出"` + Error string `json:"error" gorm:"comment:错误信息"` + ElapsedTime float64 `json:"elapsed_time" gorm:"not null;default:0;comment:耗时"` + TotalTokens int `json:"total_tokens" gorm:"not null;default:0;comment:总令牌数"` + TotalSteps int `json:"total_steps" gorm:"default:0;comment:总步骤数"` + CreatedByRole string `json:"created_by_role" gorm:"not null;comment:创建者角色"` + CreatedBy string `json:"created_by" gorm:"not null;comment:创建者ID"` + CreatedAt time.Time `json:"created_at" gorm:"not null;default:CURRENT_TIMESTAMP(0);comment:创建时间"` + FinishedAt time.Time `json:"finished_at" gorm:"comment:完成时间"` +} + +func (WorkflowRun) TableName() string { return "workflow_runs" } diff --git a/admin/server/model/system/request/jwt.go b/admin/server/model/system/request/jwt.go new file mode 100644 index 000000000..f420112e2 --- /dev/null +++ b/admin/server/model/system/request/jwt.go @@ -0,0 +1,33 @@ +package request + +import ( + "github.com/gofrs/uuid/v5" + jwt "github.com/golang-jwt/jwt/v4" +) + +// Custom claims structure +type CustomClaims struct { + BaseClaims + BufferTime int64 + jwt.RegisteredClaims + // Extend Start: add gaia token + UserId string `json:"user_id"` + Exp int64 `json:"exp"` + Sub string `json:"sub"` + Email string `json:"email,omitempty"` + // Extend Start: add gaia token +} + +type BaseClaims struct { + UUID uuid.UUID + ID uint + Username string + NickName string + AuthorityId uint + // Extend Start: add gaia token + UserId string `json:"user_id,omitempty"` + Exp int64 `json:"exp,omitempty"` + Email string `json:"email,omitempty"` + Sub string `json:"sub,omitempty"` + // Extend Start: add gaia token +} diff --git a/admin/server/model/system/request/oa_login.go b/admin/server/model/system/request/oa_login.go new file mode 100644 index 000000000..16e5e0608 --- /dev/null +++ b/admin/server/model/system/request/oa_login.go @@ -0,0 +1,5 @@ +package request + +type OaLoginReq struct { + AuthorizeCode string `json:"authorize_code" form:"authorize_code"` // OA返回的授权验证码,用于请求用户信息 +} diff --git a/admin/server/model/system/request/sys_api.go b/admin/server/model/system/request/sys_api.go new file mode 100644 index 000000000..0be8ee72d --- /dev/null +++ b/admin/server/model/system/request/sys_api.go @@ -0,0 +1,14 @@ +package request + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" +) + +// api分页条件查询及排序结构体 +type SearchApiParams struct { + system.SysApi + request.PageInfo + OrderKey string `json:"orderKey"` // 排序 + Desc bool `json:"desc"` // 排序方式:升序false(默认)|降序true +} diff --git a/admin/server/model/system/request/sys_authority_btn.go b/admin/server/model/system/request/sys_authority_btn.go new file mode 100644 index 000000000..98493ff34 --- /dev/null +++ b/admin/server/model/system/request/sys_authority_btn.go @@ -0,0 +1,7 @@ +package request + +type SysAuthorityBtnReq struct { + MenuID uint `json:"menuID"` + AuthorityId uint `json:"authorityId"` + Selected []uint `json:"selected"` +} diff --git a/admin/server/model/system/request/sys_auto_code.go b/admin/server/model/system/request/sys_auto_code.go new file mode 100644 index 000000000..ef70b8fcb --- /dev/null +++ b/admin/server/model/system/request/sys_auto_code.go @@ -0,0 +1,282 @@ +package request + +import ( + "encoding/json" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + model "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/pkg/errors" + "go/token" + "strings" +) + +type AutoCode struct { + Package string `json:"package"` + PackageT string `json:"-"` + TableName string `json:"tableName" example:"表名"` // 表名 + BusinessDB string `json:"businessDB" example:"业务数据库"` // 业务数据库 + StructName string `json:"structName" example:"Struct名称"` // Struct名称 + PackageName string `json:"packageName" example:"文件名称"` // 文件名称 + Description string `json:"description" example:"Struct中文名称"` // Struct中文名称 + Abbreviation string `json:"abbreviation" example:"Struct简称"` // Struct简称 + HumpPackageName string `json:"humpPackageName" example:"go文件名称"` // go文件名称 + GvaModel bool `json:"gvaModel" example:"false"` // 是否使用gva默认Model + AutoMigrate bool `json:"autoMigrate" example:"false"` // 是否自动迁移表结构 + AutoCreateResource bool `json:"autoCreateResource" example:"false"` // 是否自动创建资源标识 + AutoCreateApiToSql bool `json:"autoCreateApiToSql" example:"false"` // 是否自动创建api + AutoCreateMenuToSql bool `json:"autoCreateMenuToSql" example:"false"` // 是否自动创建menu + AutoCreateBtnAuth bool `json:"autoCreateBtnAuth" example:"false"` // 是否自动创建按钮权限 + OnlyTemplate bool `json:"onlyTemplate" example:"false"` // 是否只生成模板 + IsAdd bool `json:"isAdd" example:"false"` // 是否新增 + Fields []*AutoCodeField `json:"fields"` + Module string `json:"-"` + DictTypes []string `json:"-"` + PrimaryField *AutoCodeField `json:"primaryField"` + DataSourceMap map[string]*DataSource `json:"-"` + HasPic bool `json:"-"` + HasFile bool `json:"-"` + HasTimer bool `json:"-"` + NeedSort bool `json:"-"` + NeedJSON bool `json:"-"` + HasRichText bool `json:"-"` + HasDataSource bool `json:"-"` + HasSearchTimer bool `json:"-"` + HasArray bool `json:"-"` + HasExcel bool `json:"-"` +} + +type DataSource struct { + DBName string `json:"dbName"` + Table string `json:"table"` + Label string `json:"label"` + Value string `json:"value"` + Association int `json:"association"` // 关联关系 1 一对一 2 一对多 + HasDeletedAt bool `json:"hasDeletedAt"` +} + +func (r *AutoCode) Apis() []model.SysApi { + return []model.SysApi{ + { + Path: "/" + r.Abbreviation + "/" + "create" + r.StructName, + Description: "新增" + r.Description, + ApiGroup: r.Description, + Method: "POST", + }, + { + Path: "/" + r.Abbreviation + "/" + "delete" + r.StructName, + Description: "删除" + r.Description, + ApiGroup: r.Description, + Method: "DELETE", + }, + { + Path: "/" + r.Abbreviation + "/" + "delete" + r.StructName + "ByIds", + Description: "批量删除" + r.Description, + ApiGroup: r.Description, + Method: "DELETE", + }, + { + Path: "/" + r.Abbreviation + "/" + "update" + r.StructName, + Description: "更新" + r.Description, + ApiGroup: r.Description, + Method: "PUT", + }, + { + Path: "/" + r.Abbreviation + "/" + "find" + r.StructName, + Description: "根据ID获取" + r.Description, + ApiGroup: r.Description, + Method: "GET", + }, + { + Path: "/" + r.Abbreviation + "/" + "get" + r.StructName + "List", + Description: "获取" + r.Description + "列表", + ApiGroup: r.Description, + Method: "GET", + }, + } +} + +func (r *AutoCode) Menu(template string) model.SysBaseMenu { + component := fmt.Sprintf("view/%s/%s/%s.vue", r.Package, r.PackageName, r.PackageName) + if template != "package" { + component = fmt.Sprintf("plugin/%s/view/%s.vue", r.Package, r.PackageName) + } + return model.SysBaseMenu{ + ParentId: 0, + Path: r.Abbreviation, + Name: r.Abbreviation, + Component: component, + Meta: model.Meta{ + Title: r.Description, + }, + } +} + +// Pretreatment 预处理 +// Author [SliverHorn](https://github.com/SliverHorn) +func (r *AutoCode) Pretreatment() error { + r.Module = global.GVA_CONFIG.AutoCode.Module + if token.IsKeyword(r.Abbreviation) { + r.Abbreviation = r.Abbreviation + "_" + } // go 关键字处理 + if strings.HasSuffix(r.HumpPackageName, "test") { + r.HumpPackageName = r.HumpPackageName + "_" + } // test + length := len(r.Fields) + dict := make(map[string]string, length) + r.DataSourceMap = make(map[string]*DataSource, length) + for i := 0; i < length; i++ { + if r.Fields[i].Excel { + r.HasExcel = true + } + if r.Fields[i].DictType != "" { + dict[r.Fields[i].DictType] = "" + } + if r.Fields[i].Sort { + r.NeedSort = true + } + switch r.Fields[i].FieldType { + case "file": + r.HasFile = true + r.NeedJSON = true + case "json": + r.NeedJSON = true + case "array": + r.NeedJSON = true + r.HasArray = true + case "video": + r.HasPic = true + case "richtext": + r.HasRichText = true + case "picture": + r.HasPic = true + case "pictures": + r.HasPic = true + r.NeedJSON = true + case "time.Time": + r.HasTimer = true + if r.Fields[i].FieldSearchType != "" { + r.HasSearchTimer = true + } + } + if r.Fields[i].DataSource != nil { + if r.Fields[i].DataSource.Table != "" && r.Fields[i].DataSource.Label != "" && r.Fields[i].DataSource.Value != "" { + r.HasDataSource = true + r.Fields[i].CheckDataSource = true + r.DataSourceMap[r.Fields[i].FieldJson] = r.Fields[i].DataSource + } + } + if !r.GvaModel && r.PrimaryField == nil && r.Fields[i].PrimaryKey { + r.PrimaryField = r.Fields[i] + } // 自定义主键 + } + { + for key := range dict { + r.DictTypes = append(r.DictTypes, key) + } + } // DictTypes => 字典 + { + if r.GvaModel { + r.PrimaryField = &AutoCodeField{ + FieldName: "ID", + FieldType: "uint", + FieldDesc: "ID", + FieldJson: "ID", + DataTypeLong: "20", + Comment: "主键ID", + ColumnName: "id", + } + } + } // GvaModel + { + if r.IsAdd && r.PrimaryField == nil { + r.PrimaryField = new(AutoCodeField) + } + } // 新增字段模式下不关注主键 + if r.Package == "" { + return errors.New("Package为空!") + } // 增加判断:Package不为空 + packages := []rune(r.Package) + if len(packages) > 0 { + if packages[0] >= 97 && packages[0] <= 122 { + packages[0] = packages[0] - 32 + } + r.PackageT = string(packages) + } // PackageT 是 Package 的首字母大写 + return nil +} + +func (r *AutoCode) History() SysAutoHistoryCreate { + bytes, _ := json.Marshal(r) + return SysAutoHistoryCreate{ + Table: r.TableName, + Package: r.Package, + Request: string(bytes), + StructName: r.StructName, + BusinessDB: r.BusinessDB, + Description: r.Description, + } +} + +type AutoCodeField struct { + FieldName string `json:"fieldName"` // Field名 + FieldDesc string `json:"fieldDesc"` // 中文名 + FieldType string `json:"fieldType"` // Field数据类型 + FieldJson string `json:"fieldJson"` // FieldJson + DataTypeLong string `json:"dataTypeLong"` // 数据库字段长度 + Comment string `json:"comment"` // 数据库字段描述 + ColumnName string `json:"columnName"` // 数据库字段 + FieldSearchType string `json:"fieldSearchType"` // 搜索条件 + FieldSearchHide bool `json:"fieldSearchHide"` // 是否隐藏查询条件 + DictType string `json:"dictType"` // 字典 + //Front bool `json:"front"` // 是否前端可见 + Form bool `json:"form"` // 是否前端新建/编辑 + Table bool `json:"table"` // 是否前端表格列 + Desc bool `json:"desc"` // 是否前端详情 + Excel bool `json:"excel"` // 是否导入/导出 + Require bool `json:"require"` // 是否必填 + DefaultValue string `json:"defaultValue"` // 是否必填 + ErrorText string `json:"errorText"` // 校验失败文字 + Clearable bool `json:"clearable"` // 是否可清空 + Sort bool `json:"sort"` // 是否增加排序 + PrimaryKey bool `json:"primaryKey"` // 是否主键 + DataSource *DataSource `json:"dataSource"` // 数据源 + CheckDataSource bool `json:"checkDataSource"` // 是否检查数据源 + FieldIndexType string `json:"fieldIndexType"` // 索引类型 +} + +type AutoFunc struct { + Package string `json:"package"` + FuncName string `json:"funcName"` // 方法名称 + Router string `json:"router"` // 路由名称 + FuncDesc string `json:"funcDesc"` // 方法介绍 + BusinessDB string `json:"businessDB"` // 业务库 + StructName string `json:"structName"` // Struct名称 + PackageName string `json:"packageName"` // 文件名称 + Description string `json:"description"` // Struct中文名称 + Abbreviation string `json:"abbreviation"` // Struct简称 + HumpPackageName string `json:"humpPackageName"` // go文件名称 + Method string `json:"method"` // 方法 + IsPlugin bool `json:"isPlugin"` // 是否插件 + IsAuth bool `json:"isAuth"` // 是否鉴权 + IsPreview bool `json:"isPreview"` // 是否预览 + IsAi bool `json:"isAi"` // 是否AI + ApiFunc string `json:"apiFunc"` // API方法 + ServerFunc string `json:"serverFunc"` // 服务方法 + JsFunc string `json:"jsFunc"` // JS方法 +} + +type InitMenu struct { + PlugName string `json:"plugName"` + ParentMenu string `json:"parentMenu"` + Menus []uint `json:"menus"` +} + +type InitApi struct { + PlugName string `json:"plugName"` + APIs []uint `json:"apis"` +} + +type LLMAutoCode struct { + Prompt string `json:"prompt" form:"prompt" gorm:"column:prompt;comment:提示语;type:text;"` //提示语 + Mode string `json:"mode" form:"mode" gorm:"column:mode;comment:模式;type:text;"` //模式 +} diff --git a/admin/server/model/system/request/sys_auto_code_package.go b/admin/server/model/system/request/sys_auto_code_package.go new file mode 100644 index 000000000..679303a56 --- /dev/null +++ b/admin/server/model/system/request/sys_auto_code_package.go @@ -0,0 +1,31 @@ +package request + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + model "github.com/flipped-aurora/gin-vue-admin/server/model/system" +) + +type SysAutoCodePackageCreate struct { + Desc string `json:"desc" example:"描述"` + Label string `json:"label" example:"展示名"` + Template string `json:"template" example:"模版"` + PackageName string `json:"packageName" example:"包名"` + Module string `json:"-" example:"模块"` +} + +func (r *SysAutoCodePackageCreate) AutoCode() AutoCode { + return AutoCode{ + Package: r.PackageName, + Module: global.GVA_CONFIG.AutoCode.Module, + } +} + +func (r *SysAutoCodePackageCreate) Create() model.SysAutoCodePackage { + return model.SysAutoCodePackage{ + Desc: r.Desc, + Label: r.Label, + Template: r.Template, + PackageName: r.PackageName, + Module: global.GVA_CONFIG.AutoCode.Module, + } +} diff --git a/admin/server/model/system/request/sys_auto_history.go b/admin/server/model/system/request/sys_auto_history.go new file mode 100644 index 000000000..fb50a7944 --- /dev/null +++ b/admin/server/model/system/request/sys_auto_history.go @@ -0,0 +1,56 @@ +package request + +import ( + common "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + model "github.com/flipped-aurora/gin-vue-admin/server/model/system" +) + +type SysAutoHistoryCreate struct { + Table string // 表名 + Package string // 模块名/插件名 + Request string // 前端传入的结构化信息 + StructName string // 结构体名称 + BusinessDB string // 业务库 + Description string // Struct中文名称 + Injections map[string]string // 注入路径 + Templates map[string]string // 模板信息 + ApiIDs []uint // api表注册内容 + MenuID uint // 菜单ID + ExportTemplateID uint // 导出模板ID +} + +func (r *SysAutoHistoryCreate) Create() model.SysAutoCodeHistory { + entity := model.SysAutoCodeHistory{ + Package: r.Package, + Request: r.Request, + Table: r.Table, + StructName: r.StructName, + BusinessDB: r.BusinessDB, + Description: r.Description, + Injections: r.Injections, + Templates: r.Templates, + ApiIDs: r.ApiIDs, + MenuID: r.MenuID, + ExportTemplateID: r.ExportTemplateID, + } + if entity.Table == "" { + entity.Table = r.StructName + } + return entity +} + +type SysAutoHistoryRollBack struct { + common.GetById + DeleteApi bool `json:"deleteApi" form:"deleteApi"` // 是否删除接口 + DeleteMenu bool `json:"deleteMenu" form:"deleteMenu"` // 是否删除菜单 + DeleteTable bool `json:"deleteTable" form:"deleteTable"` // 是否删除表 +} + +func (r *SysAutoHistoryRollBack) ApiIds(entity model.SysAutoCodeHistory) common.IdsReq { + length := len(entity.ApiIDs) + ids := make([]int, 0) + for i := 0; i < length; i++ { + ids = append(ids, int(entity.ApiIDs[i])) + } + return common.IdsReq{Ids: ids} +} diff --git a/admin/server/model/system/request/sys_casbin.go b/admin/server/model/system/request/sys_casbin.go new file mode 100644 index 000000000..ef8c823cb --- /dev/null +++ b/admin/server/model/system/request/sys_casbin.go @@ -0,0 +1,27 @@ +package request + +// Casbin info structure +type CasbinInfo struct { + Path string `json:"path"` // 路径 + Method string `json:"method"` // 方法 +} + +// Casbin structure for input parameters +type CasbinInReceive struct { + AuthorityId uint `json:"authorityId"` // 权限id + CasbinInfos []CasbinInfo `json:"casbinInfos"` +} + +func DefaultCasbin() []CasbinInfo { + return []CasbinInfo{ + {Path: "/menu/getMenu", Method: "POST"}, + {Path: "/jwt/jsonInBlacklist", Method: "POST"}, + {Path: "/base/login", Method: "POST"}, + {Path: "/user/changePassword", Method: "POST"}, + {Path: "/user/setUserAuthority", Method: "POST"}, + {Path: "/user/getUserInfo", Method: "GET"}, + {Path: "/user/setSelfInfo", Method: "PUT"}, + {Path: "/fileUploadAndDownload/upload", Method: "POST"}, + {Path: "/sysDictionary/findSysDictionary", Method: "GET"}, + } +} diff --git a/admin/server/model/system/request/sys_dictionary_detail.go b/admin/server/model/system/request/sys_dictionary_detail.go new file mode 100644 index 000000000..2f97da280 --- /dev/null +++ b/admin/server/model/system/request/sys_dictionary_detail.go @@ -0,0 +1,11 @@ +package request + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" +) + +type SysDictionaryDetailSearch struct { + system.SysDictionaryDetail + request.PageInfo +} diff --git a/admin/server/model/system/request/sys_export_template.go b/admin/server/model/system/request/sys_export_template.go new file mode 100644 index 000000000..1010bf6b9 --- /dev/null +++ b/admin/server/model/system/request/sys_export_template.go @@ -0,0 +1,14 @@ +package request + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "time" +) + +type SysExportTemplateSearch struct { + system.SysExportTemplate + StartCreatedAt *time.Time `json:"startCreatedAt" form:"startCreatedAt"` + EndCreatedAt *time.Time `json:"endCreatedAt" form:"endCreatedAt"` + request.PageInfo +} diff --git a/admin/server/model/system/request/sys_init.go b/admin/server/model/system/request/sys_init.go new file mode 100644 index 000000000..6882895d2 --- /dev/null +++ b/admin/server/model/system/request/sys_init.go @@ -0,0 +1,123 @@ +package request + +import ( + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/config" + "os" +) + +type InitDB struct { + AdminPassword string `json:"adminPassword" binding:"required"` + DBType string `json:"dbType"` // 数据库类型 + Host string `json:"host"` // 服务器地址 + Port string `json:"port"` // 数据库连接端口 + UserName string `json:"userName"` // 数据库用户名 + Password string `json:"password"` // 数据库密码 + DBName string `json:"dbName" binding:"required"` // 数据库名 + DBPath string `json:"dbPath"` // sqlite数据库文件路径 +} + +// MysqlEmptyDsn msyql 空数据库 建库链接 +// Author SliverHorn +func (i *InitDB) MysqlEmptyDsn() string { + if i.Host == "" { + i.Host = "127.0.0.1" + } + if i.Port == "" { + i.Port = "3306" + } + return fmt.Sprintf("%s:%s@tcp(%s:%s)/", i.UserName, i.Password, i.Host, i.Port) +} + +// PgsqlEmptyDsn pgsql 空数据库 建库链接 +// Author SliverHorn +func (i *InitDB) PgsqlEmptyDsn() string { + if i.Host == "" { + i.Host = "127.0.0.1" + } + if i.Port == "" { + i.Port = "5432" + } + return "host=" + i.Host + " user=" + i.UserName + " password=" + i.Password + " port=" + i.Port + " dbname=" + "postgres" + " " + "sslmode=disable TimeZone=Asia/Shanghai" +} + +// SqliteEmptyDsn sqlite 空数据库 建库链接 +// Author Kafumio +func (i *InitDB) SqliteEmptyDsn() string { + separator := string(os.PathSeparator) + return i.DBPath + separator + i.DBName + ".db" +} + +func (i *InitDB) MssqlEmptyDsn() string { + return "sqlserver://" + i.UserName + ":" + i.Password + "@" + i.Host + ":" + i.Port + "?database=" + i.DBName + "&encrypt=disable" +} + +// ToMysqlConfig 转换 config.Mysql +// Author [SliverHorn](https://github.com/SliverHorn) +func (i *InitDB) ToMysqlConfig() config.Mysql { + return config.Mysql{ + GeneralDB: config.GeneralDB{ + Path: i.Host, + Port: i.Port, + Dbname: i.DBName, + Username: i.UserName, + Password: i.Password, + MaxIdleConns: 10, + MaxOpenConns: 100, + LogMode: "error", + Config: "charset=utf8mb4&parseTime=True&loc=Local", + }, + } +} + +// ToPgsqlConfig 转换 config.Pgsql +// Author [SliverHorn](https://github.com/SliverHorn) +func (i *InitDB) ToPgsqlConfig() config.Pgsql { + return config.Pgsql{ + GeneralDB: config.GeneralDB{ + Path: i.Host, + Port: i.Port, + Dbname: i.DBName, + Username: i.UserName, + Password: i.Password, + MaxIdleConns: 10, + MaxOpenConns: 100, + LogMode: "error", + Config: "sslmode=disable TimeZone=Asia/Shanghai", + }, + } +} + +// ToSqliteConfig 转换 config.Sqlite +// Author [Kafumio](https://github.com/Kafumio) +func (i *InitDB) ToSqliteConfig() config.Sqlite { + return config.Sqlite{ + GeneralDB: config.GeneralDB{ + Path: i.DBPath, + Port: i.Port, + Dbname: i.DBName, + Username: i.UserName, + Password: i.Password, + MaxIdleConns: 10, + MaxOpenConns: 100, + LogMode: "error", + Config: "", + }, + } +} + +func (i *InitDB) ToMssqlConfig() config.Mssql { + return config.Mssql{ + GeneralDB: config.GeneralDB{ + Path: i.DBPath, + Port: i.Port, + Dbname: i.DBName, + Username: i.UserName, + Password: i.Password, + MaxIdleConns: 10, + MaxOpenConns: 100, + LogMode: "error", + Config: "", + }, + } +} diff --git a/admin/server/model/system/request/sys_menu.go b/admin/server/model/system/request/sys_menu.go new file mode 100644 index 000000000..2f5c7c46e --- /dev/null +++ b/admin/server/model/system/request/sys_menu.go @@ -0,0 +1,27 @@ +package request + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" +) + +// Add menu authority info structure +type AddMenuAuthorityInfo struct { + Menus []system.SysBaseMenu `json:"menus"` + AuthorityId uint `json:"authorityId"` // 角色ID +} + +func DefaultMenu() []system.SysBaseMenu { + return []system.SysBaseMenu{{ + GVA_MODEL: global.GVA_MODEL{ID: 1}, + ParentId: 0, + Path: "dashboard", + Name: "dashboard", + Component: "view/dashboard/index.vue", + Sort: 1, + Meta: system.Meta{ + Title: "仪表盘", + Icon: "setting", + }, + }} +} diff --git a/admin/server/model/system/request/sys_operation_record.go b/admin/server/model/system/request/sys_operation_record.go new file mode 100644 index 000000000..e58dd59c3 --- /dev/null +++ b/admin/server/model/system/request/sys_operation_record.go @@ -0,0 +1,11 @@ +package request + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" +) + +type SysOperationRecordSearch struct { + system.SysOperationRecord + request.PageInfo +} diff --git a/admin/server/model/system/request/sys_params.go b/admin/server/model/system/request/sys_params.go new file mode 100644 index 000000000..0009271bf --- /dev/null +++ b/admin/server/model/system/request/sys_params.go @@ -0,0 +1,14 @@ +package request + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "time" +) + +type SysParamsSearch struct { + StartCreatedAt *time.Time `json:"startCreatedAt" form:"startCreatedAt"` + EndCreatedAt *time.Time `json:"endCreatedAt" form:"endCreatedAt"` + Name string `json:"name" form:"name" ` + Key string `json:"key" form:"key" ` + request.PageInfo +} diff --git a/admin/server/model/system/request/sys_user.go b/admin/server/model/system/request/sys_user.go new file mode 100644 index 000000000..b82f90038 --- /dev/null +++ b/admin/server/model/system/request/sys_user.go @@ -0,0 +1,86 @@ +package request + +import ( + common "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" +) + +// Register User register structure +type Register struct { + Username string `json:"userName" example:"用户名"` + Password string `json:"passWord" example:"密码"` + NickName string `json:"nickName" example:"昵称"` + HeaderImg string `json:"headerImg" example:"头像链接"` + AuthorityId uint `json:"authorityId" swaggertype:"string" example:"int 角色id"` + Enable int `json:"enable" swaggertype:"string" example:"int 是否启用"` + AuthorityIds []uint `json:"authorityIds" swaggertype:"string" example:"[]uint 角色id"` + Phone string `json:"phone" example:"电话号码"` + Email string `json:"email" example:"电子邮箱"` +} + +// User login structure +type Login struct { + Username string `json:"username"` // 用户名 + Password string `json:"password"` // 密码 + Captcha string `json:"captcha"` // 验证码 + CaptchaId string `json:"captchaId"` // 验证码ID +} + +// Modify password structure +type ChangePasswordReq struct { + ID uint `json:"-"` // 从 JWT 中提取 user id,避免越权 + Password string `json:"password"` // 密码 + NewPassword string `json:"newPassword"` // 新密码 +} + +// Modify user's auth structure +type SetUserAuth struct { + AuthorityId uint `json:"authorityId"` // 角色ID +} + +// Modify user's auth structure +type SetUserAuthorities struct { + ID uint + AuthorityIds []uint `json:"authorityIds"` // 角色ID +} + +type ChangeUserInfo struct { + ID uint `gorm:"primarykey"` // 主键ID + NickName string `json:"nickName" gorm:"default:系统用户;comment:用户昵称"` // 用户昵称 + Phone string `json:"phone" gorm:"comment:用户手机号"` // 用户手机号 + AuthorityIds []uint `json:"authorityIds" gorm:"-"` // 角色ID + Email string `json:"email" gorm:"comment:用户邮箱"` // 用户邮箱 + HeaderImg string `json:"headerImg" gorm:"default:https://qmplusimg.henrongyi.top/gva_header.jpg;comment:用户头像"` // 用户头像 + SideMode string `json:"sideMode" gorm:"comment:用户侧边主题"` // 用户侧边主题 + Enable int `json:"enable" gorm:"comment:冻结用户"` //冻结用户 + GlobalCode bool `json:"global_code" gorm:"comment:全局代码执行权限"` //全局代码执行权限 Extend global code + Authorities []system.SysAuthority `json:"-" gorm:"many2many:sys_user_authority;"` +} + +type GetUserList struct { + common.PageInfo + Username string `json:"username" form:"username"` + NickName string `json:"nickName" form:"nickName"` + Phone string `json:"phone" form:"phone"` + Email string `json:"email" form:"email"` +} + +// Extend Start GetUserInfoByUserName + +// GetUserInfoByUserName DingTalk API: Get User Info By UserName +type GetUserInfoByUserName struct { + ResList struct { + Name string `json:"name"` + UserID string `json:"userId"` + Mobile string `json:"mobile"` + WorkPlace string `json:"workPlace"` + } `json:"resList"` +} + +// UpdateUserPasswd update user pass my +type UpdateUserPasswd struct { + ID uint `json:"ID"` // id + Password string `json:"Password"` // 密码 +} + +// Extend Stop GetUserInfoByUserName diff --git a/admin/server/model/system/response/oa_login.go b/admin/server/model/system/response/oa_login.go new file mode 100644 index 000000000..39a06ce83 --- /dev/null +++ b/admin/server/model/system/response/oa_login.go @@ -0,0 +1,18 @@ +package response + +// OaUserInfoRes 请求OA用户信息接口返回值 +type OaUserInfoRes struct { + Code int `json:"code"` + Info string `json:"info"` + Data struct { + Username string `json:"username"` + Name string `json:"name"` + Email string `json:"email"` + } `json:"data"` +} + +// OaAccessTokenRes 请求OA access-token接口返回值 +type OaAccessTokenRes struct { + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` +} diff --git a/admin/server/model/system/response/sys_api.go b/admin/server/model/system/response/sys_api.go new file mode 100644 index 000000000..20e382b9a --- /dev/null +++ b/admin/server/model/system/response/sys_api.go @@ -0,0 +1,18 @@ +package response + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/model/system" +) + +type SysAPIResponse struct { + Api system.SysApi `json:"api"` +} + +type SysAPIListResponse struct { + Apis []system.SysApi `json:"apis"` +} + +type SysSyncApis struct { + NewApis []system.SysApi `json:"newApis"` + DeleteApis []system.SysApi `json:"deleteApis"` +} diff --git a/admin/server/model/system/response/sys_authority.go b/admin/server/model/system/response/sys_authority.go new file mode 100644 index 000000000..a05540167 --- /dev/null +++ b/admin/server/model/system/response/sys_authority.go @@ -0,0 +1,12 @@ +package response + +import "github.com/flipped-aurora/gin-vue-admin/server/model/system" + +type SysAuthorityResponse struct { + Authority system.SysAuthority `json:"authority"` +} + +type SysAuthorityCopyResponse struct { + Authority system.SysAuthority `json:"authority"` + OldAuthorityId uint `json:"oldAuthorityId"` // 旧角色ID +} diff --git a/admin/server/model/system/response/sys_authority_btn.go b/admin/server/model/system/response/sys_authority_btn.go new file mode 100644 index 000000000..2f772cf09 --- /dev/null +++ b/admin/server/model/system/response/sys_authority_btn.go @@ -0,0 +1,5 @@ +package response + +type SysAuthorityBtnRes struct { + Selected []uint `json:"selected"` +} diff --git a/admin/server/model/system/response/sys_auto_code.go b/admin/server/model/system/response/sys_auto_code.go new file mode 100644 index 000000000..9e99bde3a --- /dev/null +++ b/admin/server/model/system/response/sys_auto_code.go @@ -0,0 +1,17 @@ +package response + +type Db struct { + Database string `json:"database" gorm:"column:database"` +} + +type Table struct { + TableName string `json:"tableName" gorm:"column:table_name"` +} + +type Column struct { + DataType string `json:"dataType" gorm:"column:data_type"` + ColumnName string `json:"columnName" gorm:"column:column_name"` + DataTypeLong string `json:"dataTypeLong" gorm:"column:data_type_long"` + ColumnComment string `json:"columnComment" gorm:"column:column_comment"` + PrimaryKey bool `json:"primaryKey" gorm:"column:primary_key"` +} diff --git a/admin/server/model/system/response/sys_captcha.go b/admin/server/model/system/response/sys_captcha.go new file mode 100644 index 000000000..0c3995a1d --- /dev/null +++ b/admin/server/model/system/response/sys_captcha.go @@ -0,0 +1,8 @@ +package response + +type SysCaptchaResponse struct { + CaptchaId string `json:"captchaId"` + PicPath string `json:"picPath"` + CaptchaLength int `json:"captchaLength"` + OpenCaptcha bool `json:"openCaptcha"` +} diff --git a/admin/server/model/system/response/sys_casbin.go b/admin/server/model/system/response/sys_casbin.go new file mode 100644 index 000000000..267bb42c9 --- /dev/null +++ b/admin/server/model/system/response/sys_casbin.go @@ -0,0 +1,9 @@ +package response + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" +) + +type PolicyPathResponse struct { + Paths []request.CasbinInfo `json:"paths"` +} diff --git a/admin/server/model/system/response/sys_menu.go b/admin/server/model/system/response/sys_menu.go new file mode 100644 index 000000000..d8f80f314 --- /dev/null +++ b/admin/server/model/system/response/sys_menu.go @@ -0,0 +1,15 @@ +package response + +import "github.com/flipped-aurora/gin-vue-admin/server/model/system" + +type SysMenusResponse struct { + Menus []system.SysMenu `json:"menus"` +} + +type SysBaseMenusResponse struct { + Menus []system.SysBaseMenu `json:"menus"` +} + +type SysBaseMenuResponse struct { + Menu system.SysBaseMenu `json:"menu"` +} diff --git a/admin/server/model/system/response/sys_system.go b/admin/server/model/system/response/sys_system.go new file mode 100644 index 000000000..f19e965bf --- /dev/null +++ b/admin/server/model/system/response/sys_system.go @@ -0,0 +1,7 @@ +package response + +import "github.com/flipped-aurora/gin-vue-admin/server/config" + +type SysConfigResponse struct { + Config config.Server `json:"config"` +} diff --git a/admin/server/model/system/response/sys_user.go b/admin/server/model/system/response/sys_user.go new file mode 100644 index 000000000..d6f1074b7 --- /dev/null +++ b/admin/server/model/system/response/sys_user.go @@ -0,0 +1,15 @@ +package response + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/model/system" +) + +type SysUserResponse struct { + User system.SysUser `json:"user"` +} + +type LoginResponse struct { + User system.SysUser `json:"user"` + Token string `json:"token"` + ExpiresAt int64 `json:"expiresAt"` +} diff --git a/admin/server/model/system/sys_api.go b/admin/server/model/system/sys_api.go new file mode 100644 index 000000000..853ddb087 --- /dev/null +++ b/admin/server/model/system/sys_api.go @@ -0,0 +1,28 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +type SysApi struct { + global.GVA_MODEL + Path string `json:"path" gorm:"comment:api路径"` // api路径 + Description string `json:"description" gorm:"comment:api中文描述"` // api中文描述 + ApiGroup string `json:"apiGroup" gorm:"comment:api组"` // api组 + Method string `json:"method" gorm:"default:POST;comment:方法"` // 方法:创建POST(默认)|查看GET|更新PUT|删除DELETE +} + +func (SysApi) TableName() string { + return "sys_apis" +} + +type SysIgnoreApi struct { + global.GVA_MODEL + Path string `json:"path" gorm:"comment:api路径"` // api路径 + Method string `json:"method" gorm:"default:POST;comment:方法"` // 方法:创建POST(默认)|查看GET|更新PUT|删除DELETE + Flag bool `json:"flag" gorm:"-"` // 是否忽略 +} + +func (SysIgnoreApi) TableName() string { + return "sys_ignore_apis" +} diff --git a/admin/server/model/system/sys_authority.go b/admin/server/model/system/sys_authority.go new file mode 100644 index 000000000..01c5efad1 --- /dev/null +++ b/admin/server/model/system/sys_authority.go @@ -0,0 +1,23 @@ +package system + +import ( + "time" +) + +type SysAuthority struct { + CreatedAt time.Time // 创建时间 + UpdatedAt time.Time // 更新时间 + DeletedAt *time.Time `sql:"index"` + AuthorityId uint `json:"authorityId" gorm:"not null;unique;primary_key;comment:角色ID;size:90"` // 角色ID + AuthorityName string `json:"authorityName" gorm:"comment:角色名"` // 角色名 + ParentId *uint `json:"parentId" gorm:"comment:父角色ID"` // 父角色ID + DataAuthorityId []*SysAuthority `json:"dataAuthorityId" gorm:"many2many:sys_data_authority_id;"` + Children []SysAuthority `json:"children" gorm:"-"` + SysBaseMenus []SysBaseMenu `json:"menus" gorm:"many2many:sys_authority_menus;"` + Users []SysUser `json:"-" gorm:"many2many:sys_user_authority;"` + DefaultRouter string `json:"defaultRouter" gorm:"comment:默认菜单;default:dashboard"` // 默认菜单(默认dashboard) +} + +func (SysAuthority) TableName() string { + return "sys_authorities" +} diff --git a/admin/server/model/system/sys_authority_btn.go b/admin/server/model/system/sys_authority_btn.go new file mode 100644 index 000000000..e00598412 --- /dev/null +++ b/admin/server/model/system/sys_authority_btn.go @@ -0,0 +1,8 @@ +package system + +type SysAuthorityBtn struct { + AuthorityId uint `gorm:"comment:角色ID"` + SysMenuID uint `gorm:"comment:菜单ID"` + SysBaseMenuBtnID uint `gorm:"comment:菜单按钮ID"` + SysBaseMenuBtn SysBaseMenuBtn ` gorm:"comment:按钮详情"` +} diff --git a/admin/server/model/system/sys_authority_menu.go b/admin/server/model/system/sys_authority_menu.go new file mode 100644 index 000000000..4467a7e10 --- /dev/null +++ b/admin/server/model/system/sys_authority_menu.go @@ -0,0 +1,19 @@ +package system + +type SysMenu struct { + SysBaseMenu + MenuId uint `json:"menuId" gorm:"comment:菜单ID"` + AuthorityId uint `json:"-" gorm:"comment:角色ID"` + Children []SysMenu `json:"children" gorm:"-"` + Parameters []SysBaseMenuParameter `json:"parameters" gorm:"foreignKey:SysBaseMenuID;references:MenuId"` + Btns map[string]uint `json:"btns" gorm:"-"` +} + +type SysAuthorityMenu struct { + MenuId string `json:"menuId" gorm:"comment:菜单ID;column:sys_base_menu_id"` + AuthorityId string `json:"-" gorm:"comment:角色ID;column:sys_authority_authority_id"` +} + +func (s SysAuthorityMenu) TableName() string { + return "sys_authority_menus" +} diff --git a/admin/server/model/system/sys_auto_code_history.go b/admin/server/model/system/sys_auto_code_history.go new file mode 100644 index 000000000..c36787ddb --- /dev/null +++ b/admin/server/model/system/sys_auto_code_history.go @@ -0,0 +1,67 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "gorm.io/gorm" + "os" + "path" + "path/filepath" + "strings" +) + +// SysAutoCodeHistory 自动迁移代码记录,用于回滚,重放使用 +type SysAutoCodeHistory struct { + global.GVA_MODEL + Table string `json:"tableName" gorm:"column:table_name;comment:表名"` + Package string `json:"package" gorm:"column:package;comment:模块名/插件名"` + Request string `json:"request" gorm:"type:text;column:request;comment:前端传入的结构化信息"` + StructName string `json:"structName" gorm:"column:struct_name;comment:结构体名称"` + BusinessDB string `json:"businessDb" gorm:"column:business_db;comment:业务库"` + Description string `json:"description" gorm:"column:description;comment:Struct中文名称"` + Templates map[string]string `json:"template" gorm:"serializer:json;type:text;column:templates;comment:模板信息"` + Injections map[string]string `json:"injections" gorm:"serializer:json;type:text;column:Injections;comment:注入路径"` + Flag int `json:"flag" gorm:"column:flag;comment:[0:创建,1:回滚]"` + ApiIDs []uint `json:"apiIDs" gorm:"serializer:json;column:api_ids;comment:api表注册内容"` + MenuID uint `json:"menuId" gorm:"column:menu_id;comment:菜单ID"` + ExportTemplateID uint `json:"exportTemplateID" gorm:"column:export_template_id;comment:导出模板ID"` + AutoCodePackage SysAutoCodePackage `json:"autoCodePackage" gorm:"foreignKey:ID;references:PackageID"` + PackageID uint `json:"packageID" gorm:"column:package_id;comment:包ID"` +} + +func (s *SysAutoCodeHistory) BeforeCreate(db *gorm.DB) error { + templates := make(map[string]string, len(s.Templates)) + for key, value := range s.Templates { + server := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server) + { + hasServer := strings.Index(key, server) + if hasServer != -1 { + key = strings.TrimPrefix(key, server) + keys := strings.Split(key, string(os.PathSeparator)) + key = path.Join(keys...) + } + } // key + web := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.WebRoot()) + hasWeb := strings.Index(value, web) + if hasWeb != -1 { + value = strings.TrimPrefix(value, web) + values := strings.Split(value, string(os.PathSeparator)) + value = path.Join(values...) + templates[key] = value + continue + } + hasServer := strings.Index(value, server) + if hasServer != -1 { + value = strings.TrimPrefix(value, server) + values := strings.Split(value, string(os.PathSeparator)) + value = path.Join(values...) + templates[key] = value + continue + } + } + s.Templates = templates + return nil +} + +func (s *SysAutoCodeHistory) TableName() string { + return "sys_auto_code_histories" +} diff --git a/admin/server/model/system/sys_auto_code_package.go b/admin/server/model/system/sys_auto_code_package.go new file mode 100644 index 000000000..4099192f6 --- /dev/null +++ b/admin/server/model/system/sys_auto_code_package.go @@ -0,0 +1,18 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +type SysAutoCodePackage struct { + global.GVA_MODEL + Desc string `json:"desc" gorm:"comment:描述"` + Label string `json:"label" gorm:"comment:展示名"` + Template string `json:"template" gorm:"comment:模版"` + PackageName string `json:"packageName" gorm:"comment:包名"` + Module string `json:"-" example:"模块"` +} + +func (s *SysAutoCodePackage) TableName() string { + return "sys_auto_code_packages" +} diff --git a/admin/server/model/system/sys_base_menu.go b/admin/server/model/system/sys_base_menu.go new file mode 100644 index 000000000..41cf37631 --- /dev/null +++ b/admin/server/model/system/sys_base_menu.go @@ -0,0 +1,42 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +type SysBaseMenu struct { + global.GVA_MODEL + MenuLevel uint `json:"-"` + ParentId uint `json:"parentId" gorm:"comment:父菜单ID"` // 父菜单ID + Path string `json:"path" gorm:"comment:路由path"` // 路由path + Name string `json:"name" gorm:"comment:路由name"` // 路由name + Hidden bool `json:"hidden" gorm:"comment:是否在列表隐藏"` // 是否在列表隐藏 + Component string `json:"component" gorm:"comment:对应前端文件路径"` // 对应前端文件路径 + Sort int `json:"sort" gorm:"comment:排序标记"` // 排序标记 + Meta `json:"meta" gorm:"embedded;comment:附加属性"` // 附加属性 + SysAuthoritys []SysAuthority `json:"authoritys" gorm:"many2many:sys_authority_menus;"` + Children []SysBaseMenu `json:"children" gorm:"-"` + Parameters []SysBaseMenuParameter `json:"parameters"` + MenuBtn []SysBaseMenuBtn `json:"menuBtn"` +} + +type Meta struct { + ActiveName string `json:"activeName" gorm:"comment:高亮菜单"` + KeepAlive bool `json:"keepAlive" gorm:"comment:是否缓存"` // 是否缓存 + DefaultMenu bool `json:"defaultMenu" gorm:"comment:是否是基础路由(开发中)"` // 是否是基础路由(开发中) + Title string `json:"title" gorm:"comment:菜单名"` // 菜单名 + Icon string `json:"icon" gorm:"comment:菜单图标"` // 菜单图标 + CloseTab bool `json:"closeTab" gorm:"comment:自动关闭tab"` // 自动关闭tab +} + +type SysBaseMenuParameter struct { + global.GVA_MODEL + SysBaseMenuID uint + Type string `json:"type" gorm:"comment:地址栏携带参数为params还是query"` // 地址栏携带参数为params还是query + Key string `json:"key" gorm:"comment:地址栏携带参数的key"` // 地址栏携带参数的key + Value string `json:"value" gorm:"comment:地址栏携带参数的值"` // 地址栏携带参数的值 +} + +func (SysBaseMenu) TableName() string { + return "sys_base_menus" +} diff --git a/admin/server/model/system/sys_dictionary.go b/admin/server/model/system/sys_dictionary.go new file mode 100644 index 000000000..c0b9bf7fc --- /dev/null +++ b/admin/server/model/system/sys_dictionary.go @@ -0,0 +1,20 @@ +// 自动生成模板SysDictionary +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +// 如果含有time.Time 请自行import time包 +type SysDictionary struct { + global.GVA_MODEL + Name string `json:"name" form:"name" gorm:"column:name;comment:字典名(中)"` // 字典名(中) + Type string `json:"type" form:"type" gorm:"column:type;comment:字典名(英)"` // 字典名(英) + Status *bool `json:"status" form:"status" gorm:"column:status;comment:状态"` // 状态 + Desc string `json:"desc" form:"desc" gorm:"column:desc;comment:描述"` // 描述 + SysDictionaryDetails []SysDictionaryDetail `json:"sysDictionaryDetails" form:"sysDictionaryDetails"` +} + +func (SysDictionary) TableName() string { + return "sys_dictionaries" +} diff --git a/admin/server/model/system/sys_dictionary_detail.go b/admin/server/model/system/sys_dictionary_detail.go new file mode 100644 index 000000000..4084136c2 --- /dev/null +++ b/admin/server/model/system/sys_dictionary_detail.go @@ -0,0 +1,21 @@ +// 自动生成模板SysDictionaryDetail +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +// 如果含有time.Time 请自行import time包 +type SysDictionaryDetail struct { + global.GVA_MODEL + Label string `json:"label" form:"label" gorm:"column:label;comment:展示值"` // 展示值 + Value string `json:"value" form:"value" gorm:"column:value;comment:字典值"` // 字典值 + Extend string `json:"extend" form:"extend" gorm:"column:extend;comment:扩展值"` // 扩展值 + Status *bool `json:"status" form:"status" gorm:"column:status;comment:启用状态"` // 启用状态 + Sort int `json:"sort" form:"sort" gorm:"column:sort;comment:排序标记"` // 排序标记 + SysDictionaryID int `json:"sysDictionaryID" form:"sysDictionaryID" gorm:"column:sys_dictionary_id;comment:关联标记"` // 关联标记 +} + +func (SysDictionaryDetail) TableName() string { + return "sys_dictionary_details" +} diff --git a/admin/server/model/system/sys_export_template.go b/admin/server/model/system/sys_export_template.go new file mode 100644 index 000000000..aef24617d --- /dev/null +++ b/admin/server/model/system/sys_export_template.go @@ -0,0 +1,44 @@ +// 自动生成模板SysExportTemplate +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +// 导出模板 结构体 SysExportTemplate +type SysExportTemplate struct { + global.GVA_MODEL + DBName string `json:"dbName" form:"dbName" gorm:"column:db_name;comment:数据库名称;"` //数据库名称 + Name string `json:"name" form:"name" gorm:"column:name;comment:模板名称;"` //模板名称 + TableName string `json:"tableName" form:"tableName" gorm:"column:table_name;comment:表名称;"` //表名称 + TemplateID string `json:"templateID" form:"templateID" gorm:"column:template_id;comment:模板标识;"` //模板标识 + TemplateInfo string `json:"templateInfo" form:"templateInfo" gorm:"column:template_info;type:text;"` //模板信息 + Limit *int `json:"limit" form:"limit" gorm:"column:limit;comment:导出限制"` + Order string `json:"order" form:"order" gorm:"column:order;comment:排序"` + Conditions []Condition `json:"conditions" form:"conditions" gorm:"foreignKey:TemplateID;references:TemplateID;comment:条件"` + JoinTemplate []JoinTemplate `json:"joinTemplate" form:"joinTemplate" gorm:"foreignKey:TemplateID;references:TemplateID;comment:关联"` +} + +type JoinTemplate struct { + global.GVA_MODEL + TemplateID string `json:"templateID" form:"templateID" gorm:"column:template_id;comment:模板标识"` + JOINS string `json:"joins" form:"joins" gorm:"column:joins;comment:关联"` + Table string `json:"table" form:"table" gorm:"column:table;comment:关联表"` + ON string `json:"on" form:"on" gorm:"column:on;comment:关联条件"` +} + +func (JoinTemplate) TableName() string { + return "sys_export_template_join" +} + +type Condition struct { + global.GVA_MODEL + TemplateID string `json:"templateID" form:"templateID" gorm:"column:template_id;comment:模板标识"` + From string `json:"from" form:"from" gorm:"column:from;comment:条件取的key"` + Column string `json:"column" form:"column" gorm:"column:column;comment:作为查询条件的字段"` + Operator string `json:"operator" form:"operator" gorm:"column:operator;comment:操作符"` +} + +func (Condition) TableName() string { + return "sys_export_template_condition" +} diff --git a/admin/server/model/system/sys_jwt_blacklist.go b/admin/server/model/system/sys_jwt_blacklist.go new file mode 100644 index 000000000..4f9fa396d --- /dev/null +++ b/admin/server/model/system/sys_jwt_blacklist.go @@ -0,0 +1,10 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +type JwtBlacklist struct { + global.GVA_MODEL + Jwt string `gorm:"type:text;comment:jwt"` +} diff --git a/admin/server/model/system/sys_menu_btn.go b/admin/server/model/system/sys_menu_btn.go new file mode 100644 index 000000000..9d3276142 --- /dev/null +++ b/admin/server/model/system/sys_menu_btn.go @@ -0,0 +1,10 @@ +package system + +import "github.com/flipped-aurora/gin-vue-admin/server/global" + +type SysBaseMenuBtn struct { + global.GVA_MODEL + Name string `json:"name" gorm:"comment:按钮关键key"` + Desc string `json:"desc" gorm:"按钮备注"` + SysBaseMenuID uint `json:"sysBaseMenuID" gorm:"comment:菜单ID"` +} diff --git a/admin/server/model/system/sys_operation_record.go b/admin/server/model/system/sys_operation_record.go new file mode 100644 index 000000000..3d201d30d --- /dev/null +++ b/admin/server/model/system/sys_operation_record.go @@ -0,0 +1,24 @@ +// 自动生成模板SysOperationRecord +package system + +import ( + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +// 如果含有time.Time 请自行import time包 +type SysOperationRecord struct { + global.GVA_MODEL + Ip string `json:"ip" form:"ip" gorm:"column:ip;comment:请求ip"` // 请求ip + Method string `json:"method" form:"method" gorm:"column:method;comment:请求方法"` // 请求方法 + Path string `json:"path" form:"path" gorm:"column:path;comment:请求路径"` // 请求路径 + Status int `json:"status" form:"status" gorm:"column:status;comment:请求状态"` // 请求状态 + Latency time.Duration `json:"latency" form:"latency" gorm:"column:latency;comment:延迟" swaggertype:"string"` // 延迟 + Agent string `json:"agent" form:"agent" gorm:"type:text;column:agent;comment:代理"` // 代理 + ErrorMessage string `json:"error_message" form:"error_message" gorm:"column:error_message;comment:错误信息"` // 错误信息 + Body string `json:"body" form:"body" gorm:"type:text;column:body;comment:请求Body"` // 请求Body + Resp string `json:"resp" form:"resp" gorm:"type:text;column:resp;comment:响应Body"` // 响应Body + UserID int `json:"user_id" form:"user_id" gorm:"column:user_id;comment:用户id"` // 用户id + User SysUser `json:"user"` +} diff --git a/admin/server/model/system/sys_params.go b/admin/server/model/system/sys_params.go new file mode 100644 index 000000000..049c07f20 --- /dev/null +++ b/admin/server/model/system/sys_params.go @@ -0,0 +1,20 @@ +// 自动生成模板SysParams +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +// 参数 结构体 SysParams +type SysParams struct { + global.GVA_MODEL + Name string `json:"name" form:"name" gorm:"column:name;comment:参数名称;" binding:"required"` //参数名称 + Key string `json:"key" form:"key" gorm:"column:key;comment:参数键;" binding:"required"` //参数键 + Value string `json:"value" form:"value" gorm:"column:value;comment:参数值;" binding:"required"` //参数值 + Desc string `json:"desc" form:"desc" gorm:"column:desc;comment:参数说明;"` //参数说明 +} + +// TableName 参数 SysParams自定义表名 sys_params +func (SysParams) TableName() string { + return "sys_params" +} diff --git a/admin/server/model/system/sys_system.go b/admin/server/model/system/sys_system.go new file mode 100644 index 000000000..ad983110b --- /dev/null +++ b/admin/server/model/system/sys_system.go @@ -0,0 +1,10 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/config" +) + +// 配置文件结构体 +type System struct { + Config config.Server `json:"config"` +} diff --git a/admin/server/model/system/sys_user.go b/admin/server/model/system/sys_user.go new file mode 100644 index 000000000..50c45e9ca --- /dev/null +++ b/admin/server/model/system/sys_user.go @@ -0,0 +1,116 @@ +package system + +import ( + "context" + "errors" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common" + "github.com/flipped-aurora/gin-vue-admin/server/model/gaia" + "github.com/gofrs/uuid/v5" + "time" +) + +type Login interface { + GetUsername() string + GetNickname() string + GetUUID() uuid.UUID + GetUserId() uint + GetAuthorityId() uint + GetUserInfo() any + GetUserEmail() string // Extend add mail +} + +var _ Login = new(SysUser) + +type SysUser struct { + global.GVA_MODEL + UUID uuid.UUID `json:"uuid" gorm:"index;comment:用户UUID"` // 用户UUID + Username string `json:"userName" gorm:"index;comment:用户登录名"` // 用户登录名 + Password string `json:"-" gorm:"comment:用户登录密码"` // 用户登录密码 + NickName string `json:"nickName" gorm:"default:系统用户;comment:用户昵称"` // 用户昵称 + HeaderImg string `json:"headerImg" gorm:"default:https://qmplusimg.henrongyi.top/gva_header.jpg;comment:用户头像"` // 用户头像 + AuthorityId uint `json:"authorityId" gorm:"default:888;comment:用户角色ID"` // 用户角色ID + Authority SysAuthority `json:"authority" gorm:"foreignKey:AuthorityId;references:AuthorityId;comment:用户角色"` // 用户角色 + Authorities []SysAuthority `json:"authorities" gorm:"many2many:sys_user_authority;"` // 多用户角色 + Phone string `json:"phone" gorm:"comment:用户手机号"` // 用户手机号 + Email string `json:"email" gorm:"comment:用户邮箱"` // 用户邮箱 + Enable int `json:"enable" gorm:"default:1;comment:用户是否被冻结 1正常 2冻结"` //用户是否被冻结 1正常 2冻结 + OriginSetting common.JSONMap `json:"originSetting" form:"originSetting" gorm:"type:text;default:null;column:origin_setting;comment:配置;"` //配置 +} + +func (SysUser) TableName() string { + return "sys_users" +} + +func (s *SysUser) GetUsername() string { + return s.Username +} + +func (s *SysUser) GetNickname() string { + return s.NickName +} + +func (s *SysUser) GetUUID() uuid.UUID { + return s.UUID +} + +func (s *SysUser) GetUserId() uint { + return s.ID +} + +func (s *SysUser) GetAuthorityId() uint { + return s.AuthorityId +} + +func (s *SysUser) GetUserInfo() any { + return *s +} + +// Extend: Start Get the corresponding GAIA platform user information + +func (s *SysUser) GetUserEmail() string { + return s.Email +} + +const UserActive = 1 // 用户状态: 活跃 +const UserDeactivate = 2 // 用户状态: 停用 +const AdminGroupID = uint(888) // 管理员组ID +const DefaultGroupID = uint(1) // 普通用户组ID + +// GetAccount +// @description: Get user information through the user provider relationship table +// @return account gaia.Account, err error +func (s SysUser) GetAccount() (account gaia.Account, err error) { + // init + // get account + if err = global.GVA_DB.Where("email=?", s.Email).First(&account).Error; err != nil { + return account, errors.New("cannot find a user related to the database") + } + // return + return account, nil +} + +// SyncGaiaStatus +// @description: Sync user status to GAIA platform +// @return enable int +func (s SysUser) SyncGaiaStatus(enable int) { + key := fmt.Sprintf("login_error_rate_limit:%s", s.Email) + if enable == UserActive { + global.GVA_REDIS.Del(context.Background(), key) + } else { + global.GVA_REDIS.Set(context.Background(), key, global.GVA_CONFIG.Gaia.LoginMaxErrorLimit, time.Hour*24) + } + +} + +// Extend: Stop Get the corresponding GAIA platform user information + +// Extend: Start global code + +type SysUserGlobalCode struct { + global.GVA_MODEL + UserID uint `json:"user_id" gorm:"index;comment:用户id"` +} + +// Extend: Stop global code diff --git a/admin/server/model/system/sys_user_authority.go b/admin/server/model/system/sys_user_authority.go new file mode 100644 index 000000000..1aa83cbd5 --- /dev/null +++ b/admin/server/model/system/sys_user_authority.go @@ -0,0 +1,11 @@ +package system + +// SysUserAuthority 是 sysUser 和 sysAuthority 的连接表 +type SysUserAuthority struct { + SysUserId uint `gorm:"column:sys_user_id"` + SysAuthorityAuthorityId uint `gorm:"column:sys_authority_authority_id"` +} + +func (s *SysUserAuthority) TableName() string { + return "sys_user_authority" +} diff --git a/admin/server/model/system/sys_user_extend.go b/admin/server/model/system/sys_user_extend.go new file mode 100644 index 000000000..699bfa91a --- /dev/null +++ b/admin/server/model/system/sys_user_extend.go @@ -0,0 +1,4 @@ +package system + +const AdminAuthorityId = 888 +const NormalAuthorityId = 1 // 普通用户权限(默认注册的用户都是普通用户) diff --git a/admin/server/plugin/announcement/api/enter.go b/admin/server/plugin/announcement/api/enter.go new file mode 100644 index 000000000..7fee6fc2b --- /dev/null +++ b/admin/server/plugin/announcement/api/enter.go @@ -0,0 +1,10 @@ +package api + +import "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/service" + +var ( + Api = new(api) + serviceInfo = service.Service.Info +) + +type api struct{ Info info } diff --git a/admin/server/plugin/announcement/api/info.go b/admin/server/plugin/announcement/api/info.go new file mode 100644 index 000000000..dd0faa350 --- /dev/null +++ b/admin/server/plugin/announcement/api/info.go @@ -0,0 +1,183 @@ +package api + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/model" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/model/request" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +var Info = new(info) + +type info struct{} + +// CreateInfo 创建公告 +// @Tags Info +// @Summary 创建公告 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.Info true "创建公告" +// @Success 200 {object} response.Response{msg=string} "创建成功" +// @Router /info/createInfo [post] +func (a *info) CreateInfo(c *gin.Context) { + var info model.Info + err := c.ShouldBindJSON(&info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = serviceInfo.CreateInfo(&info) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败", c) + return + } + response.OkWithMessage("创建成功", c) +} + +// DeleteInfo 删除公告 +// @Tags Info +// @Summary 删除公告 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.Info true "删除公告" +// @Success 200 {object} response.Response{msg=string} "删除成功" +// @Router /info/deleteInfo [delete] +func (a *info) DeleteInfo(c *gin.Context) { + ID := c.Query("ID") + err := serviceInfo.DeleteInfo(ID) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败", c) + return + } + response.OkWithMessage("删除成功", c) +} + +// DeleteInfoByIds 批量删除公告 +// @Tags Info +// @Summary 批量删除公告 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "批量删除成功" +// @Router /info/deleteInfoByIds [delete] +func (a *info) DeleteInfoByIds(c *gin.Context) { + IDs := c.QueryArray("IDs[]") + if err := serviceInfo.DeleteInfoByIds(IDs); err != nil { + global.GVA_LOG.Error("批量删除失败!", zap.Error(err)) + response.FailWithMessage("批量删除失败", c) + return + } + response.OkWithMessage("批量删除成功", c) +} + +// UpdateInfo 更新公告 +// @Tags Info +// @Summary 更新公告 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.Info true "更新公告" +// @Success 200 {object} response.Response{msg=string} "更新成功" +// @Router /info/updateInfo [put] +func (a *info) UpdateInfo(c *gin.Context) { + var info model.Info + err := c.ShouldBindJSON(&info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = serviceInfo.UpdateInfo(info) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败", c) + return + } + response.OkWithMessage("更新成功", c) +} + +// FindInfo 用id查询公告 +// @Tags Info +// @Summary 用id查询公告 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query model.Info true "用id查询公告" +// @Success 200 {object} response.Response{data=model.Info,msg=string} "查询成功" +// @Router /info/findInfo [get] +func (a *info) FindInfo(c *gin.Context) { + ID := c.Query("ID") + reinfo, err := serviceInfo.GetInfo(ID) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败", c) + return + } + response.OkWithData(reinfo, c) +} + +// GetInfoList 分页获取公告列表 +// @Tags Info +// @Summary 分页获取公告列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query request.InfoSearch true "分页获取公告列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /info/getInfoList [get] +func (a *info) GetInfoList(c *gin.Context) { + var pageInfo request.InfoSearch + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := serviceInfo.GetInfoInfoList(pageInfo) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.PageSize, + }, "获取成功", c) +} + +// GetInfoDataSource 获取Info的数据源 +// @Tags Info +// @Summary 获取Info的数据源 +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=object,msg=string} "查询成功" +// @Router /info/getInfoDataSource [get] +func (a *info) GetInfoDataSource(c *gin.Context) { + // 此接口为获取数据源定义的数据 + dataSource, err := serviceInfo.GetInfoDataSource() + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败", c) + return + } + response.OkWithData(dataSource, c) +} + +// GetInfoPublic 不需要鉴权的公告接口 +// @Tags Info +// @Summary 不需要鉴权的公告接口 +// @accept application/json +// @Produce application/json +// @Param data query request.InfoSearch true "分页获取公告列表" +// @Success 200 {object} response.Response{data=object,msg=string} "获取成功" +// @Router /info/getInfoPublic [get] +func (a *info) GetInfoPublic(c *gin.Context) { + // 此接口不需要鉴权 示例为返回了一个固定的消息接口,一般本接口用于C端服务,需要自己实现业务逻辑 + response.OkWithDetailed(gin.H{"info": "不需要鉴权的公告接口信息"}, "获取成功", c) +} diff --git a/admin/server/plugin/announcement/config/config.go b/admin/server/plugin/announcement/config/config.go new file mode 100644 index 000000000..809bc990f --- /dev/null +++ b/admin/server/plugin/announcement/config/config.go @@ -0,0 +1,4 @@ +package config + +type Config struct { +} diff --git a/admin/server/plugin/announcement/gen/gen.go b/admin/server/plugin/announcement/gen/gen.go new file mode 100644 index 000000000..240749ff1 --- /dev/null +++ b/admin/server/plugin/announcement/gen/gen.go @@ -0,0 +1,17 @@ +package main + +import ( + "gorm.io/gen" + "path/filepath" //go:generate go mod tidy + //go:generate go mod download + //go:generate go run gen.go + "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/model" +) + +func main() { + g := gen.NewGenerator(gen.Config{OutPath: filepath.Join("..", "..", "..", "announcement", "blender", "model", "dao"), Mode: gen.WithoutContext | gen.WithDefaultQuery | gen.WithQueryInterface}) + g.ApplyBasic( + new(model.Info), + ) + g.Execute() +} diff --git a/admin/server/plugin/announcement/initialize/api.go b/admin/server/plugin/announcement/initialize/api.go new file mode 100644 index 000000000..6d0fed1d0 --- /dev/null +++ b/admin/server/plugin/announcement/initialize/api.go @@ -0,0 +1,49 @@ +package initialize + +import ( + "context" + model "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/plugin-tool/utils" +) + +func Api(ctx context.Context) { + entities := []model.SysApi{ + { + Path: "/info/createInfo", + Description: "新建公告", + ApiGroup: "公告", + Method: "POST", + }, + { + Path: "/info/deleteInfo", + Description: "删除公告", + ApiGroup: "公告", + Method: "DELETE", + }, + { + Path: "/info/deleteInfoByIds", + Description: "批量删除公告", + ApiGroup: "公告", + Method: "DELETE", + }, + { + Path: "/info/updateInfo", + Description: "更新公告", + ApiGroup: "公告", + Method: "PUT", + }, + { + Path: "/info/findInfo", + Description: "根据ID获取公告", + ApiGroup: "公告", + Method: "GET", + }, + { + Path: "/info/getInfoList", + Description: "获取公告列表", + ApiGroup: "公告", + Method: "GET", + }, + } + utils.RegisterApis(entities...) +} diff --git a/admin/server/plugin/announcement/initialize/gorm.go b/admin/server/plugin/announcement/initialize/gorm.go new file mode 100644 index 000000000..3a88ff25a --- /dev/null +++ b/admin/server/plugin/announcement/initialize/gorm.go @@ -0,0 +1,20 @@ +package initialize + +import ( + "context" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/model" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +func Gorm(ctx context.Context) { + err := global.GVA_DB.WithContext(ctx).AutoMigrate( + new(model.Info), + ) + if err != nil { + err = errors.Wrap(err, "注册表失败!") + zap.L().Error(fmt.Sprintf("%+v", err)) + } +} diff --git a/admin/server/plugin/announcement/initialize/menu.go b/admin/server/plugin/announcement/initialize/menu.go new file mode 100644 index 000000000..40aff2b50 --- /dev/null +++ b/admin/server/plugin/announcement/initialize/menu.go @@ -0,0 +1,22 @@ +package initialize + +import ( + "context" + model "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/plugin-tool/utils" +) + +func Menu(ctx context.Context) { + entities := []model.SysBaseMenu{ + { + ParentId: 24, + Path: "anInfo", + Name: "anInfo", + Hidden: false, + Component: "plugin/announcement/view/info.vue", + Sort: 5, + Meta: model.Meta{Title: "公告管理", Icon: "box"}, + }, + } + utils.RegisterMenus(entities...) +} diff --git a/admin/server/plugin/announcement/initialize/router.go b/admin/server/plugin/announcement/initialize/router.go new file mode 100644 index 000000000..e2c4f1787 --- /dev/null +++ b/admin/server/plugin/announcement/initialize/router.go @@ -0,0 +1,15 @@ +package initialize + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/router" + "github.com/gin-gonic/gin" +) + +func Router(engine *gin.Engine) { + public := engine.Group(global.GVA_CONFIG.System.RouterPrefix).Group("") + private := engine.Group(global.GVA_CONFIG.System.RouterPrefix).Group("") + private.Use(middleware.JWTAuth()).Use(middleware.CasbinHandler()) + router.Router.Info.Init(public, private) +} diff --git a/admin/server/plugin/announcement/initialize/viper.go b/admin/server/plugin/announcement/initialize/viper.go new file mode 100644 index 000000000..68cfff685 --- /dev/null +++ b/admin/server/plugin/announcement/initialize/viper.go @@ -0,0 +1,17 @@ +package initialize + +import ( + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/plugin" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +func Viper() { + err := global.GVA_VP.UnmarshalKey("announcement", &plugin.Config) + if err != nil { + err = errors.Wrap(err, "初始化配置文件失败!") + zap.L().Error(fmt.Sprintf("%+v", err)) + } +} diff --git a/admin/server/plugin/announcement/model/info.go b/admin/server/plugin/announcement/model/info.go new file mode 100644 index 000000000..fcaa11f59 --- /dev/null +++ b/admin/server/plugin/announcement/model/info.go @@ -0,0 +1,20 @@ +package model + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "gorm.io/datatypes" +) + +// Info 公告 结构体 +type Info struct { + global.GVA_MODEL + Title string `json:"title" form:"title" gorm:"column:title;comment:公告标题;"` //标题 + Content string `json:"content" form:"content" gorm:"column:content;comment:公告内容;type:text;"` //内容 + UserID *int `json:"userID" form:"userID" gorm:"column:user_id;comment:发布者;"` //作者 + Attachments datatypes.JSON `json:"attachments" form:"attachments" gorm:"column:attachments;comment:相关附件;"swaggertype:"array,object"` //附件 +} + +// TableName 公告 Info自定义表名 gva_announcements_info +func (Info) TableName() string { + return "gva_announcements_info" +} diff --git a/admin/server/plugin/announcement/model/request/info.go b/admin/server/plugin/announcement/model/request/info.go new file mode 100644 index 000000000..35be3e032 --- /dev/null +++ b/admin/server/plugin/announcement/model/request/info.go @@ -0,0 +1,12 @@ +package request + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "time" +) + +type InfoSearch struct { + StartCreatedAt *time.Time `json:"startCreatedAt" form:"startCreatedAt"` + EndCreatedAt *time.Time `json:"endCreatedAt" form:"endCreatedAt"` + request.PageInfo +} diff --git a/admin/server/plugin/announcement/plugin.go b/admin/server/plugin/announcement/plugin.go new file mode 100644 index 000000000..a20edb894 --- /dev/null +++ b/admin/server/plugin/announcement/plugin.go @@ -0,0 +1,26 @@ +package announcement + +import ( + "context" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/initialize" + interfaces "github.com/flipped-aurora/gin-vue-admin/server/utils/plugin/v2" + "github.com/gin-gonic/gin" +) + +var _ interfaces.Plugin = (*plugin)(nil) + +var Plugin = new(plugin) + +type plugin struct{} + +func (p *plugin) Register(group *gin.Engine) { + ctx := context.Background() + // 如果需要配置文件,请到config.Config中填充配置结构,且到下方发放中填入其在config.yaml中的key + // initialize.Viper() + // 安装插件时候自动注册的api数据请到下方法.Api方法中实现 + initialize.Api(ctx) + // 安装插件时候自动注册的api数据请到下方法.Menu方法中实现 + initialize.Menu(ctx) + initialize.Gorm(ctx) + initialize.Router(group) +} diff --git a/admin/server/plugin/announcement/plugin/plugin.go b/admin/server/plugin/announcement/plugin/plugin.go new file mode 100644 index 000000000..405823980 --- /dev/null +++ b/admin/server/plugin/announcement/plugin/plugin.go @@ -0,0 +1,5 @@ +package plugin + +import "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/config" + +var Config config.Config diff --git a/admin/server/plugin/announcement/router/enter.go b/admin/server/plugin/announcement/router/enter.go new file mode 100644 index 000000000..543e0ffb2 --- /dev/null +++ b/admin/server/plugin/announcement/router/enter.go @@ -0,0 +1,10 @@ +package router + +import "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/api" + +var ( + Router = new(router) + apiInfo = api.Api.Info +) + +type router struct{ Info info } diff --git a/admin/server/plugin/announcement/router/info.go b/admin/server/plugin/announcement/router/info.go new file mode 100644 index 000000000..8de316b35 --- /dev/null +++ b/admin/server/plugin/announcement/router/info.go @@ -0,0 +1,31 @@ +package router + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +var Info = new(info) + +type info struct{} + +// Init 初始化 公告 路由信息 +func (r *info) Init(public *gin.RouterGroup, private *gin.RouterGroup) { + { + group := private.Group("info").Use(middleware.OperationRecord()) + group.POST("createInfo", apiInfo.CreateInfo) // 新建公告 + group.DELETE("deleteInfo", apiInfo.DeleteInfo) // 删除公告 + group.DELETE("deleteInfoByIds", apiInfo.DeleteInfoByIds) // 批量删除公告 + group.PUT("updateInfo", apiInfo.UpdateInfo) // 更新公告 + } + { + group := private.Group("info") + group.GET("findInfo", apiInfo.FindInfo) // 根据ID获取公告 + group.GET("getInfoList", apiInfo.GetInfoList) // 获取公告列表 + } + { + group := public.Group("info") + group.GET("getInfoDataSource", apiInfo.GetInfoDataSource) // 获取公告数据源 + group.GET("getInfoPublic", apiInfo.GetInfoPublic) // 获取公告列表 + } +} diff --git a/admin/server/plugin/announcement/service/enter.go b/admin/server/plugin/announcement/service/enter.go new file mode 100644 index 000000000..988fbcd76 --- /dev/null +++ b/admin/server/plugin/announcement/service/enter.go @@ -0,0 +1,5 @@ +package service + +var Service = new(service) + +type service struct{ Info info } diff --git a/admin/server/plugin/announcement/service/info.go b/admin/server/plugin/announcement/service/info.go new file mode 100644 index 000000000..b52155393 --- /dev/null +++ b/admin/server/plugin/announcement/service/info.go @@ -0,0 +1,78 @@ +package service + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/model" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/model/request" +) + +var Info = new(info) + +type info struct{} + +// CreateInfo 创建公告记录 +// Author [piexlmax](https://github.com/piexlmax) +func (s *info) CreateInfo(info *model.Info) (err error) { + err = global.GVA_DB.Create(info).Error + return err +} + +// DeleteInfo 删除公告记录 +// Author [piexlmax](https://github.com/piexlmax) +func (s *info) DeleteInfo(ID string) (err error) { + err = global.GVA_DB.Delete(&model.Info{}, "id = ?", ID).Error + return err +} + +// DeleteInfoByIds 批量删除公告记录 +// Author [piexlmax](https://github.com/piexlmax) +func (s *info) DeleteInfoByIds(IDs []string) (err error) { + err = global.GVA_DB.Delete(&[]model.Info{}, "id in ?", IDs).Error + return err +} + +// UpdateInfo 更新公告记录 +// Author [piexlmax](https://github.com/piexlmax) +func (s *info) UpdateInfo(info model.Info) (err error) { + err = global.GVA_DB.Model(&model.Info{}).Where("id = ?", info.ID).Updates(&info).Error + return err +} + +// GetInfo 根据ID获取公告记录 +// Author [piexlmax](https://github.com/piexlmax) +func (s *info) GetInfo(ID string) (info model.Info, err error) { + err = global.GVA_DB.Where("id = ?", ID).First(&info).Error + return +} + +// GetInfoInfoList 分页获取公告记录 +// Author [piexlmax](https://github.com/piexlmax) +func (s *info) GetInfoInfoList(info request.InfoSearch) (list []model.Info, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := global.GVA_DB.Model(&model.Info{}) + var infos []model.Info + // 如果有条件搜索 下方会自动创建搜索语句 + if info.StartCreatedAt != nil && info.EndCreatedAt != nil { + db = db.Where("created_at BETWEEN ? AND ?", info.StartCreatedAt, info.EndCreatedAt) + } + err = db.Count(&total).Error + if err != nil { + return + } + + if limit != 0 { + db = db.Limit(limit).Offset(offset) + } + err = db.Find(&infos).Error + return infos, total, err +} +func (s *info) GetInfoDataSource() (res map[string][]map[string]any, err error) { + res = make(map[string][]map[string]any) + + userID := make([]map[string]any, 0) + global.GVA_DB.Table("sys_users").Select("nick_name as label,id as value").Scan(&userID) + res["userID"] = userID + return +} diff --git a/admin/server/plugin/email/README.MD b/admin/server/plugin/email/README.MD new file mode 100644 index 000000000..17202838d --- /dev/null +++ b/admin/server/plugin/email/README.MD @@ -0,0 +1,75 @@ +## GVA 邮件发送功能插件 +#### 开发者:GIN-VUE-ADMIN 官方 + +### 使用步骤 + +#### 1. 前往GVA主程序下的initialize/router.go 在Routers 方法最末尾按照你需要的及安全模式添加本插件 + 例: + 本插件可以采用gva的配置文件 也可以直接写死内容作为配置 建议为gva添加配置文件结构 然后将配置传入 + PluginInit(PrivateGroup, email.CreateEmailPlug( + global.GVA_CONFIG.Email.To, + global.GVA_CONFIG.Email.From, + global.GVA_CONFIG.Email.Host, + global.GVA_CONFIG.Email.Secret, + global.GVA_CONFIG.Email.Nickname, + global.GVA_CONFIG.Email.Port, + global.GVA_CONFIG.Email.IsSSL, + )) + + 同样也可以再传入时写死 + + PluginInit(PrivateGroup, email.CreateEmailPlug( + "a@qq.com", + "b@qq.com", + "smtp.qq.com", + "global.GVA_CONFIG.Email.Secret", + "登录密钥", + 465, + true, + )) + +### 2. 配置说明 + +#### 2-1 全局配置结构体说明 + //其中 Form 和 Secret 通常来说就是用户名和密码 + + type Email struct { + To string // 收件人:多个以英文逗号分隔 例:a@qq.com b@qq.com 正式开发中请把此项目作为参数使用 此处配置主要用于发送错误监控邮件 + From string // 发件人 你自己要发邮件的邮箱 + Host string // 服务器地址 例如 smtp.qq.com 请前往QQ或者你要发邮件的邮箱查看其smtp协议 + Secret string // 密钥 用于登录的密钥 最好不要用邮箱密码 去邮箱smtp申请一个用于登录的密钥 + Nickname string // 昵称 发件人昵称 自定义即可 可以不填 + Port int // 端口 请前往QQ或者你要发邮件的邮箱查看其smtp协议 大多为 465 + IsSSL bool // 是否SSL 是否开启SSL + } +#### 2-2 入参结构说明 + //其中 Form 和 Secret 通常来说就是用户名和密码 + + type Email struct { + To string `json:"to"` // 邮件发送给谁 + Subject string `json:"subject"` // 邮件标题 + Body string `json:"body"` // 邮件内容 + } + + +### 3. 方法API + + utils.EmailTest(邮件标题,邮件主体) 发送测试邮件 + 例:utils.EmailTest("测试邮件","测试邮件") + utils.ErrorToEmail(邮件标题,邮件主体) 错误监控 + 例:utils.ErrorToEmail("测试邮件","测试邮件") + utils.Email(目标邮箱多个的话用逗号分隔,邮件标题,邮件主体) 发送测试邮件 + 例:utils.Email(”a.qq.com,b.qq.com“,"测试邮件","测试邮件") + +### 4. 可直接调用的接口 + + 测试接口: /email/emailTest [post] 已配置swagger + + 发送邮件接口接口: /email/emailSend [post] 已配置swagger + 入参: + type Email struct { + To string `json:"to"` // 邮件发送给谁 + Subject string `json:"subject"` // 邮件标题 + Body string `json:"body"` // 邮件内容 + } + diff --git a/admin/server/plugin/email/api/enter.go b/admin/server/plugin/email/api/enter.go new file mode 100644 index 000000000..353404d2c --- /dev/null +++ b/admin/server/plugin/email/api/enter.go @@ -0,0 +1,7 @@ +package api + +type ApiGroup struct { + EmailApi +} + +var ApiGroupApp = new(ApiGroup) diff --git a/admin/server/plugin/email/api/sys_email.go b/admin/server/plugin/email/api/sys_email.go new file mode 100644 index 000000000..fdc76ab64 --- /dev/null +++ b/admin/server/plugin/email/api/sys_email.go @@ -0,0 +1,53 @@ +package api + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + email_response "github.com/flipped-aurora/gin-vue-admin/server/plugin/email/model/response" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/email/service" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type EmailApi struct{} + +// EmailTest +// @Tags System +// @Summary 发送测试邮件 +// @Security ApiKeyAuth +// @Produce application/json +// @Success 200 {string} string "{"success":true,"data":{},"msg":"发送成功"}" +// @Router /email/emailTest [post] +func (s *EmailApi) EmailTest(c *gin.Context) { + err := service.ServiceGroupApp.EmailTest() + if err != nil { + global.GVA_LOG.Error("发送失败!", zap.Error(err)) + response.FailWithMessage("发送失败", c) + return + } + response.OkWithMessage("发送成功", c) +} + +// SendEmail +// @Tags System +// @Summary 发送邮件 +// @Security ApiKeyAuth +// @Produce application/json +// @Param data body email_response.Email true "发送邮件必须的参数" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"发送成功"}" +// @Router /email/sendEmail [post] +func (s *EmailApi) SendEmail(c *gin.Context) { + var email email_response.Email + err := c.ShouldBindJSON(&email) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = service.ServiceGroupApp.SendEmail(email.To, email.Subject, email.Body) + if err != nil { + global.GVA_LOG.Error("发送失败!", zap.Error(err)) + response.FailWithMessage("发送失败", c) + return + } + response.OkWithMessage("发送成功", c) +} diff --git a/admin/server/plugin/email/config/email.go b/admin/server/plugin/email/config/email.go new file mode 100644 index 000000000..c535348c0 --- /dev/null +++ b/admin/server/plugin/email/config/email.go @@ -0,0 +1,11 @@ +package config + +type Email struct { + To string `mapstructure:"to" json:"to" yaml:"to"` // 收件人:多个以英文逗号分隔 例:a@qq.com b@qq.com 正式开发中请把此项目作为参数使用 + From string `mapstructure:"from" json:"from" yaml:"from"` // 发件人 你自己要发邮件的邮箱 + Host string `mapstructure:"host" json:"host" yaml:"host"` // 服务器地址 例如 smtp.qq.com 请前往QQ或者你要发邮件的邮箱查看其smtp协议 + Secret string `mapstructure:"secret" json:"secret" yaml:"secret"` // 密钥 用于登录的密钥 最好不要用邮箱密码 去邮箱smtp申请一个用于登录的密钥 + Nickname string `mapstructure:"nickname" json:"nickname" yaml:"nickname"` // 昵称 发件人昵称 通常为自己的邮箱 + Port int `mapstructure:"port" json:"port" yaml:"port"` // 端口 请前往QQ或者你要发邮件的邮箱查看其smtp协议 大多为 465 + IsSSL bool `mapstructure:"is-ssl" json:"isSSL" yaml:"is-ssl"` // 是否SSL 是否开启SSL +} diff --git a/admin/server/plugin/email/global/gloabl.go b/admin/server/plugin/email/global/gloabl.go new file mode 100644 index 000000000..13082d0db --- /dev/null +++ b/admin/server/plugin/email/global/gloabl.go @@ -0,0 +1,5 @@ +package global + +import "github.com/flipped-aurora/gin-vue-admin/server/plugin/email/config" + +var GlobalConfig = new(config.Email) diff --git a/admin/server/plugin/email/main.go b/admin/server/plugin/email/main.go new file mode 100644 index 000000000..cfc8c46b1 --- /dev/null +++ b/admin/server/plugin/email/main.go @@ -0,0 +1,28 @@ +package email + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/plugin/email/global" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/email/router" + "github.com/gin-gonic/gin" +) + +type emailPlugin struct{} + +func CreateEmailPlug(To, From, Host, Secret, Nickname string, Port int, IsSSL bool) *emailPlugin { + global.GlobalConfig.To = To + global.GlobalConfig.From = From + global.GlobalConfig.Host = Host + global.GlobalConfig.Secret = Secret + global.GlobalConfig.Nickname = Nickname + global.GlobalConfig.Port = Port + global.GlobalConfig.IsSSL = IsSSL + return &emailPlugin{} +} + +func (*emailPlugin) Register(group *gin.RouterGroup) { + router.RouterGroupApp.InitEmailRouter(group) +} + +func (*emailPlugin) RouterPath() string { + return "email" +} diff --git a/admin/server/plugin/email/model/response/email.go b/admin/server/plugin/email/model/response/email.go new file mode 100644 index 000000000..ed2547507 --- /dev/null +++ b/admin/server/plugin/email/model/response/email.go @@ -0,0 +1,7 @@ +package response + +type Email struct { + To string `json:"to"` // 邮件发送给谁 + Subject string `json:"subject"` // 邮件标题 + Body string `json:"body"` // 邮件内容 +} diff --git a/admin/server/plugin/email/router/enter.go b/admin/server/plugin/email/router/enter.go new file mode 100644 index 000000000..e081a54c3 --- /dev/null +++ b/admin/server/plugin/email/router/enter.go @@ -0,0 +1,7 @@ +package router + +type RouterGroup struct { + EmailRouter +} + +var RouterGroupApp = new(RouterGroup) diff --git a/admin/server/plugin/email/router/sys_email.go b/admin/server/plugin/email/router/sys_email.go new file mode 100644 index 000000000..1f9f07f0e --- /dev/null +++ b/admin/server/plugin/email/router/sys_email.go @@ -0,0 +1,19 @@ +package router + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/email/api" + "github.com/gin-gonic/gin" +) + +type EmailRouter struct{} + +func (s *EmailRouter) InitEmailRouter(Router *gin.RouterGroup) { + emailRouter := Router.Use(middleware.OperationRecord()) + EmailApi := api.ApiGroupApp.EmailApi.EmailTest + SendEmail := api.ApiGroupApp.EmailApi.SendEmail + { + emailRouter.POST("emailTest", EmailApi) // 发送测试邮件 + emailRouter.POST("sendEmail", SendEmail) // 发送邮件 + } +} diff --git a/admin/server/plugin/email/service/enter.go b/admin/server/plugin/email/service/enter.go new file mode 100644 index 000000000..e96e267f5 --- /dev/null +++ b/admin/server/plugin/email/service/enter.go @@ -0,0 +1,7 @@ +package service + +type ServiceGroup struct { + EmailService +} + +var ServiceGroupApp = new(ServiceGroup) diff --git a/admin/server/plugin/email/service/sys_email.go b/admin/server/plugin/email/service/sys_email.go new file mode 100644 index 000000000..57042769c --- /dev/null +++ b/admin/server/plugin/email/service/sys_email.go @@ -0,0 +1,32 @@ +package service + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/plugin/email/utils" +) + +type EmailService struct{} + +//@author: [maplepie](https://github.com/maplepie) +//@function: EmailTest +//@description: 发送邮件测试 +//@return: err error + +func (e *EmailService) EmailTest() (err error) { + subject := "test" + body := "test" + err = utils.EmailTest(subject, body) + return err +} + +//@author: [maplepie](https://github.com/maplepie) +//@function: EmailTest +//@description: 发送邮件测试 +//@return: err error +//@params to string 收件人 +//@params subject string 标题(主题) +//@params body string 邮件内容 + +func (e *EmailService) SendEmail(to, subject, body string) (err error) { + err = utils.Email(to, subject, body) + return err +} diff --git a/admin/server/plugin/email/utils/email.go b/admin/server/plugin/email/utils/email.go new file mode 100644 index 000000000..aa82e1c89 --- /dev/null +++ b/admin/server/plugin/email/utils/email.go @@ -0,0 +1,82 @@ +package utils + +import ( + "crypto/tls" + "fmt" + "net/smtp" + "strings" + + "github.com/flipped-aurora/gin-vue-admin/server/plugin/email/global" + + "github.com/jordan-wright/email" +) + +//@author: [maplepie](https://github.com/maplepie) +//@function: Email +//@description: Email发送方法 +//@param: subject string, body string +//@return: error + +func Email(To, subject string, body string) error { + to := strings.Split(To, ",") + return send(to, subject, body) +} + +//@author: [SliverHorn](https://github.com/SliverHorn) +//@function: ErrorToEmail +//@description: 给email中间件错误发送邮件到指定邮箱 +//@param: subject string, body string +//@return: error + +func ErrorToEmail(subject string, body string) error { + to := strings.Split(global.GlobalConfig.To, ",") + if to[len(to)-1] == "" { // 判断切片的最后一个元素是否为空,为空则移除 + to = to[:len(to)-1] + } + return send(to, subject, body) +} + +//@author: [maplepie](https://github.com/maplepie) +//@function: EmailTest +//@description: Email测试方法 +//@param: subject string, body string +//@return: error + +func EmailTest(subject string, body string) error { + to := []string{global.GlobalConfig.To} + return send(to, subject, body) +} + +//@author: [maplepie](https://github.com/maplepie) +//@function: send +//@description: Email发送方法 +//@param: subject string, body string +//@return: error + +func send(to []string, subject string, body string) error { + from := global.GlobalConfig.From + nickname := global.GlobalConfig.Nickname + secret := global.GlobalConfig.Secret + host := global.GlobalConfig.Host + port := global.GlobalConfig.Port + isSSL := global.GlobalConfig.IsSSL + + auth := smtp.PlainAuth("", from, secret, host) + e := email.NewEmail() + if nickname != "" { + e.From = fmt.Sprintf("%s <%s>", nickname, from) + } else { + e.From = from + } + e.To = to + e.Subject = subject + e.HTML = []byte(body) + var err error + hostAddr := fmt.Sprintf("%s:%d", host, port) + if isSSL { + err = e.SendWithTLS(hostAddr, auth, &tls.Config{ServerName: host}) + } else { + err = e.Send(hostAddr, auth) + } + return err +} diff --git a/admin/server/plugin/plugin-tool/utils/check.go b/admin/server/plugin/plugin-tool/utils/check.go new file mode 100644 index 000000000..4ea21921e --- /dev/null +++ b/admin/server/plugin/plugin-tool/utils/check.go @@ -0,0 +1,50 @@ +package utils + +import ( + "fmt" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" +) + +func RegisterApis(apis ...system.SysApi) { + var count int64 + var apiPaths []string + for i := range apis { + apiPaths = append(apiPaths, apis[i].Path) + } + global.GVA_DB.Find(&[]system.SysApi{}, "path in (?)", apiPaths).Count(&count) + if count > 0 { + return + } + err := global.GVA_DB.Create(&apis).Error + if err != nil { + fmt.Println(err) + } +} + +func RegisterMenus(menus ...system.SysBaseMenu) { + var count int64 + var menuNames []string + parentMenu := menus[0] + otherMenus := menus[1:] + for i := range menus { + menuNames = append(menuNames, menus[i].Name) + } + global.GVA_DB.Find(&[]system.SysBaseMenu{}, "name in (?)", menuNames).Count(&count) + if count > 0 { + return + } + err := global.GVA_DB.Create(&parentMenu).Error + if err != nil { + fmt.Println(err) + } + for i := range otherMenus { + pid := parentMenu.ID + otherMenus[i].ParentId = pid + } + err = global.GVA_DB.Create(&otherMenus).Error + if err != nil { + fmt.Println(err) + } +} diff --git a/admin/server/resource/function/api.go.tpl b/admin/server/resource/function/api.go.tpl new file mode 100644 index 000000000..1d276cff6 --- /dev/null +++ b/admin/server/resource/function/api.go.tpl @@ -0,0 +1,40 @@ +{{if .IsPlugin}} +// {{.FuncName}} {{.FuncDesc}} +// @Tags {{.StructName}} +// @Summary {{.FuncDesc}} +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=object,msg=string} "获取成功" +// @Router /{{.Abbreviation}}/{{.Router}} [{{.Method}}] +func (a *{{.Abbreviation}}) {{.FuncName}}(c *gin.Context) { + // 请添加自己的业务逻辑 + err := service{{ .StructName }}.{{.FuncName}}() + if err != nil { + global.GVA_LOG.Error("失败!", zap.Error(err)) + response.FailWithMessage("失败", c) + return + } + response.OkWithData("返回数据",c) +} + +{{- else -}} + +// {{.FuncName}} {{.FuncDesc}} +// @Tags {{.StructName}} +// @Summary {{.FuncDesc}} +// @accept application/json +// @Produce application/json +// @Param data query {{.Package}}Req.{{.StructName}}Search true "成功" +// @Success 200 {object} response.Response{data=object,msg=string} "成功" +// @Router /{{.Abbreviation}}/{{.Router}} [{{.Method}}] +func ({{.Abbreviation}}Api *{{.StructName}}Api){{.FuncName}}(c *gin.Context) { + // 请添加自己的业务逻辑 + err := {{.Abbreviation}}Service.{{.FuncName}}() + if err != nil { + global.GVA_LOG.Error("失败!", zap.Error(err)) + response.FailWithMessage("失败", c) + return + } + response.OkWithData("返回数据",c) +} +{{end}} \ No newline at end of file diff --git a/admin/server/resource/function/api.js.tpl b/admin/server/resource/function/api.js.tpl new file mode 100644 index 000000000..5cc491fe3 --- /dev/null +++ b/admin/server/resource/function/api.js.tpl @@ -0,0 +1,32 @@ +{{if .IsPlugin}} +// {{.FuncName}} {{.FuncDesc}} +// @Tags {{.StructName}} +// @Summary {{.FuncDesc}} +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=object,msg=string} "获取成功" +// @Router /{{.Abbreviation}}/{{.Router}} [{{.Method}}] +export const {{.Router}} = () => { + return service({ + url: '/{{.Abbreviation}}/{{.Router}}', + method: '{{.Method}}' + }) +} + +{{- else -}} + +// {{.FuncName}} {{.FuncDesc}} +// @Tags {{.StructName}} +// @Summary {{.FuncDesc}} +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=object,msg=string} "成功" +// @Router /{{.Abbreviation}}/{{.Router}} [{{.Method}}] +export const {{.Router}} = () => { + return service({ + url: '/{{.Abbreviation}}/{{.Router}}', + method: '{{.Method}}' + }) +} + +{{- end -}} \ No newline at end of file diff --git a/admin/server/resource/function/server.go.tpl b/admin/server/resource/function/server.go.tpl new file mode 100644 index 000000000..1c5191c4b --- /dev/null +++ b/admin/server/resource/function/server.go.tpl @@ -0,0 +1,25 @@ +{{- $db := "" }} +{{- if eq .BusinessDB "" }} + {{- $db = "global.GVA_DB" }} +{{- else}} + {{- $db = printf "global.MustGetGlobalDBByDBName(\"%s\")" .BusinessDB }} +{{- end}} +{{if .IsPlugin}} + +// {{.FuncName}} {{.FuncDesc}} +// Author [yourname](https://github.com/yourname) +func (s *{{.Abbreviation}}) {{.FuncName}}() (err error) { + db := {{$db}}.Model(&model.{{.StructName}}{}) + return db.Error +} + +{{- else -}} + +// {{.FuncName}} {{.FuncDesc}} +// Author [yourname](https://github.com/yourname) +func ({{.Abbreviation}}Service *{{.StructName}}Service){{.FuncName}}() (err error) { + // 请在这里实现自己的业务逻辑 + db := {{$db}}.Model(&{{.Package}}.{{.StructName}}{}) + return db.Error +} +{{end}} \ No newline at end of file diff --git a/admin/server/resource/package/readme.txt.tpl b/admin/server/resource/package/readme.txt.tpl new file mode 100644 index 000000000..25de23006 --- /dev/null +++ b/admin/server/resource/package/readme.txt.tpl @@ -0,0 +1,7 @@ +代码解压后把fe的api文件内容粘贴进前端api文件夹下并修改为自己想要的名字即可 + +后端代码解压后同理,放到自己想要的 mvc对应路径 并且到 initRouter中注册自动生成的路由 到registerTable中注册自动生成的model + +项目github:"https://github.com/piexlmax/github.com/flipped-aurora/gin-vue-admin/server" + +希望大家给个star多多鼓励 diff --git a/admin/server/resource/package/server/api/api.go.tpl b/admin/server/resource/package/server/api/api.go.tpl new file mode 100644 index 000000000..9e5bed5da --- /dev/null +++ b/admin/server/resource/package/server/api/api.go.tpl @@ -0,0 +1,212 @@ +package {{.Package}} + +import ( + {{if not .OnlyTemplate}} + "{{.Module}}/global" + "{{.Module}}/model/common/response" + "{{.Module}}/model/{{.Package}}" + {{.Package}}Req "{{.Module}}/model/{{.Package}}/request" + "github.com/gin-gonic/gin" + "go.uber.org/zap" + {{- if .AutoCreateResource}} + "{{.Module}}/utils" + {{- end }} + {{- else}} + "{{.Module}}/model/common/response" + "github.com/gin-gonic/gin" + {{- end}} +) + +type {{.StructName}}Api struct {} + +{{if not .OnlyTemplate}} + +// Create{{.StructName}} 创建{{.Description}} +// @Tags {{.StructName}} +// @Summary 创建{{.Description}} +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body {{.Package}}.{{.StructName}} true "创建{{.Description}}" +// @Success 200 {object} response.Response{msg=string} "创建成功" +// @Router /{{.Abbreviation}}/create{{.StructName}} [post] +func ({{.Abbreviation}}Api *{{.StructName}}Api) Create{{.StructName}}(c *gin.Context) { + var {{.Abbreviation}} {{.Package}}.{{.StructName}} + err := c.ShouldBindJSON(&{{.Abbreviation}}) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + {{- if .AutoCreateResource }} + {{.Abbreviation}}.CreatedBy = utils.GetUserID(c) + {{- end }} + err = {{.Abbreviation}}Service.Create{{.StructName}}(&{{.Abbreviation}}) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败:" + err.Error(), c) + return + } + response.OkWithMessage("创建成功", c) +} + +// Delete{{.StructName}} 删除{{.Description}} +// @Tags {{.StructName}} +// @Summary 删除{{.Description}} +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body {{.Package}}.{{.StructName}} true "删除{{.Description}}" +// @Success 200 {object} response.Response{msg=string} "删除成功" +// @Router /{{.Abbreviation}}/delete{{.StructName}} [delete] +func ({{.Abbreviation}}Api *{{.StructName}}Api) Delete{{.StructName}}(c *gin.Context) { + {{.PrimaryField.FieldJson}} := c.Query("{{.PrimaryField.FieldJson}}") + {{- if .AutoCreateResource }} + userID := utils.GetUserID(c) + {{- end }} + err := {{.Abbreviation}}Service.Delete{{.StructName}}({{.PrimaryField.FieldJson}} {{- if .AutoCreateResource -}},userID{{- end -}}) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败:" + err.Error(), c) + return + } + response.OkWithMessage("删除成功", c) +} + +// Delete{{.StructName}}ByIds 批量删除{{.Description}} +// @Tags {{.StructName}} +// @Summary 批量删除{{.Description}} +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "批量删除成功" +// @Router /{{.Abbreviation}}/delete{{.StructName}}ByIds [delete] +func ({{.Abbreviation}}Api *{{.StructName}}Api) Delete{{.StructName}}ByIds(c *gin.Context) { + {{.PrimaryField.FieldJson}}s := c.QueryArray("{{.PrimaryField.FieldJson}}s[]") + {{- if .AutoCreateResource }} + userID := utils.GetUserID(c) + {{- end }} + err := {{.Abbreviation}}Service.Delete{{.StructName}}ByIds({{.PrimaryField.FieldJson}}s{{- if .AutoCreateResource }},userID{{- end }}) + if err != nil { + global.GVA_LOG.Error("批量删除失败!", zap.Error(err)) + response.FailWithMessage("批量删除失败:" + err.Error(), c) + return + } + response.OkWithMessage("批量删除成功", c) +} + +// Update{{.StructName}} 更新{{.Description}} +// @Tags {{.StructName}} +// @Summary 更新{{.Description}} +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body {{.Package}}.{{.StructName}} true "更新{{.Description}}" +// @Success 200 {object} response.Response{msg=string} "更新成功" +// @Router /{{.Abbreviation}}/update{{.StructName}} [put] +func ({{.Abbreviation}}Api *{{.StructName}}Api) Update{{.StructName}}(c *gin.Context) { + var {{.Abbreviation}} {{.Package}}.{{.StructName}} + err := c.ShouldBindJSON(&{{.Abbreviation}}) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + {{- if .AutoCreateResource }} + {{.Abbreviation}}.UpdatedBy = utils.GetUserID(c) + {{- end }} + err = {{.Abbreviation}}Service.Update{{.StructName}}({{.Abbreviation}}) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败:" + err.Error(), c) + return + } + response.OkWithMessage("更新成功", c) +} + +// Find{{.StructName}} 用id查询{{.Description}} +// @Tags {{.StructName}} +// @Summary 用id查询{{.Description}} +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query {{.Package}}.{{.StructName}} true "用id查询{{.Description}}" +// @Success 200 {object} response.Response{data={{.Package}}.{{.StructName}},msg=string} "查询成功" +// @Router /{{.Abbreviation}}/find{{.StructName}} [get] +func ({{.Abbreviation}}Api *{{.StructName}}Api) Find{{.StructName}}(c *gin.Context) { + {{.PrimaryField.FieldJson}} := c.Query("{{.PrimaryField.FieldJson}}") + re{{.Abbreviation}}, err := {{.Abbreviation}}Service.Get{{.StructName}}({{.PrimaryField.FieldJson}}) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败:" + err.Error(), c) + return + } + response.OkWithData(re{{.Abbreviation}}, c) +} + +// Get{{.StructName}}List 分页获取{{.Description}}列表 +// @Tags {{.StructName}} +// @Summary 分页获取{{.Description}}列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query {{.Package}}Req.{{.StructName}}Search true "分页获取{{.Description}}列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /{{.Abbreviation}}/get{{.StructName}}List [get] +func ({{.Abbreviation}}Api *{{.StructName}}Api) Get{{.StructName}}List(c *gin.Context) { + var pageInfo {{.Package}}Req.{{.StructName}}Search + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := {{.Abbreviation}}Service.Get{{.StructName}}InfoList(pageInfo) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败:" + err.Error(), c) + return + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.PageSize, + }, "获取成功", c) +} + +{{- if .HasDataSource }} +// Get{{.StructName}}DataSource 获取{{.StructName}}的数据源 +// @Tags {{.StructName}} +// @Summary 获取{{.StructName}}的数据源 +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=object,msg=string} "查询成功" +// @Router /{{.Abbreviation}}/get{{.StructName}}DataSource [get] +func ({{.Abbreviation}}Api *{{.StructName}}Api) Get{{.StructName}}DataSource(c *gin.Context) { + // 此接口为获取数据源定义的数据 + dataSource, err := {{.Abbreviation}}Service.Get{{.StructName}}DataSource() + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败:" + err.Error(), c) + return + } + response.OkWithData(dataSource, c) +} +{{- end }} + +{{- end }} + +// Get{{.StructName}}Public 不需要鉴权的{{.Description}}接口 +// @Tags {{.StructName}} +// @Summary 不需要鉴权的{{.Description}}接口 +// @accept application/json +// @Produce application/json +// @Param data query {{.Package}}Req.{{.StructName}}Search true "分页获取{{.Description}}列表" +// @Success 200 {object} response.Response{data=object,msg=string} "获取成功" +// @Router /{{.Abbreviation}}/get{{.StructName}}Public [get] +func ({{.Abbreviation}}Api *{{.StructName}}Api) Get{{.StructName}}Public(c *gin.Context) { + // 此接口不需要鉴权 + // 示例为返回了一个固定的消息接口,一般本接口用于C端服务,需要自己实现业务逻辑 + {{.Abbreviation}}Service.Get{{.StructName}}Public() + response.OkWithDetailed(gin.H{ + "info": "不需要鉴权的{{.Description}}接口信息", + }, "获取成功", c) +} diff --git a/admin/server/resource/package/server/api/enter.go.tpl b/admin/server/resource/package/server/api/enter.go.tpl new file mode 100644 index 000000000..778b3146e --- /dev/null +++ b/admin/server/resource/package/server/api/enter.go.tpl @@ -0,0 +1,4 @@ +package {{ .Package }} + +type ApiGroup struct { +} \ No newline at end of file diff --git a/admin/server/resource/package/server/model/model.go.tpl b/admin/server/resource/package/server/model/model.go.tpl new file mode 100644 index 000000000..f1f979c59 --- /dev/null +++ b/admin/server/resource/package/server/model/model.go.tpl @@ -0,0 +1,86 @@ +{{- if .IsAdd}} +// 在结构体中新增如下字段 +{{- range .Fields}} +{{- if eq .FieldType "enum" }} +{{.FieldName}} string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};type:enum({{.DataTypeLong}});comment:{{.Comment}};" {{- if .Require }} binding:"required"{{- end -}}` +{{- else if eq .FieldType "picture" }} +{{.FieldName}} string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end -}}` +{{- else if eq .FieldType "video" }} +{{.FieldName}} string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end -}}` +{{- else if eq .FieldType "file" }} +{{.FieldName}} datatypes.JSON `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end -}} swaggertype:"array,object"` +{{- else if eq .FieldType "pictures" }} +{{.FieldName}} datatypes.JSON `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end -}} swaggertype:"array,object"` +{{- else if eq .FieldType "richtext" }} +{{.FieldName}} *string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}type:text;" {{- if .Require }} binding:"required"{{- end -}}` +{{- else if eq .FieldType "json" }} +{{.FieldName}} datatypes.JSON `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}type:text;" {{- if .Require }} binding:"required"{{- end -}} swaggertype:"object"` +{{- else if eq .FieldType "array" }} +{{.FieldName}} datatypes.JSON `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}type:text;" {{- if .Require }} binding:"required"{{- end -}} swaggertype:"array,object"` +{{- else }} +{{.FieldName}} *{{.FieldType}} `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end -}}` +{{- end }} {{ if .FieldDesc }}//{{.FieldDesc}} {{ end }} +{{- end }} + +{{ else }} +// 自动生成模板{{.StructName}} +package {{.Package}} + +{{- if not .OnlyTemplate}} +import ( + {{- if .GvaModel }} + "{{.Module}}/global" + {{- end }} + {{- if or .HasTimer }} + "time" + {{- end }} + {{- if .NeedJSON }} + "gorm.io/datatypes" + {{- end }} +) +{{- end }} + +// {{.Description}} 结构体 {{.StructName}} +type {{.StructName}} struct { +{{- if not .OnlyTemplate}} +{{- if .GvaModel }} + global.GVA_MODEL +{{- end }} +{{- range .Fields}} + {{- if eq .FieldType "enum" }} + {{.FieldName}} string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};type:enum({{.DataTypeLong}});comment:{{.Comment}};" {{- if .Require }} binding:"required"{{- end -}}` + {{- else if eq .FieldType "picture" }} + {{.FieldName}} string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end -}}` + {{- else if eq .FieldType "video" }} + {{.FieldName}} string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end -}}` + {{- else if eq .FieldType "file" }} + {{.FieldName}} datatypes.JSON `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end -}} swaggertype:"array,object"` + {{- else if eq .FieldType "pictures" }} + {{.FieldName}} datatypes.JSON `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end -}} swaggertype:"array,object"` + {{- else if eq .FieldType "richtext" }} + {{.FieldName}} *string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}type:text;" {{- if .Require }} binding:"required"{{- end -}}` + {{- else if eq .FieldType "json" }} + {{.FieldName}} datatypes.JSON `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}type:text;" {{- if .Require }} binding:"required"{{- end -}} swaggertype:"object"` + {{- else if eq .FieldType "array" }} + {{.FieldName}} datatypes.JSON `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}type:text;" {{- if .Require }} binding:"required"{{- end -}} swaggertype:"array,object"` + {{- else }} + {{.FieldName}} *{{.FieldType}} `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end -}}` + {{- end }} {{ if .FieldDesc }}//{{.FieldDesc}} {{ end }} +{{- end }} + {{- if .AutoCreateResource }} + CreatedBy uint `gorm:"column:created_by;comment:创建者"` + UpdatedBy uint `gorm:"column:updated_by;comment:更新者"` + DeletedBy uint `gorm:"column:deleted_by;comment:删除者"` + {{- end }} +{{- end }} +} + +{{ if .TableName }} +// TableName {{.Description}} {{.StructName}}自定义表名 {{.TableName}} +func ({{.StructName}}) TableName() string { + return "{{.TableName}}" +} +{{ end }} + + +{{ end }} \ No newline at end of file diff --git a/admin/server/resource/package/server/model/request/request.go.tpl b/admin/server/resource/package/server/model/request/request.go.tpl new file mode 100644 index 000000000..ee5816da3 --- /dev/null +++ b/admin/server/resource/package/server/model/request/request.go.tpl @@ -0,0 +1,58 @@ +{{- if .IsAdd}} +// 在结构体中新增如下字段 +{{- range .Fields}} + {{- if ne .FieldSearchType ""}} + {{- if eq .FieldSearchType "BETWEEN" "NOT BETWEEN"}} +Start{{.FieldName}} *{{.FieldType}} `json:"start{{.FieldName}}" form:"start{{.FieldName}}"` +End{{.FieldName}} *{{.FieldType}} `json:"end{{.FieldName}}" form:"end{{.FieldName}}"` + {{- else }} + {{- if or (eq .FieldType "enum") (eq .FieldType "picture") (eq .FieldType "pictures") (eq .FieldType "video") (eq .FieldType "json") }} +{{.FieldName}} string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" ` + {{- else }} +{{.FieldName}} *{{.FieldType}} `json:"{{.FieldJson}}" form:"{{.FieldJson}}" ` + {{- end }} + {{- end }} + {{- end}} +{{- end }} +{{- if .NeedSort}} +Sort string `json:"sort" form:"sort"` +Order string `json:"order" form:"order"` +{{- end}} +{{- else }} +package request + +import ( +{{- if not .OnlyTemplate }} + "{{.Module}}/model/common/request" + {{ if or .HasSearchTimer .GvaModel}}"time"{{ end }} +{{- end }} +) + +type {{.StructName}}Search struct{ +{{- if not .OnlyTemplate}} +{{- if .GvaModel }} + StartCreatedAt *time.Time `json:"startCreatedAt" form:"startCreatedAt"` + EndCreatedAt *time.Time `json:"endCreatedAt" form:"endCreatedAt"` +{{- end }} +{{- range .Fields}} + {{- if ne .FieldSearchType ""}} + {{- if eq .FieldSearchType "BETWEEN" "NOT BETWEEN"}} + Start{{.FieldName}} *{{.FieldType}} `json:"start{{.FieldName}}" form:"start{{.FieldName}}"` + End{{.FieldName}} *{{.FieldType}} `json:"end{{.FieldName}}" form:"end{{.FieldName}}"` + {{- else }} + {{- if or (eq .FieldType "enum") (eq .FieldType "picture") (eq .FieldType "pictures") (eq .FieldType "video") (eq .FieldType "json") }} + {{.FieldName}} string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" ` + {{- else }} + {{.FieldName}} *{{.FieldType}} `json:"{{.FieldJson}}" form:"{{.FieldJson}}" ` + {{- end }} + {{- end }} + {{- end}} +{{- end }} + request.PageInfo + {{- if .NeedSort}} + Sort string `json:"sort" form:"sort"` + Order string `json:"order" form:"order"` + {{- end}} +{{- end}} +} +{{- end }} diff --git a/admin/server/resource/package/server/router/enter.go.tpl b/admin/server/resource/package/server/router/enter.go.tpl new file mode 100644 index 000000000..178aecf3b --- /dev/null +++ b/admin/server/resource/package/server/router/enter.go.tpl @@ -0,0 +1,4 @@ +package {{ .Package }} + +type RouterGroup struct { +} \ No newline at end of file diff --git a/admin/server/resource/package/server/router/router.go.tpl b/admin/server/resource/package/server/router/router.go.tpl new file mode 100644 index 000000000..cac47ab78 --- /dev/null +++ b/admin/server/resource/package/server/router/router.go.tpl @@ -0,0 +1,42 @@ +package {{.Package}} + +import ( + {{if .OnlyTemplate}}// {{ end}}"{{.Module}}/middleware" + "github.com/gin-gonic/gin" +) + +type {{.StructName}}Router struct {} + +// Init{{.StructName}}Router 初始化 {{.Description}} 路由信息 +func (s *{{.StructName}}Router) Init{{.StructName}}Router(Router *gin.RouterGroup,PublicRouter *gin.RouterGroup) { + {{- if not .OnlyTemplate}} + {{.Abbreviation}}Router := Router.Group("{{.Abbreviation}}").Use(middleware.OperationRecord()) + {{.Abbreviation}}RouterWithoutRecord := Router.Group("{{.Abbreviation}}") + {{- else }} + // {{.Abbreviation}}Router := Router.Group("{{.Abbreviation}}").Use(middleware.OperationRecord()) + // {{.Abbreviation}}RouterWithoutRecord := Router.Group("{{.Abbreviation}}") + {{- end}} + {{.Abbreviation}}RouterWithoutAuth := PublicRouter.Group("{{.Abbreviation}}") + {{- if not .OnlyTemplate}} + { + {{.Abbreviation}}Router.POST("create{{.StructName}}", {{.Abbreviation}}Api.Create{{.StructName}}) // 新建{{.Description}} + {{.Abbreviation}}Router.DELETE("delete{{.StructName}}", {{.Abbreviation}}Api.Delete{{.StructName}}) // 删除{{.Description}} + {{.Abbreviation}}Router.DELETE("delete{{.StructName}}ByIds", {{.Abbreviation}}Api.Delete{{.StructName}}ByIds) // 批量删除{{.Description}} + {{.Abbreviation}}Router.PUT("update{{.StructName}}", {{.Abbreviation}}Api.Update{{.StructName}}) // 更新{{.Description}} + } + { + {{.Abbreviation}}RouterWithoutRecord.GET("find{{.StructName}}", {{.Abbreviation}}Api.Find{{.StructName}}) // 根据ID获取{{.Description}} + {{.Abbreviation}}RouterWithoutRecord.GET("get{{.StructName}}List", {{.Abbreviation}}Api.Get{{.StructName}}List) // 获取{{.Description}}列表 + } + { + {{- if .HasDataSource}} + {{.Abbreviation}}RouterWithoutAuth.GET("get{{.StructName}}DataSource", {{.Abbreviation}}Api.Get{{.StructName}}DataSource) // 获取{{.Description}}数据源 + {{- end}} + {{.Abbreviation}}RouterWithoutAuth.GET("get{{.StructName}}Public", {{.Abbreviation}}Api.Get{{.StructName}}Public) // {{.Description}}开放接口 + } + {{- else}} + { + {{.Abbreviation}}RouterWithoutAuth.GET("get{{.StructName}}Public", {{.Abbreviation}}Api.Get{{.StructName}}Public) // {{.Description}}开放接口 + } + {{ end }} +} diff --git a/admin/server/resource/package/server/service/enter.go.tpl b/admin/server/resource/package/server/service/enter.go.tpl new file mode 100644 index 000000000..adf1db02e --- /dev/null +++ b/admin/server/resource/package/server/service/enter.go.tpl @@ -0,0 +1,4 @@ +package {{ .Package }} + +type ServiceGroup struct { +} \ No newline at end of file diff --git a/admin/server/resource/package/server/service/service.go.tpl b/admin/server/resource/package/server/service/service.go.tpl new file mode 100644 index 000000000..ff42b4ee3 --- /dev/null +++ b/admin/server/resource/package/server/service/service.go.tpl @@ -0,0 +1,218 @@ +{{- $db := "" }} +{{- if eq .BusinessDB "" }} + {{- $db = "global.GVA_DB" }} +{{- else}} + {{- $db = printf "global.MustGetGlobalDBByDBName(\"%s\")" .BusinessDB }} +{{- end}} + +{{- if .IsAdd}} + +// Get{{.StructName}}InfoList 新增搜索语句 + {{- range .Fields}} + {{- if .FieldSearchType}} + {{- if or (eq .FieldType "enum") (eq .FieldType "pictures") (eq .FieldType "picture") (eq .FieldType "video") (eq .FieldType "json") }} +if info.{{.FieldName}} != "" { + {{- if or (eq .FieldType "enum") }} + db = db.Where("{{.ColumnName}} {{.FieldSearchType}} ?",{{if eq .FieldSearchType "LIKE"}}"%"+ {{ end }}*info.{{.FieldName}}{{if eq .FieldSearchType "LIKE"}}+"%"{{ end }}) + {{- else}} +// 数据类型为复杂类型,请根据业务需求自行实现复杂类型的查询业务 + {{- end}} +} + {{- else if eq .FieldSearchType "BETWEEN" "NOT BETWEEN"}} +if info.Start{{.FieldName}} != nil && info.End{{.FieldName}} != nil { + db = db.Where("{{.ColumnName}} {{.FieldSearchType}} ? AND ? ",info.Start{{.FieldName}},info.End{{.FieldName}}) +} + {{- else}} +if info.{{.FieldName}} != nil{{- if eq .FieldType "string" }} && *info.{{.FieldName}} != ""{{- end }} { + db = db.Where("{{.ColumnName}} {{.FieldSearchType}} ?",{{if eq .FieldSearchType "LIKE"}}"%"+{{ end }}*info.{{.FieldName}}{{if eq .FieldSearchType "LIKE"}}+"%"{{ end }}) +} + {{- end }} + {{- end }} + {{- end }} + + +// Get{{.StructName}}InfoList 新增排序语句 请自行在搜索语句中添加orderMap内容 + {{- range .Fields}} + {{- if .Sort}} +orderMap["{{.ColumnName}}"] = true + {{- end}} + {{- end}} + + +{{- if .HasDataSource }} +// Get{{.StructName}}DataSource()方法新增关联语句 + {{range $key, $value := .DataSourceMap}} +{{$key}} := make([]map[string]any, 0) +{{ $dataDB := "" }} +{{- if eq $value.DBName "" }} +{{ $dataDB = $db }} +{{- else}} +{{ $dataDB = printf "global.MustGetGlobalDBByDBName(\"%s\")" $value.DBName }} +{{- end}} +{{$dataDB}}.Table("{{$value.Table}}"){{- if $value.HasDeletedAt}}.Where("deleted_at IS NULL"){{ end }}.Select("{{$value.Label}} as label,{{$value.Value}} as value").Scan(&{{$key}}) +res["{{$key}}"] = {{$key}} + {{- end }} +{{- end }} +{{- else}} +package {{.Package}} + +import ( +{{- if not .OnlyTemplate }} + "{{.Module}}/global" + "{{.Module}}/model/{{.Package}}" + {{.Package}}Req "{{.Module}}/model/{{.Package}}/request" + {{- if .AutoCreateResource }} + "gorm.io/gorm" + {{- end}} +{{- end }} +) + +type {{.StructName}}Service struct {} + +{{- if not .OnlyTemplate }} +// Create{{.StructName}} 创建{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func ({{.Abbreviation}}Service *{{.StructName}}Service) Create{{.StructName}}({{.Abbreviation}} *{{.Package}}.{{.StructName}}) (err error) { + err = {{$db}}.Create({{.Abbreviation}}).Error + return err +} + +// Delete{{.StructName}} 删除{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func ({{.Abbreviation}}Service *{{.StructName}}Service)Delete{{.StructName}}({{.PrimaryField.FieldJson}} string{{- if .AutoCreateResource -}},userID uint{{- end -}}) (err error) { + {{- if .AutoCreateResource }} + err = {{$db}}.Transaction(func(tx *gorm.DB) error { + if err := tx.Model(&{{.Package}}.{{.StructName}}{}).Where("{{.PrimaryField.ColumnName}} = ?", {{.PrimaryField.FieldJson}}).Update("deleted_by", userID).Error; err != nil { + return err + } + if err = tx.Delete(&{{.Package}}.{{.StructName}}{},"{{.PrimaryField.ColumnName}} = ?",{{.PrimaryField.FieldJson}}).Error; err != nil { + return err + } + return nil + }) + {{- else }} + err = {{$db}}.Delete(&{{.Package}}.{{.StructName}}{},"{{.PrimaryField.ColumnName}} = ?",{{.PrimaryField.FieldJson}}).Error + {{- end }} + return err +} + +// Delete{{.StructName}}ByIds 批量删除{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func ({{.Abbreviation}}Service *{{.StructName}}Service)Delete{{.StructName}}ByIds({{.PrimaryField.FieldJson}}s []string {{- if .AutoCreateResource }},deleted_by uint{{- end}}) (err error) { + {{- if .AutoCreateResource }} + err = {{$db}}.Transaction(func(tx *gorm.DB) error { + if err := tx.Model(&{{.Package}}.{{.StructName}}{}).Where("{{.PrimaryField.ColumnName}} in ?", {{.PrimaryField.FieldJson}}s).Update("deleted_by", deleted_by).Error; err != nil { + return err + } + if err := tx.Where("{{.PrimaryField.ColumnName}} in ?", {{.PrimaryField.FieldJson}}s).Delete(&{{.Package}}.{{.StructName}}{}).Error; err != nil { + return err + } + return nil + }) + {{- else}} + err = {{$db}}.Delete(&[]{{.Package}}.{{.StructName}}{},"{{.PrimaryField.ColumnName}} in ?",{{.PrimaryField.FieldJson}}s).Error + {{- end}} + return err +} + +// Update{{.StructName}} 更新{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func ({{.Abbreviation}}Service *{{.StructName}}Service)Update{{.StructName}}({{.Abbreviation}} {{.Package}}.{{.StructName}}) (err error) { + err = {{$db}}.Model(&{{.Package}}.{{.StructName}}{}).Where("{{.PrimaryField.ColumnName}} = ?",{{.Abbreviation}}.{{.PrimaryField.FieldName}}).Updates(&{{.Abbreviation}}).Error + return err +} + +// Get{{.StructName}} 根据{{.PrimaryField.FieldJson}}获取{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}({{.PrimaryField.FieldJson}} string) ({{.Abbreviation}} {{.Package}}.{{.StructName}}, err error) { + err = {{$db}}.Where("{{.PrimaryField.ColumnName}} = ?", {{.PrimaryField.FieldJson}}).First(&{{.Abbreviation}}).Error + return +} + +// Get{{.StructName}}InfoList 分页获取{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}InfoList(info {{.Package}}Req.{{.StructName}}Search) (list []{{.Package}}.{{.StructName}}, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := {{$db}}.Model(&{{.Package}}.{{.StructName}}{}) + var {{.Abbreviation}}s []{{.Package}}.{{.StructName}} + // 如果有条件搜索 下方会自动创建搜索语句 +{{- if .GvaModel }} + if info.StartCreatedAt !=nil && info.EndCreatedAt !=nil { + db = db.Where("created_at BETWEEN ? AND ?", info.StartCreatedAt, info.EndCreatedAt) + } +{{- end }} + {{- range .Fields}} + {{- if .FieldSearchType}} + {{- if or (eq .FieldType "enum") (eq .FieldType "pictures") (eq .FieldType "picture") (eq .FieldType "video") (eq .FieldType "json") }} + if info.{{.FieldName}} != "" { + {{- if or (eq .FieldType "enum")}} + db = db.Where("{{.ColumnName}} {{.FieldSearchType}} ?",{{if eq .FieldSearchType "LIKE"}}"%"+ {{ end }}*info.{{.FieldName}}{{if eq .FieldSearchType "LIKE"}}+"%"{{ end }}) + {{- else}} + // 数据类型为复杂类型,请根据业务需求自行实现复杂类型的查询业务 + {{- end}} + } + {{- else if eq .FieldSearchType "BETWEEN" "NOT BETWEEN"}} + if info.Start{{.FieldName}} != nil && info.End{{.FieldName}} != nil { + db = db.Where("{{.ColumnName}} {{.FieldSearchType}} ? AND ? ",info.Start{{.FieldName}},info.End{{.FieldName}}) + } + {{- else}} + if info.{{.FieldName}} != nil{{- if eq .FieldType "string" }} && *info.{{.FieldName}} != ""{{- end }} { + db = db.Where("{{.ColumnName}} {{.FieldSearchType}} ?",{{if eq .FieldSearchType "LIKE"}}"%"+{{ end }}*info.{{.FieldName}}{{if eq .FieldSearchType "LIKE"}}+"%"{{ end }}) + } + {{- end }} + {{- end }} + {{- end }} + err = db.Count(&total).Error + if err!=nil { + return + } + {{- if .NeedSort}} + var OrderStr string + orderMap := make(map[string]bool) + {{- range .Fields}} + {{- if .Sort}} + orderMap["{{.ColumnName}}"] = true + {{- end}} + {{- end}} + if orderMap[info.Sort] { + OrderStr = info.Sort + if info.Order == "descending" { + OrderStr = OrderStr + " desc" + } + db = db.Order(OrderStr) + } + {{- end}} + + if limit != 0 { + db = db.Limit(limit).Offset(offset) + } + + err = db.Find(&{{.Abbreviation}}s).Error + return {{.Abbreviation}}s, total, err +} + +{{- if .HasDataSource }} +func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}DataSource() (res map[string][]map[string]any, err error) { + res = make(map[string][]map[string]any) + {{range $key, $value := .DataSourceMap}} + {{$key}} := make([]map[string]any, 0) + {{ $dataDB := "" }} + {{- if eq $value.DBName "" }} + {{ $dataDB = $db }} + {{- else}} + {{ $dataDB = printf "global.MustGetGlobalDBByDBName(\"%s\")" $value.DBName }} + {{- end}} + {{$dataDB}}.Table("{{$value.Table}}"){{- if $value.HasDeletedAt}}.Where("deleted_at IS NULL"){{ end }}.Select("{{$value.Label}} as label,{{$value.Value}} as value").Scan(&{{$key}}) + res["{{$key}}"] = {{$key}} + {{- end }} + return +} +{{- end }} +{{- end }} +func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}Public() { + // 此方法为获取数据源定义的数据 + // 请自行实现 +} +{{- end }} \ No newline at end of file diff --git a/admin/server/resource/package/web/api/api.js.tpl b/admin/server/resource/package/web/api/api.js.tpl new file mode 100644 index 000000000..94085baa1 --- /dev/null +++ b/admin/server/resource/package/web/api/api.js.tpl @@ -0,0 +1,130 @@ +import service from '@/utils/request' + +{{- if not .OnlyTemplate}} +// @Tags {{.StructName}} +// @Summary 创建{{.Description}} +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.{{.StructName}} true "创建{{.Description}}" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" +// @Router /{{.Abbreviation}}/create{{.StructName}} [post] +export const create{{.StructName}} = (data) => { + return service({ + url: '/{{.Abbreviation}}/create{{.StructName}}', + method: 'post', + data + }) +} + +// @Tags {{.StructName}} +// @Summary 删除{{.Description}} +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.{{.StructName}} true "删除{{.Description}}" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /{{.Abbreviation}}/delete{{.StructName}} [delete] +export const delete{{.StructName}} = (params) => { + return service({ + url: '/{{.Abbreviation}}/delete{{.StructName}}', + method: 'delete', + params + }) +} + +// @Tags {{.StructName}} +// @Summary 批量删除{{.Description}} +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "批量删除{{.Description}}" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /{{.Abbreviation}}/delete{{.StructName}} [delete] +export const delete{{.StructName}}ByIds = (params) => { + return service({ + url: '/{{.Abbreviation}}/delete{{.StructName}}ByIds', + method: 'delete', + params + }) +} + +// @Tags {{.StructName}} +// @Summary 更新{{.Description}} +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.{{.StructName}} true "更新{{.Description}}" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}" +// @Router /{{.Abbreviation}}/update{{.StructName}} [put] +export const update{{.StructName}} = (data) => { + return service({ + url: '/{{.Abbreviation}}/update{{.StructName}}', + method: 'put', + data + }) +} + +// @Tags {{.StructName}} +// @Summary 用id查询{{.Description}} +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query model.{{.StructName}} true "用id查询{{.Description}}" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" +// @Router /{{.Abbreviation}}/find{{.StructName}} [get] +export const find{{.StructName}} = (params) => { + return service({ + url: '/{{.Abbreviation}}/find{{.StructName}}', + method: 'get', + params + }) +} + +// @Tags {{.StructName}} +// @Summary 分页获取{{.Description}}列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query request.PageInfo true "分页获取{{.Description}}列表" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /{{.Abbreviation}}/get{{.StructName}}List [get] +export const get{{.StructName}}List = (params) => { + return service({ + url: '/{{.Abbreviation}}/get{{.StructName}}List', + method: 'get', + params + }) +} + +{{- if .HasDataSource}} +// @Tags {{.StructName}} +// @Summary 获取数据源 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" +// @Router /{{.Abbreviation}}/find{{.StructName}}DataSource [get] +export const get{{.StructName}}DataSource = () => { + return service({ + url: '/{{.Abbreviation}}/get{{.StructName}}DataSource', + method: 'get', + }) +} +{{- end}} + +{{- end}} + +// @Tags {{.StructName}} +// @Summary 不需要鉴权的{{.Description}}接口 +// @accept application/json +// @Produce application/json +// @Param data query {{.Package}}Req.{{.StructName}}Search true "分页获取{{.Description}}列表" +// @Success 200 {object} response.Response{data=object,msg=string} "获取成功" +// @Router /{{.Abbreviation}}/get{{.StructName}}Public [get] +export const get{{.StructName}}Public = () => { + return service({ + url: '/{{.Abbreviation}}/get{{.StructName}}Public', + method: 'get', + }) +} diff --git a/admin/server/resource/package/web/view/form.vue.tpl b/admin/server/resource/package/web/view/form.vue.tpl new file mode 100644 index 000000000..1e0d13299 --- /dev/null +++ b/admin/server/resource/package/web/view/form.vue.tpl @@ -0,0 +1,413 @@ +{{- if .IsAdd }} +// 新增表单中增加如下代码 +{{- range .Fields}} + {{- if .Form}} + + {{- if .CheckDataSource}} + + + + {{- else }} + {{- if eq .FieldType "bool" }} + + {{- end }} + {{- if eq .FieldType "string" }} + {{- if .DictType}} + + + + {{- else }} + + {{- end }} + {{- end }} + {{- if eq .FieldType "richtext" }} + + {{- end }} + {{- if eq .FieldType "json" }} + // 此字段为json结构,可以前端自行控制展示和数据绑定模式 需绑定json的key为 formData.{{.FieldJson}} 后端会按照json的类型进行存取 + {{"{{"}} formData.{{.FieldJson}} {{"}}"}} + {{- end }} + {{- if eq .FieldType "array" }} + + {{- end }} + {{- if eq .FieldType "int" }} + + {{- end }} + {{- if eq .FieldType "time.Time" }} + + {{- end }} + {{- if eq .FieldType "float64" }} + + {{- end }} + {{- if eq .FieldType "enum" }} + + + + {{- end }} + {{- if eq .FieldType "picture" }} + + {{- end }} + {{- if eq .FieldType "pictures" }} + + {{- end }} + {{- if eq .FieldType "video" }} + + {{- end }} + {{- if eq .FieldType "file" }} + + {{- end }} + {{- end }} + + {{- end }} + {{- end }} + +// 字典增加如下代码 + {{- range $index, $element := .DictTypes}} +const {{ $element }}Options = ref([]) + {{- end }} + +// init方法中增加如下调用 + +{{- range $index, $element := .DictTypes }} + {{ $element }}Options.value = await getDictFunc('{{$element}}') +{{- end }} + +// 基础formData结构增加如下字段 +{{- range .Fields}} + {{- if .Form}} + {{- if eq .FieldType "bool" }} +{{.FieldJson}}: false, + {{- end }} + {{- if eq .FieldType "string" }} +{{.FieldJson}}: '', + {{- end }} + {{- if eq .FieldType "richtext" }} +{{.FieldJson}}: '', + {{- end }} + {{- if eq .FieldType "int" }} +{{.FieldJson}}: {{- if or .DictType .DataSource}} undefined{{ else }} 0{{- end }}, + {{- end }} + {{- if eq .FieldType "time.Time" }} +{{.FieldJson}}: new Date(), + {{- end }} + {{- if eq .FieldType "float64" }} +{{.FieldJson}}: 0, + {{- end }} + {{- if eq .FieldType "picture" }} +{{.FieldJson}}: "", + {{- end }} + {{- if eq .FieldType "video" }} +{{.FieldJson}}: "", + {{- end }} + {{- if eq .FieldType "pictures" }} +{{.FieldJson}}: [], + {{- end }} + {{- if eq .FieldType "file" }} +{{.FieldJson}}: [], + {{- end }} + {{- if eq .FieldType "json" }} +{{.FieldJson}}: {}, + {{- end }} + {{- if eq .FieldType "array" }} +{{.FieldJson}}: [], + {{- end }} + {{- end }} + {{- end }} +// 验证规则中增加如下字段 + +{{- range .Fields }} + {{- if .Form }} + {{- if eq .Require true }} +{{.FieldJson }} : [{ + required: true, + message: '{{ .ErrorText }}', + trigger: ['input','blur'], +}, + {{- if eq .FieldType "string" }} +{ + whitespace: true, + message: '不能只输入空格', + trigger: ['input', 'blur'], +} + {{- end }} +], + {{- end }} + {{- end }} + {{- end }} + +{{- if .HasDataSource }} +// 请引用 +get{{.StructName}}DataSource, + +// 获取数据源 +const dataSource = ref([]) +const getDataSourceFunc = async()=>{ + const res = await get{{.StructName}}DataSource() + if (res.code === 0) { + dataSource.value = res.data + } +} +getDataSourceFunc() +{{- end }} +{{- else }} +{{- if not .OnlyTemplate }} + + + + + +{{- else }} + + + +{{- end }} +{{- end }} \ No newline at end of file diff --git a/admin/server/resource/package/web/view/table.vue.tpl b/admin/server/resource/package/web/view/table.vue.tpl new file mode 100644 index 000000000..f47ed5c85 --- /dev/null +++ b/admin/server/resource/package/web/view/table.vue.tpl @@ -0,0 +1,1266 @@ +{{- $global := . }} +{{- $templateID := printf "%s_%s" .Package .StructName }} +{{- if .IsAdd }} +// 请在搜索条件中增加如下代码 +{{- range .Fields}} {{- if .FieldSearchType}} {{- if eq .FieldType "bool" }} + + + + + + + + + {{- else if .DictType}} + + + + + + {{- else if .CheckDataSource}} + + + + + + {{- else}} + + {{- if eq .FieldType "float64" "int"}} + {{if eq .FieldSearchType "BETWEEN" "NOT BETWEEN"}} + +— + + {{- else}} + {{- if .DictType}} + + + + {{- else}} + + {{- end }} + {{- end}} + {{- else if eq .FieldType "time.Time"}} + {{if eq .FieldSearchType "BETWEEN" "NOT BETWEEN"}} + + +— + + {{- else}} + + {{- end}} + {{- else}} + + {{- end}} +{{ end }}{{ end }}{{ end }} + + +// 表格增加如下列代码 + +{{- range .Fields}} + {{- if .Table}} + {{- if .CheckDataSource }} + + + + {{- else if .DictType}} + + + + {{- else if eq .FieldType "bool" }} + + + + {{- else if eq .FieldType "time.Time" }} + + + + {{- else if eq .FieldType "picture" }} + + + + {{- else if eq .FieldType "pictures" }} + + + + {{- else if eq .FieldType "video" }} + + + + {{- else if eq .FieldType "richtext" }} + + + + {{- else if eq .FieldType "file" }} + + + + {{- else if eq .FieldType "json" }} + + + + {{- else if eq .FieldType "array" }} + + + + {{- else }} + + {{- end }} + {{- end }} + {{- end }} + +// 新增表单中增加如下代码 +{{- range .Fields}} + {{- if .Form}} + + {{- if .CheckDataSource}} + + + + {{- else }} + {{- if eq .FieldType "bool" }} + + {{- end }} + {{- if eq .FieldType "string" }} + {{- if .DictType}} + + + + {{- else }} + + {{- end }} + {{- end }} + {{- if eq .FieldType "richtext" }} + + {{- end }} + {{- if eq .FieldType "json" }} + // 此字段为json结构,可以前端自行控制展示和数据绑定模式 需绑定json的key为 formData.{{.FieldJson}} 后端会按照json的类型进行存取 + {{"{{"}} formData.{{.FieldJson}} {{"}}"}} + {{- end }} + {{- if eq .FieldType "array" }} + + {{- end }} + {{- if eq .FieldType "int" }} + + {{- end }} + {{- if eq .FieldType "time.Time" }} + + {{- end }} + {{- if eq .FieldType "float64" }} + + {{- end }} + {{- if eq .FieldType "enum" }} + + + + {{- end }} + {{- if eq .FieldType "picture" }} + + {{- end }} + {{- if eq .FieldType "pictures" }} + + {{- end }} + {{- if eq .FieldType "video" }} + + {{- end }} + {{- if eq .FieldType "file" }} + + {{- end }} + {{- end }} + + {{- end }} + {{- end }} + +// 查看抽屉中增加如下代码 + +{{- range .Fields}} + {{- if .Desc }} + + {{- if and (ne .FieldType "picture" ) (ne .FieldType "pictures" ) (ne .FieldType "file" ) (ne .FieldType "array" ) }} + {{"{{"}} detailFrom.{{.FieldJson}} {{"}}"}} + {{- else }} + {{- if eq .FieldType "picture" }} + + {{- end }} + {{- if eq .FieldType "array" }} + + {{- end }} + {{- if eq .FieldType "pictures" }} + + {{- end }} + {{- if eq .FieldType "file" }} +
+ + + {{"{{"}}item.name{{"}}"}} + +
+ {{- end }} + {{- end }} +
+ {{- end }} + {{- end }} + +// 字典增加如下代码 + {{- range $index, $element := .DictTypes}} +const {{ $element }}Options = ref([]) + {{- end }} + +// setOptions方法中增加如下调用 + +{{- range $index, $element := .DictTypes }} + {{ $element }}Options.value = await getDictFunc('{{$element}}') +{{- end }} + +// 基础formData结构(变量处和关闭表单处)增加如下字段 +{{- range .Fields}} + {{- if .Form}} + {{- if eq .FieldType "bool" }} +{{.FieldJson}}: false, + {{- end }} + {{- if eq .FieldType "string" }} +{{.FieldJson}}: '', + {{- end }} + {{- if eq .FieldType "richtext" }} +{{.FieldJson}}: '', + {{- end }} + {{- if eq .FieldType "int" }} +{{.FieldJson}}: {{- if or .DictType .DataSource}} undefined{{ else }} 0{{- end }}, + {{- end }} + {{- if eq .FieldType "time.Time" }} +{{.FieldJson}}: new Date(), + {{- end }} + {{- if eq .FieldType "float64" }} +{{.FieldJson}}: 0, + {{- end }} + {{- if eq .FieldType "picture" }} +{{.FieldJson}}: "", + {{- end }} + {{- if eq .FieldType "video" }} +{{.FieldJson}}: "", + {{- end }} + {{- if eq .FieldType "pictures" }} +{{.FieldJson}}: [], + {{- end }} + {{- if eq .FieldType "file" }} +{{.FieldJson}}: [], + {{- end }} + {{- if eq .FieldType "json" }} +{{.FieldJson}}: {}, + {{- end }} + {{- if eq .FieldType "array" }} +{{.FieldJson}}: [], + {{- end }} + {{- end }} + {{- end }} +// 验证规则中增加如下字段 + +{{- range .Fields }} + {{- if .Form }} + {{- if eq .Require true }} +{{.FieldJson }} : [{ + required: true, + message: '{{ .ErrorText }}', + trigger: ['input','blur'], +}, + {{- if eq .FieldType "string" }} +{ + whitespace: true, + message: '不能只输入空格', + trigger: ['input', 'blur'], +} + {{- end }} +], + {{- end }} + {{- end }} + {{- end }} + + + +{{- if .HasDataSource }} +// 请引用 +get{{.StructName}}DataSource, + +// 获取数据源 +const dataSource = ref([]) +const getDataSourceFunc = async()=>{ + const res = await get{{.StructName}}DataSource() + if (res.code === 0) { + dataSource.value = res.data + } +} +getDataSourceFunc() +{{- end }} + +{{- else }} + +{{- if not .OnlyTemplate}} + + + + + +{{- else}} + + + +{{- end }} + +{{- end }} \ No newline at end of file diff --git a/admin/server/resource/plugin/server/api/api.go.template b/admin/server/resource/plugin/server/api/api.go.template new file mode 100644 index 000000000..edf465180 --- /dev/null +++ b/admin/server/resource/plugin/server/api/api.go.template @@ -0,0 +1,207 @@ +package api + +import ( +{{if not .OnlyTemplate}} + "{{.Module}}/global" + "{{.Module}}/model/common/response" + "{{.Module}}/plugin/{{.Package}}/model" + "{{.Module}}/plugin/{{.Package}}/model/request" + "github.com/gin-gonic/gin" + "go.uber.org/zap" + {{- if .AutoCreateResource}} + "{{.Module}}/utils" + {{- end }} +{{- else }} + "{{.Module}}/model/common/response" + "github.com/gin-gonic/gin" +{{- end }} +) + +var {{.StructName}} = new({{.Abbreviation}}) + +type {{.Abbreviation}} struct {} +{{if not .OnlyTemplate}} +// Create{{.StructName}} 创建{{.Description}} +// @Tags {{.StructName}} +// @Summary 创建{{.Description}} +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.{{.StructName}} true "创建{{.Description}}" +// @Success 200 {object} response.Response{msg=string} "创建成功" +// @Router /{{.Abbreviation}}/create{{.StructName}} [post] +func (a *{{.Abbreviation}}) Create{{.StructName}}(c *gin.Context) { + var info model.{{.StructName}} + err := c.ShouldBindJSON(&info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + {{- if .AutoCreateResource }} + info.CreatedBy = utils.GetUserID(c) + {{- end }} + err = service{{ .StructName }}.Create{{.StructName}}(&info) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败:" + err.Error(), c) + return + } + response.OkWithMessage("创建成功", c) +} + +// Delete{{.StructName}} 删除{{.Description}} +// @Tags {{.StructName}} +// @Summary 删除{{.Description}} +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.{{.StructName}} true "删除{{.Description}}" +// @Success 200 {object} response.Response{msg=string} "删除成功" +// @Router /{{.Abbreviation}}/delete{{.StructName}} [delete] +func (a *{{.Abbreviation}}) Delete{{.StructName}}(c *gin.Context) { + {{.PrimaryField.FieldJson}} := c.Query("{{.PrimaryField.FieldJson}}") +{{- if .AutoCreateResource }} + userID := utils.GetUserID(c) +{{- end }} + err := service{{ .StructName }}.Delete{{.StructName}}({{.PrimaryField.FieldJson}} {{- if .AutoCreateResource -}},userID{{- end -}}) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败:" + err.Error(), c) + return + } + response.OkWithMessage("删除成功", c) +} + +// Delete{{.StructName}}ByIds 批量删除{{.Description}} +// @Tags {{.StructName}} +// @Summary 批量删除{{.Description}} +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "批量删除成功" +// @Router /{{.Abbreviation}}/delete{{.StructName}}ByIds [delete] +func (a *{{.Abbreviation}}) Delete{{.StructName}}ByIds(c *gin.Context) { + {{.PrimaryField.FieldJson}}s := c.QueryArray("{{.PrimaryField.FieldJson}}s[]") +{{- if .AutoCreateResource }} + userID := utils.GetUserID(c) +{{- end }} + err := service{{ .StructName }}.Delete{{.StructName}}ByIds({{.PrimaryField.FieldJson}}s{{- if .AutoCreateResource }},userID{{- end }}) + if err != nil { + global.GVA_LOG.Error("批量删除失败!", zap.Error(err)) + response.FailWithMessage("批量删除失败:" + err.Error(), c) + return + } + response.OkWithMessage("批量删除成功", c) +} + +// Update{{.StructName}} 更新{{.Description}} +// @Tags {{.StructName}} +// @Summary 更新{{.Description}} +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.{{.StructName}} true "更新{{.Description}}" +// @Success 200 {object} response.Response{msg=string} "更新成功" +// @Router /{{.Abbreviation}}/update{{.StructName}} [put] +func (a *{{.Abbreviation}}) Update{{.StructName}}(c *gin.Context) { + var info model.{{.StructName}} + err := c.ShouldBindJSON(&info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } +{{- if .AutoCreateResource }} + info.UpdatedBy = utils.GetUserID(c) +{{- end }} + err = service{{ .StructName }}.Update{{.StructName}}(info) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败:" + err.Error(), c) + return + } + response.OkWithMessage("更新成功", c) +} + +// Find{{.StructName}} 用id查询{{.Description}} +// @Tags {{.StructName}} +// @Summary 用id查询{{.Description}} +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query model.{{.StructName}} true "用id查询{{.Description}}" +// @Success 200 {object} response.Response{data=model.{{.StructName}},msg=string} "查询成功" +// @Router /{{.Abbreviation}}/find{{.StructName}} [get] +func (a *{{.Abbreviation}}) Find{{.StructName}}(c *gin.Context) { + {{.PrimaryField.FieldJson}} := c.Query("{{.PrimaryField.FieldJson}}") + re{{.Abbreviation}}, err := service{{ .StructName }}.Get{{.StructName}}({{.PrimaryField.FieldJson}}) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败:" + err.Error(), c) + return + } + response.OkWithData(re{{.Abbreviation}}, c) +} + +// Get{{.StructName}}List 分页获取{{.Description}}列表 +// @Tags {{.StructName}} +// @Summary 分页获取{{.Description}}列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query request.{{.StructName}}Search true "分页获取{{.Description}}列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /{{.Abbreviation}}/get{{.StructName}}List [get] +func (a *{{.Abbreviation}}) Get{{.StructName}}List(c *gin.Context) { + var pageInfo request.{{.StructName}}Search + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := service{{ .StructName }}.Get{{.StructName}}InfoList(pageInfo) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败:" + err.Error(), c) + return + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.PageSize, + }, "获取成功", c) +} + +{{- if .HasDataSource }} +// Get{{.StructName}}DataSource 获取{{.StructName}}的数据源 +// @Tags {{.StructName}} +// @Summary 获取{{.StructName}}的数据源 +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=object,msg=string} "查询成功" +// @Router /{{.Abbreviation}}/get{{.StructName}}DataSource [get] +func (a *{{.Abbreviation}}) Get{{.StructName}}DataSource(c *gin.Context) { + // 此接口为获取数据源定义的数据 + dataSource, err := service{{ .StructName }}.Get{{.StructName}}DataSource() + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败:" + err.Error(), c) + return + } + response.OkWithData(dataSource, c) +} +{{- end }} +{{- end }} +// Get{{.StructName}}Public 不需要鉴权的{{.Description}}接口 +// @Tags {{.StructName}} +// @Summary 不需要鉴权的{{.Description}}接口 +// @accept application/json +// @Produce application/json +// @Param data query request.{{.StructName}}Search true "分页获取{{.Description}}列表" +// @Success 200 {object} response.Response{data=object,msg=string} "获取成功" +// @Router /{{.Abbreviation}}/get{{.StructName}}Public [get] +func (a *{{.Abbreviation}}) Get{{.StructName}}Public(c *gin.Context) { + // 此接口不需要鉴权 示例为返回了一个固定的消息接口,一般本接口用于C端服务,需要自己实现业务逻辑 + service{{ .StructName }}.Get{{.StructName}}Public() + response.OkWithDetailed(gin.H{"info": "不需要鉴权的{{.Description}}接口信息"}, "获取成功", c) +} diff --git a/admin/server/resource/plugin/server/api/enter.go.template b/admin/server/resource/plugin/server/api/enter.go.template new file mode 100644 index 000000000..989fb3507 --- /dev/null +++ b/admin/server/resource/plugin/server/api/enter.go.template @@ -0,0 +1,6 @@ +package api + +var Api = new(api) + +type api struct { +} diff --git a/admin/server/resource/plugin/server/config/config.go.template b/admin/server/resource/plugin/server/config/config.go.template new file mode 100644 index 000000000..809bc990f --- /dev/null +++ b/admin/server/resource/plugin/server/config/config.go.template @@ -0,0 +1,4 @@ +package config + +type Config struct { +} diff --git a/admin/server/resource/plugin/server/gen/gen.go.template b/admin/server/resource/plugin/server/gen/gen.go.template new file mode 100644 index 000000000..5639d4ab3 --- /dev/null +++ b/admin/server/resource/plugin/server/gen/gen.go.template @@ -0,0 +1,18 @@ +package main + +import ( + "gorm.io/gen" + "path/filepath" +) + +//go:generate go mod tidy +//go:generate go mod download +//go:generate go run gen.go +func main() { + g := gen.NewGenerator(gen.Config{ + OutPath: filepath.Join("..", "..", "..", "{{ .Package }}", "blender", "model", "dao"), + Mode: gen.WithoutContext | gen.WithDefaultQuery | gen.WithQueryInterface, + }) + g.ApplyBasic() + g.Execute() +} diff --git a/admin/server/resource/plugin/server/initialize/api.go.template b/admin/server/resource/plugin/server/initialize/api.go.template new file mode 100644 index 000000000..dfbea23d9 --- /dev/null +++ b/admin/server/resource/plugin/server/initialize/api.go.template @@ -0,0 +1,12 @@ +package initialize + +import ( + "context" + model "{{.Module}}/model/system" + "{{.Module}}/plugin/plugin-tool/utils" +) + +func Api(ctx context.Context) { + entities := []model.SysApi{} + utils.RegisterApis(entities...) +} diff --git a/admin/server/resource/plugin/server/initialize/gorm.go.template b/admin/server/resource/plugin/server/initialize/gorm.go.template new file mode 100644 index 000000000..52c818319 --- /dev/null +++ b/admin/server/resource/plugin/server/initialize/gorm.go.template @@ -0,0 +1,17 @@ +package initialize + +import ( + "context" + "fmt" + "{{.Module}}/global" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +func Gorm(ctx context.Context) { + err := global.GVA_DB.WithContext(ctx).AutoMigrate() + if err != nil { + err = errors.Wrap(err, "注册表失败!") + zap.L().Error(fmt.Sprintf("%+v", err)) + } +} diff --git a/admin/server/resource/plugin/server/initialize/menu.go.template b/admin/server/resource/plugin/server/initialize/menu.go.template new file mode 100644 index 000000000..8774f356c --- /dev/null +++ b/admin/server/resource/plugin/server/initialize/menu.go.template @@ -0,0 +1,12 @@ +package initialize + +import ( + "context" + model "{{.Module}}/model/system" + "{{.Module}}/plugin/plugin-tool/utils" +) + +func Menu(ctx context.Context) { + entities := []model.SysBaseMenu{} + utils.RegisterMenus(entities...) +} diff --git a/admin/server/resource/plugin/server/initialize/router.go.template b/admin/server/resource/plugin/server/initialize/router.go.template new file mode 100644 index 000000000..fbf03a3aa --- /dev/null +++ b/admin/server/resource/plugin/server/initialize/router.go.template @@ -0,0 +1,14 @@ +package initialize + +import ( + "{{.Module}}/global" + "{{.Module}}/middleware" + "github.com/gin-gonic/gin" +) + +func Router(engine *gin.Engine) { + public := engine.Group(global.GVA_CONFIG.System.RouterPrefix).Group("") + public.Use() + private := engine.Group(global.GVA_CONFIG.System.RouterPrefix).Group("") + private.Use(middleware.JWTAuth()).Use(middleware.CasbinHandler()) +} diff --git a/admin/server/resource/plugin/server/initialize/viper.go.template b/admin/server/resource/plugin/server/initialize/viper.go.template new file mode 100644 index 000000000..e759ad637 --- /dev/null +++ b/admin/server/resource/plugin/server/initialize/viper.go.template @@ -0,0 +1,17 @@ +package initialize + +import ( + "fmt" + "{{.Module}}/global" + "{{.Module}}/plugin/{{ .Package }}/plugin" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +func Viper() { + err := global.GVA_VP.UnmarshalKey("{{ .Package }}", &plugin.Config) + if err != nil { + err = errors.Wrap(err, "初始化配置文件失败!") + zap.L().Error(fmt.Sprintf("%+v", err)) + } +} diff --git a/admin/server/resource/plugin/server/model/model.go.template b/admin/server/resource/plugin/server/model/model.go.template new file mode 100644 index 000000000..0f9528db3 --- /dev/null +++ b/admin/server/resource/plugin/server/model/model.go.template @@ -0,0 +1,83 @@ +{{- if .IsAdd}} +// 在结构体中新增如下字段 +{{- range .Fields}} +{{- if eq .FieldType "enum" }} +{{.FieldName}} string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};type:enum({{.DataTypeLong}});comment:{{.Comment}};" {{- if .Require }} binding:"required"{{- end -}}` +{{- else if eq .FieldType "picture" }} +{{.FieldName}} string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end -}}` +{{- else if eq .FieldType "video" }} +{{.FieldName}} string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end -}}` +{{- else if eq .FieldType "file" }} +{{.FieldName}} datatypes.JSON `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end -}} swaggertype:"array,object"` +{{- else if eq .FieldType "pictures" }} +{{.FieldName}} datatypes.JSON `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end -}} swaggertype:"array,object"` +{{- else if eq .FieldType "richtext" }} +{{.FieldName}} *string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}type:text;" {{- if .Require }} binding:"required"{{- end -}}` +{{- else if eq .FieldType "json" }} +{{.FieldName}} datatypes.JSON `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}type:text;" {{- if .Require }} binding:"required"{{- end -}} swaggertype:"object"` +{{- else if eq .FieldType "array" }} +{{.FieldName}} datatypes.JSON `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}type:text;" {{- if .Require }} binding:"required"{{- end -}} swaggertype:"array,object"` +{{- else }} +{{.FieldName}} *{{.FieldType}} `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end -}}` +{{- end }} {{ if .FieldDesc }}//{{.FieldDesc}} {{ end }} +{{- end }} + +{{ else }} +package model + +{{- if not .OnlyTemplate}} +import ( + {{- if .GvaModel }} + "{{.Module}}/global" + {{- end }} + {{- if or .HasTimer }} + "time" + {{- end }} + {{- if .NeedJSON }} + "gorm.io/datatypes" + {{- end }} +) +{{- end }} + +// {{.StructName}} {{.Description}} 结构体 +type {{.StructName}} struct { +{{- if not .OnlyTemplate}} +{{- if .GvaModel }} + global.GVA_MODEL +{{- end }} +{{- range .Fields}} + {{- if eq .FieldType "enum" }} + {{.FieldName}} string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};type:enum({{.DataTypeLong}});comment:{{.Comment}};" {{- if .Require }} binding:"required"{{- end -}}` + {{- else if eq .FieldType "picture" }} + {{.FieldName}} string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end -}}` + {{- else if eq .FieldType "video" }} + {{.FieldName}} string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end -}}` + {{- else if eq .FieldType "file" }} + {{.FieldName}} datatypes.JSON `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end -}} swaggertype:"array,object"` + {{- else if eq .FieldType "pictures" }} + {{.FieldName}} datatypes.JSON `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end -}} swaggertype:"array,object"` + {{- else if eq .FieldType "richtext" }} + {{.FieldName}} *string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}type:text;" {{- if .Require }} binding:"required"{{- end -}}` + {{- else if eq .FieldType "json" }} + {{.FieldName}} datatypes.JSON `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}type:text;" {{- if .Require }} binding:"required"{{- end -}} swaggertype:"object"` + {{- else if eq .FieldType "array" }} + {{.FieldName}} datatypes.JSON `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}type:text;" {{- if .Require }} binding:"required"{{- end -}} swaggertype:"array,object"` + {{- else }} + {{.FieldName}} *{{.FieldType}} `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end -}}` + {{- end }} {{ if .FieldDesc }}//{{.FieldDesc}}{{ end }} +{{- end }} + {{- if .AutoCreateResource }} + CreatedBy uint `gorm:"column:created_by;comment:创建者"` + UpdatedBy uint `gorm:"column:updated_by;comment:更新者"` + DeletedBy uint `gorm:"column:deleted_by;comment:删除者"` + {{- end }} + {{- end }} +} + +{{ if .TableName }} +// TableName {{.Description}} {{.StructName}}自定义表名 {{.TableName}} +func ({{.StructName}}) TableName() string { + return "{{.TableName}}" +} +{{ end }} +{{ end }} \ No newline at end of file diff --git a/admin/server/resource/plugin/server/model/request/request.go.template b/admin/server/resource/plugin/server/model/request/request.go.template new file mode 100644 index 000000000..2100a6415 --- /dev/null +++ b/admin/server/resource/plugin/server/model/request/request.go.template @@ -0,0 +1,57 @@ +{{- if .IsAdd}} +// 在结构体中新增如下字段 +{{- range .Fields}} + {{- if ne .FieldSearchType ""}} + {{- if eq .FieldSearchType "BETWEEN" "NOT BETWEEN"}} +Start{{.FieldName}} *{{.FieldType}} `json:"start{{.FieldName}}" form:"start{{.FieldName}}"` +End{{.FieldName}} *{{.FieldType}} `json:"end{{.FieldName}}" form:"end{{.FieldName}}"` + {{- else }} + {{- if or (eq .FieldType "enum") (eq .FieldType "picture") (eq .FieldType "pictures") (eq .FieldType "video") (eq .FieldType "json") }} +{{.FieldName}} string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" ` + {{- else }} +{{.FieldName}} *{{.FieldType}} `json:"{{.FieldJson}}" form:"{{.FieldJson}}" ` + {{- end }} + {{- end }} + {{- end}} +{{- end }} +{{- if .NeedSort}} +Sort string `json:"sort" form:"sort"` +Order string `json:"order" form:"order"` +{{- end}} +{{- else }} +package request +{{- if not .OnlyTemplate}} +import ( + "{{.Module}}/model/common/request" + {{ if or .HasSearchTimer .GvaModel}}"time"{{ end }} +) +{{- end}} +type {{.StructName}}Search struct{ +{{- if not .OnlyTemplate}} + +{{- if .GvaModel }} + StartCreatedAt *time.Time `json:"startCreatedAt" form:"startCreatedAt"` + EndCreatedAt *time.Time `json:"endCreatedAt" form:"endCreatedAt"` +{{- end }} +{{- range .Fields}} + {{- if ne .FieldSearchType ""}} + {{- if eq .FieldSearchType "BETWEEN" "NOT BETWEEN"}} + Start{{.FieldName}} *{{.FieldType}} `json:"start{{.FieldName}}" form:"start{{.FieldName}}"` + End{{.FieldName}} *{{.FieldType}} `json:"end{{.FieldName}}" form:"end{{.FieldName}}"` + {{- else }} + {{- if or (eq .FieldType "enum") (eq .FieldType "picture") (eq .FieldType "pictures") (eq .FieldType "video") (eq .FieldType "json") }} + {{.FieldName}} string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" ` + {{- else }} + {{.FieldName}} *{{.FieldType}} `json:"{{.FieldJson}}" form:"{{.FieldJson}}" ` + {{- end }} + {{- end }} + {{- end}} +{{- end }} + request.PageInfo + {{- if .NeedSort}} + Sort string `json:"sort" form:"sort"` + Order string `json:"order" form:"order"` + {{- end}} +{{- end }} +} +{{- end }} \ No newline at end of file diff --git a/admin/server/resource/plugin/server/plugin.go.template b/admin/server/resource/plugin/server/plugin.go.template new file mode 100644 index 000000000..255b7af00 --- /dev/null +++ b/admin/server/resource/plugin/server/plugin.go.template @@ -0,0 +1,26 @@ +package {{ .Package }} + +import ( + "context" + "{{.Module}}/plugin/{{ .Package }}/initialize" + interfaces "{{.Module}}/utils/plugin/v2" + "github.com/gin-gonic/gin" +) + +var _ interfaces.Plugin = (*plugin)(nil) + +var Plugin = new(plugin) + +type plugin struct{} + +// 如果需要配置文件,请到config.Config中填充配置结构,且到下方发放中填入其在config.yaml中的key并添加如下方法 +// initialize.Viper() +// 安装插件时候自动注册的api数据请到下方法.Api方法中实现并添加如下方法 +// initialize.Api(ctx) +// 安装插件时候自动注册的api数据请到下方法.Menu方法中实现并添加如下方法 +// initialize.Menu(ctx) +func (p *plugin) Register(group *gin.Engine) { + ctx := context.Background() + initialize.Gorm(ctx) + initialize.Router(group) +} diff --git a/admin/server/resource/plugin/server/plugin/plugin.go.template b/admin/server/resource/plugin/server/plugin/plugin.go.template new file mode 100644 index 000000000..7e25e0700 --- /dev/null +++ b/admin/server/resource/plugin/server/plugin/plugin.go.template @@ -0,0 +1,5 @@ +package plugin + +import "{{.Module}}/plugin/{{ .Package }}/config" + +var Config config.Config diff --git a/admin/server/resource/plugin/server/router/enter.go.template b/admin/server/resource/plugin/server/router/enter.go.template new file mode 100644 index 000000000..78517b371 --- /dev/null +++ b/admin/server/resource/plugin/server/router/enter.go.template @@ -0,0 +1,6 @@ +package router + +var Router = new(router) + +type router struct { +} diff --git a/admin/server/resource/plugin/server/router/router.go.template b/admin/server/resource/plugin/server/router/router.go.template new file mode 100644 index 000000000..34bf4d891 --- /dev/null +++ b/admin/server/resource/plugin/server/router/router.go.template @@ -0,0 +1,46 @@ +package router + +import ( + {{if .OnlyTemplate }} // {{end}}"{{.Module}}/middleware" + "github.com/gin-gonic/gin" +) + +var {{.StructName}} = new({{.Abbreviation}}) + +type {{.Abbreviation}} struct {} + +// Init 初始化 {{.Description}} 路由信息 +func (r *{{.Abbreviation}}) Init(public *gin.RouterGroup, private *gin.RouterGroup) { +{{- if not .OnlyTemplate }} + { + group := private.Group("{{.Abbreviation}}").Use(middleware.OperationRecord()) + group.POST("create{{.StructName}}", api{{.StructName}}.Create{{.StructName}}) // 新建{{.Description}} + group.DELETE("delete{{.StructName}}", api{{.StructName}}.Delete{{.StructName}}) // 删除{{.Description}} + group.DELETE("delete{{.StructName}}ByIds", api{{.StructName}}.Delete{{.StructName}}ByIds) // 批量删除{{.Description}} + group.PUT("update{{.StructName}}", api{{.StructName}}.Update{{.StructName}}) // 更新{{.Description}} + } + { + group := private.Group("{{.Abbreviation}}") + group.GET("find{{.StructName}}", api{{.StructName}}.Find{{.StructName}}) // 根据ID获取{{.Description}} + group.GET("get{{.StructName}}List", api{{.StructName}}.Get{{.StructName}}List) // 获取{{.Description}}列表 + } + { + group := public.Group("{{.Abbreviation}}") + {{- if .HasDataSource}} + group.GET("get{{.StructName}}DataSource", api{{.StructName}}.Get{{.StructName}}DataSource) // 获取{{.Description}}数据源 + {{- end}} + group.GET("get{{.StructName}}Public", api{{.StructName}}.Get{{.StructName}}Public) // {{.Description}}开放接口 + } +{{- else}} + // { + // group := private.Group("{{.Abbreviation}}").Use(middleware.OperationRecord()) + // } + // { + // group := private.Group("{{.Abbreviation}}") + // } + { + group := public.Group("{{.Abbreviation}}") + group.GET("get{{.StructName}}Public", api{{.StructName}}.Get{{.StructName}}Public) // {{.Description}}开放接口 + } +{{- end}} +} diff --git a/admin/server/resource/plugin/server/service/enter.go.template b/admin/server/resource/plugin/server/service/enter.go.template new file mode 100644 index 000000000..034facbaf --- /dev/null +++ b/admin/server/resource/plugin/server/service/enter.go.template @@ -0,0 +1,7 @@ +package service + +var Service = new(service) + +type service struct { +} + diff --git a/admin/server/resource/plugin/server/service/service.go.template b/admin/server/resource/plugin/server/service/service.go.template new file mode 100644 index 000000000..4b1c814ea --- /dev/null +++ b/admin/server/resource/plugin/server/service/service.go.template @@ -0,0 +1,225 @@ +{{- $db := "" }} +{{- if eq .BusinessDB "" }} + {{- $db = "global.GVA_DB" }} +{{- else}} + {{- $db = printf "global.MustGetGlobalDBByDBName(\"%s\")" .BusinessDB }} +{{- end}} + +{{- if .IsAdd}} + +// Get{{.StructName}}InfoList 新增搜索语句 + {{- range .Fields}} + {{- if .FieldSearchType}} + {{- if or (eq .FieldType "enum") (eq .FieldType "pictures") (eq .FieldType "picture") (eq .FieldType "video") (eq .FieldType "json") }} +if info.{{.FieldName}} != "" { + {{- if or (eq .FieldType "enum") }} + db = db.Where("{{.ColumnName}} {{.FieldSearchType}} ?",{{if eq .FieldSearchType "LIKE"}}"%"+ {{ end }}*info.{{.FieldName}}{{if eq .FieldSearchType "LIKE"}}+"%"{{ end }}) + {{- else}} +// 数据类型为复杂类型,请根据业务需求自行实现复杂类型的查询业务 + {{- end}} +} + {{- else if eq .FieldSearchType "BETWEEN" "NOT BETWEEN"}} +if info.Start{{.FieldName}} != nil && info.End{{.FieldName}} != nil { + db = db.Where("{{.ColumnName}} {{.FieldSearchType}} ? AND ? ",info.Start{{.FieldName}},info.End{{.FieldName}}) +} + {{- else}} +if info.{{.FieldName}} != nil{{- if eq .FieldType "string" }} && *info.{{.FieldName}} != ""{{- end }} { + db = db.Where("{{.ColumnName}} {{.FieldSearchType}} ?",{{if eq .FieldSearchType "LIKE"}}"%"+{{ end }}*info.{{.FieldName}}{{if eq .FieldSearchType "LIKE"}}+"%"{{ end }}) +} + {{- end }} + {{- end }} + {{- end }} + + +// Get{{.StructName}}InfoList 新增排序语句 请自行在搜索语句中添加orderMap内容 + {{- range .Fields}} + {{- if .Sort}} +orderMap["{{.ColumnName}}"] = true + {{- end}} + {{- end}} + + +{{- if .HasDataSource }} +// Get{{.StructName}}DataSource()方法新增关联语句 + {{range $key, $value := .DataSourceMap}} +{{$key}} := make([]map[string]any, 0) +{{ $dataDB := "" }} +{{- if eq $value.DBName "" }} +{{ $dataDB = $db }} +{{- else}} +{{ $dataDB = printf "global.MustGetGlobalDBByDBName(\"%s\")" $value.DBName }} +{{- end}} +{{$dataDB}}.Table("{{$value.Table}}"){{- if $value.HasDeletedAt}}.Where("deleted_at IS NULL"){{ end }}.Select("{{$value.Label}} as label,{{$value.Value}} as value").Scan(&{{$key}}) +res["{{$key}}"] = {{$key}} + {{- end }} +{{- end }} +{{- else}} +package service + +import ( +{{- if not .OnlyTemplate }} + "{{.Module}}/global" + "{{.Module}}/plugin/{{.Package}}/model" + "{{.Module}}/plugin/{{.Package}}/model/request" + {{- if .AutoCreateResource }} + "gorm.io/gorm" + {{- end}} +{{- end }} +) + +var {{.StructName}} = new({{.Abbreviation}}) + +type {{.Abbreviation}} struct {} + +{{- $db := "" }} +{{- if eq .BusinessDB "" }} + {{- $db = "global.GVA_DB" }} +{{- else}} + {{- $db = printf "global.MustGetGlobalDBByDBName(\"%s\")" .BusinessDB }} +{{- end}} +{{- if not .OnlyTemplate }} +// Create{{.StructName}} 创建{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func (s *{{.Abbreviation}}) Create{{.StructName}}({{.Abbreviation}} *model.{{.StructName}}) (err error) { + err = {{$db}}.Create({{.Abbreviation}}).Error + return err +} + +// Delete{{.StructName}} 删除{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func (s *{{.Abbreviation}}) Delete{{.StructName}}({{.PrimaryField.FieldJson}} string{{- if .AutoCreateResource -}},userID uint{{- end -}}) (err error) { + {{- if .AutoCreateResource }} + err = {{$db}}.Transaction(func(tx *gorm.DB) error { + if err := tx.Model(&model.{{.StructName}}{}).Where("{{.PrimaryField.ColumnName}} = ?", {{.PrimaryField.FieldJson}}).Update("deleted_by", userID).Error; err != nil { + return err + } + if err = tx.Delete(&model.{{.StructName}}{},"{{.PrimaryField.ColumnName}} = ?",{{.PrimaryField.FieldJson}}).Error; err != nil { + return err + } + return nil + }) + {{- else }} + err = {{$db}}.Delete(&model.{{.StructName}}{},"{{.PrimaryField.ColumnName}} = ?",{{.PrimaryField.FieldJson}}).Error + {{- end }} + return err +} + +// Delete{{.StructName}}ByIds 批量删除{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func (s *{{.Abbreviation}}) Delete{{.StructName}}ByIds({{.PrimaryField.FieldJson}}s []string {{- if .AutoCreateResource }},deleted_by uint{{- end}}) (err error) { + {{- if .AutoCreateResource }} + err = {{$db}}.Transaction(func(tx *gorm.DB) error { + if err := tx.Model(&model.{{.StructName}}{}).Where("{{.PrimaryField.ColumnName}} in ?", {{.PrimaryField.FieldJson}}s).Update("deleted_by", deleted_by).Error; err != nil { + return err + } + if err := tx.Where("{{.PrimaryField.ColumnName}} in ?", {{.PrimaryField.FieldJson}}s).Delete(&model.{{.StructName}}{}).Error; err != nil { + return err + } + return nil + }) + {{- else}} + err = {{$db}}.Delete(&[]model.{{.StructName}}{},"{{.PrimaryField.ColumnName}} in ?",{{.PrimaryField.FieldJson}}s).Error + {{- end}} + return err +} + +// Update{{.StructName}} 更新{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func (s *{{.Abbreviation}}) Update{{.StructName}}({{.Abbreviation}} model.{{.StructName}}) (err error) { + err = {{$db}}.Model(&model.{{.StructName}}{}).Where("{{.PrimaryField.ColumnName}} = ?",{{.Abbreviation}}.{{.PrimaryField.FieldName}}).Updates(&{{.Abbreviation}}).Error + return err +} + +// Get{{.StructName}} 根据{{.PrimaryField.FieldJson}}获取{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func (s *{{.Abbreviation}}) Get{{.StructName}}({{.PrimaryField.FieldJson}} string) ({{.Abbreviation}} model.{{.StructName}}, err error) { + err = {{$db}}.Where("{{.PrimaryField.ColumnName}} = ?", {{.PrimaryField.FieldJson}}).First(&{{.Abbreviation}}).Error + return +} + +// Get{{.StructName}}InfoList 分页获取{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func (s *{{.Abbreviation}}) Get{{.StructName}}InfoList(info request.{{.StructName}}Search) (list []model.{{.StructName}}, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := {{$db}}.Model(&model.{{.StructName}}{}) + var {{.Abbreviation}}s []model.{{.StructName}} + // 如果有条件搜索 下方会自动创建搜索语句 +{{- if .GvaModel }} + if info.StartCreatedAt !=nil && info.EndCreatedAt !=nil { + db = db.Where("created_at BETWEEN ? AND ?", info.StartCreatedAt, info.EndCreatedAt) + } +{{- end }} + {{- range .Fields}} + {{- if .FieldSearchType}} + {{- if or (eq .FieldType "enum") (eq .FieldType "pictures") (eq .FieldType "picture") (eq .FieldType "video") (eq .FieldType "json") }} + if info.{{.FieldName}} != "" { + {{- if or (eq .FieldType "enum")}} + db = db.Where("{{.ColumnName}} {{.FieldSearchType}} ?",{{if eq .FieldSearchType "LIKE"}}"%"+ {{ end }}*info.{{.FieldName}}{{if eq .FieldSearchType "LIKE"}}+"%"{{ end }}) + {{- else}} + // 数据类型为复杂类型,请根据业务需求自行实现复杂类型的查询业务 + {{- end}} + } + {{- else if eq .FieldSearchType "BETWEEN" "NOT BETWEEN"}} + if info.Start{{.FieldName}} != nil && info.End{{.FieldName}} != nil { + db = db.Where("{{.ColumnName}} {{.FieldSearchType}} ? AND ? ",info.Start{{.FieldName}},info.End{{.FieldName}}) + } + {{- else}} + if info.{{.FieldName}} != nil{{- if eq .FieldType "string" }} && *info.{{.FieldName}} != ""{{- end }} { + db = db.Where("{{.ColumnName}} {{.FieldSearchType}} ?",{{if eq .FieldSearchType "LIKE"}}"%"+{{ end }}*info.{{.FieldName}}{{if eq .FieldSearchType "LIKE"}}+"%"{{ end }}) + } + {{- end }} + {{- end }} + {{- end }} + err = db.Count(&total).Error + if err!=nil { + return + } + {{- if .NeedSort}} + var OrderStr string + orderMap := make(map[string]bool) + {{- range .Fields}} + {{- if .Sort}} + orderMap["{{.ColumnName}}"] = true + {{- end}} + {{- end}} + if orderMap[info.Sort] { + OrderStr = info.Sort + if info.Order == "descending" { + OrderStr = OrderStr + " desc" + } + db = db.Order(OrderStr) + } + {{- end}} + + if limit != 0 { + db = db.Limit(limit).Offset(offset) + } + err = db.Find(&{{.Abbreviation}}s).Error + return {{.Abbreviation}}s, total, err +} + +{{- if .HasDataSource }} +func (s *{{.Abbreviation}})Get{{.StructName}}DataSource() (res map[string][]map[string]any, err error) { + res = make(map[string][]map[string]any) + {{range $key, $value := .DataSourceMap}} + {{$key}} := make([]map[string]any, 0) + {{ $dataDB := "" }} + {{- if eq $value.DBName "" }} + {{ $dataDB = $db }} + {{- else}} + {{ $dataDB = printf "global.MustGetGlobalDBByDBName(\"%s\")" $value.DBName }} + {{- end}} + {{$dataDB}}.Table("{{$value.Table}}"){{- if $value.HasDeletedAt}}.Where("deleted_at IS NULL"){{ end }}.Select("{{$value.Label}} as label,{{$value.Value}} as value").Scan(&{{$key}}) + res["{{$key}}"] = {{$key}} + {{- end }} + return +} +{{- end }} +{{- end }} + +func (s *{{.Abbreviation}})Get{{.StructName}}Public() { + +} +{{- end }} \ No newline at end of file diff --git a/admin/server/resource/plugin/web/api/api.js.template b/admin/server/resource/plugin/web/api/api.js.template new file mode 100644 index 000000000..208f386e4 --- /dev/null +++ b/admin/server/resource/plugin/web/api/api.js.template @@ -0,0 +1,127 @@ +import service from '@/utils/request' +{{- if not .OnlyTemplate}} +// @Tags {{.StructName}} +// @Summary 创建{{.Description}} +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.{{.StructName}} true "创建{{.Description}}" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" +// @Router /{{.Abbreviation}}/create{{.StructName}} [post] +export const create{{.StructName}} = (data) => { + return service({ + url: '/{{.Abbreviation}}/create{{.StructName}}', + method: 'post', + data + }) +} + +// @Tags {{.StructName}} +// @Summary 删除{{.Description}} +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.{{.StructName}} true "删除{{.Description}}" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /{{.Abbreviation}}/delete{{.StructName}} [delete] +export const delete{{.StructName}} = (params) => { + return service({ + url: '/{{.Abbreviation}}/delete{{.StructName}}', + method: 'delete', + params + }) +} + +// @Tags {{.StructName}} +// @Summary 批量删除{{.Description}} +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "批量删除{{.Description}}" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /{{.Abbreviation}}/delete{{.StructName}} [delete] +export const delete{{.StructName}}ByIds = (params) => { + return service({ + url: '/{{.Abbreviation}}/delete{{.StructName}}ByIds', + method: 'delete', + params + }) +} + +// @Tags {{.StructName}} +// @Summary 更新{{.Description}} +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.{{.StructName}} true "更新{{.Description}}" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}" +// @Router /{{.Abbreviation}}/update{{.StructName}} [put] +export const update{{.StructName}} = (data) => { + return service({ + url: '/{{.Abbreviation}}/update{{.StructName}}', + method: 'put', + data + }) +} + +// @Tags {{.StructName}} +// @Summary 用id查询{{.Description}} +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query model.{{.StructName}} true "用id查询{{.Description}}" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" +// @Router /{{.Abbreviation}}/find{{.StructName}} [get] +export const find{{.StructName}} = (params) => { + return service({ + url: '/{{.Abbreviation}}/find{{.StructName}}', + method: 'get', + params + }) +} + +// @Tags {{.StructName}} +// @Summary 分页获取{{.Description}}列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query request.PageInfo true "分页获取{{.Description}}列表" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /{{.Abbreviation}}/get{{.StructName}}List [get] +export const get{{.StructName}}List = (params) => { + return service({ + url: '/{{.Abbreviation}}/get{{.StructName}}List', + method: 'get', + params + }) +} + +{{- if .HasDataSource}} +// @Tags {{.StructName}} +// @Summary 获取数据源 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" +// @Router /{{.Abbreviation}}/find{{.StructName}}DataSource [get] +export const get{{.StructName}}DataSource = () => { + return service({ + url: '/{{.Abbreviation}}/get{{.StructName}}DataSource', + method: 'get', + }) +} +{{- end}} +{{- end}} +// @Tags {{.StructName}} +// @Summary 不需要鉴权的{{.Description}}接口 +// @accept application/json +// @Produce application/json +// @Param data query request.{{.StructName}}Search true "分页获取{{.Description}}列表" +// @Success 200 {object} response.Response{data=object,msg=string} "获取成功" +// @Router /{{.Abbreviation}}/get{{.StructName}}Public [get] +export const get{{.StructName}}Public = () => { + return service({ + url: '/{{.Abbreviation}}/get{{.StructName}}Public', + method: 'get', + }) +} \ No newline at end of file diff --git a/admin/server/resource/plugin/web/form/form.vue.template b/admin/server/resource/plugin/web/form/form.vue.template new file mode 100644 index 000000000..2c221d912 --- /dev/null +++ b/admin/server/resource/plugin/web/form/form.vue.template @@ -0,0 +1,413 @@ +{{- if .IsAdd }} +// 新增表单中增加如下代码 +{{- range .Fields}} + {{- if .Form}} + + {{- if .CheckDataSource}} + + + + {{- else }} + {{- if eq .FieldType "bool" }} + + {{- end }} + {{- if eq .FieldType "string" }} + {{- if .DictType}} + + + + {{- else }} + + {{- end }} + {{- end }} + {{- if eq .FieldType "richtext" }} + + {{- end }} + {{- if eq .FieldType "json" }} + // 此字段为json结构,可以前端自行控制展示和数据绑定模式 需绑定json的key为 formData.{{.FieldJson}} 后端会按照json的类型进行存取 + {{"{{"}} formData.{{.FieldJson}} {{"}}"}} + {{- end }} + {{- if eq .FieldType "array" }} + + {{- end }} + {{- if eq .FieldType "int" }} + + {{- end }} + {{- if eq .FieldType "time.Time" }} + + {{- end }} + {{- if eq .FieldType "float64" }} + + {{- end }} + {{- if eq .FieldType "enum" }} + + + + {{- end }} + {{- if eq .FieldType "picture" }} + + {{- end }} + {{- if eq .FieldType "pictures" }} + + {{- end }} + {{- if eq .FieldType "video" }} + + {{- end }} + {{- if eq .FieldType "file" }} + + {{- end }} + {{- end }} + + {{- end }} + {{- end }} + +// 字典增加如下代码 + {{- range $index, $element := .DictTypes}} +const {{ $element }}Options = ref([]) + {{- end }} + +// init方法中增加如下调用 + +{{- range $index, $element := .DictTypes }} + {{ $element }}Options.value = await getDictFunc('{{$element}}') +{{- end }} + +// 基础formData结构增加如下字段 +{{- range .Fields}} + {{- if .Form}} + {{- if eq .FieldType "bool" }} +{{.FieldJson}}: false, + {{- end }} + {{- if eq .FieldType "string" }} +{{.FieldJson}}: '', + {{- end }} + {{- if eq .FieldType "richtext" }} +{{.FieldJson}}: '', + {{- end }} + {{- if eq .FieldType "int" }} +{{.FieldJson}}: {{- if or .DictType .DataSource}} undefined{{ else }} 0{{- end }}, + {{- end }} + {{- if eq .FieldType "time.Time" }} +{{.FieldJson}}: new Date(), + {{- end }} + {{- if eq .FieldType "float64" }} +{{.FieldJson}}: 0, + {{- end }} + {{- if eq .FieldType "picture" }} +{{.FieldJson}}: "", + {{- end }} + {{- if eq .FieldType "video" }} +{{.FieldJson}}: "", + {{- end }} + {{- if eq .FieldType "pictures" }} +{{.FieldJson}}: [], + {{- end }} + {{- if eq .FieldType "file" }} +{{.FieldJson}}: [], + {{- end }} + {{- if eq .FieldType "json" }} +{{.FieldJson}}: {}, + {{- end }} + {{- if eq .FieldType "array" }} +{{.FieldJson}}: [], + {{- end }} + {{- end }} + {{- end }} +// 验证规则中增加如下字段 + +{{- range .Fields }} + {{- if .Form }} + {{- if eq .Require true }} +{{.FieldJson }} : [{ + required: true, + message: '{{ .ErrorText }}', + trigger: ['input','blur'], +}, + {{- if eq .FieldType "string" }} +{ + whitespace: true, + message: '不能只输入空格', + trigger: ['input', 'blur'], +} + {{- end }} +], + {{- end }} + {{- end }} + {{- end }} + +{{- if .HasDataSource }} +// 请引用 +get{{.StructName}}DataSource, +// 获取数据源 +const dataSource = ref([]) +const getDataSourceFunc = async()=>{ + const res = await get{{.StructName}}DataSource() + if (res.code === 0) { + dataSource.value = res.data + } +} +getDataSourceFunc() +{{- end }} +{{- else }} +{{- if not .OnlyTemplate}} + + + + + +{{- else }} + + + +{{- end }} +{{- end }} diff --git a/admin/server/resource/plugin/web/view/view.vue.template b/admin/server/resource/plugin/web/view/view.vue.template new file mode 100644 index 000000000..88f0e9a9d --- /dev/null +++ b/admin/server/resource/plugin/web/view/view.vue.template @@ -0,0 +1,1269 @@ +{{- $global := . }} +{{- $templateID := printf "%s_%s" .Package .StructName }} + +{{- if .IsAdd }} +// 请在搜索条件中增加如下代码 +{{- range .Fields}} {{- if .FieldSearchType}} {{- if eq .FieldType "bool" }} + + + + + + + + + {{- else if .DictType}} + + + + + + {{- else if .CheckDataSource}} + + + + + + {{- else}} + + {{- if eq .FieldType "float64" "int"}} + {{if eq .FieldSearchType "BETWEEN" "NOT BETWEEN"}} + +— + + {{- else}} + {{- if .DictType}} + + + + {{- else}} + + {{- end }} + {{- end}} + {{- else if eq .FieldType "time.Time"}} + {{if eq .FieldSearchType "BETWEEN" "NOT BETWEEN"}} + + +— + + {{- else}} + + {{- end}} + {{- else}} + + {{- end}} +{{ end }}{{ end }}{{ end }} + + +// 表格增加如下列代码 + +{{- range .Fields}} + {{- if .Table}} + {{- if .CheckDataSource }} + + + + {{- else if .DictType}} + + + + {{- else if eq .FieldType "bool" }} + + + + {{- else if eq .FieldType "time.Time" }} + + + + {{- else if eq .FieldType "picture" }} + + + + {{- else if eq .FieldType "pictures" }} + + + + {{- else if eq .FieldType "video" }} + + + + {{- else if eq .FieldType "richtext" }} + + + + {{- else if eq .FieldType "file" }} + + + + {{- else if eq .FieldType "json" }} + + + + {{- else if eq .FieldType "array" }} + + + + {{- else }} + + {{- end }} + {{- end }} + {{- end }} + +// 新增表单中增加如下代码 +{{- range .Fields}} + {{- if .Form}} + + {{- if .CheckDataSource}} + + + + {{- else }} + {{- if eq .FieldType "bool" }} + + {{- end }} + {{- if eq .FieldType "string" }} + {{- if .DictType}} + + + + {{- else }} + + {{- end }} + {{- end }} + {{- if eq .FieldType "richtext" }} + + {{- end }} + {{- if eq .FieldType "json" }} + // 此字段为json结构,可以前端自行控制展示和数据绑定模式 需绑定json的key为 formData.{{.FieldJson}} 后端会按照json的类型进行存取 + {{"{{"}} formData.{{.FieldJson}} {{"}}"}} + {{- end }} + {{- if eq .FieldType "array" }} + + {{- end }} + {{- if eq .FieldType "int" }} + + {{- end }} + {{- if eq .FieldType "time.Time" }} + + {{- end }} + {{- if eq .FieldType "float64" }} + + {{- end }} + {{- if eq .FieldType "enum" }} + + + + {{- end }} + {{- if eq .FieldType "picture" }} + + {{- end }} + {{- if eq .FieldType "pictures" }} + + {{- end }} + {{- if eq .FieldType "video" }} + + {{- end }} + {{- if eq .FieldType "file" }} + + {{- end }} + {{- end }} + + {{- end }} + {{- end }} + +// 查看抽屉中增加如下代码 + +{{- range .Fields}} + {{- if .Desc }} + + {{- if and (ne .FieldType "picture" ) (ne .FieldType "pictures" ) (ne .FieldType "file" ) (ne .FieldType "array" ) }} + {{"{{"}} detailFrom.{{.FieldJson}} {{"}}"}} + {{- else }} + {{- if eq .FieldType "picture" }} + + {{- end }} + {{- if eq .FieldType "array" }} + + {{- end }} + {{- if eq .FieldType "pictures" }} + + {{- end }} + {{- if eq .FieldType "file" }} +
+ + + {{"{{"}}item.name{{"}}"}} + +
+ {{- end }} + {{- end }} +
+ {{- end }} + {{- end }} + +// 字典增加如下代码 + {{- range $index, $element := .DictTypes}} +const {{ $element }}Options = ref([]) + {{- end }} +// setOptions方法中增加如下调用 + +{{- range $index, $element := .DictTypes }} + {{ $element }}Options.value = await getDictFunc('{{$element}}') +{{- end }} + +// 基础formData结构(变量处和关闭表单处)增加如下字段 +{{- range .Fields}} + {{- if .Form}} + {{- if eq .FieldType "bool" }} +{{.FieldJson}}: false, + {{- end }} + {{- if eq .FieldType "string" }} +{{.FieldJson}}: '', + {{- end }} + {{- if eq .FieldType "richtext" }} +{{.FieldJson}}: '', + {{- end }} + {{- if eq .FieldType "int" }} +{{.FieldJson}}: {{- if or .DictType .DataSource}} undefined{{ else }} 0{{- end }}, + {{- end }} + {{- if eq .FieldType "time.Time" }} +{{.FieldJson}}: new Date(), + {{- end }} + {{- if eq .FieldType "float64" }} +{{.FieldJson}}: 0, + {{- end }} + {{- if eq .FieldType "picture" }} +{{.FieldJson}}: "", + {{- end }} + {{- if eq .FieldType "video" }} +{{.FieldJson}}: "", + {{- end }} + {{- if eq .FieldType "pictures" }} +{{.FieldJson}}: [], + {{- end }} + {{- if eq .FieldType "file" }} +{{.FieldJson}}: [], + {{- end }} + {{- if eq .FieldType "json" }} +{{.FieldJson}}: {}, + {{- end }} + {{- if eq .FieldType "array" }} +{{.FieldJson}}: [], + {{- end }} + {{- end }} + {{- end }} +// 验证规则中增加如下字段 + +{{- range .Fields }} + {{- if .Form }} + {{- if eq .Require true }} +{{.FieldJson }} : [{ + required: true, + message: '{{ .ErrorText }}', + trigger: ['input','blur'], +}, + {{- if eq .FieldType "string" }} +{ + whitespace: true, + message: '不能只输入空格', + trigger: ['input', 'blur'], +} + {{- end }} +], + {{- end }} + {{- end }} + {{- end }} + +{{- if .HasDataSource }} +// 请引用 +get{{.StructName}}DataSource, +// 获取数据源 +const dataSource = ref([]) +const getDataSourceFunc = async()=>{ + const res = await get{{.StructName}}DataSource() + if (res.code === 0) { + dataSource.value = res.data + } +} +getDataSourceFunc() +{{- end }} + +{{- else }} + +{{- if not .OnlyTemplate}} + + + + + +{{- else}} + + + +{{- end}} +{{- end}} diff --git a/admin/server/router/enter.go b/admin/server/router/enter.go new file mode 100644 index 000000000..8759e8508 --- /dev/null +++ b/admin/server/router/enter.go @@ -0,0 +1,15 @@ +package router + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/router/example" + "github.com/flipped-aurora/gin-vue-admin/server/router/gaia" + "github.com/flipped-aurora/gin-vue-admin/server/router/system" +) + +var RouterGroupApp = new(RouterGroup) + +type RouterGroup struct { + System system.RouterGroup + Example example.RouterGroup + Gaia gaia.RouterGroup +} diff --git a/admin/server/router/example/enter.go b/admin/server/router/example/enter.go new file mode 100644 index 000000000..ce87aa2f9 --- /dev/null +++ b/admin/server/router/example/enter.go @@ -0,0 +1,15 @@ +package example + +import ( + api "github.com/flipped-aurora/gin-vue-admin/server/api/v1" +) + +type RouterGroup struct { + CustomerRouter + FileUploadAndDownloadRouter +} + +var ( + exaCustomerApi = api.ApiGroupApp.ExampleApiGroup.CustomerApi + exaFileUploadAndDownloadApi = api.ApiGroupApp.ExampleApiGroup.FileUploadAndDownloadApi +) diff --git a/admin/server/router/example/exa_customer.go b/admin/server/router/example/exa_customer.go new file mode 100644 index 000000000..acdf3c7e5 --- /dev/null +++ b/admin/server/router/example/exa_customer.go @@ -0,0 +1,22 @@ +package example + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +type CustomerRouter struct{} + +func (e *CustomerRouter) InitCustomerRouter(Router *gin.RouterGroup) { + customerRouter := Router.Group("customer").Use(middleware.OperationRecord()) + customerRouterWithoutRecord := Router.Group("customer") + { + customerRouter.POST("customer", exaCustomerApi.CreateExaCustomer) // 创建客户 + customerRouter.PUT("customer", exaCustomerApi.UpdateExaCustomer) // 更新客户 + customerRouter.DELETE("customer", exaCustomerApi.DeleteExaCustomer) // 删除客户 + } + { + customerRouterWithoutRecord.GET("customer", exaCustomerApi.GetExaCustomer) // 获取单一客户信息 + customerRouterWithoutRecord.GET("customerList", exaCustomerApi.GetExaCustomerList) // 获取客户列表 + } +} diff --git a/admin/server/router/example/exa_file_upload_and_download.go b/admin/server/router/example/exa_file_upload_and_download.go new file mode 100644 index 000000000..84f6ecdb0 --- /dev/null +++ b/admin/server/router/example/exa_file_upload_and_download.go @@ -0,0 +1,22 @@ +package example + +import ( + "github.com/gin-gonic/gin" +) + +type FileUploadAndDownloadRouter struct{} + +func (e *FileUploadAndDownloadRouter) InitFileUploadAndDownloadRouter(Router *gin.RouterGroup) { + fileUploadAndDownloadRouter := Router.Group("fileUploadAndDownload") + { + fileUploadAndDownloadRouter.POST("upload", exaFileUploadAndDownloadApi.UploadFile) // 上传文件 + fileUploadAndDownloadRouter.POST("getFileList", exaFileUploadAndDownloadApi.GetFileList) // 获取上传文件列表 + fileUploadAndDownloadRouter.POST("deleteFile", exaFileUploadAndDownloadApi.DeleteFile) // 删除指定文件 + fileUploadAndDownloadRouter.POST("editFileName", exaFileUploadAndDownloadApi.EditFileName) // 编辑文件名或者备注 + fileUploadAndDownloadRouter.POST("breakpointContinue", exaFileUploadAndDownloadApi.BreakpointContinue) // 断点续传 + fileUploadAndDownloadRouter.GET("findFile", exaFileUploadAndDownloadApi.FindFile) // 查询当前文件成功的切片 + fileUploadAndDownloadRouter.POST("breakpointContinueFinish", exaFileUploadAndDownloadApi.BreakpointContinueFinish) // 切片传输完成 + fileUploadAndDownloadRouter.POST("removeChunk", exaFileUploadAndDownloadApi.RemoveChunk) // 删除切片 + fileUploadAndDownloadRouter.POST("importURL", exaFileUploadAndDownloadApi.ImportURL) // 导入URL + } +} diff --git a/admin/server/router/gaia/dashboard.go b/admin/server/router/gaia/dashboard.go new file mode 100644 index 000000000..57fabda61 --- /dev/null +++ b/admin/server/router/gaia/dashboard.go @@ -0,0 +1,19 @@ +package gaia + +import ( + "github.com/gin-gonic/gin" +) + +type DashboardRouter struct{} + +// InitDashboardRouter 初始化 dashboard表 路由信息 +func (d *DashboardRouter) InitDashboardRouter(Router *gin.RouterGroup, PublicRouter *gin.RouterGroup) { + dashboardRouterWithoutRecord := Router.Group("gaia/dashboard") + { + dashboardRouterWithoutRecord.GET("getAccountQuotaRankingData", dashboardApi.GetAccountQuotaRankingData) // 分页获取账号额度排名 + dashboardRouterWithoutRecord.GET("getAppQuotaRankingData", dashboardApi.GetAppQuotaRankingData) // 分页获取【应用】配额排名数据 + dashboardRouterWithoutRecord.GET("getAppTokenQuotaRankingData", dashboardApi.GetAppTokenQuotaRankingData) // 分页获取【应用密钥】配额排名数据列表 + dashboardRouterWithoutRecord.GET("getAppTokenDailyQuotaData", dashboardApi.GetAppTokenDailyQuotaData) // 获取每天密钥花费数据列表 + dashboardRouterWithoutRecord.GET("getAiImageQuotaRankingData", dashboardApi.GetAiImageQuotaRankingData) // 获取每天ai图片额度排名 + } +} diff --git a/admin/server/router/gaia/enter.go b/admin/server/router/gaia/enter.go new file mode 100644 index 000000000..db632ba3b --- /dev/null +++ b/admin/server/router/gaia/enter.go @@ -0,0 +1,19 @@ +package gaia + +import api "github.com/flipped-aurora/gin-vue-admin/server/api/v1" + +type RouterGroup struct { + DashboardRouter + QuotaRouter + TenantsRouter + SystemRouter + TestRouter +} + +var ( + dashboardApi = api.ApiGroupApp.GaiaApiGroup.DashboardApi + tenantsApi = api.ApiGroupApp.GaiaApiGroup.TenantsApi +) +var systemApi = api.ApiGroupApp.GaiaApiGroup.SystemApi +var quotaApi = api.ApiGroupApp.GaiaApiGroup.QuotaApi +var testApi = api.ApiGroupApp.GaiaApiGroup.TestApi diff --git a/admin/server/router/gaia/quota.go b/admin/server/router/gaia/quota.go new file mode 100644 index 000000000..7321d8b86 --- /dev/null +++ b/admin/server/router/gaia/quota.go @@ -0,0 +1,16 @@ +package gaia + +import ( + "github.com/gin-gonic/gin" +) + +type QuotaRouter struct{} + +// InitQuotaRouter 初始化 quota表 路由信息 +func (d *QuotaRouter) InitQuotaRouter(Router *gin.RouterGroup, PublicRouter *gin.RouterGroup) { + dashboardRouterWithoutRecord := Router.Group("gaia/quota") + { + dashboardRouterWithoutRecord.POST("setUserQuota", quotaApi.SetUserQuota) // 设置用户额度 + dashboardRouterWithoutRecord.GET("getManagementList", quotaApi.QuotaManagementList) // 额度管理列表 + } +} diff --git a/admin/server/router/gaia/system.go b/admin/server/router/gaia/system.go new file mode 100644 index 000000000..7e640b24c --- /dev/null +++ b/admin/server/router/gaia/system.go @@ -0,0 +1,16 @@ +package gaia + +import ( + "github.com/gin-gonic/gin" +) + +type SystemRouter struct{} + +// InitSystemRouter 初始化 Dify 系统 关联系统表 路由信息 +func (d *SystemRouter) InitSystemRouter(Router *gin.RouterGroup) { + dashboardRouterWithoutRecord := Router.Group("gaia/system") + { + dashboardRouterWithoutRecord.GET("dingtalk", systemApi.GetDingTalk) // 获取钉钉系统配置 + dashboardRouterWithoutRecord.POST("dingtalk", systemApi.SetDingTalk) // 设置钉钉系统配置 + } +} diff --git a/admin/server/router/gaia/tenants.go b/admin/server/router/gaia/tenants.go new file mode 100644 index 000000000..1c3cf5ac1 --- /dev/null +++ b/admin/server/router/gaia/tenants.go @@ -0,0 +1,17 @@ +package gaia + +import ( + "github.com/gin-gonic/gin" +) + +type TenantsRouter struct{} + +// InitTenantsRouter 初始化 tenants表 路由信息 +func (s *TenantsRouter) InitTenantsRouter(Router *gin.RouterGroup, PublicRouter *gin.RouterGroup) { + tenantsRouterWithoutRecord := Router.Group("tenants") + { + tenantsRouterWithoutRecord.GET("findTenants", tenantsApi.FindTenants) // 根据ID获取tenants表 + tenantsRouterWithoutRecord.GET("getTenantsList", tenantsApi.GetTenantsList) // 获取tenants表列表 + tenantsRouterWithoutRecord.GET("getAllTenants", tenantsApi.GetAllTenants) // 获取所有工作区 + } +} diff --git a/admin/server/router/gaia/test.go b/admin/server/router/gaia/test.go new file mode 100644 index 000000000..164d14637 --- /dev/null +++ b/admin/server/router/gaia/test.go @@ -0,0 +1,18 @@ +package gaia + +import ( + "github.com/gin-gonic/gin" +) + +type TestRouter struct{} + +// InitTestRouter 初始化 测试表 路由信息 +func (d *TestRouter) InitTestRouter(Router *gin.RouterGroup, PublicRouter *gin.RouterGroup) { + dashboardRouterWithoutRecord := Router.Group("gaia/test") + { + dashboardRouterWithoutRecord.POST("app/request", testApi.GaiaAppRequestTest) // 发起gaia应用请求测试 + dashboardRouterWithoutRecord.GET("app/request/list", testApi.GaiaAppRequestTestList) // gaia应用请求测试结果列表 + dashboardRouterWithoutRecord.GET("app/request/batch", testApi.GaiaAppRequestTestBatch) // gaia应用请求测试批次列表 + dashboardRouterWithoutRecord.POST("sync/database", testApi.SyncDatabaseTableData) // 同步数据库表数据 + } +} diff --git a/admin/server/router/system/enter.go b/admin/server/router/system/enter.go new file mode 100644 index 000000000..7127d9e9f --- /dev/null +++ b/admin/server/router/system/enter.go @@ -0,0 +1,44 @@ +package system + +import api "github.com/flipped-aurora/gin-vue-admin/server/api/v1" + +type RouterGroup struct { + ApiRouter + JwtRouter + SysRouter + BaseRouter + InitRouter + MenuRouter + UserRouter + CasbinRouter + AutoCodeRouter + AuthorityRouter + DictionaryRouter + OperationRecordRouter + DictionaryDetailRouter + AuthorityBtnRouter + SysExportTemplateRouter + SysParamsRouter +} + +var ( + dbApi = api.ApiGroupApp.SystemApiGroup.DBApi + jwtApi = api.ApiGroupApp.SystemApiGroup.JwtApi + baseApi = api.ApiGroupApp.SystemApiGroup.BaseApi + casbinApi = api.ApiGroupApp.SystemApiGroup.CasbinApi + systemApi = api.ApiGroupApp.SystemApiGroup.SystemApi + sysParamsApi = api.ApiGroupApp.SystemApiGroup.SysParamsApi + autoCodeApi = api.ApiGroupApp.SystemApiGroup.AutoCodeApi + authorityApi = api.ApiGroupApp.SystemApiGroup.AuthorityApi + apiRouterApi = api.ApiGroupApp.SystemApiGroup.SystemApiApi + dictionaryApi = api.ApiGroupApp.SystemApiGroup.DictionaryApi + authorityBtnApi = api.ApiGroupApp.SystemApiGroup.AuthorityBtnApi + authorityMenuApi = api.ApiGroupApp.SystemApiGroup.AuthorityMenuApi + autoCodePluginApi = api.ApiGroupApp.SystemApiGroup.AutoCodePluginApi + autocodeHistoryApi = api.ApiGroupApp.SystemApiGroup.AutoCodeHistoryApi + operationRecordApi = api.ApiGroupApp.SystemApiGroup.OperationRecordApi + autoCodePackageApi = api.ApiGroupApp.SystemApiGroup.AutoCodePackageApi + dictionaryDetailApi = api.ApiGroupApp.SystemApiGroup.DictionaryDetailApi + autoCodeTemplateApi = api.ApiGroupApp.SystemApiGroup.AutoCodeTemplateApi + exportTemplateApi = api.ApiGroupApp.SystemApiGroup.SysExportTemplateApi +) diff --git a/admin/server/router/system/sys_api.go b/admin/server/router/system/sys_api.go new file mode 100644 index 000000000..c98785e94 --- /dev/null +++ b/admin/server/router/system/sys_api.go @@ -0,0 +1,33 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +type ApiRouter struct{} + +func (s *ApiRouter) InitApiRouter(Router *gin.RouterGroup, RouterPub *gin.RouterGroup) { + apiRouter := Router.Group("api").Use(middleware.OperationRecord()) + apiRouterWithoutRecord := Router.Group("api") + + apiPublicRouterWithoutRecord := RouterPub.Group("api") + { + apiRouter.GET("getApiGroups", apiRouterApi.GetApiGroups) // 获取路由组 + apiRouter.GET("syncApi", apiRouterApi.SyncApi) // 同步Api + apiRouter.POST("ignoreApi", apiRouterApi.IgnoreApi) // 忽略Api + apiRouter.POST("enterSyncApi", apiRouterApi.EnterSyncApi) // 确认同步Api + apiRouter.POST("createApi", apiRouterApi.CreateApi) // 创建Api + apiRouter.POST("deleteApi", apiRouterApi.DeleteApi) // 删除Api + apiRouter.POST("getApiById", apiRouterApi.GetApiById) // 获取单条Api消息 + apiRouter.POST("updateApi", apiRouterApi.UpdateApi) // 更新api + apiRouter.DELETE("deleteApisByIds", apiRouterApi.DeleteApisByIds) // 删除选中api + } + { + apiRouterWithoutRecord.POST("getAllApis", apiRouterApi.GetAllApis) // 获取所有api + apiRouterWithoutRecord.POST("getApiList", apiRouterApi.GetApiList) // 获取Api列表 + } + { + apiPublicRouterWithoutRecord.GET("freshCasbin", apiRouterApi.FreshCasbin) // 刷新casbin权限 + } +} diff --git a/admin/server/router/system/sys_authority.go b/admin/server/router/system/sys_authority.go new file mode 100644 index 000000000..9bef92ff1 --- /dev/null +++ b/admin/server/router/system/sys_authority.go @@ -0,0 +1,23 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +type AuthorityRouter struct{} + +func (s *AuthorityRouter) InitAuthorityRouter(Router *gin.RouterGroup) { + authorityRouter := Router.Group("authority").Use(middleware.OperationRecord()) + authorityRouterWithoutRecord := Router.Group("authority") + { + authorityRouter.POST("createAuthority", authorityApi.CreateAuthority) // 创建角色 + authorityRouter.POST("deleteAuthority", authorityApi.DeleteAuthority) // 删除角色 + authorityRouter.PUT("updateAuthority", authorityApi.UpdateAuthority) // 更新角色 + authorityRouter.POST("copyAuthority", authorityApi.CopyAuthority) // 拷贝角色 + authorityRouter.POST("setDataAuthority", authorityApi.SetDataAuthority) // 设置角色资源权限 + } + { + authorityRouterWithoutRecord.POST("getAuthorityList", authorityApi.GetAuthorityList) // 获取角色列表 + } +} diff --git a/admin/server/router/system/sys_authority_btn.go b/admin/server/router/system/sys_authority_btn.go new file mode 100644 index 000000000..370db85f8 --- /dev/null +++ b/admin/server/router/system/sys_authority_btn.go @@ -0,0 +1,19 @@ +package system + +import ( + "github.com/gin-gonic/gin" +) + +type AuthorityBtnRouter struct{} + +var AuthorityBtnRouterApp = new(AuthorityBtnRouter) + +func (s *AuthorityBtnRouter) InitAuthorityBtnRouterRouter(Router *gin.RouterGroup) { + // authorityRouter := Router.Group("authorityBtn").Use(middleware.OperationRecord()) + authorityRouterWithoutRecord := Router.Group("authorityBtn") + { + authorityRouterWithoutRecord.POST("getAuthorityBtn", authorityBtnApi.GetAuthorityBtn) + authorityRouterWithoutRecord.POST("setAuthorityBtn", authorityBtnApi.SetAuthorityBtn) + authorityRouterWithoutRecord.POST("canRemoveAuthorityBtn", authorityBtnApi.CanRemoveAuthorityBtn) + } +} diff --git a/admin/server/router/system/sys_auto_code.go b/admin/server/router/system/sys_auto_code.go new file mode 100644 index 000000000..e25e1cef8 --- /dev/null +++ b/admin/server/router/system/sys_auto_code.go @@ -0,0 +1,40 @@ +package system + +import ( + "github.com/gin-gonic/gin" +) + +type AutoCodeRouter struct{} + +func (s *AutoCodeRouter) InitAutoCodeRouter(Router *gin.RouterGroup, RouterPublic *gin.RouterGroup) { + autoCodeRouter := Router.Group("autoCode") + publicAutoCodeRouter := RouterPublic.Group("autoCode") + { + autoCodeRouter.GET("getDB", autoCodeApi.GetDB) // 获取数据库 + autoCodeRouter.GET("getTables", autoCodeApi.GetTables) // 获取对应数据库的表 + autoCodeRouter.GET("getColumn", autoCodeApi.GetColumn) // 获取指定表所有字段信息 + } + { + autoCodeRouter.POST("preview", autoCodeTemplateApi.Preview) // 获取自动创建代码预览 + autoCodeRouter.POST("createTemp", autoCodeTemplateApi.Create) // 创建自动化代码 + autoCodeRouter.POST("addFunc", autoCodeTemplateApi.AddFunc) // 为代码插入方法 + } + { + autoCodeRouter.POST("getPackage", autoCodePackageApi.All) // 获取package包 + autoCodeRouter.POST("delPackage", autoCodePackageApi.Delete) // 删除package包 + autoCodeRouter.POST("createPackage", autoCodePackageApi.Create) // 创建package包 + } + { + autoCodeRouter.GET("getTemplates", autoCodePackageApi.Templates) // 创建package包 + } + { + autoCodeRouter.POST("pubPlug", autoCodePluginApi.Packaged) // 打包插件 + autoCodeRouter.POST("installPlugin", autoCodePluginApi.Install) // 自动安装插件 + + } + { + publicAutoCodeRouter.POST("llmAuto", autoCodeApi.LLMAuto) + publicAutoCodeRouter.POST("initMenu", autoCodePluginApi.InitMenu) // 同步插件菜单 + publicAutoCodeRouter.POST("initAPI", autoCodePluginApi.InitAPI) // 同步插件API + } +} diff --git a/admin/server/router/system/sys_auto_code_history.go b/admin/server/router/system/sys_auto_code_history.go new file mode 100644 index 000000000..42a2bef86 --- /dev/null +++ b/admin/server/router/system/sys_auto_code_history.go @@ -0,0 +1,17 @@ +package system + +import ( + "github.com/gin-gonic/gin" +) + +type AutoCodeHistoryRouter struct{} + +func (s *AutoCodeRouter) InitAutoCodeHistoryRouter(Router *gin.RouterGroup) { + autoCodeHistoryRouter := Router.Group("autoCode") + { + autoCodeHistoryRouter.POST("getMeta", autocodeHistoryApi.First) // 根据id获取meta信息 + autoCodeHistoryRouter.POST("rollback", autocodeHistoryApi.RollBack) // 回滚 + autoCodeHistoryRouter.POST("delSysHistory", autocodeHistoryApi.Delete) // 删除回滚记录 + autoCodeHistoryRouter.POST("getSysHistory", autocodeHistoryApi.GetList) // 获取回滚记录分页 + } +} diff --git a/admin/server/router/system/sys_base.go b/admin/server/router/system/sys_base.go new file mode 100644 index 000000000..2d539c109 --- /dev/null +++ b/admin/server/router/system/sys_base.go @@ -0,0 +1,17 @@ +package system + +import ( + "github.com/gin-gonic/gin" +) + +type BaseRouter struct{} + +func (s *BaseRouter) InitBaseRouter(Router *gin.RouterGroup) (R gin.IRoutes) { + baseRouter := Router.Group("base") + { + baseRouter.POST("login", baseApi.Login) + baseRouter.POST("captcha", baseApi.Captcha) + baseRouter.POST("oaLogin", baseApi.OaLogin) // 新增OA登录 + } + return baseRouter +} diff --git a/admin/server/router/system/sys_casbin.go b/admin/server/router/system/sys_casbin.go new file mode 100644 index 000000000..e4a3eb12c --- /dev/null +++ b/admin/server/router/system/sys_casbin.go @@ -0,0 +1,19 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +type CasbinRouter struct{} + +func (s *CasbinRouter) InitCasbinRouter(Router *gin.RouterGroup) { + casbinRouter := Router.Group("casbin").Use(middleware.OperationRecord()) + casbinRouterWithoutRecord := Router.Group("casbin") + { + casbinRouter.POST("updateCasbin", casbinApi.UpdateCasbin) + } + { + casbinRouterWithoutRecord.POST("getPolicyPathByAuthorityId", casbinApi.GetPolicyPathByAuthorityId) + } +} diff --git a/admin/server/router/system/sys_dictionary.go b/admin/server/router/system/sys_dictionary.go new file mode 100644 index 000000000..41ce85ec9 --- /dev/null +++ b/admin/server/router/system/sys_dictionary.go @@ -0,0 +1,22 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +type DictionaryRouter struct{} + +func (s *DictionaryRouter) InitSysDictionaryRouter(Router *gin.RouterGroup) { + sysDictionaryRouter := Router.Group("sysDictionary").Use(middleware.OperationRecord()) + sysDictionaryRouterWithoutRecord := Router.Group("sysDictionary") + { + sysDictionaryRouter.POST("createSysDictionary", dictionaryApi.CreateSysDictionary) // 新建SysDictionary + sysDictionaryRouter.DELETE("deleteSysDictionary", dictionaryApi.DeleteSysDictionary) // 删除SysDictionary + sysDictionaryRouter.PUT("updateSysDictionary", dictionaryApi.UpdateSysDictionary) // 更新SysDictionary + } + { + sysDictionaryRouterWithoutRecord.GET("findSysDictionary", dictionaryApi.FindSysDictionary) // 根据ID获取SysDictionary + sysDictionaryRouterWithoutRecord.GET("getSysDictionaryList", dictionaryApi.GetSysDictionaryList) // 获取SysDictionary列表 + } +} diff --git a/admin/server/router/system/sys_dictionary_detail.go b/admin/server/router/system/sys_dictionary_detail.go new file mode 100644 index 000000000..cde6bdcb6 --- /dev/null +++ b/admin/server/router/system/sys_dictionary_detail.go @@ -0,0 +1,22 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +type DictionaryDetailRouter struct{} + +func (s *DictionaryDetailRouter) InitSysDictionaryDetailRouter(Router *gin.RouterGroup) { + dictionaryDetailRouter := Router.Group("sysDictionaryDetail").Use(middleware.OperationRecord()) + dictionaryDetailRouterWithoutRecord := Router.Group("sysDictionaryDetail") + { + dictionaryDetailRouter.POST("createSysDictionaryDetail", dictionaryDetailApi.CreateSysDictionaryDetail) // 新建SysDictionaryDetail + dictionaryDetailRouter.DELETE("deleteSysDictionaryDetail", dictionaryDetailApi.DeleteSysDictionaryDetail) // 删除SysDictionaryDetail + dictionaryDetailRouter.PUT("updateSysDictionaryDetail", dictionaryDetailApi.UpdateSysDictionaryDetail) // 更新SysDictionaryDetail + } + { + dictionaryDetailRouterWithoutRecord.GET("findSysDictionaryDetail", dictionaryDetailApi.FindSysDictionaryDetail) // 根据ID获取SysDictionaryDetail + dictionaryDetailRouterWithoutRecord.GET("getSysDictionaryDetailList", dictionaryDetailApi.GetSysDictionaryDetailList) // 获取SysDictionaryDetail列表 + } +} diff --git a/admin/server/router/system/sys_export_template.go b/admin/server/router/system/sys_export_template.go new file mode 100644 index 000000000..3e92a90c1 --- /dev/null +++ b/admin/server/router/system/sys_export_template.go @@ -0,0 +1,28 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +type SysExportTemplateRouter struct { +} + +// InitSysExportTemplateRouter 初始化 导出模板 路由信息 +func (s *SysExportTemplateRouter) InitSysExportTemplateRouter(Router *gin.RouterGroup) { + sysExportTemplateRouter := Router.Group("sysExportTemplate").Use(middleware.OperationRecord()) + sysExportTemplateRouterWithoutRecord := Router.Group("sysExportTemplate") + { + sysExportTemplateRouter.POST("createSysExportTemplate", exportTemplateApi.CreateSysExportTemplate) // 新建导出模板 + sysExportTemplateRouter.DELETE("deleteSysExportTemplate", exportTemplateApi.DeleteSysExportTemplate) // 删除导出模板 + sysExportTemplateRouter.DELETE("deleteSysExportTemplateByIds", exportTemplateApi.DeleteSysExportTemplateByIds) // 批量删除导出模板 + sysExportTemplateRouter.PUT("updateSysExportTemplate", exportTemplateApi.UpdateSysExportTemplate) // 更新导出模板 + sysExportTemplateRouter.POST("importExcel", exportTemplateApi.ImportExcel) // 更新导出模板 + } + { + sysExportTemplateRouterWithoutRecord.GET("findSysExportTemplate", exportTemplateApi.FindSysExportTemplate) // 根据ID获取导出模板 + sysExportTemplateRouterWithoutRecord.GET("getSysExportTemplateList", exportTemplateApi.GetSysExportTemplateList) // 获取导出模板列表 + sysExportTemplateRouterWithoutRecord.GET("exportExcel", exportTemplateApi.ExportExcel) // 导出表格 + sysExportTemplateRouterWithoutRecord.GET("exportTemplate", exportTemplateApi.ExportTemplate) // 导出表格模板 + } +} diff --git a/admin/server/router/system/sys_initdb.go b/admin/server/router/system/sys_initdb.go new file mode 100644 index 000000000..3a6de5035 --- /dev/null +++ b/admin/server/router/system/sys_initdb.go @@ -0,0 +1,15 @@ +package system + +import ( + "github.com/gin-gonic/gin" +) + +type InitRouter struct{} + +func (s *InitRouter) InitInitRouter(Router *gin.RouterGroup) { + initRouter := Router.Group("init") + { + initRouter.POST("initdb", dbApi.InitDB) // 初始化数据库 + initRouter.POST("checkdb", dbApi.CheckDB) // 检测是否需要初始化数据库 + } +} diff --git a/admin/server/router/system/sys_jwt.go b/admin/server/router/system/sys_jwt.go new file mode 100644 index 000000000..471603158 --- /dev/null +++ b/admin/server/router/system/sys_jwt.go @@ -0,0 +1,14 @@ +package system + +import ( + "github.com/gin-gonic/gin" +) + +type JwtRouter struct{} + +func (s *JwtRouter) InitJwtRouter(Router *gin.RouterGroup) { + jwtRouter := Router.Group("jwt") + { + jwtRouter.POST("jsonInBlacklist", jwtApi.JsonInBlacklist) // jwt加入黑名单 + } +} diff --git a/admin/server/router/system/sys_menu.go b/admin/server/router/system/sys_menu.go new file mode 100644 index 000000000..09584f4f7 --- /dev/null +++ b/admin/server/router/system/sys_menu.go @@ -0,0 +1,27 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +type MenuRouter struct{} + +func (s *MenuRouter) InitMenuRouter(Router *gin.RouterGroup) (R gin.IRoutes) { + menuRouter := Router.Group("menu").Use(middleware.OperationRecord()) + menuRouterWithoutRecord := Router.Group("menu") + { + menuRouter.POST("addBaseMenu", authorityMenuApi.AddBaseMenu) // 新增菜单 + menuRouter.POST("addMenuAuthority", authorityMenuApi.AddMenuAuthority) // 增加menu和角色关联关系 + menuRouter.POST("deleteBaseMenu", authorityMenuApi.DeleteBaseMenu) // 删除菜单 + menuRouter.POST("updateBaseMenu", authorityMenuApi.UpdateBaseMenu) // 更新菜单 + } + { + menuRouterWithoutRecord.POST("getMenu", authorityMenuApi.GetMenu) // 获取菜单树 + menuRouterWithoutRecord.POST("getMenuList", authorityMenuApi.GetMenuList) // 分页获取基础menu列表 + menuRouterWithoutRecord.POST("getBaseMenuTree", authorityMenuApi.GetBaseMenuTree) // 获取用户动态路由 + menuRouterWithoutRecord.POST("getMenuAuthority", authorityMenuApi.GetMenuAuthority) // 获取指定角色menu + menuRouterWithoutRecord.POST("getBaseMenuById", authorityMenuApi.GetBaseMenuById) // 根据id获取菜单 + } + return menuRouter +} diff --git a/admin/server/router/system/sys_operation_record.go b/admin/server/router/system/sys_operation_record.go new file mode 100644 index 000000000..11b841db7 --- /dev/null +++ b/admin/server/router/system/sys_operation_record.go @@ -0,0 +1,19 @@ +package system + +import ( + "github.com/gin-gonic/gin" +) + +type OperationRecordRouter struct{} + +func (s *OperationRecordRouter) InitSysOperationRecordRouter(Router *gin.RouterGroup) { + operationRecordRouter := Router.Group("sysOperationRecord") + { + operationRecordRouter.POST("createSysOperationRecord", operationRecordApi.CreateSysOperationRecord) // 新建SysOperationRecord + operationRecordRouter.DELETE("deleteSysOperationRecord", operationRecordApi.DeleteSysOperationRecord) // 删除SysOperationRecord + operationRecordRouter.DELETE("deleteSysOperationRecordByIds", operationRecordApi.DeleteSysOperationRecordByIds) // 批量删除SysOperationRecord + operationRecordRouter.GET("findSysOperationRecord", operationRecordApi.FindSysOperationRecord) // 根据ID获取SysOperationRecord + operationRecordRouter.GET("getSysOperationRecordList", operationRecordApi.GetSysOperationRecordList) // 获取SysOperationRecord列表 + + } +} diff --git a/admin/server/router/system/sys_params.go b/admin/server/router/system/sys_params.go new file mode 100644 index 000000000..50dd2364e --- /dev/null +++ b/admin/server/router/system/sys_params.go @@ -0,0 +1,25 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +type SysParamsRouter struct{} + +// InitSysParamsRouter 初始化 参数 路由信息 +func (s *SysParamsRouter) InitSysParamsRouter(Router *gin.RouterGroup, PublicRouter *gin.RouterGroup) { + sysParamsRouter := Router.Group("sysParams").Use(middleware.OperationRecord()) + sysParamsRouterWithoutRecord := Router.Group("sysParams") + { + sysParamsRouter.POST("createSysParams", sysParamsApi.CreateSysParams) // 新建参数 + sysParamsRouter.DELETE("deleteSysParams", sysParamsApi.DeleteSysParams) // 删除参数 + sysParamsRouter.DELETE("deleteSysParamsByIds", sysParamsApi.DeleteSysParamsByIds) // 批量删除参数 + sysParamsRouter.PUT("updateSysParams", sysParamsApi.UpdateSysParams) // 更新参数 + } + { + sysParamsRouterWithoutRecord.GET("findSysParams", sysParamsApi.FindSysParams) // 根据ID获取参数 + sysParamsRouterWithoutRecord.GET("getSysParamsList", sysParamsApi.GetSysParamsList) // 获取参数列表 + sysParamsRouterWithoutRecord.GET("getSysParam", sysParamsApi.GetSysParam) // 根据Key获取参数 + } +} diff --git a/admin/server/router/system/sys_system.go b/admin/server/router/system/sys_system.go new file mode 100644 index 000000000..1a9643f35 --- /dev/null +++ b/admin/server/router/system/sys_system.go @@ -0,0 +1,22 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +type SysRouter struct{} + +func (s *SysRouter) InitSystemRouter(Router *gin.RouterGroup) { + sysRouter := Router.Group("system").Use(middleware.OperationRecord()) + sysRouterWithoutRecord := Router.Group("system") + + { + sysRouter.POST("setSystemConfig", systemApi.SetSystemConfig) // 设置配置文件内容 + sysRouter.POST("reloadSystem", systemApi.ReloadSystem) // 重启服务 + } + { + sysRouterWithoutRecord.POST("getSystemConfig", systemApi.GetSystemConfig) // 获取配置文件内容 + sysRouterWithoutRecord.POST("getServerInfo", systemApi.GetServerInfo) // 获取服务器信息 + } +} diff --git a/admin/server/router/system/sys_user.go b/admin/server/router/system/sys_user.go new file mode 100644 index 000000000..3302e345e --- /dev/null +++ b/admin/server/router/system/sys_user.go @@ -0,0 +1,29 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +type UserRouter struct{} + +func (s *UserRouter) InitUserRouter(Router *gin.RouterGroup) { + userRouter := Router.Group("user").Use(middleware.OperationRecord()) + userRouterWithoutRecord := Router.Group("user") + { + userRouter.POST("admin_register", baseApi.Register) // 管理员注册账号 + userRouter.POST("changePassword", baseApi.ChangePassword) // 用户修改密码 + userRouter.POST("setUserAuthority", baseApi.SetUserAuthority) // 设置用户权限 + userRouter.DELETE("deleteUser", baseApi.DeleteUser) // 删除用户 + userRouter.PUT("setUserInfo", baseApi.SetUserInfo) // 设置用户信息 + userRouter.PUT("setSelfInfo", baseApi.SetSelfInfo) // 设置自身信息 + userRouter.POST("setUserAuthorities", baseApi.SetUserAuthorities) // 设置用户权限组 + userRouter.POST("resetPassword", baseApi.ResetPassword) // 设置用户权限组 + userRouter.PUT("setSelfSetting", baseApi.SetSelfSetting) // 用户界面配置 + } + { + userRouterWithoutRecord.POST("getUserList", baseApi.GetUserList) // 分页获取用户列表 + userRouterWithoutRecord.GET("getUserInfo", baseApi.GetUserInfo) // 获取自身信息 + userRouterWithoutRecord.POST("sync", baseApi.SyncUser) // Extend: 执行同步用户 + } +} diff --git a/admin/server/service/enter.go b/admin/server/service/enter.go new file mode 100644 index 000000000..866ef9fc8 --- /dev/null +++ b/admin/server/service/enter.go @@ -0,0 +1,15 @@ +package service + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/service/example" + "github.com/flipped-aurora/gin-vue-admin/server/service/gaia" + "github.com/flipped-aurora/gin-vue-admin/server/service/system" +) + +var ServiceGroupApp = new(ServiceGroup) + +type ServiceGroup struct { + SystemServiceGroup system.ServiceGroup + ExampleServiceGroup example.ServiceGroup + GaiaServiceGroup gaia.ServiceGroup +} diff --git a/admin/server/service/example/enter.go b/admin/server/service/example/enter.go new file mode 100644 index 000000000..c5a7ddaa2 --- /dev/null +++ b/admin/server/service/example/enter.go @@ -0,0 +1,6 @@ +package example + +type ServiceGroup struct { + CustomerService + FileUploadAndDownloadService +} diff --git a/admin/server/service/example/exa_breakpoint_continue.go b/admin/server/service/example/exa_breakpoint_continue.go new file mode 100644 index 000000000..d0363bb5d --- /dev/null +++ b/admin/server/service/example/exa_breakpoint_continue.go @@ -0,0 +1,71 @@ +package example + +import ( + "errors" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/example" + "gorm.io/gorm" +) + +type FileUploadAndDownloadService struct{} + +var FileUploadAndDownloadServiceApp = new(FileUploadAndDownloadService) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: FindOrCreateFile +//@description: 上传文件时检测当前文件属性,如果没有文件则创建,有则返回文件的当前切片 +//@param: fileMd5 string, fileName string, chunkTotal int +//@return: file model.ExaFile, err error + +func (e *FileUploadAndDownloadService) FindOrCreateFile(fileMd5 string, fileName string, chunkTotal int) (file example.ExaFile, err error) { + var cfile example.ExaFile + cfile.FileMd5 = fileMd5 + cfile.FileName = fileName + cfile.ChunkTotal = chunkTotal + + if errors.Is(global.GVA_DB.Where("file_md5 = ? AND is_finish = ?", fileMd5, true).First(&file).Error, gorm.ErrRecordNotFound) { + err = global.GVA_DB.Where("file_md5 = ? AND file_name = ?", fileMd5, fileName).Preload("ExaFileChunk").FirstOrCreate(&file, cfile).Error + return file, err + } + cfile.IsFinish = true + cfile.FilePath = file.FilePath + err = global.GVA_DB.Create(&cfile).Error + return cfile, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: CreateFileChunk +//@description: 创建文件切片记录 +//@param: id uint, fileChunkPath string, fileChunkNumber int +//@return: error + +func (e *FileUploadAndDownloadService) CreateFileChunk(id uint, fileChunkPath string, fileChunkNumber int) error { + var chunk example.ExaFileChunk + chunk.FileChunkPath = fileChunkPath + chunk.ExaFileID = id + chunk.FileChunkNumber = fileChunkNumber + err := global.GVA_DB.Create(&chunk).Error + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: DeleteFileChunk +//@description: 删除文件切片记录 +//@param: fileMd5 string, fileName string, filePath string +//@return: error + +func (e *FileUploadAndDownloadService) DeleteFileChunk(fileMd5 string, filePath string) error { + var chunks []example.ExaFileChunk + var file example.ExaFile + err := global.GVA_DB.Where("file_md5 = ? ", fileMd5).First(&file). + Updates(map[string]interface{}{ + "IsFinish": true, + "file_path": filePath, + }).Error + if err != nil { + return err + } + err = global.GVA_DB.Where("exa_file_id = ?", file.ID).Delete(&chunks).Unscoped().Error + return err +} diff --git a/admin/server/service/example/exa_customer.go b/admin/server/service/example/exa_customer.go new file mode 100644 index 000000000..cf816f5a3 --- /dev/null +++ b/admin/server/service/example/exa_customer.go @@ -0,0 +1,87 @@ +package example + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/example" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + systemService "github.com/flipped-aurora/gin-vue-admin/server/service/system" +) + +type CustomerService struct{} + +var CustomerServiceApp = new(CustomerService) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: CreateExaCustomer +//@description: 创建客户 +//@param: e model.ExaCustomer +//@return: err error + +func (exa *CustomerService) CreateExaCustomer(e example.ExaCustomer) (err error) { + err = global.GVA_DB.Create(&e).Error + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: DeleteFileChunk +//@description: 删除客户 +//@param: e model.ExaCustomer +//@return: err error + +func (exa *CustomerService) DeleteExaCustomer(e example.ExaCustomer) (err error) { + err = global.GVA_DB.Delete(&e).Error + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: UpdateExaCustomer +//@description: 更新客户 +//@param: e *model.ExaCustomer +//@return: err error + +func (exa *CustomerService) UpdateExaCustomer(e *example.ExaCustomer) (err error) { + err = global.GVA_DB.Save(e).Error + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetExaCustomer +//@description: 获取客户信息 +//@param: id uint +//@return: customer model.ExaCustomer, err error + +func (exa *CustomerService) GetExaCustomer(id uint) (customer example.ExaCustomer, err error) { + err = global.GVA_DB.Where("id = ?", id).First(&customer).Error + return +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetCustomerInfoList +//@description: 分页获取客户列表 +//@param: sysUserAuthorityID string, info request.PageInfo +//@return: list interface{}, total int64, err error + +func (exa *CustomerService) GetCustomerInfoList(sysUserAuthorityID uint, info request.PageInfo) (list interface{}, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + db := global.GVA_DB.Model(&example.ExaCustomer{}) + var a system.SysAuthority + a.AuthorityId = sysUserAuthorityID + auth, err := systemService.AuthorityServiceApp.GetAuthorityInfo(a) + if err != nil { + return + } + var dataId []uint + for _, v := range auth.DataAuthorityId { + dataId = append(dataId, v.AuthorityId) + } + var CustomerList []example.ExaCustomer + err = db.Where("sys_user_authority_id in ?", dataId).Count(&total).Error + if err != nil { + return CustomerList, total, err + } else { + err = db.Limit(limit).Offset(offset).Preload("SysUser").Where("sys_user_authority_id in ?", dataId).Find(&CustomerList).Error + } + return CustomerList, total, err +} diff --git a/admin/server/service/example/exa_file_upload_download.go b/admin/server/service/example/exa_file_upload_download.go new file mode 100644 index 000000000..cca3ec516 --- /dev/null +++ b/admin/server/service/example/exa_file_upload_download.go @@ -0,0 +1,118 @@ +package example + +import ( + "errors" + "mime/multipart" + "strings" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/example" + "github.com/flipped-aurora/gin-vue-admin/server/utils/upload" +) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: Upload +//@description: 创建文件上传记录 +//@param: file model.ExaFileUploadAndDownload +//@return: error + +func (e *FileUploadAndDownloadService) Upload(file example.ExaFileUploadAndDownload) error { + return global.GVA_DB.Create(&file).Error +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: FindFile +//@description: 查询文件记录 +//@param: id uint +//@return: model.ExaFileUploadAndDownload, error + +func (e *FileUploadAndDownloadService) FindFile(id uint) (example.ExaFileUploadAndDownload, error) { + var file example.ExaFileUploadAndDownload + err := global.GVA_DB.Where("id = ?", id).First(&file).Error + return file, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: DeleteFile +//@description: 删除文件记录 +//@param: file model.ExaFileUploadAndDownload +//@return: err error + +func (e *FileUploadAndDownloadService) DeleteFile(file example.ExaFileUploadAndDownload) (err error) { + var fileFromDb example.ExaFileUploadAndDownload + fileFromDb, err = e.FindFile(file.ID) + if err != nil { + return + } + oss := upload.NewOss() + if err = oss.DeleteFile(fileFromDb.Key); err != nil { + return errors.New("文件删除失败") + } + err = global.GVA_DB.Where("id = ?", file.ID).Unscoped().Delete(&file).Error + return err +} + +// EditFileName 编辑文件名或者备注 +func (e *FileUploadAndDownloadService) EditFileName(file example.ExaFileUploadAndDownload) (err error) { + var fileFromDb example.ExaFileUploadAndDownload + return global.GVA_DB.Where("id = ?", file.ID).First(&fileFromDb).Update("name", file.Name).Error +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetFileRecordInfoList +//@description: 分页获取数据 +//@param: info request.PageInfo +//@return: list interface{}, total int64, err error + +func (e *FileUploadAndDownloadService) GetFileRecordInfoList(info request.PageInfo) (list interface{}, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + keyword := info.Keyword + db := global.GVA_DB.Model(&example.ExaFileUploadAndDownload{}) + var fileLists []example.ExaFileUploadAndDownload + if len(keyword) > 0 { + db = db.Where("name LIKE ?", "%"+keyword+"%") + } + err = db.Count(&total).Error + if err != nil { + return + } + err = db.Limit(limit).Offset(offset).Order("updated_at desc").Find(&fileLists).Error + return fileLists, total, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: UploadFile +//@description: 根据配置文件判断是文件上传到本地或者七牛云 +//@param: header *multipart.FileHeader, noSave string +//@return: file model.ExaFileUploadAndDownload, err error + +func (e *FileUploadAndDownloadService) UploadFile(header *multipart.FileHeader, noSave string) (file example.ExaFileUploadAndDownload, err error) { + oss := upload.NewOss() + filePath, key, uploadErr := oss.UploadFile(header) + if uploadErr != nil { + return file, uploadErr + } + s := strings.Split(header.Filename, ".") + f := example.ExaFileUploadAndDownload{ + Url: filePath, + Name: header.Filename, + Tag: s[len(s)-1], + Key: key, + } + if noSave == "0" { + return f, e.Upload(f) + } + return f, nil +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: ImportURL +//@description: 导入URL +//@param: file model.ExaFileUploadAndDownload +//@return: error + +func (e *FileUploadAndDownloadService) ImportURL(file *[]example.ExaFileUploadAndDownload) error { + return global.GVA_DB.Create(&file).Error +} diff --git a/admin/server/service/gaia/account.go b/admin/server/service/gaia/account.go new file mode 100644 index 000000000..79b2dc990 --- /dev/null +++ b/admin/server/service/gaia/account.go @@ -0,0 +1,230 @@ +package gaia + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/gaia" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "go.uber.org/zap" + "io" + "net/http" + "regexp" + "time" +) + +// IsUserPasswordValid +// @author: [piexlmax](https://github.com/piexlmax) +// @author: [SliverHorn](https://github.com/SliverHorn) +// @function: IsUserPasswordValid +// @description: User Password Valid +// @param: passwd +// @return info request.GetUserInfoByUserName, err error +func IsUserPasswordValid(passwd string) (err error) { + // Check if the string is at least 8 characters long + if len(passwd) < 8 { + return errors.New("请使用最少8位且最少有一个字母数字组合的密码") + } + + // Check if the string contains at least one letter + if containsLetter := regexp.MustCompile(`[a-zA-Z]`).MatchString(passwd); !containsLetter { + return errors.New("请最少最少有一个字母组合的密码") + } + + // Check if the string contains at least one digit + if containsLetter := regexp.MustCompile(`\d`).MatchString(passwd); !containsLetter { + return errors.New("请最少最少有一个数字组合的密码") + } + + return nil +} + +// GetSysUser +// @author: [piexlmax](https://github.com/piexlmax) +// @author: [SliverHorn](https://github.com/SliverHorn) +// @function: GetSysUser +// @description: Get user information by account +// @param: account gaia.Account +// @return system.SysUser, error +func GetSysUser(account gaia.Account) (system.SysUser, error) { + // init + var err error + var user system.SysUser + var integrate gaia.AccountIntegrate + if err = global.GVA_DB.Where("account_id=? AND provider=?", + account.ID, gaia.DefaultProviderType).First(&integrate).Error; err != nil { + return user, errors.New("the relation between provider and user cannot be found") + } + // 查询用户信息 + if err = global.GVA_DB.Where("username=?", account.Name).First(&user).Error; err != nil { + return user, errors.New("no relevant users found locally") + } + // 获取相关用户信息 + if err = global.GVA_DB.Where("username=?", integrate.OpenID).First(&user).Error; err != nil { + return user, errors.New("unable to find any related user in the database") + } + // return + return user, nil +} + +// SyncUserStatus +// @author: [piexlmax](https://github.com/piexlmax) +// @author: [SliverHorn](https://github.com/SliverHorn) +// @function: SyncUserStatus +// @description: 同步用户状态 +func SyncUserStatus() { + // init + var err error + var account []gaia.Account + var userDick = make(map[string]int) + if err = global.GVA_DB.Find(&account).Error; err != nil { + return + } + for _, v := range account { + var userStatus = system.UserActive + if v.Status != gaia.UserActive { + userStatus = system.UserDeactivate + } + userDick[v.Email] = userStatus + } + // 获取gva用户列表 + var userList []system.SysUser + if err = global.GVA_DB.Find(&userList).Error; err != nil { + return + } + // 循环用户列表 + for _, v := range userList { + if info, ok := userDick[v.Email]; ok { + if v.Enable != info { + global.GVA_DB.Model(&v).Updates(&map[string]interface{}{ + "enable": info, + }) + } + } + } +} + +// RegisterUser +// @author: [piexlmax](https://github.com/piexlmax) +// @author: [SliverHorn](https://github.com/SliverHorn) +// @function: RegisterUser +// @description: Gaia用户注册函数 +// @param: u system.SysUser, token string +// @return: err error, userInter *model.SysUser +func RegisterUser(u system.SysUser, token string) (err error) { + // 初始化密码 + var body []byte + var s PasswdEncode + var passwordHashed, salt string + global.GVA_LOG.Debug("注册用户信息:", zap.Any("1", 1)) + if passwordHashed, salt, err = s.EncodePassword(u.Password); err != nil { + return + } + global.GVA_LOG.Debug("注册用户信息:", zap.Any("1", 1)) + var acc gaia.Account + if err = global.GVA_DB.Where("email=?", u.Email).First(&acc).Error; err == nil { + // 用户已存在 + global.GVA_LOG.Info(fmt.Sprintf("account %s", acc.Name)) + return nil + } + // 默认以root执行 + var adminUser system.SysUser + if err = global.GVA_DB.Where("authority_id=?", system.AdminAuthorityId).Order( + "id asc").First(&adminUser).Error; err != nil { + return err + } + + global.GVA_LOG.Debug("注册用户信息:", zap.Any("1", 1)) + if token, _, err = utils.LoginToken(&adminUser); err != nil { + return err + } + + global.GVA_LOG.Debug("注册用户信息:", zap.Any("1", 1)) + // 合成用户新建 + if body, err = json.Marshal(&map[string]interface{}{ + "name": u.Username, + "nick": u.NickName, + "email": u.Email, + }); err != nil { + return err + } + + global.GVA_LOG.Debug("注册用户信息:", zap.Any("1", 1)) + // 请求远程创建 + var res *http.Response + req, _ := http.NewRequest("POST", fmt.Sprintf( + "%s/console/api/admin_register_user", global.GVA_CONFIG.Gaia.Url), bytes.NewBuffer(body)) + req.Header.Add("content-type", "application/json") + req.Header.Add("Authorization", "Bearer "+token) + req.Header.Add("console_token", token) + if res, err = http.DefaultClient.Do(req); err != nil { + return err + } + var bodyByte []byte + var bodyMap map[string]string + if bodyByte, err = io.ReadAll(res.Body); err != nil { + return err + } + + global.GVA_LOG.Debug("注册用户信息:", zap.Any("1", 1)) + _ = res.Body.Close() + if err = json.Unmarshal(bodyByte, &bodyMap); err != nil { + return err + } + + global.GVA_LOG.Debug("注册用户信息:", zap.Any("1", 1)) + // result + if result, ok := bodyMap["result"]; !ok && result != "success" { + return errors.New("failed to create user") + } + // 修改密码 + var account gaia.Account + if account, err = u.GetAccount(); err != nil { + return err + } + + global.GVA_LOG.Debug("注册用户信息:", zap.Any("1", 1)) + // 修改密码 + global.GVA_DB.Model(&account).Updates(&map[string]interface{}{ + "password": passwordHashed, + "password_salt": salt, + }) + // 完成 + return nil +} + +// SyncExecuteCode +// @author: [piexlmax](https://github.com/piexlmax) +// @author: [SliverHorn](https://github.com/SliverHorn) +// @function: SyncExecuteCode +// @description: 同步代码执行器 +func SyncExecuteCode() { + // init + var err error + var uidList []uint + var globalCode []system.SysUserGlobalCode + if err = global.GVA_DB.Find(&globalCode).Error; err == nil { + for _, v := range globalCode { + uidList = append(uidList, v.UserID) + } + } + // 获取所有邮箱号列表 + var mailList []string + var userList []system.SysUser + if err = global.GVA_DB.Select("email").Where("id IN (?)", uidList).Find(&userList).Error; err == nil { + for _, v := range userList { + mailList = append(mailList, v.Email) + } + } + // 储存redis + var mailByte []byte + if mailByte, err = json.Marshal(&mailList); err != nil { + mailByte = []byte("[]") + } + // save + global.GVA_Dify_REDIS.Set(context.Background(), "control_mail", string(mailByte), time.Hour*168) +} diff --git a/admin/server/service/gaia/copy.go b/admin/server/service/gaia/copy.go new file mode 100644 index 000000000..510d21e00 --- /dev/null +++ b/admin/server/service/gaia/copy.go @@ -0,0 +1,267 @@ +package gaia + +import ( + "database/sql" + "errors" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/gaia/request" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/google/uuid" + "strconv" + "strings" + "time" +) + +var SyncDatabaseLock bool + +// GetDatabaseTableColumns +// @Tags Database +// @Summary 获取数据库表结构 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +func (e *TestService) GetDatabaseTableColumns(table string) (nameList []request.DatabaseTableColumn, err error) { + var rows *sql.Rows + if rows, err = global.GVA_DB.Raw("SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE "+ + "table_schema=? AND table_name=?", request.PostgreSQLDefaultSchema, table).Rows(); err != nil { + return nameList, errors.New("table columns error:" + err.Error()) + } + // 读取column_name + for rows.Next() { + var columnName, dataType, isNullable sql.NullString + if err = rows.Scan(&columnName, &dataType, &isNullable); err == nil { + var nullable = false + if isNullable.String == "YES" { + nullable = true + } + nameList = append(nameList, request.DatabaseTableColumn{ + ColumnName: columnName.String, + DataType: dataType.String, + IsNullable: nullable, + }) + } + } + // return + return nameList, nil +} + +// ForeachInstall +// @Tags Database +// @Summary 循环同步数据 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +func (e *TestService) ForeachInstall( + logTable, newTable, keyName, orderName string, logColumnList, + queryColumnList []string, addColumn map[string]interface{}, groupName, groupValue string) bool { + // init + var err error + var where string + // 查询新表最旧的数据到了哪 + var rows *sql.Rows + var orderNumber sql.NullString + orderText := fmt.Sprintf("\"%s\",\"%s\"", groupName, orderName) + if err = global.GVA_DB.Raw(fmt.Sprintf("SELECT \"%s\" FROM \"%s\" WHERE \"%s\"='%s' ORDER BY %s ASC LIMIT 1", + orderName, newTable, groupName, groupValue, orderText)).Row().Scan(&orderNumber); err != nil { + // 查询旧表数据 + if len(groupName) > 0 { + where = fmt.Sprintf("WHERE \"%s\"='%s'", groupName, groupValue) + } + } else { + if len(groupName) > 0 { + where = fmt.Sprintf("WHERE \"%s\"='%s' AND \"%s\"<='%s'", groupName, groupValue, orderName, orderNumber.String) + } else { + where = fmt.Sprintf("WHERE \"%s\"<='%s'", orderName, orderNumber.String) + } + } + // 查询旧表数据 + global.GVA_LOG.Info("ForeachInstall:" + groupName + " " + groupValue + " " + orderNumber.String) + if rows, err = global.GVA_DB.Raw(fmt.Sprintf("SELECT %s FROM \"%s\" %s ORDER BY %s DESC LIMIT %d", + strings.Join(queryColumnList, ","), logTable, where, orderText, request.PostgreSQLDataLimit)).Rows(); err != nil { + return true + } + defer rows.Close() + var results []map[string]interface{} + for rows.Next() { + // 创建一个 map 来保存每一条记录 + record := make(map[string]interface{}) + vals := make([]interface{}, len(logColumnList)) + for i := range vals { + vals[i] = new(interface{}) + } + if err = rows.Scan(vals...); err != nil { + global.GVA_LOG.Fatal("failed to scan row: %v" + err.Error()) + } + for i, col := range logColumnList { + record[col] = *(vals[i].(*interface{})) + } + // 不跳过执行 + results = append(results, record) + } + + // 3. 构建并执行 INSERT INTO 语句 + var values []interface{} + var sqlStatement []string + for _, result := range results { + placeholders := make([]string, 0, len(logColumnList)) + for _, col := range logColumnList { + placeholders = append(placeholders, "?") + values = append(values, result[col]) + } + // 新增增加的key + for _, value := range addColumn { + placeholders = append(placeholders, "?") + values = append(values, value) + } + // 整合成 (a, b, add) + sqlStatement = append(sqlStatement, fmt.Sprintf("(%s)", strings.Join(placeholders, ","))) + } + // logColumnList 追加新增键 + for key, _ := range addColumn { + logColumnList = append(logColumnList, key) + } + // sqlStatement length + if len(sqlStatement) > 0 { + // 执行插入 + if result := global.GVA_DB.Exec(fmt.Sprintf("INSERT INTO %s (%s) VALUES %s ON CONFLICT (%s) DO NOTHING;", + newTable, strings.Join(logColumnList, ", "), strings.Join(sqlStatement, ", "), keyName), values...); result.Error != nil { + global.GVA_LOG.Debug("insert error: %v" + result.Error.Error()) + return true + } + } + // return + if len(results) == request.PostgreSQLDataLimit { + return true + } + return false +} + +// SyncDatabaseTableData +// @Tags Test +// @Summary 同步数据库表数据 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +func (e *TestService) SyncDatabaseTableData(logTable, newTable, keyName, orderName, groupName string) { + // 查询旧表 + var err error + if SyncDatabaseLock { + return + } + SyncDatabaseLock = true + var logColumns, newColumns []request.DatabaseTableColumn + if logColumns, err = e.GetDatabaseTableColumns(logTable); err != nil { + global.GVA_LOG.Debug("log " + err.Error()) + SyncDatabaseLock = false + return + } + // new columns + if newColumns, err = e.GetDatabaseTableColumns(newTable); err != nil { + global.GVA_LOG.Debug("log " + err.Error()) + SyncDatabaseLock = false + return + } + // 储存旧列表 + var logColumnList, queryColumnList []string + var addColumn = make(map[string]interface{}) + var newTime = time.Now().Format("2006-01-02 15:04:05") + for _, v := range logColumns { + logColumnList = append(logColumnList, v.ColumnName) + queryColumnList = append(queryColumnList, fmt.Sprintf("\"%s\"", v.ColumnName)) + } + // 判断新增了什么字段 + for _, v := range newColumns { + if utils.InStringArray(v.ColumnName, logColumnList) { + // 非新增数据 + continue + } + // 新增的字段 + if v.IsNullable { + // 默认为空 + addColumn[v.ColumnName] = nil + } else { + // 新增的字段 + switch v.DataType { + case request.PostgreSQLDataTypeUUID: + addColumn[v.ColumnName] = uuid.New().String() + break + case request.PostgreSQLDataTypeCharacterVarying, request.PostgreSQLDataTypeText: + addColumn[v.ColumnName] = "" + break + case request.PostgreSQLDataTypeJSON: + addColumn[v.ColumnName] = "{}" + break + case request.PostgreSQLDataTypeInteger, request.PostgreSQLDataTypeDoublePrecision: + addColumn[v.ColumnName] = 0 + break + case request.PostgreSQLDataTypeNumeric: + addColumn[v.ColumnName] = 0.01 + break + case request.PostgreSQLDataTypeTimestampWithoutTZ: + addColumn[v.ColumnName] = newTime + break + case request.PostgreSQLDataTypeBoolean: + addColumn[v.ColumnName] = false + break + } + } + } + // 判断是否有groupName + if len(groupName) > 0 { + var rows *sql.Rows + if rows, err = global.GVA_DB.Raw(fmt.Sprintf("SELECT %s FROM %s GROUP BY %s", groupName, logTable, groupName)).Rows(); err != nil { + global.GVA_LOG.Debug("log SELECT" + err.Error()) + SyncDatabaseLock = false + return + } + // 循环app列表 + var i = 0 + for rows.Next() { + // 提取所有关联 group 列 + var groupValue string + var group interface{} + if err = rows.Scan(&group); err != nil { + global.GVA_LOG.Debug("log Scan" + err.Error()) + SyncDatabaseLock = false + return + } + // 区分group列内容 + switch value := group.(type) { + case string: + groupValue = value + case int: + groupValue = strconv.Itoa(value) + case int32: + groupValue = strconv.Itoa(int(value)) + case int64: + groupValue = strconv.Itoa(int(value)) + case uint: + groupValue = strconv.Itoa(int(value)) + } + // 是否有内容 + if len(groupValue) == 0 { + global.GVA_LOG.Debug("log groupValue is null") + SyncDatabaseLock = false + return + } + // 同步完后歇半秒 + global.GVA_LOG.Info(fmt.Sprintf("SyncDatabaseTableData run app: %d", i)) + for e.ForeachInstall(logTable, newTable, keyName, orderName, logColumnList, queryColumnList, addColumn, groupName, groupValue) { + // 延迟半秒 + time.Sleep(time.Millisecond * 500) + } + // i++ + i += 1 + } + } else { + // 同步完后歇半秒 + for e.ForeachInstall(logTable, newTable, keyName, orderName, logColumnList, queryColumnList, addColumn, groupName, "") { + // 延迟半秒 + time.Sleep(time.Millisecond * 500) + } + } + // stop + SyncDatabaseLock = false + fmt.Println("SyncDatabaseTableData stop") +} diff --git a/admin/server/service/gaia/dashboard.go b/admin/server/service/gaia/dashboard.go new file mode 100644 index 000000000..5cf14f913 --- /dev/null +++ b/admin/server/service/gaia/dashboard.go @@ -0,0 +1,478 @@ +package gaia + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/gaia" + gaiaReq "github.com/flipped-aurora/gin-vue-admin/server/model/gaia/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/gaia/response" + "github.com/gofrs/uuid/v5" + "github.com/redis/go-redis/v9" + "go.uber.org/zap" + "time" +) + +type DashboardService struct{} + +// GetAccountQuotaRankingData 分页获取【账号】额度排名列表 +func (dashboardService *DashboardService) GetAccountQuotaRankingData(info gaiaReq.GetAccountQuotaRankingDataReq) (list []response.GetAccountQuotaRankingDataRes, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + + db := global.GVA_DB.Model(&gaia.AccountMoneyExtend{}).Order("used_quota desc") + var accountMoneys []gaia.AccountMoneyExtend + err = db.Count(&total).Error + if err != nil { + return + } + + if limit != 0 { + db = db.Limit(limit).Offset(offset) + } + + err = db.Find(&accountMoneys).Error + if err != nil { + err = fmt.Errorf("查询账号额度信息失败:%s", err.Error()) + return + } + + // 账号ID集合,方便后面一次性查出 + var accountIds []uuid.UUID + for _, money := range accountMoneys { + accountIds = append(accountIds, money.AccountId) + } + + // 查询用户信息 + var accountInfos = make(map[uuid.UUID]gaia.Account) + var accounts []gaia.Account + err = global.GVA_DB.Model(&gaia.Account{}).Where("id in ?", accountIds).Find(&accounts).Error + if err != nil { + err = fmt.Errorf("查询账户信息失败:%s", err.Error()) + return + } + for _, account := range accounts { + accountInfos[account.ID] = account + } + + // 拼接结果 + for i, money := range accountMoneys { + var accountInfo gaia.Account + var isExist bool + if accountInfo, isExist = accountInfos[money.AccountId]; !isExist { + global.GVA_LOG.Error("账户信息不存在!", zap.String("account_id", money.AccountId.String())) + continue + } + row := response.GetAccountQuotaRankingDataRes{ + Ranking: i + 1 + offset, + Name: accountInfo.Name, + UsedQuota: money.UsedQuota, + TotalQuota: money.TotalQuota, + } + list = append(list, row) + } + + return list, total, err +} + +// GetAppQuotaRankingData 分页获取【应用】配额排名数据 +func (dashboardService *DashboardService) GetAppQuotaRankingData(info gaiaReq.GetAppQuotaRankingDataReq) (list []response.GetAppQuotaRankingDataRes, total int64, err error) { + + cacheKey := fmt.Sprintf("app_token_quota_ranking:%d:%d", info.Page, info.PageSize) + var cachedResult struct { + List []response.GetAppQuotaRankingDataRes + Total int64 + } + + if found, err := dashboardService.getCachedResult(cacheKey, &cachedResult); err == nil && found { + return cachedResult.List, cachedResult.Total, nil + } + + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + + /** + 查出应用花费最多的应用排序 + */ + // 创建子查询 + messageCosts := global.GVA_DB.Table("public.messages"). + Select("" + + "app_id, " + + "COUNT(id) as message_num, " + + "SUM(total_price) as message_cost"). + Group("app_id") + + workflowCosts := global.GVA_DB.Table("public.workflow_node_executions"). + Select("" + + "app_id, " + + "COUNT(id) as workflow_num, " + + "SUM(CAST((execution_metadata::json->>'total_price') AS NUMERIC)) AS workflow_cost"). + Where("execution_metadata IS NOT NULL AND execution_metadata != '' AND (execution_metadata::json->>'total_price') IS NOT NULL"). + Group("app_id") + + // 主查询 + query := global.GVA_DB.Table("(?) AS m", messageCosts). + Select(""+ + "COALESCE(m.app_id, w.app_id) AS app_id, "+ + "COALESCE(m.message_cost, 0) AS message_cost, "+ + "COALESCE(w.workflow_cost, 0) AS workflow_cost, "+ + "COALESCE(m.message_num, 0) + COALESCE(w.workflow_num, 0) AS record_num, "+ + "COALESCE(m.message_cost, 0) + COALESCE(w.workflow_cost, 0) AS total_cost"). + Joins("FULL OUTER JOIN (?) AS w ON m.app_id = w.app_id", workflowCosts). + Order("total_cost DESC") + + // 获取总数 + err = query.Count(&total).Error + if err != nil { + return nil, 0, fmt.Errorf("获取总数失败:%w", err) + } + + // 应用分页 + if limit != 0 { + query = query.Limit(limit).Offset(offset) + } + + // 执行查询 + var results []struct { + AppID string `gorm:"column:app_id"` + TotalCost float64 `gorm:"column:total_cost"` + MessageCost float64 `gorm:"column:message_cost"` + WorkflowCost float64 `gorm:"column:workflow_cost"` + RecordNum float64 `gorm:"column:record_num"` + } + + err = query.Find(&results).Error + if err != nil { + return nil, 0, fmt.Errorf("查询数据失败:%w", err) + } + + // 构建返回结果并获取app_id集合 + appIDs := make([]string, 0, len(results)) + for _, r := range results { + appIDs = append(appIDs, r.AppID) + } + + /** + 查询APP信息 + */ + var appInfos = make(map[string]gaia.Apps) + var apps []gaia.Apps + err = global.GVA_DB.Model(&gaia.Apps{}).Where("id in ?", appIDs).Find(&apps).Error + if err != nil { + err = fmt.Errorf("查询应用信息失败:%w", err) + return + } + tenantIDs := make([]string, 0, len(results)) + for _, app := range apps { + appInfos[app.ID.String()] = app + tenantIDs = append(tenantIDs, app.TenantID.String()) + } + + /** + 查询所在的工作区信息 + */ + var tenants []gaia.Tenants + err = global.GVA_DB.Model(&gaia.Tenants{}).Where("id in ?", tenantIDs).Find(&tenants).Error + if err != nil { + err = fmt.Errorf("查询租户信息失败:%w", err) + return + } + tenantMap := make(map[string]gaia.Tenants) + for _, tenant := range tenants { + tenantMap[tenant.Id] = tenant + } + + /** + 查询工作区对应用户信息 + */ + // 1. 查询 TenantAccountJoins + var tenantAccountJoins []gaia.TenantAccountJoins + err = global.GVA_DB.Model(&gaia.TenantAccountJoins{}). + Where("tenant_id IN ? AND role = ?", tenantIDs, "owner"). + Find(&tenantAccountJoins).Error + if err != nil { + err = fmt.Errorf("查询租户账号关联信息失败:%w", err) + return + } + + // 2. 提取账号 ID + accountIDs := make([]string, 0, len(tenantAccountJoins)) + tenantToAccountMap := make(map[string]string) + for _, join := range tenantAccountJoins { + accountIDs = append(accountIDs, join.AccountID.String()) + tenantToAccountMap[join.TenantID.String()] = join.AccountID.String() + } + + // 3. 查询账号信息 + var accounts []gaia.Account + err = global.GVA_DB.Model(&gaia.Account{}). + Where("id IN ?", accountIDs). + Find(&accounts).Error + if err != nil { + err = fmt.Errorf("查询账号信息失败:%w", err) + return + } + + // 4. 创建账号 ID 到账号名称的映射 + accountMap := make(map[string]string) + for _, account := range accounts { + accountMap[account.ID.String()] = account.Name + } + + /** + 查出应用使用次数AppStatisticsExtend + */ + var appStatistics []gaia.AppStatisticsExtend + err = global.GVA_DB.Model(&gaia.AppStatisticsExtend{}). + Where("app_id in ?", appIDs). + Find(&appStatistics).Error + if err != nil { + err = fmt.Errorf("查询应用界面使用次数信息失败:%w", err) + return + } + var appStatisticsMap = make(map[string]gaia.AppStatisticsExtend) + for _, appStatistic := range appStatistics { + appStatisticsMap[appStatistic.AppID.String()] = appStatistic + } + + // 组装数据 + for i, r := range results { + appInfo := appInfos[r.AppID] + tenantID := appInfo.TenantID.String() + accountID := tenantToAccountMap[tenantID] + accountName := accountMap[accountID] + appStatistic := appStatisticsMap[r.AppID] + list = append(list, response.GetAppQuotaRankingDataRes{ + Ranking: i + 1 + offset, + Name: appInfo.Name, + AccountName: accountName, + Mode: appInfos[r.AppID].Mode, + AppID: r.AppID, + TotalCost: r.TotalCost, + MessageCost: r.MessageCost, + WorkflowCost: r.WorkflowCost, + RecordNum: r.RecordNum, + UseNum: appStatistic.Number, + }) + } + + // TODO 因为只缓存了3页, + if total >= 30 { + total = 30 + } + + // 在返回结果之前,缓存结果 + result := struct { + List []response.GetAppQuotaRankingDataRes + Total int64 + }{list, total} + + if err := dashboardService.cacheResult(cacheKey, result, 1800*time.Second); err != nil { + global.GVA_LOG.Error("Failed to cache result", zap.Error(err)) + } + + return list, total, nil +} + +// GetAppTokenQuotaRankingData 分页获取【应用密钥】配额排名数据列表 +func (dashboardService *DashboardService) GetAppTokenQuotaRankingData(info gaiaReq.GetAppTokenQuotaRankingDataReq) (list []response.GetAppTokenQuotaRankingDataRes, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + + db := global.GVA_DB.Model(&gaia.ApiTokenMoneyExtend{}).Order("accumulated_quota desc") + var apiTokenMoneys []gaia.ApiTokenMoneyExtend + err = db.Count(&total).Error + if err != nil { + return + } + + if limit != 0 { + db = db.Limit(limit).Offset(offset) + } + + err = db.Find(&apiTokenMoneys).Error + if err != nil { + err = fmt.Errorf("查询密钥额度信息失败:%s", err.Error()) + return + } + + // Api_token ID集合,方便后面一次性查出 + var apiTokenIds []uuid.UUID + for _, money := range apiTokenMoneys { + apiTokenIds = append(apiTokenIds, money.AppTokenID) + } + + // 查询密钥信息 + var apiTokenInfos = make(map[uuid.UUID]gaia.ApiTokens) + var apiIDs []uuid.UUID + var apiTokens []gaia.ApiTokens + err = global.GVA_DB.Where("id in ?", apiTokenIds).Find(&apiTokens).Error + if err != nil { + err = fmt.Errorf("查询密钥信息失败:%s", err.Error()) + return + } + for _, apiToken := range apiTokens { + apiTokenInfos[apiToken.ID] = apiToken + apiIDs = append(apiIDs, apiToken.AppID) + } + + // 查询对应应用信息 + var appInfos = make(map[uuid.UUID]gaia.Apps) + var apps []gaia.Apps + err = global.GVA_DB.Where("id in ?", apiIDs).Find(&apps).Error + if err != nil { + err = fmt.Errorf("查询应用信息失败:%s", err.Error()) + return + } + for _, app := range apps { + appInfos[app.ID] = app + } + + // 拼接结果 + for i, money := range apiTokenMoneys { + var apiToken gaia.ApiTokens + var isExist bool + if apiToken, isExist = apiTokenInfos[money.AppTokenID]; !isExist { + global.GVA_LOG.Error("密钥信息不存在!", zap.String("account_id", money.AppTokenID.String())) + continue + } + var appInfo gaia.Apps + if appInfo, isExist = appInfos[apiToken.AppID]; !isExist { + global.GVA_LOG.Error("密钥对应应用信息不存在!", zap.String("account_id", apiToken.AppID.String())) + continue + } + row := response.GetAppTokenQuotaRankingDataRes{ + Ranking: i + 1 + offset, + Name: appInfo.Name, + AppToken: apiToken.GenerateToken(), + AccumulatedQuota: money.AccumulatedQuota, + DayUsedQuota: money.DayUsedQuota, + MonthUsedQuota: money.MonthUsedQuota, + DayLimitQuota: money.DayLimitQuota, + MonthLimitQuota: money.MonthLimitQuota, + } + list = append(list, row) + } + + return list, total, err +} + +// GetAppTokenDailyQuotaData 获取每天密钥花费数据列表 +func (dashboardService *DashboardService) GetAppTokenDailyQuotaData(info gaiaReq.GetAppTokenDailyQuotaDataReq) (list []response.GetAppTokenDailyQuotaDataRes, err error) { + + db := global.GVA_DB.Select("DATE(stat_at) as stat_at, SUM(day_used_quota) as day_used_quota").Model(&gaia.ApiTokenMoneyDailyStatExtend{}).Order("stat_at desc").Group("DATE(stat_at)") + var apiTokenMoneyDailyStatExtends []gaia.ApiTokenMoneyDailyStatExtend + + if info.AppId != "" { + db = db.Where("app_token_id = ?", info.AppId) + } + + if !info.StatAt.IsZero() { + db = db.Where("stat_at = ?", info.StatAt) + } + + err = db.Find(&apiTokenMoneyDailyStatExtends).Error + if err != nil { + err = fmt.Errorf("查询每日密钥花费信息失败:%s", err.Error()) + return + } + + // 拼接结果 + for _, money := range apiTokenMoneyDailyStatExtends { + row := response.GetAppTokenDailyQuotaDataRes{ + StatDate: money.StatAt.Format("2006-01-02"), + TotalUsed: money.DayUsedQuota, + } + list = append(list, row) + } + + return list, err +} + +// GetAiImageQuotaRankingData 获取【AI图片】使用量排名数据列表 +func (dashboardService *DashboardService) GetAiImageQuotaRankingData(info gaiaReq.GetAiImageQuotaRankingDataReq) (list []response.GetAiImageQuotaRankingRes, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + + db := global.GVA_DB.Table("account_layover_record_extend") + db = db.Select(` + forwarding_extend.address, + forwarding_extend.path, + SUM(account_layover_record_extend.money) AS total_cost, + COUNT(*) AS record_num, + account_layover_record_extend.info->>'model' AS model + `) + db = db.Joins("RIGHT JOIN accounts ON account_layover_record_extend.account_id = accounts.id") + db = db.Joins("RIGHT JOIN forwarding_extend ON account_layover_record_extend.forwarding_id = forwarding_extend.id") + + // 添加时间范围筛选 + if !info.StatAt.IsZero() { + startDate := info.StatAt + endDate := startDate.AddDate(0, 1, 0) // 假设查询一个月的数据 + db = db.Where("account_layover_record_extend.created_at BETWEEN ? AND ?", startDate, endDate) + } + db = db.Having("SUM(account_layover_record_extend.money) > 0") + db = db.Group("forwarding_extend.id, forwarding_extend.address, forwarding_extend.path, account_layover_record_extend.info->>'model'") + db = db.Order("total_cost DESC") + + // 应用分页 + if limit != 0 { + db = db.Limit(limit).Offset(offset) + } + + var results []struct { + Address string + Path string + TotalCost float64 + RecordNum int + Model string + } + + err = db.Find(&results).Error + if err != nil { + return nil, fmt.Errorf("查询AI图片使用量排名数据失败:%s", err.Error()) + } + + // 构建响应 + for i, result := range results { + row := response.GetAiImageQuotaRankingRes{ + Ranking: i + 1, // 假设按查询结果顺序排名 + Address: result.Address, + Path: result.Path, + Model: result.Model, + TotalCost: result.TotalCost, + RecordNum: result.RecordNum, + } + list = append(list, row) + } + + return list, nil +} + +func (dashboardService *DashboardService) cacheResult(key string, data interface{}, expiration time.Duration) error { + + jsonData, err := json.Marshal(data) + if err != nil { + return fmt.Errorf("failed to marshal data: %w", err) + } + return global.GVA_REDIS.Set(context.Background(), key, jsonData, expiration).Err() +} + +func (dashboardService *DashboardService) getCachedResult(key string, result interface{}) (bool, error) { + data, err := global.GVA_REDIS.Get(context.Background(), key).Bytes() + if err != nil { + if errors.Is(err, redis.Nil) { + return false, nil + } + return false, fmt.Errorf("failed to get cached data: %w", err) + } + + err = json.Unmarshal(data, result) + if err != nil { + return false, fmt.Errorf("failed to unmarshal cached data: %w", err) + } + + return true, nil +} diff --git a/admin/server/service/gaia/encode.go b/admin/server/service/gaia/encode.go new file mode 100644 index 000000000..c5cf5591b --- /dev/null +++ b/admin/server/service/gaia/encode.go @@ -0,0 +1,59 @@ +package gaia + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "errors" + "regexp" + + "golang.org/x/crypto/pbkdf2" +) + +type PasswdEncode struct{} + +// validPassword checks if the password matches the required pattern. +func (PasswdEncode) validPassword(password string) (string, error) { + re := regexp.MustCompile(`^(?=.*[a-zA-Z])(?=.*\d).{8,}$`) + if re.MatchString(password) { + return password, nil + } + return "", errors.New("密码必须包含字母和数字,且长度必须大于8位") +} + +// hashPassword hashes the password with the given salt using PBKDF2 and SHA-256. +func hashPassword(passwordStr string, salt []byte) string { + dk := pbkdf2.Key([]byte(passwordStr), salt, 10000, sha256.Size, sha256.New) + return hex.EncodeToString(dk) +} + +// ComparePassword compares the given password with the stored hashed password. +func (PasswdEncode) ComparePassword(passwordStr, passwordHashedBase64, saltBase64 string) (bool, error) { + salt, err := base64.StdEncoding.DecodeString(saltBase64) + if err != nil { + return false, err + } + hashedPassword := hashPassword(passwordStr, salt) + + expectedHash, err := base64.StdEncoding.DecodeString(passwordHashedBase64) + if err != nil { + return false, err + } + + return hex.EncodeToString([]byte(hashedPassword)) == hex.EncodeToString(expectedHash), nil +} + +// EncodePassword generates a salt, hashes the password, and encodes both using Base64. +func (PasswdEncode) EncodePassword(password string) (string, string, error) { + salt := make([]byte, 16) + if _, err := rand.Read(salt); err != nil { + return "", "", err + } + + base64Salt := base64.StdEncoding.EncodeToString(salt) + passwordHashed := hashPassword(password, salt) + base64PasswordHashed := base64.StdEncoding.EncodeToString([]byte(passwordHashed)) + + return base64PasswordHashed, base64Salt, nil +} diff --git a/admin/server/service/gaia/enter.go b/admin/server/service/gaia/enter.go new file mode 100644 index 000000000..3685b74ab --- /dev/null +++ b/admin/server/service/gaia/enter.go @@ -0,0 +1,9 @@ +package gaia + +type ServiceGroup struct { + SystemIntegratedService + DashboardService + QuotaService + TenantsService + TestService +} diff --git a/admin/server/service/gaia/quota.go b/admin/server/service/gaia/quota.go new file mode 100644 index 000000000..106d3c5f7 --- /dev/null +++ b/admin/server/service/gaia/quota.go @@ -0,0 +1,119 @@ +package gaia + +import ( + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/gaia" + gaiaReq "github.com/flipped-aurora/gin-vue-admin/server/model/gaia/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/gaia/response" + "github.com/gofrs/uuid/v5" + "go.uber.org/zap" + "strings" +) + +type QuotaService struct{} + +// GetQuotaManagementData +// @Tags Quota +// @Summary 额度管理列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param info gaiaReq.GetAccountQuotaRankingDataReq +// @Return list []response.GetQuotaManagementDataResponse, total int64, err error +func (dashboardService *QuotaService) GetQuotaManagementData(info gaiaReq.GetAccountQuotaRankingDataReq) ( + list []response.GetQuotaManagementDataResponse, total int64, err error) { + if info.PageSize == 0 { + info.PageSize = 10 + } + var uuidList []string + limit := info.PageSize + var accountList []gaia.Account + s := strings.TrimSpace(info.Keyword) + offset := info.PageSize * (info.Page - 1) + db := global.GVA_DB.Model(&gaia.AccountMoneyExtend{}).Order("used_quota desc") + if len(s) > 0 { + s = fmt.Sprintf("%%%s%%", s) + if err = global.GVA_DB.Debug().Select("id").Where( + "\"name\" LIKE ? OR \"email\" LIKE ?", s, s).Find(&accountList).Error; err == nil { + for _, v := range accountList { + uuidList = append(uuidList, v.ID.String()) + } + } + // len + if len(uuidList) > 0 { + db.Where("account_id IN (?)", uuidList) + } else { + db.Where("account_id is null") + } + } + + var accountMoneys []gaia.AccountMoneyExtend + err = db.Count(&total).Error + if err != nil { + return + } + + if limit != 0 { + db = db.Limit(limit).Offset(offset) + } + + err = db.Find(&accountMoneys).Error + if err != nil { + err = fmt.Errorf("查询账号额度信息失败:%s", err.Error()) + return + } + + // 账号ID集合,方便后面一次性查出 + var accountIds []uuid.UUID + for _, money := range accountMoneys { + accountIds = append(accountIds, money.AccountId) + } + + // 查询用户信息 + var accountInfos = make(map[uuid.UUID]gaia.Account) + var accounts []gaia.Account + err = global.GVA_DB.Model(&gaia.Account{}).Where("id in ?", accountIds).Find(&accounts).Error + if err != nil { + err = fmt.Errorf("查询账户信息失败:%s", err.Error()) + return + } + for _, account := range accounts { + accountInfos[account.ID] = account + } + + // 拼接结果 + for i, money := range accountMoneys { + var accountInfo gaia.Account + var isExist bool + if accountInfo, isExist = accountInfos[money.AccountId]; !isExist { + global.GVA_LOG.Error("账户信息不存在!", zap.String("account_id", money.AccountId.String())) + continue + } + list = append(list, response.GetQuotaManagementDataResponse{ + Uid: money.AccountId.String(), + Ranking: i + 1 + offset, + Name: accountInfo.Name, + UsedQuota: money.UsedQuota, + TotalQuota: money.TotalQuota, + }) + } + + return list, total, err +} + +// SetUserQuota +// @Tags Quota +// @Summary 设置指定用户额度 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param info gaiaReq.SetUserQuotaRequest +// @Return err error +func (dashboardService *QuotaService) SetUserQuota(uid uuid.UUID, quota float64) error { + + return global.GVA_DB.Model(&gaia.AccountMoneyExtend{}).Where( + "account_id = ?", uid).Updates(&map[string]interface{}{ + "total_quota": quota, + }).Error +} diff --git a/admin/server/service/gaia/system.go b/admin/server/service/gaia/system.go new file mode 100644 index 000000000..23360ac40 --- /dev/null +++ b/admin/server/service/gaia/system.go @@ -0,0 +1,136 @@ +package gaia + +import ( + "errors" + "net/http" + "net/url" + "os" + + "github.com/faabiosr/cachego/file" + "github.com/fastwego/dingding" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/gaia" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/google/uuid" +) + +type SystemIntegratedService struct{} + +// GetIntegratedConfig +// @Tags System Integrated +// @Summary 获取系统集成配置 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +func (e *SystemIntegratedService) GetIntegratedConfig(classID uint) (integrate gaia.SystemIntegration) { + // classID是否在 + var err error + if err = global.GVA_DB.Where("classify = ?", classID).First(&integrate).Error; err != nil { + integrate = gaia.SystemIntegration{ + Classify: classID, + Status: false, + } + // 创建相关集成信息 + global.GVA_DB.Create(&integrate) + } + // 隐藏部分加密信息 + var secret string + if secret, err = utils.DecryptBlowfish(integrate.AppSecret, global.GVA_CONFIG.JWT.SigningKey); err == nil { + integrate.AppSecret = utils.AddAsteriskToString(secret) + } + integrate.CorpID = utils.AddAsteriskToString(integrate.CorpID) + return integrate +} + +// SetIntegratedConfig +// @Tags System Integrated +// @Summary 设置系统集成配置 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +func (e *SystemIntegratedService) SetIntegratedConfig(integrate gaia.SystemIntegration, test bool) (err error) { + // classID是否在 + var log gaia.SystemIntegration + if err = global.GVA_DB.Where("classify = ?", integrate.Classify).First(&log).Error; err != nil { + return err + } + // AppSecret + var secret string + if secret, err = utils.DecryptBlowfish(log.AppSecret, global.GVA_CONFIG.JWT.SigningKey); err == nil { + encodeSecret := utils.AddAsteriskToString(secret) + if encodeSecret != integrate.AppSecret { + if secret, err = utils.EncryptBlowfish( + []byte(integrate.AppSecret), global.GVA_CONFIG.JWT.SigningKey); err != nil { + return errors.New("AppSecret加密失败") + } + // save + log.AppSecret = secret + } else { + // 为什么不用 integrate.AppSecret, 被加*了 + if secret, err = utils.DecryptBlowfish(log.AppSecret, global.GVA_CONFIG.JWT.SigningKey); err != nil { + return errors.New("AppSecret解析失败") + } + integrate.AppSecret = secret + } + } + // CorpID + var ding *dingding.Client + if utils.AddAsteriskToString(log.CorpID) != integrate.CorpID { + log.CorpID = integrate.CorpID + } + // AppID 不加密,直接赋值 + log.AppID = integrate.AppID + // 关闭不需要请求 + if integrate.Status || test { + if ding, err = e.DingTalkConfigAvailable(integrate); err != nil { + return errors.New("钉钉链接失败" + err.Error()) + } + // token + if _, err = ding.AccessTokenManager.GetAccessToken(); err != nil { + return errors.New("钉钉token获取失败:" + err.Error()) + } + } + // Test completed + if test { + return err + } + // save + if err = global.GVA_DB.Model(&gaia.SystemIntegration{}).Where("id=?", log.Id).Updates(&map[string]interface{}{ + "status": integrate.Status, + "agent_id": integrate.AgentID, + "app_key": integrate.AppKey, + "app_secret": log.AppSecret, + "corp_id": log.CorpID, + "app_id": log.AppID, + }).Error; err != nil { + return err + } + return nil +} + +// DingTalkConfigAvailable +// @Tags System Integrated +// @Summary 测试钉钉配置是否可用 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @param: req gaia.SystemIntegration +// @return: *dingding.Client, error +func (e *SystemIntegratedService) DingTalkConfigAvailable(req gaia.SystemIntegration) (*dingding.Client, error) { + var err error + var reqs *http.Request + dingding.ServerUrl = "https://api.dingtalk.com" + // 特殊需要,检查可用性就不设置缓存了 + return dingding.NewClient(&dingding.DefaultAccessTokenManager{ + Id: uuid.New().String(), + Cache: file.New(os.TempDir()), + Name: "x-acs-dingtalk-access-token", + GetRefreshRequestFunc: func() *http.Request { + params := url.Values{} + params.Add("appkey", req.AppKey) + params.Add("appsecret", req.AppSecret) + reqs, err = http.NewRequest(http.MethodGet, "https://oapi.dingtalk.com/gettoken?"+params.Encode(), nil) + return reqs + }, + }), err +} diff --git a/admin/server/service/gaia/tenants.go b/admin/server/service/gaia/tenants.go new file mode 100644 index 000000000..5e408f9ec --- /dev/null +++ b/admin/server/service/gaia/tenants.go @@ -0,0 +1,42 @@ +package gaia + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/gaia" + gaiaReq "github.com/flipped-aurora/gin-vue-admin/server/model/gaia/request" +) + +type TenantsService struct{} + +// GetTenants 根据id获取tenants表记录 +func (tenantsService *TenantsService) GetTenants(id string) (tenants gaia.Tenants, err error) { + err = global.GVA_DB.Where("id = ?", id).First(&tenants).Error + return +} + +// GetTenantsInfoList 分页获取tenants表记录 +func (tenantsService *TenantsService) GetTenantsInfoList(info gaiaReq.TenantsSearch) (list []gaia.Tenants, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := global.GVA_DB.Model(&gaia.Tenants{}) + var tenantss []gaia.Tenants + // 如果有条件搜索 下方会自动创建搜索语句 + err = db.Count(&total).Error + if err != nil { + return + } + + if limit != 0 { + db = db.Limit(limit).Offset(offset) + } + + err = db.Find(&tenantss).Error + return tenantss, total, err +} + +// GetAllTenants 获取所有工作区 +func (tenantsService *TenantsService) GetAllTenants() (tenants []gaia.Tenants, err error) { + err = global.GVA_DB.Find(&tenants).Error + return +} diff --git a/admin/server/service/gaia/test.go b/admin/server/service/gaia/test.go new file mode 100644 index 000000000..9569ce3f3 --- /dev/null +++ b/admin/server/service/gaia/test.go @@ -0,0 +1,564 @@ +package gaia + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/gaia" + "github.com/flipped-aurora/gin-vue-admin/server/model/gaia/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/gaia/response" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/gofrs/uuid/v5" + "go.uber.org/zap" + "gorm.io/gorm" + "io" + "math" + "net/http" + "regexp" + "strconv" + "time" +) + +var sysRegexp = regexp.MustCompile("^sys\\.(.*?)$") +var urlDick = make(map[string]string) + +type TestService struct{} + +var RunAppList []string +var LOCK bool + +// GetAppUrl +// @Tags Test +// @Summary 获取 app 关联的url +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +func (e *TestService) GetAppUrl(appId string) (string, error) { + var err error + if url, ok := urlDick[appId]; ok { + return url, err + } + // 查询数据库 + var app gaia.Apps + if err = global.GVA_DB.Where("id=?", appId).First(&app).Error; err != nil { + return "", errors.New("找不到对应appid: " + appId) + } + // save + switch app.Mode { + case "completion": + urlDick[appId] = "/v1/completion-messages" + case "agent-chat", "advanced-chat", "chat": + urlDick[appId] = "/v1/chat-messages" + case "workflow": + urlDick[appId] = "/v1/workflows/run" + default: + return "", errors.New("url not found") + } + return urlDick[appId], err +} + +// GetAppToken +// @Tags Test +// @Summary 获取 app token +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +func (e *TestService) GetAppToken(appid string) (token string, err error) { + // 获取对应的token + var tokens gaia.ApiTokens + if err = global.GVA_DB.Where("app_id=?", appid).First(&tokens).Error; err != nil { + return "", errors.New(fmt.Sprintf("AppRequestTest Token Error: %s %s", appid, err.Error())) + } + // + return tokens.Token, nil +} + +// RunRequest +// @Tags Test +// @Summary 执行gaia请求 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +func (e *TestService) RunRequest(url, token, jsonData, query string) (id string, err error) { + // 将请求体编码为 JSON + client := &http.Client{} + // 解析JSON字符串为map + var cacheDick map[string]interface{} + var data = make(map[string]interface{}) + if err = json.Unmarshal([]byte(jsonData), &cacheDick); err != nil { + return id, fmt.Errorf("error unmarshalling JSON: %s %s", url, err) + } + // 强制阻塞模式 + for key, value := range cacheDick { + var keyList [][]string + if keyList = sysRegexp.FindAllStringSubmatch(key, 1); len(keyList) > 0 { + cacheDick[keyList[0][1]] = value + delete(cacheDick, key) + } + } + // 强制替换 + if len(query) > 0 { + data["query"] = query + } + data["inputs"] = cacheDick + data["response_mode"] = "blocking" + data["user"] = gaia.UsernameUsingApiRequest + // 将修改后的map重新编码为JSON字符串 + var modifiedJsonStr []byte + modifiedJsonStr, err = json.Marshal(data) + if err != nil { + return id, fmt.Errorf("error marshalling JSON: %s %s", url, err) + } + // 创建新的 POST 请求 + req, err := http.NewRequest("POST", fmt.Sprintf( + "%s%s", global.GVA_CONFIG.Gaia.Url, url), bytes.NewBuffer(modifiedJsonStr)) + if err != nil { + return id, fmt.Errorf("error creating request: %s %s", url, err) + } + // 设置请求头 + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + // 发送请求 + resp, err := client.Do(req) + if err != nil { + return id, fmt.Errorf("error sending request: %s %v", url, err) + } + defer resp.Body.Close() + // 检查响应状态 + if resp.StatusCode != http.StatusOK { + return id, fmt.Errorf("request failed with status: %s %s", url, resp.Status) + } + var bodyByte []byte + if bodyByte, err = io.ReadAll(resp.Body); err != nil { + return id, fmt.Errorf("request io.ReadAll: %s %s", url, resp.Status) + } + // 解析获取task_id或者id + var result map[string]interface{} + if err = json.Unmarshal(bodyByte, &result); err != nil { + return id, fmt.Errorf("request json.Unmarsha: %s %s", url, resp.Status) + } + // 获取id + var ok bool + var cacheID interface{} + if cacheID, ok = result["id"]; ok { + // 使用类型断言来判断和转换 + if id, ok = cacheID.(string); ok { + return id, nil + } else { + return id, fmt.Errorf("switch id error: %s %s", url, resp.Status) + } + } else if cacheID, ok = result["workflow_run_id"]; ok { + // 使用类型断言来判断和转换 + if id, ok = cacheID.(string); ok { + return id, nil + } else { + return id, fmt.Errorf("switch id error: %s %s", url, resp.Status) + } + } else if cacheID, ok = result["task_id"]; ok { + // 使用类型断言来判断和转换 + if id, ok = cacheID.(string); ok { + return id, nil + } else { + return id, fmt.Errorf("switch id error: %s %s", url, resp.Status) + } + } + // return + return id, fmt.Errorf("get id error: %s", url) +} + +// SaveTestLog +// @Tags Test +// @Summary 储存测试日志 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @P appID, inputs, outputs, comparison, status, err string, logTime, elapsed float64, batchID uint +func (e *TestService) SaveTestLog( + appID, inputs, outputs, comparison, status, err string, logTime, elapsed float64, batchID uint) { + // 查询workflow列表uu + var appNumber = 0 + var id = uint(1) + var log gaia.AppRequestTest + if global.GVA_DB.Select("id").Order("id desc").First(&log).Error == nil { + id = log.ID + 1 + } + // 是否新的appid + if !utils.InStringArray(appID, RunAppList) { + RunAppList = append(RunAppList, appID) + appNumber = 1 + } + // inputs原始 Unicode 转义字符串 + if cacheStr, iErr := strconv.Unquote(inputs); iErr == nil { + inputs = cacheStr + } + // outputs原始 Unicode 转义字符串 + if cacheStr, iErr := strconv.Unquote(outputs); iErr == nil { + outputs = cacheStr + } + // comparison原始 Unicode 转义字符串 + if cacheStr, iErr := strconv.Unquote(comparison); iErr == nil { + comparison = cacheStr + } + // 修改创建 + global.GVA_DB.Create(&gaia.AppRequestTest{ + ID: id, + Error: err, + AppID: appID, + Status: status, + Inputs: inputs, + Outputs: outputs, + BatchId: batchID, + Comparison: comparison, + LogTime: math.Round(logTime*100) / 100, + ElapsedTime: math.Round(elapsed*100) / 100, + }) + // 是否正常状态 + var failureNumber = 1 + var successNumber = 0 + if status == gaia.MessagesSucceeded || status == gaia.WorkflowSucceeded { + successNumber = 1 + failureNumber = 0 + } + // 修改批次状态 + global.GVA_DB.Model(&gaia.AppRequestTestBatch{}). + Where("id = ?", batchID). + Updates(&map[string]interface{}{ + "sum": gorm.Expr("sum + ?", 1), + "app": gorm.Expr("app + ?", appNumber), + "success_count": gorm.Expr("success_count + ?", successNumber), + "failure_count": gorm.Expr("failure_count + ?", failureNumber), + }) +} + +// TestRunWorkflow +// @Tags Test +// @Summary 运行工作流测试 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param appList, endList []string, batchID uint +func (e *TestService) TestRunWorkflow(appList, endList []string, batchID uint) { + // workflow_runs all + var err error + var workflowRun []gaia.WorkflowRun + if err = global.GVA_DB.Select("app_id").Where( + "app_id IN (?)", appList).Group("app_id").Find(&workflowRun).Error; err != nil { + global.GVA_LOG.Debug("AppRequestTest TestRunWorkflow Error: " + err.Error()) + return + } + // 提取关联app_id + for _, v := range workflowRun { + // 获取token + var token string + var appId = v.AppID + var workflow []gaia.WorkflowRun + if token, err = e.GetAppToken(appId); err != nil { + fmt.Println(err.Error()) + } + // 获取最近10个 end_user的聊天信息 + if err = global.GVA_DB.Select("inputs", "outputs", "elapsed_time").Where( + "app_id=? AND status=? AND created_by_role=? AND created_by IN (?) AND inputs IS NOT NULL AND NOT (inputs::text = '{}' OR inputs::text = 'null')", + appId, gaia.WorkflowSucceeded, gaia.IndirectAccessUser, endList).Order("id desc").Limit( + gaia.TestDefaultNumber).Find(&workflow).Error; err != nil { + global.GVA_LOG.Debug(fmt.Sprintf("AppRequestTest TestRunWorkflow Error: %s %s", appId, err.Error())) + continue + } + // 执行 + for _, item := range workflow { + // 提取id + var id string + if id, err = e.RunRequest("/v1/workflows/run", token, item.Inputs, ""); err != nil { + errStr := "workflows/run error" + err.Error() + e.SaveTestLog(appId, item.Inputs, item.Outputs, errStr, gaia.UserClosed, + "", item.ElapsedTime, 0, batchID) + global.GVA_LOG.Debug(errStr) + continue + } + // 查询对应请求详情 + var newWorkflow gaia.WorkflowRun + if err = global.GVA_DB.Where("id=?", id).First(&newWorkflow).Error; err != nil { + errStr := "WorkflowRun get new error" + err.Error() + e.SaveTestLog(appId, item.Inputs, item.Outputs, errStr, gaia.UserClosed, + newWorkflow.Error, item.ElapsedTime, newWorkflow.ElapsedTime, batchID) + global.GVA_LOG.Debug(errStr) + continue + } + // create + e.SaveTestLog(appId, item.Inputs, item.Outputs, newWorkflow.Outputs, + newWorkflow.Status, newWorkflow.Error, item.ElapsedTime, newWorkflow.ElapsedTime, batchID) + } + } +} + +// TestRunMessages +// @Tags Test +// @Summary 运行聊天测试 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +func (e *TestService) TestRunMessages(appList []string, batchID uint) { + // workflow_runs all + var err error + var message []gaia.Messages + if err = global.GVA_DB.Select("app_id").Where( + "app_id IN (?)", appList).Group("app_id").Find(&message).Error; err != nil { + global.GVA_LOG.Debug("AppRequestTest TestRunMessages Error: " + err.Error()) + return + } + // 循环聊天列表 + for _, v := range message { + // 获取token + var token string + var appId = v.AppID.String() + var messages []gaia.Messages + if token, err = e.GetAppToken(appId); err != nil { + global.GVA_LOG.Debug(fmt.Sprintf("AppRequestTest GetAppToken Error: %s", err.Error())) + continue + } + // 获取最近10个 end_user的聊天信息 + if err = global.GVA_DB.Select("query", "app_id", "inputs", "answer", "provider_response_latency").Where( + "app_id=? AND status=? AND from_source=? AND inputs IS NOT NULL AND NOT (inputs::text = '{}' OR inputs::text = 'null')", + appId, gaia.MessagesSucceeded, gaia.ChatRequestTypeApi).Order("created_at desc").Limit(gaia.TestDefaultNumber).Find( + &messages).Error; err != nil { + global.GVA_LOG.Debug(fmt.Sprintf("AppRequestTest TestRunMessages Error: %s %s", appId, err.Error())) + continue + } + // 执行 + asterisk := utils.AddAsteriskToString(token) + for _, item := range messages { + // 提取id + var id, url string + if url, err = e.GetAppUrl(appId); err != nil { + errStr := "AppRequestTest TestRunMessages app url error" + err.Error() + e.SaveTestLog(appId, item.Inputs, item.Answer, errStr, gaia.UserClosed, + "", item.ProviderResponseLatency, 0, batchID) + global.GVA_LOG.Debug(errStr) + continue + } + // 请求 + if id, err = e.RunRequest(url, token, item.Inputs, item.Query); err != nil { + errStr := fmt.Sprintf("AppRequestTest RunRequest error\n%s\ntoken:%s", err.Error(), asterisk) + e.SaveTestLog(appId, item.Inputs, item.Answer, errStr, gaia.UserClosed, + "", item.ProviderResponseLatency, 0, batchID) + global.GVA_LOG.Debug(errStr) + continue + } + // 查询对应请求详情 + var newWorkflow gaia.Messages + if err = global.GVA_DB.Where("id=?", id).First(&newWorkflow).Error; err != nil { + errStr := "AppRequestTest TestRunMessages get new error" + err.Error() + e.SaveTestLog(appId, item.Inputs, item.Answer, errStr, gaia.UserClosed, + newWorkflow.Error, item.ProviderResponseLatency, newWorkflow.ProviderResponseLatency, batchID) + global.GVA_LOG.Debug(errStr) + continue + } + // create + e.SaveTestLog(appId, item.Inputs, item.Answer, newWorkflow.Answer, newWorkflow.Status, + newWorkflow.Error, item.ProviderResponseLatency, newWorkflow.ProviderResponseLatency, batchID) + } + } +} + +// AppRequestTest +// @Tags Test +// @Summary 应用请求测试 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +func (e *TestService) AppRequestTest() (err error) { + // 获取APP列表 + var tenantList []string + var endUser []gaia.EndUser + var endList, appList []string + var batch gaia.AppRequestTestBatch + var tenant []gaia.TenantAccountJoin + if LOCK { + return errors.New("AppRequestTest is running") + } + if err = global.GVA_DB.Where("account_id=? AND role=?", + global.GVA_CONFIG.Gaia.SuperAdminAccountId, "owner").Find(&tenant).Error; err != nil { + return errors.New("AppRequestTest TenantAccountJoin Error: " + err.Error()) + } + if len(tenant) == -0 { + return errors.New("AppRequestTest Tenant is null ") + } + for _, v := range tenant { + tenantList = append(tenantList, v.TenantID) + } + // 循环获取ADMIN关联空间表 + if err = global.GVA_DB.Where("tenant_id IN (?) AND \"type\"=? AND session_id != ?", + tenantList, gaia.UserUsingApiRequest, gaia.UsernameUsingApiRequest).Find(&endUser).Error; err != nil { + return errors.New("AppRequestTest EndUser Error: " + err.Error()) + } + // + if len(endUser) == 0 { + return errors.New("AppRequestTest No EndUser") + } + // 循环获取用户列表和app_id列表 + for _, v := range endUser { + appList = append(appList, v.AppID) + endList = append(endList, v.ID) + } + // 获取最新的batch_id + LOCK = true + if err = global.GVA_DB.Order("id desc").First(&batch).Error; err == nil { + batch.ID += 1 + } else { + batch.ID = 1 + } + // 创建批次 + if err = global.GVA_DB.Create(&gaia.AppRequestTestBatch{ + App: 0, + Sum: 0, + EndTime: 0, + SuccessCount: 0, + FailureCount: 0, + ID: batch.ID, + CreateTime: time.Now().Unix(), + Status: gaia.BatchStatusInProgress, + }).Error; err != nil { + LOCK = false + return errors.New("批次创建失败") + } + // 异步请求 + RunAppList = []string{} + go func(app, end []string, id uint) { + e.TestRunWorkflow(app, end, id) + e.TestRunMessages(app, id) + // 标记结束 + global.GVA_DB.Model(&gaia.AppRequestTestBatch{}). + Where("id = ?", id). + Updates(&map[string]interface{}{ + "end_time": time.Now().Unix(), + "status": gaia.BatchStatusCompleted, + }) + LOCK = false + }(appList, endList, batch.ID) + return err +} + +// AppRequestTestList +// @Tags Test +// @Summary gaia应用请求测试结果列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param info request.PageInfo +// @Return list []response.GetQuotaManagementDataResponse, total int64, err error +func (e *TestService) AppRequestTestList(info request.GetAppRequestTestRequest) ( + _ bool, list []response.GetAppRequestTestDataResponse, total int64, err error) { + if info.PageSize == 0 { + info.PageSize = 10 + } + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + db := global.GVA_DB.Model(&gaia.AppRequestTest{}).Where("batch_id = ?", info.BatchId) + // 筛选app列表 + if len(info.Apps) > 0 { + db.Where("app_id IN (?)", info.Apps) + } + // 是否筛选状态 + switch info.Status { + case request.GetAppRequestFilterSuccess: + db.Where("status IN (?)", []string{gaia.MessagesSucceeded, gaia.WorkflowSucceeded}) + case request.GetAppRequestFilterFailure: + db.Where("status NOT IN (?)", []string{gaia.MessagesSucceeded, gaia.WorkflowSucceeded}) + } + + err = db.Count(&total).Error + if err != nil { + return + } + db = db.Order("id desc").Limit(limit).Offset(offset) + var requestTest []gaia.AppRequestTest + err = db.Find(&requestTest).Error + if err != nil { + err = fmt.Errorf("查询测试信息失败:%s", err.Error()) + return + } + + // 账号ID集合,方便后面一次性查出 + var appList []uuid.UUID + for _, money := range requestTest { + var uid uuid.UUID + if uid, err = uuid.FromString(money.AppID); err == nil { + appList = append(appList, uid) + } + } + + // 查询用户信息 + var apps []gaia.Apps + var appInfos = make(map[string]string) + if len(appList) > 0 { + err = global.GVA_DB.Model(&gaia.Apps{}).Where("id in (?)", appList).Find(&apps).Error + if err != nil { + err = fmt.Errorf("查询应用信息失败:%s", err.Error()) + return + } + } + // 获取appInfos + for _, app := range apps { + appInfos[app.ID.String()] = app.Name + } + + // 拼接结果 + for _, item := range requestTest { + var status bool + var name string + var isExist bool + if name, isExist = appInfos[item.AppID]; !isExist { + global.GVA_LOG.Error("AppRequestTestList app信息不存在!", zap.String("app", item.AppID)) + continue + } + // 区分状态 + if item.Status == gaia.MessagesSucceeded || item.Status == gaia.WorkflowSucceeded { + status = true + } + // push + list = append(list, response.GetAppRequestTestDataResponse{ + Name: name, + Status: status, + Error: item.Error, + Inputs: item.Inputs, + Outputs: item.Outputs, + LogTime: item.LogTime, + Comparison: item.Comparison, + ElapsedTime: item.ElapsedTime, + }) + } + return LOCK, list, total, err +} + +// AppRequestTestBatch +// @Tags Test +// @Summary gaia应用请求测试批次列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param info request.PageInfo +// @Return list []response.GetQuotaManagementDataResponse, total int64, err error +func (e *TestService) AppRequestTestBatch(info request.GetAppRequestTestRequest) ( + _ bool, list []gaia.AppRequestTestBatch, total int64, err error) { + // init + if info.PageSize == 0 { + info.PageSize = 10 + } + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + db := global.GVA_DB.Model(&gaia.AppRequestTestBatch{}) + + err = db.Count(&total).Error + if err != nil { + return + } + db = db.Order("id desc").Limit(limit).Offset(offset) + // 查询用户信息 + err = db.Find(&list).Error + if err != nil { + err = fmt.Errorf("查询测试信息失败:%s", err.Error()) + return + } + return LOCK, list, total, err +} diff --git a/admin/server/service/system/auto_code_history.go b/admin/server/service/system/auto_code_history.go new file mode 100644 index 000000000..8d1ec4ba4 --- /dev/null +++ b/admin/server/service/system/auto_code_history.go @@ -0,0 +1,217 @@ +package system + +import ( + "context" + "encoding/json" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/utils/ast" + "github.com/pkg/errors" + "path" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + common "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + model "github.com/flipped-aurora/gin-vue-admin/server/model/system" + request "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + + "go.uber.org/zap" +) + +var AutocodeHistory = new(autoCodeHistory) + +type autoCodeHistory struct{} + +// Create 创建代码生成器历史记录 +// Author [SliverHorn](https://github.com/SliverHorn) +// Author [songzhibin97](https://github.com/songzhibin97) +func (s *autoCodeHistory) Create(ctx context.Context, info request.SysAutoHistoryCreate) error { + create := info.Create() + err := global.GVA_DB.WithContext(ctx).Create(&create).Error + if err != nil { + return errors.Wrap(err, "创建失败!") + } + return nil +} + +// First 根据id获取代码生成器历史的数据 +// Author [SliverHorn](https://github.com/SliverHorn) +// Author [songzhibin97](https://github.com/songzhibin97) +func (s *autoCodeHistory) First(ctx context.Context, info common.GetById) (string, error) { + var meta string + err := global.GVA_DB.WithContext(ctx).Model(model.SysAutoCodeHistory{}).Where("id = ?", info.ID).Pluck("request", &meta).Error + if err != nil { + return "", errors.Wrap(err, "获取失败!") + } + return meta, nil +} + +// Repeat 检测重复 +// Author [SliverHorn](https://github.com/SliverHorn) +// Author [songzhibin97](https://github.com/songzhibin97) +func (s *autoCodeHistory) Repeat(businessDB, structName, Package string) bool { + var count int64 + global.GVA_DB.Model(&model.SysAutoCodeHistory{}).Where("business_db = ? and struct_name = ? and package = ? and flag = 0", businessDB, structName, Package).Count(&count) + return count > 0 +} + +// RollBack 回滚 +// Author [SliverHorn](https://github.com/SliverHorn) +// Author [songzhibin97](https://github.com/songzhibin97) +func (s *autoCodeHistory) RollBack(ctx context.Context, info request.SysAutoHistoryRollBack) error { + var history model.SysAutoCodeHistory + err := global.GVA_DB.Where("id = ?", info.ID).First(&history).Error + if err != nil { + return err + } + if history.ExportTemplateID != 0 { + err = global.GVA_DB.Delete(&model.SysExportTemplate{}, "id = ?", history.ExportTemplateID).Error + if err != nil { + return err + } + } + if info.DeleteApi { + ids := info.ApiIds(history) + err = ApiServiceApp.DeleteApisByIds(ids) + if err != nil { + global.GVA_LOG.Error("ClearTag DeleteApiByIds:", zap.Error(err)) + } + } // 清除API表 + if info.DeleteMenu { + err = BaseMenuServiceApp.DeleteBaseMenu(int(history.MenuID)) + if err != nil { + return errors.Wrap(err, "删除菜单失败!") + } + } // 清除菜单表 + if info.DeleteTable { + err = s.DropTable(history.BusinessDB, history.Table) + if err != nil { + return errors.Wrap(err, "删除表失败!") + } + } // 删除表 + templates := make(map[string]string, len(history.Templates)) + for key, template := range history.Templates { + { + server := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server) + keys := strings.Split(key, "/") + key = filepath.Join(keys...) + key = strings.TrimPrefix(key, server) + } // key + { + web := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.WebRoot()) + server := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server) + slices := strings.Split(template, "/") + template = filepath.Join(slices...) + ext := path.Ext(template) + switch ext { + case ".js", ".vue": + template = filepath.Join(web, template) + case ".go": + template = filepath.Join(server, template) + } + } // value + templates[key] = template + } + history.Templates = templates + for key, value := range history.Injections { + var injection ast.Ast + switch key { + case ast.TypePackageApiEnter, ast.TypePackageRouterEnter, ast.TypePackageServiceEnter: + + case ast.TypePackageApiModuleEnter, ast.TypePackageRouterModuleEnter, ast.TypePackageServiceModuleEnter: + var entity ast.PackageModuleEnter + _ = json.Unmarshal([]byte(value), &entity) + injection = &entity + case ast.TypePackageInitializeGorm: + var entity ast.PackageInitializeGorm + _ = json.Unmarshal([]byte(value), &entity) + injection = &entity + case ast.TypePackageInitializeRouter: + var entity ast.PackageInitializeRouter + _ = json.Unmarshal([]byte(value), &entity) + injection = &entity + case ast.TypePluginGen: + var entity ast.PluginGen + _ = json.Unmarshal([]byte(value), &entity) + injection = &entity + case ast.TypePluginApiEnter, ast.TypePluginRouterEnter, ast.TypePluginServiceEnter: + var entity ast.PluginEnter + _ = json.Unmarshal([]byte(value), &entity) + injection = &entity + case ast.TypePluginInitializeGorm: + var entity ast.PluginInitializeGorm + _ = json.Unmarshal([]byte(value), &entity) + injection = &entity + case ast.TypePluginInitializeRouter: + var entity ast.PluginInitializeRouter + _ = json.Unmarshal([]byte(value), &entity) + injection = &entity + } + if injection == nil { + continue + } + file, _ := injection.Parse("", nil) + if file != nil { + _ = injection.Rollback(file) + err = injection.Format("", nil, file) + if err != nil { + return err + } + fmt.Printf("[filepath:%s]回滚注入代码成功!\n", key) + } + } // 清除注入代码 + removeBasePath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, "rm_file", strconv.FormatInt(int64(time.Now().Nanosecond()), 10)) + for _, value := range history.Templates { + if !filepath.IsAbs(value) { + continue + } + removePath := filepath.Join(removeBasePath, strings.TrimPrefix(value, global.GVA_CONFIG.AutoCode.Root)) + err = utils.FileMove(value, removePath) + if err != nil { + return errors.Wrapf(err, "[src:%s][dst:%s]文件移动失败!", value, removePath) + } + } // 移动文件 + err = global.GVA_DB.WithContext(ctx).Model(&model.SysAutoCodeHistory{}).Where("id = ?", info.ID).Update("flag", 1).Error + if err != nil { + return errors.Wrap(err, "更新失败!") + } + return nil +} + +// Delete 删除历史数据 +// Author [SliverHorn](https://github.com/SliverHorn) +// Author [songzhibin97](https://github.com/songzhibin97) +func (s *autoCodeHistory) Delete(ctx context.Context, info common.GetById) error { + err := global.GVA_DB.WithContext(ctx).Where("id = ?", info.Uint()).Delete(&model.SysAutoCodeHistory{}).Error + if err != nil { + return errors.Wrap(err, "删除失败!") + } + return nil +} + +// GetList 获取系统历史数据 +// Author [SliverHorn](https://github.com/SliverHorn) +// Author [songzhibin97](https://github.com/songzhibin97) +func (s *autoCodeHistory) GetList(ctx context.Context, info common.PageInfo) (list []model.SysAutoCodeHistory, total int64, err error) { + var entities []model.SysAutoCodeHistory + db := global.GVA_DB.WithContext(ctx).Model(&model.SysAutoCodeHistory{}) + err = db.Count(&total).Error + if err != nil { + return nil, total, err + } + err = db.Scopes(info.Paginate()).Order("updated_at desc").Find(&entities).Error + return entities, total, err +} + +// DropTable 获取指定数据库和指定数据表的所有字段名,类型值等 +// @author: [piexlmax](https://github.com/piexlmax) +func (s *autoCodeHistory) DropTable(BusinessDb, tableName string) error { + if BusinessDb != "" { + return global.MustGetGlobalDBByDBName(BusinessDb).Exec("DROP TABLE " + tableName).Error + } else { + return global.GVA_DB.Exec("DROP TABLE " + tableName).Error + } +} diff --git a/admin/server/service/system/auto_code_package.go b/admin/server/service/system/auto_code_package.go new file mode 100644 index 000000000..c9b8cfe2d --- /dev/null +++ b/admin/server/service/system/auto_code_package.go @@ -0,0 +1,673 @@ +package system + +import ( + "context" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + common "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + model "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/flipped-aurora/gin-vue-admin/server/utils/ast" + "github.com/pkg/errors" + "go/token" + "gorm.io/gorm" + "os" + "path/filepath" + "strings" + "text/template" +) + +var AutoCodePackage = new(autoCodePackage) + +type autoCodePackage struct{} + +// Create 创建包信息 +// @author: [piexlmax](https://github.com/piexlmax) +// @author: [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodePackage) Create(ctx context.Context, info *request.SysAutoCodePackageCreate) error { + switch { + case info.Template == "": + return errors.New("模板不能为空!") + case info.Template == "page": + return errors.New("page为表单生成器!") + case info.PackageName == "": + return errors.New("PackageName不能为空!") + case token.IsKeyword(info.PackageName): + return errors.Errorf("%s为go的关键字!", info.PackageName) + case info.Template == "package": + if info.PackageName == "system" || info.PackageName == "example" { + return errors.New("不能使用已保留的package name") + } + default: + break + } + if !errors.Is(global.GVA_DB.Where("package_name = ? and template = ?", info.PackageName, info.Template).First(&model.SysAutoCodePackage{}).Error, gorm.ErrRecordNotFound) { + return errors.New("存在相同PackageName") + } + create := info.Create() + return global.GVA_DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + err := tx.Create(&create).Error + if err != nil { + return errors.Wrap(err, "创建失败!") + } + code := info.AutoCode() + _, asts, creates, err := s.templates(ctx, create, code) + if err != nil { + return err + } + for key, value := range creates { // key 为 模版绝对路径 + var files *template.Template + files, err = template.ParseFiles(key) + if err != nil { + return errors.Wrapf(err, "[filepath:%s]读取模版文件失败!", key) + } + err = os.MkdirAll(filepath.Dir(value), os.ModePerm) + if err != nil { + return errors.Wrapf(err, "[filepath:%s]创建文件夹失败!", value) + } + var file *os.File + file, err = os.Create(value) + if err != nil { + return errors.Wrapf(err, "[filepath:%s]创建文件夹失败!", value) + } + err = files.Execute(file, code) + _ = file.Close() + if err != nil { + return errors.Wrapf(err, "[filepath:%s]生成失败!", value) + } + fmt.Printf("[template:%s][filepath:%s]生成成功!\n", key, value) + } + for key, value := range asts { + keys := strings.Split(key, "=>") + if len(keys) == 2 { + switch keys[1] { + case ast.TypePluginInitializeV2, ast.TypePackageApiEnter, ast.TypePackageRouterEnter, ast.TypePackageServiceEnter: + file, _ := value.Parse("", nil) + if file != nil { + err = value.Injection(file) + if err != nil { + return err + } + err = value.Format("", nil, file) + if err != nil { + return err + } + } + fmt.Printf("[type:%s]注入成功!\n", key) + } + } + } + return nil + }) +} + +// Delete 删除包记录 +// @author: [piexlmax](https://github.com/piexlmax) +// @author: [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodePackage) Delete(ctx context.Context, info common.GetById) error { + err := global.GVA_DB.WithContext(ctx).Delete(&model.SysAutoCodePackage{}, info.Uint()).Error + if err != nil { + return errors.Wrap(err, "删除失败!") + } + return nil +} + +// All 获取所有包 +// @author: [piexlmax](https://github.com/piexlmax) +// @author: [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodePackage) All(ctx context.Context) (entities []model.SysAutoCodePackage, err error) { + server := make([]model.SysAutoCodePackage, 0) + plugin := make([]model.SysAutoCodePackage, 0) + serverPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service") + pluginPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin") + serverDir, err := os.ReadDir(serverPath) + if err != nil { + return nil, errors.Wrap(err, "读取service文件夹失败!") + } + pluginDir, err := os.ReadDir(pluginPath) + if err != nil { + return nil, errors.Wrap(err, "读取plugin文件夹失败!") + } + for i := 0; i < len(serverDir); i++ { + if serverDir[i].IsDir() { + serverPackage := model.SysAutoCodePackage{ + PackageName: serverDir[i].Name(), + Template: "package", + Label: serverDir[i].Name() + "包", + Desc: "系统自动读取" + serverDir[i].Name() + "包", + Module: global.GVA_CONFIG.AutoCode.Module, + } + server = append(server, serverPackage) + } + } + for i := 0; i < len(pluginDir); i++ { + if pluginDir[i].IsDir() { + dirNameMap := map[string]bool{ + "api": true, + "config": true, + "initialize": true, + "model": true, + "plugin": true, + "router": true, + "service": true, + } + dir, e := os.ReadDir(filepath.Join(pluginPath, pluginDir[i].Name())) + if e != nil { + return nil, errors.Wrap(err, "读取plugin文件夹失败!") + } + //dir目录需要包含所有的dirNameMap + for k := 0; k < len(dir); k++ { + if dir[k].IsDir() { + if _, ok := dirNameMap[dir[k].Name()]; ok { + delete(dirNameMap, dir[k].Name()) + } + } + } + if len(dirNameMap) != 0 { + continue + } + pluginPackage := model.SysAutoCodePackage{ + PackageName: pluginDir[i].Name(), + Template: "plugin", + Label: pluginDir[i].Name() + "插件", + Desc: "系统自动读取" + pluginDir[i].Name() + "插件,使用前请确认是否为v2版本插件", + Module: global.GVA_CONFIG.AutoCode.Module, + } + plugin = append(plugin, pluginPackage) + } + } + + err = global.GVA_DB.WithContext(ctx).Find(&entities).Error + if err != nil { + return nil, errors.Wrap(err, "获取所有包失败!") + } + entitiesMap := make(map[string]model.SysAutoCodePackage) + for i := 0; i < len(entities); i++ { + entitiesMap[entities[i].PackageName] = entities[i] + } + createEntity := []model.SysAutoCodePackage{} + for i := 0; i < len(server); i++ { + if _, ok := entitiesMap[server[i].PackageName]; !ok { + if server[i].Template == "package" { + createEntity = append(createEntity, server[i]) + } + } + } + for i := 0; i < len(plugin); i++ { + if _, ok := entitiesMap[plugin[i].PackageName]; !ok { + if plugin[i].Template == "plugin" { + createEntity = append(createEntity, plugin[i]) + } + } + } + + if len(createEntity) > 0 { + err = global.GVA_DB.WithContext(ctx).Create(&createEntity).Error + if err != nil { + return nil, errors.Wrap(err, "同步失败!") + } + entities = append(entities, createEntity...) + } + + return entities, nil +} + +// Templates 获取所有模版文件夹 +// @author: [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodePackage) Templates(ctx context.Context) ([]string, error) { + templates := make([]string, 0) + entries, err := os.ReadDir("resource") + if err != nil { + return nil, errors.Wrap(err, "读取模版文件夹失败!") + } + for i := 0; i < len(entries); i++ { + if entries[i].IsDir() { + if entries[i].Name() == "page" { + continue + } // page 为表单生成器 + if entries[i].Name() == "function" { + continue + } // function 为函数生成器 + if entries[i].Name() == "preview" { + continue + } // preview 为预览代码生成器的代码 + templates = append(templates, entries[i].Name()) + } + } + return templates, nil +} + +func (s *autoCodePackage) templates(ctx context.Context, entity model.SysAutoCodePackage, info request.AutoCode) (code map[string]string, asts map[string]ast.Ast, creates map[string]string, err error) { + code = make(map[string]string) + asts = make(map[string]ast.Ast) + creates = make(map[string]string) + templateDir := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "resource", entity.Template) + templateDirs, err := os.ReadDir(templateDir) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "读取模版文件夹[%s]失败!", templateDir) + } + for i := 0; i < len(templateDirs); i++ { + second := filepath.Join(templateDir, templateDirs[i].Name()) + switch templateDirs[i].Name() { + case "server": + var secondDirs []os.DirEntry + secondDirs, err = os.ReadDir(second) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "读取模版文件夹[%s]失败!", second) + } + for j := 0; j < len(secondDirs); j++ { + if secondDirs[j].Name() == ".DS_Store" { + continue + } + three := filepath.Join(second, secondDirs[j].Name()) + if !secondDirs[j].IsDir() { + ext := filepath.Ext(secondDirs[j].Name()) + if ext != ".template" && ext != ".tpl" { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版后缀!", three) + } + name := strings.TrimSuffix(secondDirs[j].Name(), ext) + if name == "main.go" || name == "plugin.go" { + pluginInitialize := &ast.PluginInitializeV2{ + Type: ast.TypePluginInitializeV2, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, name), + PluginPath: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "plugin_biz_v2.go"), + ImportPath: fmt.Sprintf(`"%s/plugin/%s"`, global.GVA_CONFIG.AutoCode.Module, entity.PackageName), + PackageName: entity.PackageName, + } + asts[pluginInitialize.PluginPath+"=>"+pluginInitialize.Type.String()] = pluginInitialize + creates[three] = pluginInitialize.Path + continue + } + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件!", three) + } + switch secondDirs[j].Name() { + case "api", "router", "service": + var threeDirs []os.DirEntry + threeDirs, err = os.ReadDir(three) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "读取模版文件夹[%s]失败!", three) + } + for k := 0; k < len(threeDirs); k++ { + if threeDirs[k].Name() == ".DS_Store" { + continue + } + four := filepath.Join(three, threeDirs[k].Name()) + if threeDirs[k].IsDir() { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件夹!", four) + } + ext := filepath.Ext(four) + if ext != ".template" && ext != ".tpl" { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版后缀!", four) + } + api := strings.Index(threeDirs[k].Name(), "api") + hasEnter := strings.Index(threeDirs[k].Name(), "enter") + router := strings.Index(threeDirs[k].Name(), "router") + service := strings.Index(threeDirs[k].Name(), "service") + if router == -1 && api == -1 && service == -1 && hasEnter == -1 { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件!", four) + } + if entity.Template == "package" { + create := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), entity.PackageName, info.HumpPackageName+".go") + if api != -1 { + create = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), "v1", entity.PackageName, info.HumpPackageName+".go") + } + if hasEnter != -1 { + isApi := strings.Index(secondDirs[j].Name(), "api") + isRouter := strings.Index(secondDirs[j].Name(), "router") + isService := strings.Index(secondDirs[j].Name(), "service") + if isApi != -1 { + packageApiEnter := &ast.PackageEnter{ + Type: ast.TypePackageApiEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), "v1", "enter.go"), + ImportPath: fmt.Sprintf(`"%s/%s/%s/%s"`, global.GVA_CONFIG.AutoCode.Module, "api", "v1", entity.PackageName), + StructName: utils.FirstUpper(entity.PackageName) + "ApiGroup", + PackageName: entity.PackageName, + PackageStructName: "ApiGroup", + } + asts[packageApiEnter.Path+"=>"+packageApiEnter.Type.String()] = packageApiEnter + packageApiModuleEnter := &ast.PackageModuleEnter{ + Type: ast.TypePackageApiModuleEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), "v1", entity.PackageName, "enter.go"), + ImportPath: fmt.Sprintf(`"%s/service"`, global.GVA_CONFIG.AutoCode.Module), + StructName: info.StructName + "Api", + AppName: "ServiceGroupApp", + GroupName: utils.FirstUpper(entity.PackageName) + "ServiceGroup", + ModuleName: info.Abbreviation + "Service", + PackageName: "service", + ServiceName: info.StructName + "Service", + } + asts[packageApiModuleEnter.Path+"=>"+packageApiModuleEnter.Type.String()] = packageApiModuleEnter + creates[four] = packageApiModuleEnter.Path + } + if isRouter != -1 { + packageRouterEnter := &ast.PackageEnter{ + Type: ast.TypePackageRouterEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), "enter.go"), + ImportPath: fmt.Sprintf(`"%s/%s/%s"`, global.GVA_CONFIG.AutoCode.Module, secondDirs[j].Name(), entity.PackageName), + StructName: utils.FirstUpper(entity.PackageName), + PackageName: entity.PackageName, + PackageStructName: "RouterGroup", + } + asts[packageRouterEnter.Path+"=>"+packageRouterEnter.Type.String()] = packageRouterEnter + packageRouterModuleEnter := &ast.PackageModuleEnter{ + Type: ast.TypePackageRouterModuleEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), entity.PackageName, "enter.go"), + ImportPath: fmt.Sprintf(`api "%s/api/v1"`, global.GVA_CONFIG.AutoCode.Module), + StructName: info.StructName + "Router", + AppName: "ApiGroupApp", + GroupName: utils.FirstUpper(entity.PackageName) + "ApiGroup", + ModuleName: info.Abbreviation + "Api", + PackageName: "api", + ServiceName: info.StructName + "Api", + } + creates[four] = packageRouterModuleEnter.Path + asts[packageRouterModuleEnter.Path+"=>"+packageRouterModuleEnter.Type.String()] = packageRouterModuleEnter + packageInitializeRouter := &ast.PackageInitializeRouter{ + Type: ast.TypePackageInitializeRouter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "router_biz.go"), + ImportPath: fmt.Sprintf(`"%s/router"`, global.GVA_CONFIG.AutoCode.Module), + AppName: "RouterGroupApp", + GroupName: utils.FirstUpper(entity.PackageName), + ModuleName: entity.PackageName + "Router", + PackageName: "router", + FunctionName: "Init" + info.StructName + "Router", + LeftRouterGroupName: "privateGroup", + RightRouterGroupName: "publicGroup", + } + asts[packageInitializeRouter.Path+"=>"+packageInitializeRouter.Type.String()] = packageInitializeRouter + } + if isService != -1 { + path := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), strings.TrimSuffix(threeDirs[k].Name(), ext)) + importPath := fmt.Sprintf(`"%s/service/%s"`, global.GVA_CONFIG.AutoCode.Module, entity.PackageName) + packageServiceEnter := &ast.PackageEnter{ + Type: ast.TypePackageServiceEnter, + Path: path, + ImportPath: importPath, + StructName: utils.FirstUpper(entity.PackageName) + "ServiceGroup", + PackageName: entity.PackageName, + PackageStructName: "ServiceGroup", + } + asts[packageServiceEnter.Path+"=>"+packageServiceEnter.Type.String()] = packageServiceEnter + packageServiceModuleEnter := &ast.PackageModuleEnter{ + Type: ast.TypePackageServiceModuleEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), entity.PackageName, "enter.go"), + StructName: info.StructName + "Service", + } + asts[packageServiceModuleEnter.Path+"=>"+packageServiceModuleEnter.Type.String()] = packageServiceModuleEnter + creates[four] = packageServiceModuleEnter.Path + } + continue + } + code[four] = create + continue + } + if hasEnter != -1 { + isApi := strings.Index(secondDirs[j].Name(), "api") + isRouter := strings.Index(secondDirs[j].Name(), "router") + isService := strings.Index(secondDirs[j].Name(), "service") + if isRouter != -1 { + pluginRouterEnter := &ast.PluginEnter{ + Type: ast.TypePluginRouterEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), strings.TrimSuffix(threeDirs[k].Name(), ext)), + ImportPath: fmt.Sprintf(`"%s/plugin/%s/api"`, global.GVA_CONFIG.AutoCode.Module, entity.PackageName), + StructName: info.StructName, + StructCamelName: info.Abbreviation, + ModuleName: "api" + info.StructName, + GroupName: "Api", + PackageName: "api", + ServiceName: info.StructName, + } + asts[pluginRouterEnter.Path+"=>"+pluginRouterEnter.Type.String()] = pluginRouterEnter + creates[four] = pluginRouterEnter.Path + } + if isApi != -1 { + pluginApiEnter := &ast.PluginEnter{ + Type: ast.TypePluginApiEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), strings.TrimSuffix(threeDirs[k].Name(), ext)), + ImportPath: fmt.Sprintf(`"%s/plugin/%s/service"`, global.GVA_CONFIG.AutoCode.Module, entity.PackageName), + StructName: info.StructName, + StructCamelName: info.Abbreviation, + ModuleName: "service" + info.StructName, + GroupName: "Service", + PackageName: "service", + ServiceName: info.StructName, + } + asts[pluginApiEnter.Path+"=>"+pluginApiEnter.Type.String()] = pluginApiEnter + creates[four] = pluginApiEnter.Path + } + if isService != -1 { + pluginServiceEnter := &ast.PluginEnter{ + Type: ast.TypePluginServiceEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), strings.TrimSuffix(threeDirs[k].Name(), ext)), + StructName: info.StructName, + StructCamelName: info.Abbreviation, + } + asts[pluginServiceEnter.Path+"=>"+pluginServiceEnter.Type.String()] = pluginServiceEnter + creates[four] = pluginServiceEnter.Path + } + continue + } // enter.go + create := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), info.HumpPackageName+".go") + code[four] = create + } + case "gen", "config", "initialize", "plugin", "response": + if entity.Template == "package" { + continue + } // package模板不需要生成gen, config, initialize + var threeDirs []os.DirEntry + threeDirs, err = os.ReadDir(three) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "读取模版文件夹[%s]失败!", three) + } + for k := 0; k < len(threeDirs); k++ { + if threeDirs[k].Name() == ".DS_Store" { + continue + } + four := filepath.Join(three, threeDirs[k].Name()) + if threeDirs[k].IsDir() { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件夹!", four) + } + ext := filepath.Ext(four) + if ext != ".template" && ext != ".tpl" { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版后缀!", four) + } + gen := strings.Index(threeDirs[k].Name(), "gen") + api := strings.Index(threeDirs[k].Name(), "api") + menu := strings.Index(threeDirs[k].Name(), "menu") + viper := strings.Index(threeDirs[k].Name(), "viper") + plugin := strings.Index(threeDirs[k].Name(), "plugin") + config := strings.Index(threeDirs[k].Name(), "config") + router := strings.Index(threeDirs[k].Name(), "router") + hasGorm := strings.Index(threeDirs[k].Name(), "gorm") + response := strings.Index(threeDirs[k].Name(), "response") + if gen != -1 && api != -1 && menu != -1 && viper != -1 && plugin != -1 && config != -1 && router != -1 && hasGorm != -1 && response != -1 { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件!", four) + } + if api != -1 || menu != -1 || viper != -1 || response != -1 || plugin != -1 || config != -1 { + creates[four] = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), strings.TrimSuffix(threeDirs[k].Name(), ext)) + } + if gen != -1 { + pluginGen := &ast.PluginGen{ + Type: ast.TypePluginGen, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), strings.TrimSuffix(threeDirs[k].Name(), ext)), + ImportPath: fmt.Sprintf(`"%s/plugin/%s/model"`, global.GVA_CONFIG.AutoCode.Module, entity.PackageName), + StructName: info.StructName, + PackageName: "model", + IsNew: true, + } + asts[pluginGen.Path+"=>"+pluginGen.Type.String()] = pluginGen + creates[four] = pluginGen.Path + } + if hasGorm != -1 { + pluginInitializeGorm := &ast.PluginInitializeGorm{ + Type: ast.TypePluginInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), strings.TrimSuffix(threeDirs[k].Name(), ext)), + ImportPath: fmt.Sprintf(`"%s/plugin/%s/model"`, global.GVA_CONFIG.AutoCode.Module, entity.PackageName), + StructName: info.StructName, + PackageName: "model", + IsNew: true, + } + asts[pluginInitializeGorm.Path+"=>"+pluginInitializeGorm.Type.String()] = pluginInitializeGorm + creates[four] = pluginInitializeGorm.Path + } + if router != -1 { + pluginInitializeRouter := &ast.PluginInitializeRouter{ + Type: ast.TypePluginInitializeRouter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), strings.TrimSuffix(threeDirs[k].Name(), ext)), + ImportPath: fmt.Sprintf(`"%s/plugin/%s/router"`, global.GVA_CONFIG.AutoCode.Module, entity.PackageName), + AppName: "Router", + GroupName: info.StructName, + PackageName: "router", + FunctionName: "Init", + LeftRouterGroupName: "public", + RightRouterGroupName: "private", + } + asts[pluginInitializeRouter.Path+"=>"+pluginInitializeRouter.Type.String()] = pluginInitializeRouter + creates[four] = pluginInitializeRouter.Path + } + } + case "model": + var threeDirs []os.DirEntry + threeDirs, err = os.ReadDir(three) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "读取模版文件夹[%s]失败!", three) + } + for k := 0; k < len(threeDirs); k++ { + if threeDirs[k].Name() == ".DS_Store" { + continue + } + four := filepath.Join(three, threeDirs[k].Name()) + if threeDirs[k].IsDir() { + var fourDirs []os.DirEntry + fourDirs, err = os.ReadDir(four) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "读取模版文件夹[%s]失败!", four) + } + for l := 0; l < len(fourDirs); l++ { + if fourDirs[l].Name() == ".DS_Store" { + continue + } + five := filepath.Join(four, fourDirs[l].Name()) + if fourDirs[l].IsDir() { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件夹!", five) + } + ext := filepath.Ext(five) + if ext != ".template" && ext != ".tpl" { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版后缀!", five) + } + hasRequest := strings.Index(fourDirs[l].Name(), "request") + if hasRequest == -1 { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件!", five) + } + create := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), threeDirs[k].Name(), info.HumpPackageName+".go") + if entity.Template == "package" { + create = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), entity.PackageName, threeDirs[k].Name(), info.HumpPackageName+".go") + } + code[five] = create + } + continue + } + ext := filepath.Ext(threeDirs[k].Name()) + if ext != ".template" && ext != ".tpl" { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版后缀!", four) + } + hasModel := strings.Index(threeDirs[k].Name(), "model") + if hasModel == -1 { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件!", four) + } + create := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), info.HumpPackageName+".go") + if entity.Template == "package" { + packageInitializeGorm := &ast.PackageInitializeGorm{ + Type: ast.TypePackageInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"), + ImportPath: fmt.Sprintf(`"%s/model/%s"`, global.GVA_CONFIG.AutoCode.Module, entity.PackageName), + Business: info.BusinessDB, + StructName: info.StructName, + PackageName: entity.PackageName, + IsNew: true, + } + code[four] = packageInitializeGorm.Path + asts[packageInitializeGorm.Path+"=>"+packageInitializeGorm.Type.String()] = packageInitializeGorm + create = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), entity.PackageName, info.HumpPackageName+".go") + } + code[four] = create + } + default: + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件夹!", three) + } + } + case "web": + var secondDirs []os.DirEntry + secondDirs, err = os.ReadDir(second) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "读取模版文件夹[%s]失败!", second) + } + for j := 0; j < len(secondDirs); j++ { + if secondDirs[j].Name() == ".DS_Store" { + continue + } + three := filepath.Join(second, secondDirs[j].Name()) + if !secondDirs[j].IsDir() { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件!", three) + } + switch secondDirs[j].Name() { + case "api", "form", "view", "table": + var threeDirs []os.DirEntry + threeDirs, err = os.ReadDir(three) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "读取模版文件夹[%s]失败!", three) + } + for k := 0; k < len(threeDirs); k++ { + if threeDirs[k].Name() == ".DS_Store" { + continue + } + four := filepath.Join(three, threeDirs[k].Name()) + if threeDirs[k].IsDir() { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件夹!", four) + } + ext := filepath.Ext(four) + if ext != ".template" && ext != ".tpl" { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版后缀!", four) + } + api := strings.Index(threeDirs[k].Name(), "api") + form := strings.Index(threeDirs[k].Name(), "form") + view := strings.Index(threeDirs[k].Name(), "view") + table := strings.Index(threeDirs[k].Name(), "table") + if api == -1 && form == -1 && view == -1 && table == -1 { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件!", four) + } + if entity.Template == "package" { + if view != -1 || table != -1 { + formPath := filepath.Join(three, "form.vue"+ext) + value, ok := code[formPath] + if ok { + value = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.WebRoot(), secondDirs[j].Name(), entity.PackageName, info.PackageName, info.PackageName+"Form"+filepath.Ext(strings.TrimSuffix(threeDirs[k].Name(), ext))) + code[formPath] = value + } + } + create := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.WebRoot(), secondDirs[j].Name(), entity.PackageName, info.PackageName, info.PackageName+filepath.Ext(strings.TrimSuffix(threeDirs[k].Name(), ext))) + if api != -1 { + create = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.WebRoot(), secondDirs[j].Name(), entity.PackageName, info.PackageName+filepath.Ext(strings.TrimSuffix(threeDirs[k].Name(), ext))) + } + code[four] = create + continue + } + create := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.WebRoot(), "plugin", entity.PackageName, secondDirs[j].Name(), info.PackageName+filepath.Ext(strings.TrimSuffix(threeDirs[k].Name(), ext))) + code[four] = create + } + default: + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件夹!", three) + } + } + case "readme.txt.tpl", "readme.txt.template": + continue + default: + if templateDirs[i].Name() == ".DS_Store" { + continue + } + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件!", second) + } + } + return code, asts, creates, nil +} diff --git a/admin/server/service/system/auto_code_package_test.go b/admin/server/service/system/auto_code_package_test.go new file mode 100644 index 000000000..94285e979 --- /dev/null +++ b/admin/server/service/system/auto_code_package_test.go @@ -0,0 +1,105 @@ +package system + +import ( + "context" + model "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "reflect" + "testing" +) + +func Test_autoCodePackage_Create(t *testing.T) { + type args struct { + ctx context.Context + info *request.SysAutoCodePackageCreate + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "测试 package", + args: args{ + ctx: context.Background(), + info: &request.SysAutoCodePackageCreate{ + Template: "package", + PackageName: "gva", + }, + }, + wantErr: false, + }, + { + name: "测试 plugin", + args: args{ + ctx: context.Background(), + info: &request.SysAutoCodePackageCreate{ + Template: "plugin", + PackageName: "gva", + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &autoCodePackage{} + if err := a.Create(tt.args.ctx, tt.args.info); (err != nil) != tt.wantErr { + t.Errorf("Create() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_autoCodePackage_templates(t *testing.T) { + type args struct { + ctx context.Context + entity model.SysAutoCodePackage + info request.AutoCode + } + tests := []struct { + name string + args args + wantCode map[string]string + wantEnter map[string]map[string]string + wantErr bool + }{ + { + name: "测试1", + args: args{ + ctx: context.Background(), + entity: model.SysAutoCodePackage{ + Desc: "描述", + Label: "展示名", + Template: "plugin", + PackageName: "preview", + }, + info: request.AutoCode{ + Abbreviation: "user", + HumpPackageName: "user", + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &autoCodePackage{} + gotCode, gotEnter, gotCreates, err := s.templates(tt.args.ctx, tt.args.entity, tt.args.info) + if (err != nil) != tt.wantErr { + t.Errorf("templates() error = %v, wantErr %v", err, tt.wantErr) + return + } + for key, value := range gotCode { + t.Logf("\n") + t.Logf(key) + t.Logf(value) + t.Logf("\n") + } + t.Log(gotCreates) + if !reflect.DeepEqual(gotEnter, tt.wantEnter) { + t.Errorf("templates() gotEnter = %v, want %v", gotEnter, tt.wantEnter) + } + }) + } +} diff --git a/admin/server/service/system/auto_code_plugin.go b/admin/server/service/system/auto_code_plugin.go new file mode 100644 index 000000000..a626d0941 --- /dev/null +++ b/admin/server/service/system/auto_code_plugin.go @@ -0,0 +1,249 @@ +package system + +import ( + "bytes" + "context" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/flipped-aurora/gin-vue-admin/server/utils/ast" + "github.com/mholt/archiver/v4" + cp "github.com/otiai10/copy" + "github.com/pkg/errors" + "go.uber.org/zap" + "go/parser" + "go/printer" + "go/token" + "io" + "mime/multipart" + "os" + "path/filepath" + "strings" +) + +var AutoCodePlugin = new(autoCodePlugin) + +type autoCodePlugin struct{} + +// Install 插件安装 +func (s *autoCodePlugin) Install(file *multipart.FileHeader) (web, server int, err error) { + const GVAPLUGPINATH = "./gva-plug-temp/" + defer os.RemoveAll(GVAPLUGPINATH) + _, err = os.Stat(GVAPLUGPINATH) + if os.IsNotExist(err) { + os.Mkdir(GVAPLUGPINATH, os.ModePerm) + } + + src, err := file.Open() + if err != nil { + return -1, -1, err + } + defer src.Close() + + out, err := os.Create(GVAPLUGPINATH + file.Filename) + if err != nil { + return -1, -1, err + } + defer out.Close() + + _, err = io.Copy(out, src) + + paths, err := utils.Unzip(GVAPLUGPINATH+file.Filename, GVAPLUGPINATH) + paths = filterFile(paths) + var webIndex = -1 + var serverIndex = -1 + webPlugin := "" + serverPlugin := "" + + for i := range paths { + paths[i] = filepath.ToSlash(paths[i]) + pathArr := strings.Split(paths[i], "/") + ln := len(pathArr) + + if ln < 4 { + continue + } + if pathArr[2]+"/"+pathArr[3] == `server/plugin` && len(serverPlugin) == 0 { + serverPlugin = filepath.Join(pathArr[0], pathArr[1], pathArr[2], pathArr[3]) + } + if pathArr[2]+"/"+pathArr[3] == `web/plugin` && len(webPlugin) == 0 { + webPlugin = filepath.Join(pathArr[0], pathArr[1], pathArr[2], pathArr[3]) + } + } + if len(serverPlugin) == 0 && len(webPlugin) == 0 { + zap.L().Error("非标准插件,请按照文档自动迁移使用") + return webIndex, serverIndex, errors.New("非标准插件,请按照文档自动迁移使用") + } + + if len(serverPlugin) != 0 { + err = installation(serverPlugin, global.GVA_CONFIG.AutoCode.Server, global.GVA_CONFIG.AutoCode.Server) + if err != nil { + return webIndex, serverIndex, err + } + } + + if len(webPlugin) != 0 { + err = installation(webPlugin, global.GVA_CONFIG.AutoCode.Server, global.GVA_CONFIG.AutoCode.Web) + if err != nil { + return webIndex, serverIndex, err + } + } + + return 1, 1, err +} + +func installation(path string, formPath string, toPath string) error { + arr := strings.Split(filepath.ToSlash(path), "/") + ln := len(arr) + if ln < 3 { + return errors.New("arr") + } + name := arr[ln-3] + + var form = filepath.Join(global.GVA_CONFIG.AutoCode.Root, formPath, path) + var to = filepath.Join(global.GVA_CONFIG.AutoCode.Root, toPath, "plugin") + _, err := os.Stat(to + name) + if err == nil { + zap.L().Error("autoPath 已存在同名插件,请自行手动安装", zap.String("to", to)) + return errors.New(toPath + "已存在同名插件,请自行手动安装") + } + return cp.Copy(form, to, cp.Options{Skip: skipMacSpecialDocument}) +} + +func filterFile(paths []string) []string { + np := make([]string, 0, len(paths)) + for _, path := range paths { + if ok, _ := skipMacSpecialDocument(nil, path, ""); ok { + continue + } + np = append(np, path) + } + return np +} + +func skipMacSpecialDocument(_ os.FileInfo, src, _ string) (bool, error) { + if strings.Contains(src, ".DS_Store") || strings.Contains(src, "__MACOSX") { + return true, nil + } + return false, nil +} + +func (s *autoCodePlugin) PubPlug(plugName string) (zipPath string, err error) { + if plugName == "" { + return "", errors.New("插件名称不能为空") + } + + // 防止路径穿越 + plugName = filepath.Clean(plugName) + + webPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Web, "plugin", plugName) + serverPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", plugName) + // 创建一个新的zip文件 + + // 判断目录是否存在 + _, err = os.Stat(webPath) + if err != nil { + return "", errors.New("web路径不存在") + } + _, err = os.Stat(serverPath) + if err != nil { + return "", errors.New("server路径不存在") + } + + fileName := plugName + ".zip" + // 创建一个新的zip文件 + files, err := archiver.FilesFromDisk(nil, map[string]string{ + webPath: plugName + "/web/plugin/" + plugName, + serverPath: plugName + "/server/plugin/" + plugName, + }) + + // create the output file we'll write to + out, err := os.Create(fileName) + if err != nil { + return + } + defer out.Close() + + // we can use the CompressedArchive type to gzip a tarball + // (compression is not required; you could use Tar directly) + format := archiver.CompressedArchive{ + Archival: archiver.Zip{}, + } + + // create the archive + err = format.Archive(context.Background(), out, files) + if err != nil { + return + } + + return filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, fileName), nil +} + +func (s *autoCodePlugin) InitMenu(menuInfo request.InitMenu) (err error) { + menuPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", menuInfo.PlugName, "initialize", "menu.go") + src, err := os.ReadFile(menuPath) + if err != nil { + fmt.Println(err) + } + fileSet := token.NewFileSet() + astFile, err := parser.ParseFile(fileSet, "", src, 0) + arrayAst := ast.FindArray(astFile, "model", "SysBaseMenu") + var menus []system.SysBaseMenu + + parentMenu := []system.SysBaseMenu{ + { + ParentId: 0, + Path: menuInfo.PlugName + "Menu", + Name: menuInfo.PlugName + "Menu", + Hidden: false, + Component: "view/routerHolder.vue", + Sort: 0, + Meta: system.Meta{ + Title: menuInfo.ParentMenu, + Icon: "school", + }, + }, + } + + err = global.GVA_DB.Find(&menus, "id in (?)", menuInfo.Menus).Error + if err != nil { + return err + } + menus = append(parentMenu, menus...) + menuExpr := ast.CreateMenuStructAst(menus) + arrayAst.Elts = *menuExpr + + var out []byte + bf := bytes.NewBuffer(out) + printer.Fprint(bf, fileSet, astFile) + + os.WriteFile(menuPath, bf.Bytes(), 0666) + return nil +} + +func (s *autoCodePlugin) InitAPI(apiInfo request.InitApi) (err error) { + apiPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", apiInfo.PlugName, "initialize", "api.go") + src, err := os.ReadFile(apiPath) + if err != nil { + fmt.Println(err) + } + fileSet := token.NewFileSet() + astFile, err := parser.ParseFile(fileSet, "", src, 0) + arrayAst := ast.FindArray(astFile, "model", "SysApi") + var apis []system.SysApi + err = global.GVA_DB.Find(&apis, "id in (?)", apiInfo.APIs).Error + if err != nil { + return err + } + apisExpr := ast.CreateApiStructAst(apis) + arrayAst.Elts = *apisExpr + + var out []byte + bf := bytes.NewBuffer(out) + printer.Fprint(bf, fileSet, astFile) + + os.WriteFile(apiPath, bf.Bytes(), 0666) + return nil +} diff --git a/admin/server/service/system/auto_code_template.go b/admin/server/service/system/auto_code_template.go new file mode 100644 index 000000000..bf366ee1b --- /dev/null +++ b/admin/server/service/system/auto_code_template.go @@ -0,0 +1,444 @@ +package system + +import ( + "context" + "encoding/json" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + model "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + utilsAst "github.com/flipped-aurora/gin-vue-admin/server/utils/ast" + "github.com/pkg/errors" + "go/ast" + "go/format" + "go/parser" + "go/token" + "gorm.io/gorm" + "os" + "path/filepath" + "strings" + "text/template" +) + +var AutoCodeTemplate = new(autoCodeTemplate) + +type autoCodeTemplate struct{} + +func (s *autoCodeTemplate) checkPackage(Pkg string, template string) (err error) { + switch template { + case "package": + apiEnter := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", Pkg, "enter.go") + _, err = os.Stat(apiEnter) + if err != nil { + return fmt.Errorf("package结构异常,缺少api/v1/%s/enter.go", Pkg) + } + serviceEnter := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", Pkg, "enter.go") + _, err = os.Stat(serviceEnter) + if err != nil { + return fmt.Errorf("package结构异常,缺少service/%s/enter.go", Pkg) + } + routerEnter := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", Pkg, "enter.go") + _, err = os.Stat(routerEnter) + if err != nil { + return fmt.Errorf("package结构异常,缺少router/%s/enter.go", Pkg) + } + case "plugin": + pluginEnter := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", Pkg, "plugin.go") + _, err = os.Stat(pluginEnter) + if err != nil { + return fmt.Errorf("plugin结构异常,缺少plugin/%s/plugin.go", Pkg) + } + } + return nil +} + +// Create 创建生成自动化代码 +func (s *autoCodeTemplate) Create(ctx context.Context, info request.AutoCode) error { + history := info.History() + var autoPkg model.SysAutoCodePackage + err := global.GVA_DB.WithContext(ctx).Where("package_name = ?", info.Package).First(&autoPkg).Error + if err != nil { + return errors.Wrap(err, "查询包失败!") + } + err = s.checkPackage(info.Package, autoPkg.Template) + if err != nil { + return err + } + // 增加判断: 重复创建struct + if AutocodeHistory.Repeat(info.BusinessDB, info.StructName, info.Package) { + return errors.New("已经创建过此数据结构,请勿重复创建!") + } + + generate, templates, injections, err := s.generate(ctx, info, autoPkg) + if err != nil { + return err + } + for key, builder := range generate { + err = os.MkdirAll(filepath.Dir(key), os.ModePerm) + if err != nil { + return errors.Wrapf(err, "[filepath:%s]创建文件夹失败!", key) + } + err = os.WriteFile(key, []byte(builder.String()), 0666) + if err != nil { + return errors.Wrapf(err, "[filepath:%s]写入文件失败!", key) + } + } + + // 自动创建api + if info.AutoCreateApiToSql && !info.OnlyTemplate { + apis := info.Apis() + err := global.GVA_DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + for _, v := range apis { + var api model.SysApi + var id uint + err := tx.Where("path = ? AND method = ?", v.Path, v.Method).First(&api).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + if err = tx.Create(&v).Error; err != nil { // 遇到错误时回滚事务 + return err + } + id = v.ID + } else { + id = api.ID + } + history.ApiIDs = append(history.ApiIDs, id) + } + return nil + }) + if err != nil { + return err + } + } + + // 自动创建menu + if info.AutoCreateMenuToSql { + var entity model.SysBaseMenu + var id uint + err := global.GVA_DB.WithContext(ctx).First(&entity, "name = ?", info.Abbreviation).Error + if err == nil { + id = entity.ID + } else { + entity = info.Menu(autoPkg.Template) + if info.AutoCreateBtnAuth && !info.OnlyTemplate { + entity.MenuBtn = []model.SysBaseMenuBtn{ + {SysBaseMenuID: entity.ID, Name: "add", Desc: "新增"}, + {SysBaseMenuID: entity.ID, Name: "batchDelete", Desc: "批量删除"}, + {SysBaseMenuID: entity.ID, Name: "delete", Desc: "删除"}, + {SysBaseMenuID: entity.ID, Name: "edit", Desc: "编辑"}, + {SysBaseMenuID: entity.ID, Name: "info", Desc: "详情"}, + } + if info.HasExcel { + excelBtn := []model.SysBaseMenuBtn{ + {SysBaseMenuID: entity.ID, Name: "exportTemplate", Desc: "导出模板"}, + {SysBaseMenuID: entity.ID, Name: "exportExcel", Desc: "导出Excel"}, + {SysBaseMenuID: entity.ID, Name: "importExcel", Desc: "导入Excel"}, + } + entity.MenuBtn = append(entity.MenuBtn, excelBtn...) + } + } + err = global.GVA_DB.WithContext(ctx).Create(&entity).Error + id = entity.ID + if err != nil { + return errors.Wrap(err, "创建菜单失败!") + } + } + history.MenuID = id + } + + if info.HasExcel { + dbName := info.BusinessDB + name := info.Package + "_" + info.StructName + tableName := info.TableName + fieldsMap := make(map[string]string, len(info.Fields)) + for _, field := range info.Fields { + if field.Excel { + fieldsMap[field.ColumnName] = field.FieldDesc + } + } + templateInfo, _ := json.Marshal(fieldsMap) + sysExportTemplate := model.SysExportTemplate{ + DBName: dbName, + Name: name, + TableName: tableName, + TemplateID: name, + TemplateInfo: string(templateInfo), + } + err = SysExportTemplateServiceApp.CreateSysExportTemplate(&sysExportTemplate) + if err != nil { + return err + } + history.ExportTemplateID = sysExportTemplate.ID + } + + // 创建历史记录 + history.Templates = templates + history.Injections = make(map[string]string, len(injections)) + for key, value := range injections { + bytes, _ := json.Marshal(value) + history.Injections[key] = string(bytes) + } + err = AutocodeHistory.Create(ctx, history) + if err != nil { + return err + } + return nil +} + +// Preview 预览自动化代码 +func (s *autoCodeTemplate) Preview(ctx context.Context, info request.AutoCode) (map[string]string, error) { + var entity model.SysAutoCodePackage + err := global.GVA_DB.WithContext(ctx).Where("package_name = ?", info.Package).First(&entity).Error + if err != nil { + return nil, errors.Wrap(err, "查询包失败!") + } + codes := make(map[string]strings.Builder) + preview := make(map[string]string) + codes, _, _, err = s.generate(ctx, info, entity) + if err != nil { + return nil, err + } + for key, writer := range codes { + if len(key) > len(global.GVA_CONFIG.AutoCode.Root) { + key, _ = filepath.Rel(global.GVA_CONFIG.AutoCode.Root, key) + } + // 获取key的后缀 取消. + suffix := filepath.Ext(key)[1:] + var builder strings.Builder + builder.WriteString("```" + suffix + "\n\n") + builder.WriteString(writer.String()) + builder.WriteString("\n\n```") + preview[key] = builder.String() + } + return preview, nil +} + +func (s *autoCodeTemplate) generate(ctx context.Context, info request.AutoCode, entity model.SysAutoCodePackage) (map[string]strings.Builder, map[string]string, map[string]utilsAst.Ast, error) { + templates, asts, _, err := AutoCodePackage.templates(ctx, entity, info) + if err != nil { + return nil, nil, nil, err + } + code := make(map[string]strings.Builder) + for key, create := range templates { + var files *template.Template + files, err = template.ParseFiles(key) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "[filpath:%s]读取模版文件失败!", key) + } + var builder strings.Builder + err = files.Execute(&builder, info) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "[filpath:%s]生成文件失败!", create) + } + code[create] = builder + } // 生成文件 + injections := make(map[string]utilsAst.Ast, len(asts)) + for key, value := range asts { + keys := strings.Split(key, "=>") + if len(keys) == 2 { + if keys[1] == utilsAst.TypePluginInitializeV2 { + continue + } + if info.OnlyTemplate { + if keys[1] == utilsAst.TypePackageInitializeGorm || keys[1] == utilsAst.TypePluginInitializeGorm { + continue + } + } + if !info.AutoMigrate { + if keys[1] == utilsAst.TypePackageInitializeGorm || keys[1] == utilsAst.TypePluginInitializeGorm { + continue + } + } + var builder strings.Builder + parse, _ := value.Parse("", &builder) + if parse != nil { + _ = value.Injection(parse) + err = value.Format("", &builder, parse) + if err != nil { + return nil, nil, nil, err + } + code[keys[0]] = builder + injections[keys[1]] = value + fmt.Println(keys[0], "注入成功!") + } + } + } + // 注入代码 + return code, templates, injections, nil +} + +func (s *autoCodeTemplate) AddFunc(info request.AutoFunc) error { + autoPkg := model.SysAutoCodePackage{} + err := global.GVA_DB.First(&autoPkg, "package_name = ?", info.Package).Error + if err != nil { + return err + } + if autoPkg.Template != "package" { + info.IsPlugin = true + } + err = s.addTemplateToFile("api.go", info) + if err != nil { + return err + } + err = s.addTemplateToFile("server.go", info) + if err != nil { + return err + } + err = s.addTemplateToFile("api.js", info) + if err != nil { + return err + } + err = s.addTemplateToAst("router", info) + return nil +} + +func (s *autoCodeTemplate) GetApiAndServer(info request.AutoFunc) (map[string]string, error) { + autoPkg := model.SysAutoCodePackage{} + err := global.GVA_DB.First(&autoPkg, "package_name = ?", info.Package).Error + if err != nil { + return nil, err + } + if autoPkg.Template != "package" { + info.IsPlugin = true + } + + apiStr, err := s.getTemplateStr("api.go", info) + if err != nil { + return nil, err + } + serverStr, err := s.getTemplateStr("server.go", info) + if err != nil { + return nil, err + } + jsStr, err := s.getTemplateStr("api.js", info) + if err != nil { + return nil, err + } + return map[string]string{"api": apiStr, "server": serverStr, "js": jsStr}, nil + +} + +func (s *autoCodeTemplate) getTemplateStr(t string, info request.AutoFunc) (string, error) { + tempPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "resource", "function", t+".tpl") + files, err := template.ParseFiles(tempPath) + if err != nil { + return "", errors.Wrapf(err, "[filepath:%s]读取模版文件失败!", tempPath) + } + var builder strings.Builder + err = files.Execute(&builder, info) + if err != nil { + fmt.Println(err.Error()) + return "", errors.Wrapf(err, "[filpath:%s]生成文件失败!", tempPath) + } + return builder.String(), nil +} + +func (s *autoCodeTemplate) addTemplateToAst(t string, info request.AutoFunc) error { + tPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", info.Package, info.HumpPackageName+".go") + funcName := fmt.Sprintf("Init%sRouter", info.StructName) + + routerStr := "RouterWithoutAuth" + if info.IsAuth { + routerStr = "Router" + } + + stmtStr := fmt.Sprintf("%s%s.%s(\"%s\", %sApi.%s)", info.Abbreviation, routerStr, info.Method, info.Router, info.Abbreviation, info.FuncName) + if info.IsPlugin { + tPath = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", info.Package, "router", info.HumpPackageName+".go") + stmtStr = fmt.Sprintf("group.%s(\"%s\", api%s.%s)", info.Method, info.Router, info.StructName, info.FuncName) + funcName = "Init" + } + + src, err := os.ReadFile(tPath) + fileSet := token.NewFileSet() + astFile, err := parser.ParseFile(fileSet, "", src, 0) + if err != nil { + fmt.Println(err) + } + funcDecl := utilsAst.FindFunction(astFile, funcName) + stmtNode := utilsAst.CreateStmt(stmtStr) + + if info.IsAuth { + for i := 0; i < len(funcDecl.Body.List); i++ { + st := funcDecl.Body.List[i] + // 使用类型断言来检查stmt是否是一个块语句 + if blockStmt, ok := st.(*ast.BlockStmt); ok { + // 如果是,插入代码 跳出 + blockStmt.List = append(blockStmt.List, stmtNode) + break + } + } + } else { + for i := len(funcDecl.Body.List) - 1; i >= 0; i-- { + st := funcDecl.Body.List[i] + // 使用类型断言来检查stmt是否是一个块语句 + if blockStmt, ok := st.(*ast.BlockStmt); ok { + // 如果是,插入代码 跳出 + blockStmt.List = append(blockStmt.List, stmtNode) + break + } + } + } + + // 创建一个新的文件 + f, err := os.Create(tPath) + if err != nil { + return err + } + defer f.Close() + + if err := format.Node(f, fileSet, astFile); err != nil { + return err + } + return err +} + +func (s *autoCodeTemplate) addTemplateToFile(t string, info request.AutoFunc) error { + getTemplateStr, err := s.getTemplateStr(t, info) + if err != nil { + return err + } + var target string + + switch t { + case "api.go": + if info.IsAi && info.ApiFunc != "" { + getTemplateStr = info.ApiFunc + } + target = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", info.Package, info.HumpPackageName+".go") + case "server.go": + if info.IsAi && info.ServerFunc != "" { + getTemplateStr = info.ServerFunc + } + target = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", info.Package, info.HumpPackageName+".go") + case "api.js": + if info.IsAi && info.JsFunc != "" { + getTemplateStr = info.JsFunc + } + target = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Web, "api", info.Package, info.PackageName+".js") + } + if info.IsPlugin { + switch t { + case "api.go": + target = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", info.Package, "api", info.HumpPackageName+".go") + case "server.go": + target = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", info.Package, "service", info.HumpPackageName+".go") + case "api.js": + target = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Web, "plugin", info.Package, "api", info.PackageName+".js") + } + } + + // 打开文件,如果不存在则返回错误 + file, err := os.OpenFile(target, os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + return err + } + defer file.Close() + + // 写入内容 + _, err = fmt.Fprintln(file, getTemplateStr) + if err != nil { + fmt.Printf("写入文件失败: %s\n", err.Error()) + return err + } + + return nil +} diff --git a/admin/server/service/system/auto_code_template_test.go b/admin/server/service/system/auto_code_template_test.go new file mode 100644 index 000000000..09d8191dd --- /dev/null +++ b/admin/server/service/system/auto_code_template_test.go @@ -0,0 +1,84 @@ +package system + +import ( + "context" + "encoding/json" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "reflect" + "testing" +) + +func Test_autoCodeTemplate_Create(t *testing.T) { + type args struct { + ctx context.Context + info request.AutoCode + } + tests := []struct { + name string + args args + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &autoCodeTemplate{} + if err := s.Create(tt.args.ctx, tt.args.info); (err != nil) != tt.wantErr { + t.Errorf("Create() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_autoCodeTemplate_Preview(t *testing.T) { + type args struct { + ctx context.Context + info request.AutoCode + } + tests := []struct { + name string + args args + want map[string]string + wantErr bool + }{ + { + name: "测试 package", + args: args{ + ctx: context.Background(), + info: request.AutoCode{}, + }, + wantErr: false, + }, + { + name: "测试 plugin", + args: args{ + ctx: context.Background(), + info: request.AutoCode{}, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testJson := `{"structName":"SysUser","tableName":"sys_users","packageName":"sysUsers","package":"gva","abbreviation":"sysUsers","description":"sysUsers表","businessDB":"","autoCreateApiToSql":true,"autoCreateMenuToSql":true,"autoMigrate":true,"gvaModel":true,"autoCreateResource":false,"fields":[{"fieldName":"Uuid","fieldDesc":"用户UUID","fieldType":"string","dataType":"varchar","fieldJson":"uuid","primaryKey":false,"dataTypeLong":"191","columnName":"uuid","comment":"用户UUID","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"Username","fieldDesc":"用户登录名","fieldType":"string","dataType":"varchar","fieldJson":"username","primaryKey":false,"dataTypeLong":"191","columnName":"username","comment":"用户登录名","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"Password","fieldDesc":"用户登录密码","fieldType":"string","dataType":"varchar","fieldJson":"password","primaryKey":false,"dataTypeLong":"191","columnName":"password","comment":"用户登录密码","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"NickName","fieldDesc":"用户昵称","fieldType":"string","dataType":"varchar","fieldJson":"nickName","primaryKey":false,"dataTypeLong":"191","columnName":"nick_name","comment":"用户昵称","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"SideMode","fieldDesc":"用户侧边主题","fieldType":"string","dataType":"varchar","fieldJson":"sideMode","primaryKey":false,"dataTypeLong":"191","columnName":"side_mode","comment":"用户侧边主题","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"HeaderImg","fieldDesc":"用户头像","fieldType":"string","dataType":"varchar","fieldJson":"headerImg","primaryKey":false,"dataTypeLong":"191","columnName":"header_img","comment":"用户头像","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"BaseColor","fieldDesc":"基础颜色","fieldType":"string","dataType":"varchar","fieldJson":"baseColor","primaryKey":false,"dataTypeLong":"191","columnName":"base_color","comment":"基础颜色","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"AuthorityId","fieldDesc":"用户角色ID","fieldType":"int","dataType":"bigint","fieldJson":"authorityId","primaryKey":false,"dataTypeLong":"20","columnName":"authority_id","comment":"用户角色ID","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"Phone","fieldDesc":"用户手机号","fieldType":"string","dataType":"varchar","fieldJson":"phone","primaryKey":false,"dataTypeLong":"191","columnName":"phone","comment":"用户手机号","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"Email","fieldDesc":"用户邮箱","fieldType":"string","dataType":"varchar","fieldJson":"email","primaryKey":false,"dataTypeLong":"191","columnName":"email","comment":"用户邮箱","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"Enable","fieldDesc":"用户是否被冻结 1正常 2冻结","fieldType":"int","dataType":"bigint","fieldJson":"enable","primaryKey":false,"dataTypeLong":"19","columnName":"enable","comment":"用户是否被冻结 1正常 2冻结","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}}],"humpPackageName":"sys_users"}` + err := json.Unmarshal([]byte(testJson), &tt.args.info) + if err != nil { + t.Error(err) + return + } + err = tt.args.info.Pretreatment() + if err != nil { + t.Error(err) + return + } + got, err := AutoCodeTemplate.Preview(tt.args.ctx, tt.args.info) + if (err != nil) != tt.wantErr { + t.Errorf("Preview() error = %+v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Preview() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/admin/server/service/system/enter.go b/admin/server/service/system/enter.go new file mode 100644 index 000000000..875cb45fa --- /dev/null +++ b/admin/server/service/system/enter.go @@ -0,0 +1,25 @@ +package system + +type ServiceGroup struct { + JwtService + ApiService + MenuService + UserService + UserExtendService // 新增OA登录 + CasbinService + InitDBService + AutoCodeService + BaseMenuService + AuthorityService + DictionaryService + SystemConfigService + OperationRecordService + DictionaryDetailService + AuthorityBtnService + SysExportTemplateService + SysParamsService + AutoCodePlugin autoCodePlugin + AutoCodePackage autoCodePackage + AutoCodeHistory autoCodeHistory + AutoCodeTemplate autoCodeTemplate +} diff --git a/admin/server/service/system/jwt_black_list.go b/admin/server/service/system/jwt_black_list.go new file mode 100644 index 000000000..78ae38a7e --- /dev/null +++ b/admin/server/service/system/jwt_black_list.go @@ -0,0 +1,84 @@ +package system + +import ( + "context" + + "go.uber.org/zap" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/utils" +) + +type JwtService struct{} + +var JwtServiceApp = new(JwtService) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: JsonInBlacklist +//@description: 拉黑jwt +//@param: jwtList model.JwtBlacklist +//@return: err error + +func (jwtService *JwtService) JsonInBlacklist(jwtList system.JwtBlacklist) (err error) { + err = global.GVA_DB.Create(&jwtList).Error + if err != nil { + return + } + global.BlackCache.SetDefault(jwtList.Jwt, struct{}{}) + return +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: IsBlacklist +//@description: 判断JWT是否在黑名单内部 +//@param: jwt string +//@return: bool + +func (jwtService *JwtService) IsBlacklist(jwt string) bool { + _, ok := global.BlackCache.Get(jwt) + return ok + // err := global.GVA_DB.Where("jwt = ?", jwt).First(&system.JwtBlacklist{}).Error + // isNotFound := errors.Is(err, gorm.ErrRecordNotFound) + // return !isNotFound +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetRedisJWT +//@description: 从redis取jwt +//@param: userName string +//@return: redisJWT string, err error + +func (jwtService *JwtService) GetRedisJWT(userName string) (redisJWT string, err error) { + redisJWT, err = global.GVA_REDIS.Get(context.Background(), userName).Result() + return redisJWT, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: SetRedisJWT +//@description: jwt存入redis并设置过期时间 +//@param: jwt string, userName string +//@return: err error + +func (jwtService *JwtService) SetRedisJWT(jwt string, userName string) (err error) { + // 此处过期时间等于jwt过期时间 + dr, err := utils.ParseDuration(global.GVA_CONFIG.JWT.ExpiresTime) + if err != nil { + return err + } + timer := dr + err = global.GVA_REDIS.Set(context.Background(), userName, jwt, timer).Err() + return err +} + +func LoadAll() { + var data []string + err := global.GVA_DB.Model(&system.JwtBlacklist{}).Select("jwt").Find(&data).Error + if err != nil { + global.GVA_LOG.Error("加载数据库jwt黑名单失败!", zap.Error(err)) + return + } + for i := 0; i < len(data); i++ { + global.BlackCache.SetDefault(data[i], struct{}{}) + } // jwt黑名单 加入 BlackCache 中 +} diff --git a/admin/server/service/system/sys_api.go b/admin/server/service/system/sys_api.go new file mode 100644 index 000000000..92c995109 --- /dev/null +++ b/admin/server/service/system/sys_api.go @@ -0,0 +1,325 @@ +package system + +import ( + "errors" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" + "gorm.io/gorm" + "strings" +) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: CreateApi +//@description: 新增基础api +//@param: api model.SysApi +//@return: err error + +type ApiService struct{} + +var ApiServiceApp = new(ApiService) + +func (apiService *ApiService) CreateApi(api system.SysApi) (err error) { + if !errors.Is(global.GVA_DB.Where("path = ? AND method = ?", api.Path, api.Method).First(&system.SysApi{}).Error, gorm.ErrRecordNotFound) { + return errors.New("存在相同api") + } + return global.GVA_DB.Create(&api).Error +} + +func (apiService *ApiService) GetApiGroups() (groups []string, groupApiMap map[string]string, err error) { + var apis []system.SysApi + err = global.GVA_DB.Find(&apis).Error + if err != nil { + return + } + groupApiMap = make(map[string]string, 0) + for i := range apis { + pathArr := strings.Split(apis[i].Path, "/") + newGroup := true + for i2 := range groups { + if groups[i2] == apis[i].ApiGroup { + newGroup = false + } + } + if newGroup { + groups = append(groups, apis[i].ApiGroup) + } + groupApiMap[pathArr[1]] = apis[i].ApiGroup + } + return +} + +func (apiService *ApiService) SyncApi() (newApis, deleteApis, ignoreApis []system.SysApi, err error) { + newApis = make([]system.SysApi, 0) + deleteApis = make([]system.SysApi, 0) + ignoreApis = make([]system.SysApi, 0) + var apis []system.SysApi + err = global.GVA_DB.Find(&apis).Error + if err != nil { + return + } + var ignores []system.SysIgnoreApi + err = global.GVA_DB.Find(&ignores).Error + if err != nil { + return + } + + for i := range ignores { + ignoreApis = append(ignoreApis, system.SysApi{ + Path: ignores[i].Path, + Description: "", + ApiGroup: "", + Method: ignores[i].Method, + }) + } + + var cacheApis []system.SysApi + for i := range global.GVA_ROUTERS { + ignoresFlag := false + for j := range ignores { + if ignores[j].Path == global.GVA_ROUTERS[i].Path && ignores[j].Method == global.GVA_ROUTERS[i].Method { + ignoresFlag = true + } + } + if !ignoresFlag { + cacheApis = append(cacheApis, system.SysApi{ + Path: global.GVA_ROUTERS[i].Path, + Method: global.GVA_ROUTERS[i].Method, + }) + } + } + + //对比数据库中的api和内存中的api,如果数据库中的api不存在于内存中,则把api放入删除数组,如果内存中的api不存在于数据库中,则把api放入新增数组 + for i := range cacheApis { + var flag bool + // 如果存在于内存不存在于api数组中 + for j := range apis { + if cacheApis[i].Path == apis[j].Path && cacheApis[i].Method == apis[j].Method { + flag = true + } + } + if !flag { + newApis = append(newApis, system.SysApi{ + Path: cacheApis[i].Path, + Description: "", + ApiGroup: "", + Method: cacheApis[i].Method, + }) + } + } + + for i := range apis { + var flag bool + // 如果存在于api数组不存在于内存 + for j := range cacheApis { + if cacheApis[j].Path == apis[i].Path && cacheApis[j].Method == apis[i].Method { + flag = true + } + } + if !flag { + deleteApis = append(deleteApis, apis[i]) + } + } + return +} + +func (apiService *ApiService) IgnoreApi(ignoreApi system.SysIgnoreApi) (err error) { + if ignoreApi.Flag { + return global.GVA_DB.Create(&ignoreApi).Error + } + return global.GVA_DB.Unscoped().Delete(&ignoreApi, "path = ? AND method = ?", ignoreApi.Path, ignoreApi.Method).Error +} + +func (apiService *ApiService) EnterSyncApi(syncApis systemRes.SysSyncApis) (err error) { + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + var txErr error + if syncApis.NewApis != nil && len(syncApis.NewApis) > 0 { + txErr = tx.Create(&syncApis.NewApis).Error + if txErr != nil { + return txErr + } + } + for i := range syncApis.DeleteApis { + CasbinServiceApp.ClearCasbin(1, syncApis.DeleteApis[i].Path, syncApis.DeleteApis[i].Method) + txErr = tx.Delete(&system.SysApi{}, "path = ? AND method = ?", syncApis.DeleteApis[i].Path, syncApis.DeleteApis[i].Method).Error + if txErr != nil { + return txErr + } + } + return nil + }) +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: DeleteApi +//@description: 删除基础api +//@param: api model.SysApi +//@return: err error + +func (apiService *ApiService) DeleteApi(api system.SysApi) (err error) { + var entity system.SysApi + err = global.GVA_DB.First(&entity, "id = ?", api.ID).Error // 根据id查询api记录 + if errors.Is(err, gorm.ErrRecordNotFound) { // api记录不存在 + return err + } + err = global.GVA_DB.Delete(&entity).Error + if err != nil { + return err + } + CasbinServiceApp.ClearCasbin(1, entity.Path, entity.Method) + return nil +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetAPIInfoList +//@description: 分页获取数据, +//@param: api model.SysApi, info request.PageInfo, order string, desc bool +//@return: list interface{}, total int64, err error + +func (apiService *ApiService) GetAPIInfoList(api system.SysApi, info request.PageInfo, order string, desc bool) (list interface{}, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + db := global.GVA_DB.Model(&system.SysApi{}) + var apiList []system.SysApi + + if api.Path != "" { + db = db.Where("path LIKE ?", "%"+api.Path+"%") + } + + if api.Description != "" { + db = db.Where("description LIKE ?", "%"+api.Description+"%") + } + + if api.Method != "" { + db = db.Where("method = ?", api.Method) + } + + if api.ApiGroup != "" { + db = db.Where("api_group = ?", api.ApiGroup) + } + + err = db.Count(&total).Error + + if err != nil { + return apiList, total, err + } + + db = db.Limit(limit).Offset(offset) + OrderStr := "id desc" + if order != "" { + orderMap := make(map[string]bool, 5) + orderMap["id"] = true + orderMap["path"] = true + orderMap["api_group"] = true + orderMap["description"] = true + orderMap["method"] = true + if !orderMap[order] { + err = fmt.Errorf("非法的排序字段: %v", order) + return apiList, total, err + } + OrderStr = order + if desc { + OrderStr = order + " desc" + } + } + err = db.Order(OrderStr).Find(&apiList).Error + return apiList, total, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetAllApis +//@description: 获取所有的api +//@return: apis []model.SysApi, err error + +func (apiService *ApiService) GetAllApis(authorityID uint) (apis []system.SysApi, err error) { + parentAuthorityID, err := AuthorityServiceApp.GetParentAuthorityID(authorityID) + if err != nil { + return nil, err + } + err = global.GVA_DB.Order("id desc").Find(&apis).Error + if parentAuthorityID == 0 || !global.GVA_CONFIG.System.UseStrictAuth { + return + } + paths := CasbinServiceApp.GetPolicyPathByAuthorityId(authorityID) + // 挑选 apis里面的path和method也在paths里面的api + var authApis []system.SysApi + for i := range apis { + for j := range paths { + if paths[j].Path == apis[i].Path && paths[j].Method == apis[i].Method { + authApis = append(authApis, apis[i]) + } + } + } + return authApis, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetApiById +//@description: 根据id获取api +//@param: id float64 +//@return: api model.SysApi, err error + +func (apiService *ApiService) GetApiById(id int) (api system.SysApi, err error) { + err = global.GVA_DB.First(&api, "id = ?", id).Error + return +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: UpdateApi +//@description: 根据id更新api +//@param: api model.SysApi +//@return: err error + +func (apiService *ApiService) UpdateApi(api system.SysApi) (err error) { + var oldA system.SysApi + err = global.GVA_DB.First(&oldA, "id = ?", api.ID).Error + if oldA.Path != api.Path || oldA.Method != api.Method { + var duplicateApi system.SysApi + if ferr := global.GVA_DB.First(&duplicateApi, "path = ? AND method = ?", api.Path, api.Method).Error; ferr != nil { + if !errors.Is(ferr, gorm.ErrRecordNotFound) { + return ferr + } + } else { + if duplicateApi.ID != api.ID { + return errors.New("存在相同api路径") + } + } + + } + if err != nil { + return err + } + + err = CasbinServiceApp.UpdateCasbinApi(oldA.Path, api.Path, oldA.Method, api.Method) + if err != nil { + return err + } + + return global.GVA_DB.Save(&api).Error +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: DeleteApisByIds +//@description: 删除选中API +//@param: apis []model.SysApi +//@return: err error + +func (apiService *ApiService) DeleteApisByIds(ids request.IdsReq) (err error) { + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + var apis []system.SysApi + err = tx.Find(&apis, "id in ?", ids.Ids).Error + if err != nil { + return err + } + err = tx.Delete(&[]system.SysApi{}, "id in ?", ids.Ids).Error + if err != nil { + return err + } + for _, sysApi := range apis { + CasbinServiceApp.ClearCasbin(1, sysApi.Path, sysApi.Method) + } + return err + }) +} diff --git a/admin/server/service/system/sys_authority.go b/admin/server/service/system/sys_authority.go new file mode 100644 index 000000000..087da6a4e --- /dev/null +++ b/admin/server/service/system/sys_authority.go @@ -0,0 +1,327 @@ +package system + +import ( + "errors" + "strconv" + + systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" + "gorm.io/gorm" +) + +var ErrRoleExistence = errors.New("存在相同角色id") + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: CreateAuthority +//@description: 创建一个角色 +//@param: auth model.SysAuthority +//@return: authority system.SysAuthority, err error + +type AuthorityService struct{} + +var AuthorityServiceApp = new(AuthorityService) + +func (authorityService *AuthorityService) CreateAuthority(auth system.SysAuthority) (authority system.SysAuthority, err error) { + + if err = global.GVA_DB.Where("authority_id = ?", auth.AuthorityId).First(&system.SysAuthority{}).Error; !errors.Is(err, gorm.ErrRecordNotFound) { + return auth, ErrRoleExistence + } + + e := global.GVA_DB.Transaction(func(tx *gorm.DB) error { + + if err = tx.Create(&auth).Error; err != nil { + return err + } + + auth.SysBaseMenus = systemReq.DefaultMenu() + if err = tx.Model(&auth).Association("SysBaseMenus").Replace(&auth.SysBaseMenus); err != nil { + return err + } + casbinInfos := systemReq.DefaultCasbin() + authorityId := strconv.Itoa(int(auth.AuthorityId)) + rules := [][]string{} + for _, v := range casbinInfos { + rules = append(rules, []string{authorityId, v.Path, v.Method}) + } + return CasbinServiceApp.AddPolicies(tx, rules) + }) + + return auth, e +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: CopyAuthority +//@description: 复制一个角色 +//@param: copyInfo response.SysAuthorityCopyResponse +//@return: authority system.SysAuthority, err error + +func (authorityService *AuthorityService) CopyAuthority(adminAuthorityID uint, copyInfo response.SysAuthorityCopyResponse) (authority system.SysAuthority, err error) { + var authorityBox system.SysAuthority + if !errors.Is(global.GVA_DB.Where("authority_id = ?", copyInfo.Authority.AuthorityId).First(&authorityBox).Error, gorm.ErrRecordNotFound) { + return authority, ErrRoleExistence + } + copyInfo.Authority.Children = []system.SysAuthority{} + menus, err := MenuServiceApp.GetMenuAuthority(&request.GetAuthorityId{AuthorityId: copyInfo.OldAuthorityId}) + if err != nil { + return + } + var baseMenu []system.SysBaseMenu + for _, v := range menus { + intNum := v.MenuId + v.SysBaseMenu.ID = uint(intNum) + baseMenu = append(baseMenu, v.SysBaseMenu) + } + copyInfo.Authority.SysBaseMenus = baseMenu + err = global.GVA_DB.Create(©Info.Authority).Error + if err != nil { + return + } + + var btns []system.SysAuthorityBtn + + err = global.GVA_DB.Find(&btns, "authority_id = ?", copyInfo.OldAuthorityId).Error + if err != nil { + return + } + if len(btns) > 0 { + for i := range btns { + btns[i].AuthorityId = copyInfo.Authority.AuthorityId + } + err = global.GVA_DB.Create(&btns).Error + + if err != nil { + return + } + } + paths := CasbinServiceApp.GetPolicyPathByAuthorityId(copyInfo.OldAuthorityId) + err = CasbinServiceApp.UpdateCasbin(adminAuthorityID, copyInfo.Authority.AuthorityId, paths) + if err != nil { + _ = authorityService.DeleteAuthority(©Info.Authority) + } + return copyInfo.Authority, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: UpdateAuthority +//@description: 更改一个角色 +//@param: auth model.SysAuthority +//@return: authority system.SysAuthority, err error + +func (authorityService *AuthorityService) UpdateAuthority(auth system.SysAuthority) (authority system.SysAuthority, err error) { + var oldAuthority system.SysAuthority + err = global.GVA_DB.Where("authority_id = ?", auth.AuthorityId).First(&oldAuthority).Error + if err != nil { + global.GVA_LOG.Debug(err.Error()) + return system.SysAuthority{}, errors.New("查询角色数据失败") + } + err = global.GVA_DB.Model(&oldAuthority).Updates(&auth).Error + return auth, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: DeleteAuthority +//@description: 删除角色 +//@param: auth *model.SysAuthority +//@return: err error + +func (authorityService *AuthorityService) DeleteAuthority(auth *system.SysAuthority) error { + if errors.Is(global.GVA_DB.Debug().Preload("Users").First(&auth).Error, gorm.ErrRecordNotFound) { + return errors.New("该角色不存在") + } + if len(auth.Users) != 0 { + return errors.New("此角色有用户正在使用禁止删除") + } + if !errors.Is(global.GVA_DB.Where("authority_id = ?", auth.AuthorityId).First(&system.SysUser{}).Error, gorm.ErrRecordNotFound) { + return errors.New("此角色有用户正在使用禁止删除") + } + if !errors.Is(global.GVA_DB.Where("parent_id = ?", auth.AuthorityId).First(&system.SysAuthority{}).Error, gorm.ErrRecordNotFound) { + return errors.New("此角色存在子角色不允许删除") + } + + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + var err error + if err = tx.Preload("SysBaseMenus").Preload("DataAuthorityId").Where("authority_id = ?", auth.AuthorityId).First(auth).Unscoped().Delete(auth).Error; err != nil { + return err + } + + if len(auth.SysBaseMenus) > 0 { + if err = tx.Model(auth).Association("SysBaseMenus").Delete(auth.SysBaseMenus); err != nil { + return err + } + // err = db.Association("SysBaseMenus").Delete(&auth) + } + if len(auth.DataAuthorityId) > 0 { + if err = tx.Model(auth).Association("DataAuthorityId").Delete(auth.DataAuthorityId); err != nil { + return err + } + } + + if err = tx.Delete(&system.SysUserAuthority{}, "sys_authority_authority_id = ?", auth.AuthorityId).Error; err != nil { + return err + } + if err = tx.Where("authority_id = ?", auth.AuthorityId).Delete(&[]system.SysAuthorityBtn{}).Error; err != nil { + return err + } + + authorityId := strconv.Itoa(int(auth.AuthorityId)) + + if err = CasbinServiceApp.RemoveFilteredPolicy(tx, authorityId); err != nil { + return err + } + + return nil + }) +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetAuthorityInfoList +//@description: 分页获取数据 +//@param: info request.PageInfo +//@return: list interface{}, total int64, err error + +func (authorityService *AuthorityService) GetAuthorityInfoList(authorityID uint) (list []system.SysAuthority, err error) { + var authority system.SysAuthority + err = global.GVA_DB.Where("authority_id = ?", authorityID).First(&authority).Error + if err != nil { + return nil, err + } + var authorities []system.SysAuthority + db := global.GVA_DB.Model(&system.SysAuthority{}) + if global.GVA_CONFIG.System.UseStrictAuth { + // 当开启了严格树形结构后 + if *authority.ParentId == 0 { + // 只有顶级角色可以修改自己的权限和以下权限 + err = db.Preload("DataAuthorityId").Where("authority_id = ?", authorityID).Find(&authorities).Error + } else { + // 非顶级角色只能修改以下权限 + err = db.Debug().Preload("DataAuthorityId").Where("parent_id = ?", authorityID).Find(&authorities).Error + } + } else { + err = db.Preload("DataAuthorityId").Where("parent_id = ?", "0").Find(&authorities).Error + } + + for k := range authorities { + err = authorityService.findChildrenAuthority(&authorities[k]) + } + return authorities, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetAuthorityInfoList +//@description: 分页获取数据 +//@param: info request.PageInfo +//@return: list interface{}, total int64, err error + +func (authorityService *AuthorityService) GetStructAuthorityList(authorityID uint) (list []uint, err error) { + var auth system.SysAuthority + _ = global.GVA_DB.First(&auth, "authority_id = ?", authorityID).Error + var authorities []system.SysAuthority + err = global.GVA_DB.Preload("DataAuthorityId").Where("parent_id = ?", authorityID).Find(&authorities).Error + if len(authorities) > 0 { + for k := range authorities { + list = append(list, authorities[k].AuthorityId) + _, err = authorityService.GetStructAuthorityList(authorities[k].AuthorityId) + } + } + if *auth.ParentId == 0 { + list = append(list, authorityID) + } + return list, err +} + +func (authorityService *AuthorityService) CheckAuthorityIDAuth(authorityID, targetID uint) (err error) { + if !global.GVA_CONFIG.System.UseStrictAuth { + return nil + } + authIDS, err := authorityService.GetStructAuthorityList(authorityID) + if err != nil { + return err + } + hasAuth := false + for _, v := range authIDS { + if v == targetID { + hasAuth = true + break + } + } + if !hasAuth { + return errors.New("您提交的角色ID不合法") + } + return nil +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetAuthorityInfo +//@description: 获取所有角色信息 +//@param: auth model.SysAuthority +//@return: sa system.SysAuthority, err error + +func (authorityService *AuthorityService) GetAuthorityInfo(auth system.SysAuthority) (sa system.SysAuthority, err error) { + err = global.GVA_DB.Preload("DataAuthorityId").Where("authority_id = ?", auth.AuthorityId).First(&sa).Error + return sa, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: SetDataAuthority +//@description: 设置角色资源权限 +//@param: auth model.SysAuthority +//@return: error + +func (authorityService *AuthorityService) SetDataAuthority(adminAuthorityID uint, auth system.SysAuthority) error { + var checkIDs []uint + checkIDs = append(checkIDs, auth.AuthorityId) + for i := range auth.DataAuthorityId { + checkIDs = append(checkIDs, auth.DataAuthorityId[i].AuthorityId) + } + + for i := range checkIDs { + err := authorityService.CheckAuthorityIDAuth(adminAuthorityID, checkIDs[i]) + if err != nil { + return err + } + } + + var s system.SysAuthority + global.GVA_DB.Preload("DataAuthorityId").First(&s, "authority_id = ?", auth.AuthorityId) + err := global.GVA_DB.Model(&s).Association("DataAuthorityId").Replace(&auth.DataAuthorityId) + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: SetMenuAuthority +//@description: 菜单与角色绑定 +//@param: auth *model.SysAuthority +//@return: error + +func (authorityService *AuthorityService) SetMenuAuthority(auth *system.SysAuthority) error { + var s system.SysAuthority + global.GVA_DB.Preload("SysBaseMenus").First(&s, "authority_id = ?", auth.AuthorityId) + err := global.GVA_DB.Model(&s).Association("SysBaseMenus").Replace(&auth.SysBaseMenus) + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: findChildrenAuthority +//@description: 查询子角色 +//@param: authority *model.SysAuthority +//@return: err error + +func (authorityService *AuthorityService) findChildrenAuthority(authority *system.SysAuthority) (err error) { + err = global.GVA_DB.Preload("DataAuthorityId").Where("parent_id = ?", authority.AuthorityId).Find(&authority.Children).Error + if len(authority.Children) > 0 { + for k := range authority.Children { + err = authorityService.findChildrenAuthority(&authority.Children[k]) + } + } + return err +} + +func (authorityService *AuthorityService) GetParentAuthorityID(authorityID uint) (parentID uint, err error) { + var authority system.SysAuthority + err = global.GVA_DB.Where("authority_id = ?", authorityID).First(&authority).Error + return *authority.ParentId, err +} diff --git a/admin/server/service/system/sys_authority_btn.go b/admin/server/service/system/sys_authority_btn.go new file mode 100644 index 000000000..1cc8f1fc7 --- /dev/null +++ b/admin/server/service/system/sys_authority_btn.go @@ -0,0 +1,60 @@ +package system + +import ( + "errors" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" + "gorm.io/gorm" +) + +type AuthorityBtnService struct{} + +var AuthorityBtnServiceApp = new(AuthorityBtnService) + +func (a *AuthorityBtnService) GetAuthorityBtn(req request.SysAuthorityBtnReq) (res response.SysAuthorityBtnRes, err error) { + var authorityBtn []system.SysAuthorityBtn + err = global.GVA_DB.Find(&authorityBtn, "authority_id = ? and sys_menu_id = ?", req.AuthorityId, req.MenuID).Error + if err != nil { + return + } + var selected []uint + for _, v := range authorityBtn { + selected = append(selected, v.SysBaseMenuBtnID) + } + res.Selected = selected + return res, err +} + +func (a *AuthorityBtnService) SetAuthorityBtn(req request.SysAuthorityBtnReq) (err error) { + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + var authorityBtn []system.SysAuthorityBtn + err = tx.Delete(&[]system.SysAuthorityBtn{}, "authority_id = ? and sys_menu_id = ?", req.AuthorityId, req.MenuID).Error + if err != nil { + return err + } + for _, v := range req.Selected { + authorityBtn = append(authorityBtn, system.SysAuthorityBtn{ + AuthorityId: req.AuthorityId, + SysMenuID: req.MenuID, + SysBaseMenuBtnID: v, + }) + } + if len(authorityBtn) > 0 { + err = tx.Create(&authorityBtn).Error + } + if err != nil { + return err + } + return err + }) +} + +func (a *AuthorityBtnService) CanRemoveAuthorityBtn(ID string) (err error) { + fErr := global.GVA_DB.First(&system.SysAuthorityBtn{}, "sys_base_menu_btn_id = ?", ID).Error + if errors.Is(fErr, gorm.ErrRecordNotFound) { + return nil + } + return errors.New("此按钮正在被使用无法删除") +} diff --git a/admin/server/service/system/sys_auto_code_interface.go b/admin/server/service/system/sys_auto_code_interface.go new file mode 100644 index 000000000..7939f14c1 --- /dev/null +++ b/admin/server/service/system/sys_auto_code_interface.go @@ -0,0 +1,55 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" +) + +type AutoCodeService struct{} + +type Database interface { + GetDB(businessDB string) (data []response.Db, err error) + GetTables(businessDB string, dbName string) (data []response.Table, err error) + GetColumn(businessDB string, tableName string, dbName string) (data []response.Column, err error) +} + +func (autoCodeService *AutoCodeService) Database(businessDB string) Database { + + if businessDB == "" { + switch global.GVA_CONFIG.System.DbType { + case "mysql": + return AutoCodeMysql + case "pgsql": + return AutoCodePgsql + case "mssql": + return AutoCodeMssql + case "oracle": + return AutoCodeOracle + case "sqlite": + return AutoCodeSqlite + default: + return AutoCodeMysql + } + } else { + for _, info := range global.GVA_CONFIG.DBList { + if info.AliasName == businessDB { + switch info.Type { + case "mysql": + return AutoCodeMysql + case "mssql": + return AutoCodeMssql + case "pgsql": + return AutoCodePgsql + case "oracle": + return AutoCodeOracle + case "sqlite": + return AutoCodeSqlite + default: + return AutoCodeMysql + } + } + } + return AutoCodeMysql + } + +} diff --git a/admin/server/service/system/sys_auto_code_mssql.go b/admin/server/service/system/sys_auto_code_mssql.go new file mode 100644 index 000000000..68a916054 --- /dev/null +++ b/admin/server/service/system/sys_auto_code_mssql.go @@ -0,0 +1,83 @@ +package system + +import ( + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" +) + +var AutoCodeMssql = new(autoCodeMssql) + +type autoCodeMssql struct{} + +// GetDB 获取数据库的所有数据库名 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodeMssql) GetDB(businessDB string) (data []response.Db, err error) { + var entities []response.Db + sql := "select name AS 'database' from sysdatabases;" + if businessDB == "" { + err = global.GVA_DB.Raw(sql).Scan(&entities).Error + } else { + err = global.GVA_DBList[businessDB].Raw(sql).Scan(&entities).Error + } + return entities, err +} + +// GetTables 获取数据库的所有表名 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodeMssql) GetTables(businessDB string, dbName string) (data []response.Table, err error) { + var entities []response.Table + + sql := fmt.Sprintf(`select name as 'table_name' from %s.DBO.sysobjects where xtype='U'`, dbName) + if businessDB == "" { + err = global.GVA_DB.Raw(sql).Scan(&entities).Error + } else { + err = global.GVA_DBList[businessDB].Raw(sql).Scan(&entities).Error + } + + return entities, err +} + +// GetColumn 获取指定数据库和指定数据表的所有字段名,类型值等 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodeMssql) GetColumn(businessDB string, tableName string, dbName string) (data []response.Column, err error) { + var entities []response.Column + sql := fmt.Sprintf(` +SELECT + sc.name AS column_name, + st.name AS data_type, + sc.max_length AS data_type_long, + CASE + WHEN pk.object_id IS NOT NULL THEN 1 + ELSE 0 + END AS primary_key, + sc.column_id +FROM + %s.sys.columns sc +JOIN + sys.types st ON sc.user_type_id=st.user_type_id +LEFT JOIN + %s.sys.objects so ON so.name='%s' AND so.type='U' +LEFT JOIN + %s.sys.indexes si ON si.object_id = so.object_id AND si.is_primary_key = 1 +LEFT JOIN + %s.sys.index_columns sic ON sic.object_id = si.object_id AND sic.index_id = si.index_id AND sic.column_id = sc.column_id +LEFT JOIN + %s.sys.key_constraints pk ON pk.object_id = si.object_id +WHERE + st.is_user_defined=0 AND sc.object_id = so.object_id +ORDER BY + sc.column_id +`, dbName, dbName, tableName, dbName, dbName, dbName) + + if businessDB == "" { + err = global.GVA_DB.Raw(sql).Scan(&entities).Error + } else { + err = global.GVA_DBList[businessDB].Raw(sql).Scan(&entities).Error + } + + return entities, err +} diff --git a/admin/server/service/system/sys_auto_code_mysql.go b/admin/server/service/system/sys_auto_code_mysql.go new file mode 100644 index 000000000..c7f0f1bc4 --- /dev/null +++ b/admin/server/service/system/sys_auto_code_mysql.go @@ -0,0 +1,83 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" +) + +var AutoCodeMysql = new(autoCodeMysql) + +type autoCodeMysql struct{} + +// GetDB 获取数据库的所有数据库名 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodeMysql) GetDB(businessDB string) (data []response.Db, err error) { + var entities []response.Db + sql := "SELECT SCHEMA_NAME AS `database` FROM INFORMATION_SCHEMA.SCHEMATA;" + if businessDB == "" { + err = global.GVA_DB.Raw(sql).Scan(&entities).Error + } else { + err = global.GVA_DBList[businessDB].Raw(sql).Scan(&entities).Error + } + return entities, err +} + +// GetTables 获取数据库的所有表名 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodeMysql) GetTables(businessDB string, dbName string) (data []response.Table, err error) { + var entities []response.Table + sql := `select table_name as table_name from information_schema.tables where table_schema = ?` + if businessDB == "" { + err = global.GVA_DB.Raw(sql, dbName).Scan(&entities).Error + } else { + err = global.GVA_DBList[businessDB].Raw(sql, dbName).Scan(&entities).Error + } + + return entities, err +} + +// GetColumn 获取指定数据库和指定数据表的所有字段名,类型值等 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodeMysql) GetColumn(businessDB string, tableName string, dbName string) (data []response.Column, err error) { + var entities []response.Column + sql := ` + SELECT + c.COLUMN_NAME column_name, + c.DATA_TYPE data_type, + CASE c.DATA_TYPE + WHEN 'longtext' THEN c.CHARACTER_MAXIMUM_LENGTH + WHEN 'varchar' THEN c.CHARACTER_MAXIMUM_LENGTH + WHEN 'double' THEN CONCAT_WS(',', c.NUMERIC_PRECISION, c.NUMERIC_SCALE) + WHEN 'decimal' THEN CONCAT_WS(',', c.NUMERIC_PRECISION, c.NUMERIC_SCALE) + WHEN 'int' THEN c.NUMERIC_PRECISION + WHEN 'bigint' THEN c.NUMERIC_PRECISION + ELSE '' + END AS data_type_long, + c.COLUMN_COMMENT column_comment, + CASE WHEN kcu.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END AS primary_key, + c.ORDINAL_POSITION +FROM + INFORMATION_SCHEMA.COLUMNS c +LEFT JOIN + INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu +ON + c.TABLE_SCHEMA = kcu.TABLE_SCHEMA + AND c.TABLE_NAME = kcu.TABLE_NAME + AND c.COLUMN_NAME = kcu.COLUMN_NAME + AND kcu.CONSTRAINT_NAME = 'PRIMARY' +WHERE + c.TABLE_NAME = ? + AND c.TABLE_SCHEMA = ? +ORDER BY + c.ORDINAL_POSITION;` + if businessDB == "" { + err = global.GVA_DB.Raw(sql, tableName, dbName).Scan(&entities).Error + } else { + err = global.GVA_DBList[businessDB].Raw(sql, tableName, dbName).Scan(&entities).Error + } + + return entities, err +} diff --git a/admin/server/service/system/sys_auto_code_oracle.go b/admin/server/service/system/sys_auto_code_oracle.go new file mode 100644 index 000000000..3cdb362e9 --- /dev/null +++ b/admin/server/service/system/sys_auto_code_oracle.go @@ -0,0 +1,72 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" +) + +var AutoCodeOracle = new(autoCodeOracle) + +type autoCodeOracle struct{} + +// GetDB 获取数据库的所有数据库名 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodeOracle) GetDB(businessDB string) (data []response.Db, err error) { + var entities []response.Db + sql := `SELECT lower(username) AS "database" FROM all_users` + err = global.GVA_DBList[businessDB].Raw(sql).Scan(&entities).Error + return entities, err +} + +// GetTables 获取数据库的所有表名 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodeOracle) GetTables(businessDB string, dbName string) (data []response.Table, err error) { + var entities []response.Table + sql := `select lower(table_name) as "table_name" from all_tables where lower(owner) = ?` + + err = global.GVA_DBList[businessDB].Raw(sql, dbName).Scan(&entities).Error + return entities, err +} + +// GetColumn 获取指定数据库和指定数据表的所有字段名,类型值等 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodeOracle) GetColumn(businessDB string, tableName string, dbName string) (data []response.Column, err error) { + var entities []response.Column + sql := ` + SELECT + lower(a.COLUMN_NAME) as "column_name", + (CASE WHEN a.DATA_TYPE = 'NUMBER' AND a.DATA_SCALE=0 THEN 'int' else lower(a.DATA_TYPE) end) as "data_type", + (CASE WHEN a.DATA_TYPE = 'NUMBER' THEN a.DATA_PRECISION else a.DATA_LENGTH end) as "data_type_long", + b.COMMENTS as "column_comment", + (CASE WHEN pk.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END) as "primary_key", + a.COLUMN_ID +FROM + all_tab_columns a +JOIN + all_col_comments b ON a.OWNER = b.OWNER AND a.TABLE_NAME = b.TABLE_NAME AND a.COLUMN_NAME = b.COLUMN_NAME +LEFT JOIN + ( + SELECT + acc.OWNER, + acc.TABLE_NAME, + acc.COLUMN_NAME + FROM + all_cons_columns acc + JOIN + all_constraints ac ON acc.OWNER = ac.OWNER AND acc.CONSTRAINT_NAME = ac.CONSTRAINT_NAME + WHERE + ac.CONSTRAINT_TYPE = 'P' + ) pk ON a.OWNER = pk.OWNER AND a.TABLE_NAME = pk.TABLE_NAME AND a.COLUMN_NAME = pk.COLUMN_NAME +WHERE + lower(a.table_name) = ? + AND lower(a.OWNER) = ? +ORDER BY + a.COLUMN_ID; +` + + err = global.GVA_DBList[businessDB].Raw(sql, tableName, dbName).Scan(&entities).Error + return entities, err +} diff --git a/admin/server/service/system/sys_auto_code_pgsql.go b/admin/server/service/system/sys_auto_code_pgsql.go new file mode 100644 index 000000000..fae16fb95 --- /dev/null +++ b/admin/server/service/system/sys_auto_code_pgsql.go @@ -0,0 +1,135 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" +) + +var AutoCodePgsql = new(autoCodePgsql) + +type autoCodePgsql struct{} + +// GetDB 获取数据库的所有数据库名 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (a *autoCodePgsql) GetDB(businessDB string) (data []response.Db, err error) { + var entities []response.Db + sql := `SELECT datname as database FROM pg_database WHERE datistemplate = false` + if businessDB == "" { + err = global.GVA_DB.Raw(sql).Scan(&entities).Error + } else { + err = global.GVA_DBList[businessDB].Raw(sql).Scan(&entities).Error + } + + return entities, err +} + +// GetTables 获取数据库的所有表名 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (a *autoCodePgsql) GetTables(businessDB string, dbName string) (data []response.Table, err error) { + var entities []response.Table + sql := `select table_name as table_name from information_schema.tables where table_catalog = ? and table_schema = ?` + + db := global.GVA_DB + if businessDB != "" { + db = global.GVA_DBList[businessDB] + } + + err = db.Raw(sql, dbName, "public").Scan(&entities).Error + return entities, err +} + +// GetColumn 获取指定数据库和指定数据表的所有字段名,类型值等 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (a *autoCodePgsql) GetColumn(businessDB string, tableName string, dbName string) (data []response.Column, err error) { + // todo 数据获取不全, 待完善sql + sql := ` +SELECT + psc.COLUMN_NAME AS COLUMN_NAME, + psc.udt_name AS data_type, + CASE + psc.udt_name + WHEN 'text' THEN + concat_ws ( '', '', psc.CHARACTER_MAXIMUM_LENGTH ) + WHEN 'varchar' THEN + concat_ws ( '', '', psc.CHARACTER_MAXIMUM_LENGTH ) + WHEN 'smallint' THEN + concat_ws ( ',', psc.NUMERIC_PRECISION, psc.NUMERIC_SCALE ) + WHEN 'decimal' THEN + concat_ws ( ',', psc.NUMERIC_PRECISION, psc.NUMERIC_SCALE ) + WHEN 'integer' THEN + concat_ws ( '', '', psc.NUMERIC_PRECISION ) + WHEN 'int4' THEN + concat_ws ( '', '', psc.NUMERIC_PRECISION ) + WHEN 'int8' THEN + concat_ws ( '', '', psc.NUMERIC_PRECISION ) + WHEN 'bigint' THEN + concat_ws ( '', '', psc.NUMERIC_PRECISION ) + WHEN 'timestamp' THEN + concat_ws ( '', '', psc.datetime_precision ) + ELSE '' + END AS data_type_long, + ( + SELECT + pd.description + FROM + pg_description pd + WHERE + (pd.objoid,pd.objsubid) in ( + SELECT pa.attrelid,pa.attnum + FROM + pg_attribute pa + WHERE pa.attrelid = ( SELECT oid FROM pg_class pc WHERE + pc.relname = psc.table_name + ) + and attname = psc.column_name + ) + ) AS column_comment, + ( + SELECT + COUNT(*) + FROM + pg_constraint + WHERE + contype = 'p' + AND conrelid = ( + SELECT + oid + FROM + pg_class + WHERE + relname = psc.table_name + ) + AND conkey::int[] @> ARRAY[( + SELECT + attnum::integer + FROM + pg_attribute + WHERE + attrelid = conrelid + AND attname = psc.column_name + )] + ) > 0 AS primary_key, + psc.ordinal_position +FROM + INFORMATION_SCHEMA.COLUMNS psc +WHERE + table_catalog = ? + AND table_schema = 'public' + AND TABLE_NAME = ? +ORDER BY + psc.ordinal_position; +` + var entities []response.Column + //sql = strings.ReplaceAll(sql, "@table_catalog", dbName) + //sql = strings.ReplaceAll(sql, "@table_name", tableName) + db := global.GVA_DB + if businessDB != "" { + db = global.GVA_DBList[businessDB] + } + + err = db.Raw(sql, dbName, tableName).Scan(&entities).Error + return entities, err +} diff --git a/admin/server/service/system/sys_auto_code_sqlite.go b/admin/server/service/system/sys_auto_code_sqlite.go new file mode 100644 index 000000000..59bcfce70 --- /dev/null +++ b/admin/server/service/system/sys_auto_code_sqlite.go @@ -0,0 +1,84 @@ +package system + +import ( + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" + "path/filepath" + "strings" +) + +var AutoCodeSqlite = new(autoCodeSqlite) + +type autoCodeSqlite struct{} + +// GetDB 获取数据库的所有数据库名 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (a *autoCodeSqlite) GetDB(businessDB string) (data []response.Db, err error) { + var entities []response.Db + sql := "PRAGMA database_list;" + var databaseList []struct { + File string `gorm:"column:file"` + } + if businessDB == "" { + err = global.GVA_DB.Raw(sql).Find(&databaseList).Error + } else { + err = global.GVA_DBList[businessDB].Raw(sql).Find(&databaseList).Error + } + for _, database := range databaseList { + if database.File != "" { + fileName := filepath.Base(database.File) + fileExt := filepath.Ext(fileName) + fileNameWithoutExt := strings.TrimSuffix(fileName, fileExt) + + entities = append(entities, response.Db{fileNameWithoutExt}) + } + } + // entities = append(entities, response.Db{global.GVA_CONFIG.Sqlite.Dbname}) + return entities, err +} + +// GetTables 获取数据库的所有表名 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (a *autoCodeSqlite) GetTables(businessDB string, dbName string) (data []response.Table, err error) { + var entities []response.Table + sql := `SELECT name FROM sqlite_master WHERE type='table'` + tabelNames := []string{} + if businessDB == "" { + err = global.GVA_DB.Raw(sql).Find(&tabelNames).Error + } else { + err = global.GVA_DBList[businessDB].Raw(sql).Find(&tabelNames).Error + } + for _, tabelName := range tabelNames { + entities = append(entities, response.Table{tabelName}) + } + return entities, err +} + +// GetColumn 获取指定数据表的所有字段名,类型值等 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (a *autoCodeSqlite) GetColumn(businessDB string, tableName string, dbName string) (data []response.Column, err error) { + var entities []response.Column + sql := fmt.Sprintf("PRAGMA table_info(%s);", tableName) + var columnInfos []struct { + Name string `gorm:"column:name"` + Type string `gorm:"column:type"` + Pk int `gorm:"column:pk"` + } + if businessDB == "" { + err = global.GVA_DB.Raw(sql).Scan(&columnInfos).Error + } else { + err = global.GVA_DBList[businessDB].Raw(sql).Scan(&columnInfos).Error + } + for _, columnInfo := range columnInfos { + entities = append(entities, response.Column{ + ColumnName: columnInfo.Name, + DataType: columnInfo.Type, + PrimaryKey: columnInfo.Pk == 1, + }) + } + return entities, err +} diff --git a/admin/server/service/system/sys_base_menu.go b/admin/server/service/system/sys_base_menu.go new file mode 100644 index 000000000..913bab2f3 --- /dev/null +++ b/admin/server/service/system/sys_base_menu.go @@ -0,0 +1,146 @@ +package system + +import ( + "errors" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "gorm.io/gorm" +) + +type BaseMenuService struct{} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: DeleteBaseMenu +//@description: 删除基础路由 +//@param: id float64 +//@return: err error + +var BaseMenuServiceApp = new(BaseMenuService) + +func (baseMenuService *BaseMenuService) DeleteBaseMenu(id int) (err error) { + err = global.GVA_DB.First(&system.SysBaseMenu{}, "parent_id = ?", id).Error + if err == nil { + return errors.New("此菜单存在子菜单不可删除") + } + var menu system.SysBaseMenu + err = global.GVA_DB.First(&menu, id).Error + if err != nil { + return errors.New("记录不存在") + } + err = global.GVA_DB.First(&system.SysAuthority{}, "default_router = ?", menu.Name).Error + if err == nil { + return errors.New("此菜单有角色正在作为首页,不可删除") + } + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + + err = tx.Delete(&system.SysBaseMenu{}, "id = ?", id).Error + if err != nil { + return err + } + + err = tx.Delete(&system.SysBaseMenuParameter{}, "sys_base_menu_id = ?", id).Error + if err != nil { + return err + } + + err = tx.Delete(&system.SysBaseMenuBtn{}, "sys_base_menu_id = ?", id).Error + if err != nil { + return err + } + err = tx.Delete(&system.SysAuthorityBtn{}, "sys_menu_id = ?", id).Error + if err != nil { + return err + } + + err = tx.Delete(&system.SysAuthorityMenu{}, "sys_base_menu_id = ?", id).Error + if err != nil { + return err + } + return nil + }) + +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: UpdateBaseMenu +//@description: 更新路由 +//@param: menu model.SysBaseMenu +//@return: err error + +func (baseMenuService *BaseMenuService) UpdateBaseMenu(menu system.SysBaseMenu) (err error) { + var oldMenu system.SysBaseMenu + upDateMap := make(map[string]interface{}) + upDateMap["keep_alive"] = menu.KeepAlive + upDateMap["close_tab"] = menu.CloseTab + upDateMap["default_menu"] = menu.DefaultMenu + upDateMap["parent_id"] = menu.ParentId + upDateMap["path"] = menu.Path + upDateMap["name"] = menu.Name + upDateMap["hidden"] = menu.Hidden + upDateMap["component"] = menu.Component + upDateMap["title"] = menu.Title + upDateMap["active_name"] = menu.ActiveName + upDateMap["icon"] = menu.Icon + upDateMap["sort"] = menu.Sort + + err = global.GVA_DB.Transaction(func(tx *gorm.DB) error { + tx.Where("id = ?", menu.ID).Find(&oldMenu) + if oldMenu.Name != menu.Name { + if !errors.Is(tx.Where("id <> ? AND name = ?", menu.ID, menu.Name).First(&system.SysBaseMenu{}).Error, gorm.ErrRecordNotFound) { + global.GVA_LOG.Debug("存在相同name修改失败") + return errors.New("存在相同name修改失败") + } + } + txErr := tx.Unscoped().Delete(&system.SysBaseMenuParameter{}, "sys_base_menu_id = ?", menu.ID).Error + if txErr != nil { + global.GVA_LOG.Debug(txErr.Error()) + return txErr + } + txErr = tx.Unscoped().Delete(&system.SysBaseMenuBtn{}, "sys_base_menu_id = ?", menu.ID).Error + if txErr != nil { + global.GVA_LOG.Debug(txErr.Error()) + return txErr + } + if len(menu.Parameters) > 0 { + for k := range menu.Parameters { + menu.Parameters[k].SysBaseMenuID = menu.ID + } + txErr = tx.Create(&menu.Parameters).Error + if txErr != nil { + global.GVA_LOG.Debug(txErr.Error()) + return txErr + } + } + + if len(menu.MenuBtn) > 0 { + for k := range menu.MenuBtn { + menu.MenuBtn[k].SysBaseMenuID = menu.ID + } + txErr = tx.Create(&menu.MenuBtn).Error + if txErr != nil { + global.GVA_LOG.Debug(txErr.Error()) + return txErr + } + } + + txErr = tx.Model(&oldMenu).Updates(upDateMap).Error + if txErr != nil { + global.GVA_LOG.Debug(txErr.Error()) + return txErr + } + return nil + }) + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetBaseMenuById +//@description: 返回当前选中menu +//@param: id float64 +//@return: menu system.SysBaseMenu, err error + +func (baseMenuService *BaseMenuService) GetBaseMenuById(id int) (menu system.SysBaseMenu, err error) { + err = global.GVA_DB.Preload("MenuBtn").Preload("Parameters").Where("id = ?", id).First(&menu).Error + return +} diff --git a/admin/server/service/system/sys_casbin.go b/admin/server/service/system/sys_casbin.go new file mode 100644 index 000000000..32edc579e --- /dev/null +++ b/admin/server/service/system/sys_casbin.go @@ -0,0 +1,221 @@ +package system + +import ( + "errors" + "strconv" + "sync" + + "gorm.io/gorm" + + "github.com/casbin/casbin/v2" + "github.com/casbin/casbin/v2/model" + gormadapter "github.com/casbin/gorm-adapter/v3" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + _ "github.com/go-sql-driver/mysql" + "go.uber.org/zap" +) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: UpdateCasbin +//@description: 更新casbin权限 +//@param: authorityId string, casbinInfos []request.CasbinInfo +//@return: error + +type CasbinService struct{} + +var CasbinServiceApp = new(CasbinService) + +func (casbinService *CasbinService) UpdateCasbin(adminAuthorityID, AuthorityID uint, casbinInfos []request.CasbinInfo) error { + + err := AuthorityServiceApp.CheckAuthorityIDAuth(adminAuthorityID, AuthorityID) + if err != nil { + return err + } + + if global.GVA_CONFIG.System.UseStrictAuth { + apis, e := ApiServiceApp.GetAllApis(adminAuthorityID) + if e != nil { + return e + } + + for i := range casbinInfos { + hasApi := false + for j := range apis { + if apis[j].Path == casbinInfos[i].Path && apis[j].Method == casbinInfos[i].Method { + hasApi = true + break + } + } + if !hasApi { + return errors.New("存在api不在权限列表中") + } + } + } + + authorityId := strconv.Itoa(int(AuthorityID)) + casbinService.ClearCasbin(0, authorityId) + rules := [][]string{} + //做权限去重处理 + deduplicateMap := make(map[string]bool) + for _, v := range casbinInfos { + key := authorityId + v.Path + v.Method + if _, ok := deduplicateMap[key]; !ok { + deduplicateMap[key] = true + rules = append(rules, []string{authorityId, v.Path, v.Method}) + } + } + if len(rules) == 0 { + return nil + } // 设置空权限无需调用 AddPolicies 方法 + e := casbinService.Casbin() + success, _ := e.AddPolicies(rules) + if !success { + return errors.New("存在相同api,添加失败,请联系管理员") + } + return nil +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: UpdateCasbinApi +//@description: API更新随动 +//@param: oldPath string, newPath string, oldMethod string, newMethod string +//@return: error + +func (casbinService *CasbinService) UpdateCasbinApi(oldPath string, newPath string, oldMethod string, newMethod string) error { + err := global.GVA_DB.Model(&gormadapter.CasbinRule{}).Where("v1 = ? AND v2 = ?", oldPath, oldMethod).Updates(map[string]interface{}{ + "v1": newPath, + "v2": newMethod, + }).Error + e := casbinService.Casbin() + err = e.LoadPolicy() + if err != nil { + return err + } + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetPolicyPathByAuthorityId +//@description: 获取权限列表 +//@param: authorityId string +//@return: pathMaps []request.CasbinInfo + +func (casbinService *CasbinService) GetPolicyPathByAuthorityId(AuthorityID uint) (pathMaps []request.CasbinInfo) { + e := casbinService.Casbin() + authorityId := strconv.Itoa(int(AuthorityID)) + list, _ := e.GetFilteredPolicy(0, authorityId) + for _, v := range list { + pathMaps = append(pathMaps, request.CasbinInfo{ + Path: v[1], + Method: v[2], + }) + } + return pathMaps +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: ClearCasbin +//@description: 清除匹配的权限 +//@param: v int, p ...string +//@return: bool + +func (casbinService *CasbinService) ClearCasbin(v int, p ...string) bool { + e := casbinService.Casbin() + success, _ := e.RemoveFilteredPolicy(v, p...) + return success +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: RemoveFilteredPolicy +//@description: 使用数据库方法清理筛选的politicy 此方法需要调用FreshCasbin方法才可以在系统中即刻生效 +//@param: db *gorm.DB, authorityId string +//@return: error + +func (casbinService *CasbinService) RemoveFilteredPolicy(db *gorm.DB, authorityId string) error { + return db.Delete(&gormadapter.CasbinRule{}, "v0 = ?", authorityId).Error +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: SyncPolicy +//@description: 同步目前数据库的policy 此方法需要调用FreshCasbin方法才可以在系统中即刻生效 +//@param: db *gorm.DB, authorityId string, rules [][]string +//@return: error + +func (casbinService *CasbinService) SyncPolicy(db *gorm.DB, authorityId string, rules [][]string) error { + err := casbinService.RemoveFilteredPolicy(db, authorityId) + if err != nil { + return err + } + return casbinService.AddPolicies(db, rules) +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: AddPolicies +//@description: 添加匹配的权限 +//@param: v int, p ...string +//@return: bool + +func (casbinService *CasbinService) AddPolicies(db *gorm.DB, rules [][]string) error { + var casbinRules []gormadapter.CasbinRule + for i := range rules { + casbinRules = append(casbinRules, gormadapter.CasbinRule{ + Ptype: "p", + V0: rules[i][0], + V1: rules[i][1], + V2: rules[i][2], + }) + } + return db.Create(&casbinRules).Error +} + +func (CasbinService *CasbinService) FreshCasbin() (err error) { + e := CasbinService.Casbin() + err = e.LoadPolicy() + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: Casbin +//@description: 持久化到数据库 引入自定义规则 +//@return: *casbin.Enforcer + +var ( + syncedCachedEnforcer *casbin.SyncedCachedEnforcer + once sync.Once +) + +func (casbinService *CasbinService) Casbin() *casbin.SyncedCachedEnforcer { + once.Do(func() { + a, err := gormadapter.NewAdapterByDB(global.GVA_DB) + if err != nil { + zap.L().Error("适配数据库失败请检查casbin表是否为InnoDB引擎!", zap.Error(err)) + return + } + text := ` + [request_definition] + r = sub, obj, act + + [policy_definition] + p = sub, obj, act + + [role_definition] + g = _, _ + + [policy_effect] + e = some(where (p.eft == allow)) + + [matchers] + m = r.sub == p.sub && keyMatch2(r.obj,p.obj) && r.act == p.act + ` + m, err := model.NewModelFromString(text) + if err != nil { + zap.L().Error("字符串加载模型失败!", zap.Error(err)) + return + } + syncedCachedEnforcer, _ = casbin.NewSyncedCachedEnforcer(m, a) + syncedCachedEnforcer.SetExpireTime(60 * 60) + _ = syncedCachedEnforcer.LoadPolicy() + }) + return syncedCachedEnforcer +} diff --git a/admin/server/service/system/sys_dictionary.go b/admin/server/service/system/sys_dictionary.go new file mode 100644 index 000000000..d540a9602 --- /dev/null +++ b/admin/server/service/system/sys_dictionary.go @@ -0,0 +1,112 @@ +package system + +import ( + "errors" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "gorm.io/gorm" +) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: CreateSysDictionary +//@description: 创建字典数据 +//@param: sysDictionary model.SysDictionary +//@return: err error + +type DictionaryService struct{} + +var DictionaryServiceApp = new(DictionaryService) + +func (dictionaryService *DictionaryService) CreateSysDictionary(sysDictionary system.SysDictionary) (err error) { + if (!errors.Is(global.GVA_DB.First(&system.SysDictionary{}, "type = ?", sysDictionary.Type).Error, gorm.ErrRecordNotFound)) { + return errors.New("存在相同的type,不允许创建") + } + err = global.GVA_DB.Create(&sysDictionary).Error + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: DeleteSysDictionary +//@description: 删除字典数据 +//@param: sysDictionary model.SysDictionary +//@return: err error + +func (dictionaryService *DictionaryService) DeleteSysDictionary(sysDictionary system.SysDictionary) (err error) { + err = global.GVA_DB.Where("id = ?", sysDictionary.ID).Preload("SysDictionaryDetails").First(&sysDictionary).Error + if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("请不要搞事") + } + if err != nil { + return err + } + err = global.GVA_DB.Delete(&sysDictionary).Error + if err != nil { + return err + } + + if sysDictionary.SysDictionaryDetails != nil { + return global.GVA_DB.Where("sys_dictionary_id=?", sysDictionary.ID).Delete(sysDictionary.SysDictionaryDetails).Error + } + return +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: UpdateSysDictionary +//@description: 更新字典数据 +//@param: sysDictionary *model.SysDictionary +//@return: err error + +func (dictionaryService *DictionaryService) UpdateSysDictionary(sysDictionary *system.SysDictionary) (err error) { + var dict system.SysDictionary + sysDictionaryMap := map[string]interface{}{ + "Name": sysDictionary.Name, + "Type": sysDictionary.Type, + "Status": sysDictionary.Status, + "Desc": sysDictionary.Desc, + } + err = global.GVA_DB.Where("id = ?", sysDictionary.ID).First(&dict).Error + if err != nil { + global.GVA_LOG.Debug(err.Error()) + return errors.New("查询字典数据失败") + } + if dict.Type != sysDictionary.Type { + if !errors.Is(global.GVA_DB.First(&system.SysDictionary{}, "type = ?", sysDictionary.Type).Error, gorm.ErrRecordNotFound) { + return errors.New("存在相同的type,不允许创建") + } + } + err = global.GVA_DB.Model(&dict).Updates(sysDictionaryMap).Error + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetSysDictionary +//@description: 根据id或者type获取字典单条数据 +//@param: Type string, Id uint +//@return: err error, sysDictionary model.SysDictionary + +func (dictionaryService *DictionaryService) GetSysDictionary(Type string, Id uint, status *bool) (sysDictionary system.SysDictionary, err error) { + var flag = false + if status == nil { + flag = true + } else { + flag = *status + } + err = global.GVA_DB.Where("(type = ? OR id = ?) and status = ?", Type, Id, flag).Preload("SysDictionaryDetails", func(db *gorm.DB) *gorm.DB { + return db.Where("status = ?", true).Order("sort") + }).First(&sysDictionary).Error + return +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@author: [SliverHorn](https://github.com/SliverHorn) +//@function: GetSysDictionaryInfoList +//@description: 分页获取字典列表 +//@param: info request.SysDictionarySearch +//@return: err error, list interface{}, total int64 + +func (dictionaryService *DictionaryService) GetSysDictionaryInfoList() (list interface{}, err error) { + var sysDictionarys []system.SysDictionary + err = global.GVA_DB.Find(&sysDictionarys).Error + return sysDictionarys, err +} diff --git a/admin/server/service/system/sys_dictionary_detail.go b/admin/server/service/system/sys_dictionary_detail.go new file mode 100644 index 000000000..18042c788 --- /dev/null +++ b/admin/server/service/system/sys_dictionary_detail.go @@ -0,0 +1,118 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" +) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: CreateSysDictionaryDetail +//@description: 创建字典详情数据 +//@param: sysDictionaryDetail model.SysDictionaryDetail +//@return: err error + +type DictionaryDetailService struct{} + +var DictionaryDetailServiceApp = new(DictionaryDetailService) + +func (dictionaryDetailService *DictionaryDetailService) CreateSysDictionaryDetail(sysDictionaryDetail system.SysDictionaryDetail) (err error) { + err = global.GVA_DB.Create(&sysDictionaryDetail).Error + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: DeleteSysDictionaryDetail +//@description: 删除字典详情数据 +//@param: sysDictionaryDetail model.SysDictionaryDetail +//@return: err error + +func (dictionaryDetailService *DictionaryDetailService) DeleteSysDictionaryDetail(sysDictionaryDetail system.SysDictionaryDetail) (err error) { + err = global.GVA_DB.Delete(&sysDictionaryDetail).Error + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: UpdateSysDictionaryDetail +//@description: 更新字典详情数据 +//@param: sysDictionaryDetail *model.SysDictionaryDetail +//@return: err error + +func (dictionaryDetailService *DictionaryDetailService) UpdateSysDictionaryDetail(sysDictionaryDetail *system.SysDictionaryDetail) (err error) { + err = global.GVA_DB.Save(sysDictionaryDetail).Error + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetSysDictionaryDetail +//@description: 根据id获取字典详情单条数据 +//@param: id uint +//@return: sysDictionaryDetail system.SysDictionaryDetail, err error + +func (dictionaryDetailService *DictionaryDetailService) GetSysDictionaryDetail(id uint) (sysDictionaryDetail system.SysDictionaryDetail, err error) { + err = global.GVA_DB.Where("id = ?", id).First(&sysDictionaryDetail).Error + return +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetSysDictionaryDetailInfoList +//@description: 分页获取字典详情列表 +//@param: info request.SysDictionaryDetailSearch +//@return: list interface{}, total int64, err error + +func (dictionaryDetailService *DictionaryDetailService) GetSysDictionaryDetailInfoList(info request.SysDictionaryDetailSearch) (list interface{}, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := global.GVA_DB.Model(&system.SysDictionaryDetail{}) + var sysDictionaryDetails []system.SysDictionaryDetail + // 如果有条件搜索 下方会自动创建搜索语句 + if info.Label != "" { + db = db.Where("label LIKE ?", "%"+info.Label+"%") + } + if info.Value != "" { + db = db.Where("value = ?", info.Value) + } + if info.Status != nil { + db = db.Where("status = ?", info.Status) + } + if info.SysDictionaryID != 0 { + db = db.Where("sys_dictionary_id = ?", info.SysDictionaryID) + } + err = db.Count(&total).Error + if err != nil { + return + } + err = db.Limit(limit).Offset(offset).Order("sort").Find(&sysDictionaryDetails).Error + return sysDictionaryDetails, total, err +} + +// 按照字典id获取字典全部内容的方法 +func (dictionaryDetailService *DictionaryDetailService) GetDictionaryList(dictionaryID uint) (list []system.SysDictionaryDetail, err error) { + var sysDictionaryDetails []system.SysDictionaryDetail + err = global.GVA_DB.Find(&sysDictionaryDetails, "sys_dictionary_id = ?", dictionaryID).Error + return sysDictionaryDetails, err +} + +// 按照字典type获取字典全部内容的方法 +func (dictionaryDetailService *DictionaryDetailService) GetDictionaryListByType(t string) (list []system.SysDictionaryDetail, err error) { + var sysDictionaryDetails []system.SysDictionaryDetail + db := global.GVA_DB.Model(&system.SysDictionaryDetail{}).Joins("JOIN sys_dictionaries ON sys_dictionaries.id = sys_dictionary_details.sys_dictionary_id") + err = db.Debug().Find(&sysDictionaryDetails, "type = ?", t).Error + return sysDictionaryDetails, err +} + +// 按照字典id+字典内容value获取单条字典内容 +func (dictionaryDetailService *DictionaryDetailService) GetDictionaryInfoByValue(dictionaryID uint, value string) (detail system.SysDictionaryDetail, err error) { + var sysDictionaryDetail system.SysDictionaryDetail + err = global.GVA_DB.First(&sysDictionaryDetail, "sys_dictionary_id = ? and value = ?", dictionaryID, value).Error + return sysDictionaryDetail, err +} + +// 按照字典type+字典内容value获取单条字典内容 +func (dictionaryDetailService *DictionaryDetailService) GetDictionaryInfoByTypeValue(t string, value string) (detail system.SysDictionaryDetail, err error) { + var sysDictionaryDetails system.SysDictionaryDetail + db := global.GVA_DB.Model(&system.SysDictionaryDetail{}).Joins("JOIN sys_dictionaries ON sys_dictionaries.id = sys_dictionary_details.sys_dictionary_id") + err = db.First(&sysDictionaryDetails, "sys_dictionaries.type = ? and sys_dictionary_details.value = ?", t, value).Error + return sysDictionaryDetails, err +} diff --git a/admin/server/service/system/sys_export_template.go b/admin/server/service/system/sys_export_template.go new file mode 100644 index 000000000..19a7a4520 --- /dev/null +++ b/admin/server/service/system/sys_export_template.go @@ -0,0 +1,423 @@ +package system + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/xuri/excelize/v2" + "gorm.io/gorm" + "mime/multipart" + "net/url" + "strconv" + "strings" + "time" +) + +type SysExportTemplateService struct { +} + +var SysExportTemplateServiceApp = new(SysExportTemplateService) + +// CreateSysExportTemplate 创建导出模板记录 +// Author [piexlmax](https://github.com/piexlmax) +func (sysExportTemplateService *SysExportTemplateService) CreateSysExportTemplate(sysExportTemplate *system.SysExportTemplate) (err error) { + err = global.GVA_DB.Create(sysExportTemplate).Error + return err +} + +// DeleteSysExportTemplate 删除导出模板记录 +// Author [piexlmax](https://github.com/piexlmax) +func (sysExportTemplateService *SysExportTemplateService) DeleteSysExportTemplate(sysExportTemplate system.SysExportTemplate) (err error) { + err = global.GVA_DB.Delete(&sysExportTemplate).Error + return err +} + +// DeleteSysExportTemplateByIds 批量删除导出模板记录 +// Author [piexlmax](https://github.com/piexlmax) +func (sysExportTemplateService *SysExportTemplateService) DeleteSysExportTemplateByIds(ids request.IdsReq) (err error) { + err = global.GVA_DB.Delete(&[]system.SysExportTemplate{}, "id in ?", ids.Ids).Error + return err +} + +// UpdateSysExportTemplate 更新导出模板记录 +// Author [piexlmax](https://github.com/piexlmax) +func (sysExportTemplateService *SysExportTemplateService) UpdateSysExportTemplate(sysExportTemplate system.SysExportTemplate) (err error) { + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + conditions := sysExportTemplate.Conditions + e := tx.Delete(&[]system.Condition{}, "template_id = ?", sysExportTemplate.TemplateID).Error + if e != nil { + return e + } + sysExportTemplate.Conditions = nil + + joins := sysExportTemplate.JoinTemplate + e = tx.Delete(&[]system.JoinTemplate{}, "template_id = ?", sysExportTemplate.TemplateID).Error + if e != nil { + return e + } + sysExportTemplate.JoinTemplate = nil + + e = tx.Updates(&sysExportTemplate).Error + if e != nil { + return e + } + if len(conditions) > 0 { + for i := range conditions { + conditions[i].ID = 0 + } + e = tx.Create(&conditions).Error + } + if len(joins) > 0 { + for i := range joins { + joins[i].ID = 0 + } + e = tx.Create(&joins).Error + } + return e + }) +} + +// GetSysExportTemplate 根据id获取导出模板记录 +// Author [piexlmax](https://github.com/piexlmax) +func (sysExportTemplateService *SysExportTemplateService) GetSysExportTemplate(id uint) (sysExportTemplate system.SysExportTemplate, err error) { + err = global.GVA_DB.Where("id = ?", id).Preload("JoinTemplate").Preload("Conditions").First(&sysExportTemplate).Error + return +} + +// GetSysExportTemplateInfoList 分页获取导出模板记录 +// Author [piexlmax](https://github.com/piexlmax) +func (sysExportTemplateService *SysExportTemplateService) GetSysExportTemplateInfoList(info systemReq.SysExportTemplateSearch) (list []system.SysExportTemplate, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := global.GVA_DB.Model(&system.SysExportTemplate{}) + var sysExportTemplates []system.SysExportTemplate + // 如果有条件搜索 下方会自动创建搜索语句 + if info.StartCreatedAt != nil && info.EndCreatedAt != nil { + db = db.Where("created_at BETWEEN ? AND ?", info.StartCreatedAt, info.EndCreatedAt) + } + if info.Name != "" { + db = db.Where("name LIKE ?", "%"+info.Name+"%") + } + if info.TableName != "" { + db = db.Where("table_name = ?", info.TableName) + } + if info.TemplateID != "" { + db = db.Where("template_id = ?", info.TemplateID) + } + err = db.Count(&total).Error + if err != nil { + return + } + + if limit != 0 { + db = db.Limit(limit).Offset(offset) + } + + err = db.Find(&sysExportTemplates).Error + return sysExportTemplates, total, err +} + +// ExportExcel 导出Excel +// Author [piexlmax](https://github.com/piexlmax) +func (sysExportTemplateService *SysExportTemplateService) ExportExcel(templateID string, values url.Values) (file *bytes.Buffer, name string, err error) { + var template system.SysExportTemplate + err = global.GVA_DB.Preload("Conditions").Preload("JoinTemplate").First(&template, "template_id = ?", templateID).Error + if err != nil { + return nil, "", err + } + f := excelize.NewFile() + defer func() { + if err := f.Close(); err != nil { + fmt.Println(err) + } + }() + // Create a new sheet. + index, err := f.NewSheet("Sheet1") + if err != nil { + fmt.Println(err) + return + } + var templateInfoMap = make(map[string]string) + columns, err := utils.GetJSONKeys(template.TemplateInfo) + if err != nil { + return nil, "", err + } + err = json.Unmarshal([]byte(template.TemplateInfo), &templateInfoMap) + if err != nil { + return nil, "", err + } + var tableTitle []string + var selectKeyFmt []string + for _, key := range columns { + selectKeyFmt = append(selectKeyFmt, fmt.Sprintf("%s", key)) + tableTitle = append(tableTitle, templateInfoMap[key]) + } + + selects := strings.Join(selectKeyFmt, ", ") + var tableMap []map[string]interface{} + db := global.GVA_DB + if template.DBName != "" { + db = global.MustGetGlobalDBByDBName(template.DBName) + } + + if len(template.JoinTemplate) > 0 { + for _, join := range template.JoinTemplate { + db = db.Joins(join.JOINS + " " + join.Table + " ON " + join.ON) + } + } + + db = db.Select(selects).Table(template.TableName) + + if len(template.Conditions) > 0 { + for _, condition := range template.Conditions { + sql := fmt.Sprintf("%s %s ?", condition.Column, condition.Operator) + value := values.Get(condition.From) + if value != "" { + if condition.Operator == "LIKE" { + value = "%" + value + "%" + } + db = db.Where(sql, value) + } + } + } + // 通过参数传入limit + limit := values.Get("limit") + if limit != "" { + l, e := strconv.Atoi(limit) + if e == nil { + db = db.Limit(l) + } + } + // 模板的默认limit + if limit == "" && template.Limit != nil && *template.Limit != 0 { + db = db.Limit(*template.Limit) + } + + // 通过参数传入offset + offset := values.Get("offset") + if offset != "" { + o, e := strconv.Atoi(offset) + if e == nil { + db = db.Offset(o) + } + } + + // 获取当前表的所有字段 + table := template.TableName + orderColumns, err := db.Migrator().ColumnTypes(table) + if err != nil { + return nil, "", err + } + + // 创建一个 map 来存储字段名 + fields := make(map[string]bool) + + for _, column := range orderColumns { + fields[column.Name()] = true + } + + // 通过参数传入order + order := values.Get("order") + + if order == "" && template.Order != "" { + // 如果没有order入参,这里会使用模板的默认排序 + order = template.Order + } + + if order != "" { + checkOrderArr := strings.Split(order, " ") + orderStr := "" + // 检查请求的排序字段是否在字段列表中 + if _, ok := fields[checkOrderArr[0]]; !ok { + return nil, "", fmt.Errorf("order by %s is not in the fields", order) + } + orderStr = checkOrderArr[0] + if len(checkOrderArr) > 1 { + if checkOrderArr[1] != "asc" && checkOrderArr[1] != "desc" { + return nil, "", fmt.Errorf("order by %s is not secure", order) + } + orderStr = orderStr + " " + checkOrderArr[1] + } + db = db.Order(orderStr) + } + + err = db.Debug().Find(&tableMap).Error + if err != nil { + return nil, "", err + } + var rows [][]string + rows = append(rows, tableTitle) + for _, exTable := range tableMap { + var row []string + for _, column := range columns { + column = strings.ReplaceAll(column, "\"", "") + column = strings.ReplaceAll(column, "`", "") + if len(template.JoinTemplate) > 0 { + columnAs := strings.Split(column, " as ") + if len(columnAs) > 1 { + column = strings.TrimSpace(strings.Split(column, " as ")[1]) + } else { + columnArr := strings.Split(column, ".") + if len(columnArr) > 1 { + column = strings.Split(column, ".")[1] + } + } + } + // 需要对时间类型特殊处理 + if t, ok := exTable[column].(time.Time); ok { + row = append(row, t.Format("2006-01-02 15:04:05")) + } else { + row = append(row, fmt.Sprintf("%v", exTable[column])) + } + } + rows = append(rows, row) + } + for i, row := range rows { + for j, colCell := range row { + sErr := f.SetCellValue("Sheet1", fmt.Sprintf("%s%d", getColumnName(j+1), i+1), colCell) + if sErr != nil { + return nil, "", sErr + } + } + } + f.SetActiveSheet(index) + file, err = f.WriteToBuffer() + if err != nil { + return nil, "", err + } + + return file, template.Name, nil +} + +// ExportTemplate 导出Excel模板 +// Author [piexlmax](https://github.com/piexlmax) +func (sysExportTemplateService *SysExportTemplateService) ExportTemplate(templateID string) (file *bytes.Buffer, name string, err error) { + var template system.SysExportTemplate + err = global.GVA_DB.First(&template, "template_id = ?", templateID).Error + if err != nil { + return nil, "", err + } + f := excelize.NewFile() + defer func() { + if err := f.Close(); err != nil { + fmt.Println(err) + } + }() + // Create a new sheet. + index, err := f.NewSheet("Sheet1") + if err != nil { + fmt.Println(err) + return + } + var templateInfoMap = make(map[string]string) + + columns, err := utils.GetJSONKeys(template.TemplateInfo) + + err = json.Unmarshal([]byte(template.TemplateInfo), &templateInfoMap) + if err != nil { + return nil, "", err + } + var tableTitle []string + for _, key := range columns { + tableTitle = append(tableTitle, templateInfoMap[key]) + } + + for i := range tableTitle { + fErr := f.SetCellValue("Sheet1", fmt.Sprintf("%s%d", getColumnName(i+1), 1), tableTitle[i]) + if fErr != nil { + return nil, "", fErr + } + } + f.SetActiveSheet(index) + file, err = f.WriteToBuffer() + if err != nil { + return nil, "", err + } + + return file, template.Name, nil +} + +// ImportExcel 导入Excel +// Author [piexlmax](https://github.com/piexlmax) +func (sysExportTemplateService *SysExportTemplateService) ImportExcel(templateID string, file *multipart.FileHeader) (err error) { + var template system.SysExportTemplate + err = global.GVA_DB.First(&template, "template_id = ?", templateID).Error + if err != nil { + return err + } + + src, err := file.Open() + if err != nil { + return err + } + defer src.Close() + + f, err := excelize.OpenReader(src) + if err != nil { + return err + } + + rows, err := f.GetRows("Sheet1") + if err != nil { + return err + } + + var templateInfoMap = make(map[string]string) + err = json.Unmarshal([]byte(template.TemplateInfo), &templateInfoMap) + if err != nil { + return err + } + + var titleKeyMap = make(map[string]string) + for key, title := range templateInfoMap { + titleKeyMap[title] = key + } + + db := global.GVA_DB + if template.DBName != "" { + db = global.MustGetGlobalDBByDBName(template.DBName) + } + + return db.Transaction(func(tx *gorm.DB) error { + excelTitle := rows[0] + values := rows[1:] + items := make([]map[string]interface{}, 0, len(values)) + for _, row := range values { + var item = make(map[string]interface{}) + for ii, value := range row { + key := titleKeyMap[excelTitle[ii]] + item[key] = value + } + + needCreated := tx.Migrator().HasColumn(template.TableName, "created_at") + needUpdated := tx.Migrator().HasColumn(template.TableName, "updated_at") + + if item["created_at"] == nil && needCreated { + item["created_at"] = time.Now() + } + if item["updated_at"] == nil && needUpdated { + item["updated_at"] = time.Now() + } + + items = append(items, item) + } + cErr := tx.Table(template.TableName).CreateInBatches(&items, 1000).Error + return cErr + }) +} + +func getColumnName(n int) string { + columnName := "" + for n > 0 { + n-- + columnName = string(rune('A'+n%26)) + columnName + n /= 26 + } + return columnName +} diff --git a/admin/server/service/system/sys_initdb.go b/admin/server/service/system/sys_initdb.go new file mode 100644 index 000000000..eac74a09d --- /dev/null +++ b/admin/server/service/system/sys_initdb.go @@ -0,0 +1,189 @@ +package system + +import ( + "context" + "database/sql" + "errors" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "gorm.io/gorm" + "sort" +) + +const ( + Mysql = "mysql" + Pgsql = "pgsql" + Sqlite = "sqlite" + Mssql = "mssql" + InitSuccess = "\n[%v] --> 初始数据成功!\n" + InitDataExist = "\n[%v] --> %v 的初始数据已存在!\n" + InitDataFailed = "\n[%v] --> %v 初始数据失败! \nerr: %+v\n" + InitDataSuccess = "\n[%v] --> %v 初始数据成功!\n" +) + +const ( + InitOrderSystem = 10 + InitOrderInternal = 1000 + InitOrderExternal = 100000 +) + +var ( + ErrMissingDBContext = errors.New("missing db in context") + ErrMissingDependentContext = errors.New("missing dependent value in context") + ErrDBTypeMismatch = errors.New("db type mismatch") +) + +// SubInitializer 提供 source/*/init() 使用的接口,每个 initializer 完成一个初始化过程 +type SubInitializer interface { + InitializerName() string // 不一定代表单独一个表,所以改成了更宽泛的语义 + MigrateTable(ctx context.Context) (next context.Context, err error) + InitializeData(ctx context.Context) (next context.Context, err error) + TableCreated(ctx context.Context) bool + DataInserted(ctx context.Context) bool +} + +// TypedDBInitHandler 执行传入的 initializer +type TypedDBInitHandler interface { + EnsureDB(ctx context.Context, conf *request.InitDB) (context.Context, error) // 建库,失败属于 fatal error,因此让它 panic + WriteConfig(ctx context.Context) error // 回写配置 + InitTables(ctx context.Context, inits initSlice) error // 建表 handler + InitData(ctx context.Context, inits initSlice) error // 建数据 handler +} + +// orderedInitializer 组合一个顺序字段,以供排序 +type orderedInitializer struct { + order int + SubInitializer +} + +// initSlice 供 initializer 排序依赖时使用 +type initSlice []*orderedInitializer + +var ( + initializers initSlice + cache map[string]*orderedInitializer +) + +// RegisterInit 注册要执行的初始化过程,会在 InitDB() 时调用 +func RegisterInit(order int, i SubInitializer) { + if initializers == nil { + initializers = initSlice{} + } + if cache == nil { + cache = map[string]*orderedInitializer{} + } + name := i.InitializerName() + if _, existed := cache[name]; existed { + panic(fmt.Sprintf("Name conflict on %s", name)) + } + ni := orderedInitializer{order, i} + initializers = append(initializers, &ni) + cache[name] = &ni +} + +/* ---- * service * ---- */ + +type InitDBService struct{} + +// InitDB 创建数据库并初始化 总入口 +func (initDBService *InitDBService) InitDB(conf request.InitDB) (err error) { + ctx := context.TODO() + ctx = context.WithValue(ctx, "adminPassword", conf.AdminPassword) + if len(initializers) == 0 { + return errors.New("无可用初始化过程,请检查初始化是否已执行完成") + } + sort.Sort(&initializers) // 保证有依赖的 initializer 排在后面执行 + // Note: 若 initializer 只有单一依赖,可以写为 B=A+1, C=A+1; 由于 BC 之间没有依赖关系,所以谁先谁后并不影响初始化 + // 若存在多个依赖,可以写为 C=A+B, D=A+B+C, E=A+1; + // C必然>A|B,因此在AB之后执行,D必然>A|B|C,因此在ABC后执行,而E只依赖A,顺序与CD无关,因此E与CD哪个先执行并不影响 + var initHandler TypedDBInitHandler + switch conf.DBType { + case "mysql": + initHandler = NewMysqlInitHandler() + ctx = context.WithValue(ctx, "dbtype", "mysql") + case "pgsql": + initHandler = NewPgsqlInitHandler() + ctx = context.WithValue(ctx, "dbtype", "pgsql") + case "sqlite": + initHandler = NewSqliteInitHandler() + ctx = context.WithValue(ctx, "dbtype", "sqlite") + case "mssql": + initHandler = NewMssqlInitHandler() + ctx = context.WithValue(ctx, "dbtype", "mssql") + default: + initHandler = NewMysqlInitHandler() + ctx = context.WithValue(ctx, "dbtype", "mysql") + } + ctx, err = initHandler.EnsureDB(ctx, &conf) + if err != nil { + return err + } + + db := ctx.Value("db").(*gorm.DB) + global.GVA_DB = db + + if err = initHandler.InitTables(ctx, initializers); err != nil { + return err + } + if err = initHandler.InitData(ctx, initializers); err != nil { + return err + } + + if err = initHandler.WriteConfig(ctx); err != nil { + return err + } + initializers = initSlice{} + cache = map[string]*orderedInitializer{} + return nil +} + +// createDatabase 创建数据库( EnsureDB() 中调用 ) +func createDatabase(dsn string, driver string, createSql string) error { + db, err := sql.Open(driver, dsn) + if err != nil { + return err + } + defer func(db *sql.DB) { + err = db.Close() + if err != nil { + fmt.Println(err) + } + }(db) + if err = db.Ping(); err != nil { + return err + } + _, err = db.Exec(createSql) + return err +} + +// createTables 创建表(默认 dbInitHandler.initTables 行为) +func createTables(ctx context.Context, inits initSlice) error { + next, cancel := context.WithCancel(ctx) + defer func(c func()) { c() }(cancel) + for _, init := range inits { + if init.TableCreated(next) { + continue + } + if n, err := init.MigrateTable(next); err != nil { + return err + } else { + next = n + } + } + return nil +} + +/* -- sortable interface -- */ + +func (a initSlice) Len() int { + return len(a) +} + +func (a initSlice) Less(i, j int) bool { + return a[i].order < a[j].order +} + +func (a initSlice) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +} diff --git a/admin/server/service/system/sys_initdb_mssql.go b/admin/server/service/system/sys_initdb_mssql.go new file mode 100644 index 000000000..eeeeb514f --- /dev/null +++ b/admin/server/service/system/sys_initdb_mssql.go @@ -0,0 +1,92 @@ +package system + +import ( + "context" + "errors" + "github.com/flipped-aurora/gin-vue-admin/server/config" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/gofrs/uuid/v5" + "github.com/gookit/color" + "gorm.io/driver/sqlserver" + "gorm.io/gorm" + "path/filepath" +) + +type MssqlInitHandler struct{} + +func NewMssqlInitHandler() *MssqlInitHandler { + return &MssqlInitHandler{} +} + +// WriteConfig mssql回写配置 +func (h MssqlInitHandler) WriteConfig(ctx context.Context) error { + c, ok := ctx.Value("config").(config.Mssql) + if !ok { + return errors.New("mssql config invalid") + } + global.GVA_CONFIG.System.DbType = "mssql" + global.GVA_CONFIG.Mssql = c + global.GVA_CONFIG.JWT.SigningKey = uuid.Must(uuid.NewV4()).String() + cs := utils.StructToMap(global.GVA_CONFIG) + for k, v := range cs { + global.GVA_VP.Set(k, v) + } + global.GVA_ACTIVE_DBNAME = &c.Dbname + return global.GVA_VP.WriteConfig() +} + +// EnsureDB 创建数据库并初始化 mssql +func (h MssqlInitHandler) EnsureDB(ctx context.Context, conf *request.InitDB) (next context.Context, err error) { + if s, ok := ctx.Value("dbtype").(string); !ok || s != "mssql" { + return ctx, ErrDBTypeMismatch + } + + c := conf.ToMssqlConfig() + next = context.WithValue(ctx, "config", c) + if c.Dbname == "" { + return ctx, nil + } // 如果没有数据库名, 则跳出初始化数据 + + dsn := conf.MssqlEmptyDsn() + + mssqlConfig := sqlserver.Config{ + DSN: dsn, // DSN data source name + DefaultStringSize: 191, // string 类型字段的默认长度 + } + + var db *gorm.DB + + if db, err = gorm.Open(sqlserver.New(mssqlConfig), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true}); err != nil { + return nil, err + } + + global.GVA_CONFIG.AutoCode.Root, _ = filepath.Abs("..") + next = context.WithValue(next, "db", db) + return next, err +} + +func (h MssqlInitHandler) InitTables(ctx context.Context, inits initSlice) error { + return createTables(ctx, inits) +} + +func (h MssqlInitHandler) InitData(ctx context.Context, inits initSlice) error { + next, cancel := context.WithCancel(ctx) + defer func(c func()) { c() }(cancel) + for _, init := range inits { + if init.DataInserted(next) { + color.Info.Printf(InitDataExist, Mssql, init.InitializerName()) + continue + } + if n, err := init.InitializeData(next); err != nil { + color.Info.Printf(InitDataFailed, Mssql, init.InitializerName(), err) + return err + } else { + next = n + color.Info.Printf(InitDataSuccess, Mssql, init.InitializerName()) + } + } + color.Info.Printf(InitSuccess, Mssql) + return nil +} diff --git a/admin/server/service/system/sys_initdb_mysql.go b/admin/server/service/system/sys_initdb_mysql.go new file mode 100644 index 000000000..62575d385 --- /dev/null +++ b/admin/server/service/system/sys_initdb_mysql.go @@ -0,0 +1,97 @@ +package system + +import ( + "context" + "errors" + "fmt" + "path/filepath" + + "github.com/flipped-aurora/gin-vue-admin/server/config" + "github.com/gookit/color" + + "github.com/flipped-aurora/gin-vue-admin/server/utils" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/gofrs/uuid/v5" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +type MysqlInitHandler struct{} + +func NewMysqlInitHandler() *MysqlInitHandler { + return &MysqlInitHandler{} +} + +// WriteConfig mysql回写配置 +func (h MysqlInitHandler) WriteConfig(ctx context.Context) error { + c, ok := ctx.Value("config").(config.Mysql) + if !ok { + return errors.New("mysql config invalid") + } + global.GVA_CONFIG.System.DbType = "mysql" + global.GVA_CONFIG.Mysql = c + global.GVA_CONFIG.JWT.SigningKey = uuid.Must(uuid.NewV4()).String() + cs := utils.StructToMap(global.GVA_CONFIG) + for k, v := range cs { + global.GVA_VP.Set(k, v) + } + global.GVA_ACTIVE_DBNAME = &c.Dbname + return global.GVA_VP.WriteConfig() +} + +// EnsureDB 创建数据库并初始化 mysql +func (h MysqlInitHandler) EnsureDB(ctx context.Context, conf *request.InitDB) (next context.Context, err error) { + if s, ok := ctx.Value("dbtype").(string); !ok || s != "mysql" { + return ctx, ErrDBTypeMismatch + } + + c := conf.ToMysqlConfig() + next = context.WithValue(ctx, "config", c) + if c.Dbname == "" { + return ctx, nil + } // 如果没有数据库名, 则跳出初始化数据 + + dsn := conf.MysqlEmptyDsn() + createSql := fmt.Sprintf("CREATE DATABASE IF NOT EXISTS `%s` DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;", c.Dbname) + if err = createDatabase(dsn, "mysql", createSql); err != nil { + return nil, err + } // 创建数据库 + + var db *gorm.DB + if db, err = gorm.Open(mysql.New(mysql.Config{ + DSN: c.Dsn(), // DSN data source name + DefaultStringSize: 191, // string 类型字段的默认长度 + SkipInitializeWithVersion: true, // 根据版本自动配置 + }), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true}); err != nil { + return ctx, err + } + global.GVA_CONFIG.AutoCode.Root, _ = filepath.Abs("..") + next = context.WithValue(next, "db", db) + return next, err +} + +func (h MysqlInitHandler) InitTables(ctx context.Context, inits initSlice) error { + return createTables(ctx, inits) +} + +func (h MysqlInitHandler) InitData(ctx context.Context, inits initSlice) error { + next, cancel := context.WithCancel(ctx) + defer func(c func()) { c() }(cancel) + for _, init := range inits { + if init.DataInserted(next) { + color.Info.Printf(InitDataExist, Mysql, init.InitializerName()) + continue + } + if n, err := init.InitializeData(next); err != nil { + color.Info.Printf(InitDataFailed, Mysql, init.InitializerName(), err) + return err + } else { + next = n + color.Info.Printf(InitDataSuccess, Mysql, init.InitializerName()) + } + } + color.Info.Printf(InitSuccess, Mysql) + return nil +} diff --git a/admin/server/service/system/sys_initdb_pgsql.go b/admin/server/service/system/sys_initdb_pgsql.go new file mode 100644 index 000000000..a31eaf453 --- /dev/null +++ b/admin/server/service/system/sys_initdb_pgsql.go @@ -0,0 +1,118 @@ +package system + +import ( + "context" + "errors" + "fmt" + "github.com/spf13/viper" + "path/filepath" + + "github.com/flipped-aurora/gin-vue-admin/server/config" + "github.com/gookit/color" + + "github.com/flipped-aurora/gin-vue-admin/server/utils" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +type PgsqlInitHandler struct{} + +func NewPgsqlInitHandler() *PgsqlInitHandler { + return &PgsqlInitHandler{} +} + +// WriteConfig pgsql 回写配置 +func (h PgsqlInitHandler) WriteConfig(ctx context.Context) error { + c, ok := ctx.Value("config").(config.Pgsql) + if !ok { + return errors.New("postgresql config invalid") + } + global.GVA_CONFIG.System.DbType = "pgsql" + global.GVA_CONFIG.Pgsql = c + + // 改成拿dify的配置,如果不是docker运行,则从dify api的.env文件中获取jwt的加密key + if !global.GVA_CONFIG.System.DockerRun { + var err error + global.GVA_CONFIG.JWT.SigningKey, err = h.GetJwtSigningKeyFormDifyApiEnv() + if err != nil { + return err + } + } + cs := utils.StructToMap(global.GVA_CONFIG) + for k, v := range cs { + global.GVA_VP.Set(k, v) + } + global.GVA_ACTIVE_DBNAME = &c.Dbname + return global.GVA_VP.WriteConfig() +} + +// EnsureDB 创建数据库并初始化 pg +func (h PgsqlInitHandler) EnsureDB(ctx context.Context, conf *request.InitDB) (next context.Context, err error) { + if s, ok := ctx.Value("dbtype").(string); !ok || s != "pgsql" { + return ctx, ErrDBTypeMismatch + } + + c := conf.ToPgsqlConfig() + next = context.WithValue(ctx, "config", c) + if c.Dbname == "" { + return ctx, nil + } // 如果没有数据库名, 则跳出初始化数据 + + //dsn := conf.PgsqlEmptyDsn() + //createSql := fmt.Sprintf("CREATE DATABASE %s;", c.Dbname) + //if err = createDatabase(dsn, "pgx", createSql); err != nil { + // return nil, err + //} // 创建数据库 + + var db *gorm.DB + if db, err = gorm.Open(postgres.New(postgres.Config{ + DSN: c.Dsn(), // DSN data source name + PreferSimpleProtocol: false, + }), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true}); err != nil { + return ctx, err + } + global.GVA_CONFIG.AutoCode.Root, _ = filepath.Abs("..") + next = context.WithValue(next, "db", db) + return next, err +} + +func (h PgsqlInitHandler) InitTables(ctx context.Context, inits initSlice) error { + return createTables(ctx, inits) +} + +func (h PgsqlInitHandler) InitData(ctx context.Context, inits initSlice) error { + next, cancel := context.WithCancel(ctx) + defer func(c func()) { c() }(cancel) + for i := 0; i < len(inits); i++ { + if inits[i].DataInserted(next) { + color.Info.Printf(InitDataExist, Pgsql, inits[i].InitializerName()) + continue + } + if n, err := inits[i].InitializeData(next); err != nil { + color.Info.Printf(InitDataFailed, Pgsql, inits[i].InitializerName(), err) + return err + } else { + next = n + color.Info.Printf(InitDataSuccess, Pgsql, inits[i].InitializerName()) + } + } + color.Info.Printf(InitSuccess, Pgsql) + return nil +} + +// GetJwtSigningKeyFormDifyApiEnv 从dify api的.env文件中获取jwt的加密key +func (h PgsqlInitHandler) GetJwtSigningKeyFormDifyApiEnv() (secretKey string, err error) { + apiEnvPath := filepath.Join("../../", "api", ".env") + viperAPI := viper.New() + viperAPI.SetConfigFile(apiEnvPath) + viperAPI.SetConfigType("env") + if err = viperAPI.ReadInConfig(); err != nil { + err = fmt.Errorf("Failed to read .env file: %s\n", err) + return + } + secretKey = viperAPI.GetString("SECRET_KEY") + return +} diff --git a/admin/server/service/system/sys_initdb_sqlite.go b/admin/server/service/system/sys_initdb_sqlite.go new file mode 100644 index 000000000..d7a97eba5 --- /dev/null +++ b/admin/server/service/system/sys_initdb_sqlite.go @@ -0,0 +1,88 @@ +package system + +import ( + "context" + "errors" + "github.com/glebarez/sqlite" + "github.com/gofrs/uuid/v5" + "github.com/gookit/color" + "gorm.io/gorm" + "path/filepath" + + "github.com/flipped-aurora/gin-vue-admin/server/config" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/flipped-aurora/gin-vue-admin/server/utils" +) + +type SqliteInitHandler struct{} + +func NewSqliteInitHandler() *SqliteInitHandler { + return &SqliteInitHandler{} +} + +// WriteConfig mysql回写配置 +func (h SqliteInitHandler) WriteConfig(ctx context.Context) error { + c, ok := ctx.Value("config").(config.Sqlite) + if !ok { + return errors.New("sqlite config invalid") + } + global.GVA_CONFIG.System.DbType = "sqlite" + global.GVA_CONFIG.Sqlite = c + global.GVA_CONFIG.JWT.SigningKey = uuid.Must(uuid.NewV4()).String() + cs := utils.StructToMap(global.GVA_CONFIG) + for k, v := range cs { + global.GVA_VP.Set(k, v) + } + global.GVA_ACTIVE_DBNAME = &c.Dbname + return global.GVA_VP.WriteConfig() +} + +// EnsureDB 创建数据库并初始化 sqlite +func (h SqliteInitHandler) EnsureDB(ctx context.Context, conf *request.InitDB) (next context.Context, err error) { + if s, ok := ctx.Value("dbtype").(string); !ok || s != "sqlite" { + return ctx, ErrDBTypeMismatch + } + + c := conf.ToSqliteConfig() + next = context.WithValue(ctx, "config", c) + if c.Dbname == "" { + return ctx, nil + } // 如果没有数据库名, 则跳出初始化数据 + + dsn := conf.SqliteEmptyDsn() + + var db *gorm.DB + if db, err = gorm.Open(sqlite.Open(dsn), &gorm.Config{ + DisableForeignKeyConstraintWhenMigrating: true, + }); err != nil { + return ctx, err + } + global.GVA_CONFIG.AutoCode.Root, _ = filepath.Abs("..") + next = context.WithValue(next, "db", db) + return next, err +} + +func (h SqliteInitHandler) InitTables(ctx context.Context, inits initSlice) error { + return createTables(ctx, inits) +} + +func (h SqliteInitHandler) InitData(ctx context.Context, inits initSlice) error { + next, cancel := context.WithCancel(ctx) + defer func(c func()) { c() }(cancel) + for _, init := range inits { + if init.DataInserted(next) { + color.Info.Printf(InitDataExist, Sqlite, init.InitializerName()) + continue + } + if n, err := init.InitializeData(next); err != nil { + color.Info.Printf(InitDataFailed, Sqlite, init.InitializerName(), err) + return err + } else { + next = n + color.Info.Printf(InitDataSuccess, Sqlite, init.InitializerName()) + } + } + color.Info.Printf(InitSuccess, Sqlite) + return nil +} diff --git a/admin/server/service/system/sys_menu.go b/admin/server/service/system/sys_menu.go new file mode 100644 index 000000000..4d5a0ea71 --- /dev/null +++ b/admin/server/service/system/sys_menu.go @@ -0,0 +1,289 @@ +package system + +import ( + "errors" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "gorm.io/gorm" + "strconv" +) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: getMenuTreeMap +//@description: 获取路由总树map +//@param: authorityId string +//@return: treeMap map[string][]system.SysMenu, err error + +type MenuService struct{} + +var MenuServiceApp = new(MenuService) + +func (menuService *MenuService) getMenuTreeMap(authorityId uint) (treeMap map[uint][]system.SysMenu, err error) { + var allMenus []system.SysMenu + var baseMenu []system.SysBaseMenu + var btns []system.SysAuthorityBtn + treeMap = make(map[uint][]system.SysMenu) + + var SysAuthorityMenus []system.SysAuthorityMenu + err = global.GVA_DB.Where("sys_authority_authority_id = ?", authorityId).Find(&SysAuthorityMenus).Error + if err != nil { + return + } + + var MenuIds []string + + for i := range SysAuthorityMenus { + MenuIds = append(MenuIds, SysAuthorityMenus[i].MenuId) + } + + err = global.GVA_DB.Where("id in (?)", MenuIds).Order("sort").Preload("Parameters").Find(&baseMenu).Error + if err != nil { + return + } + + for i := range baseMenu { + allMenus = append(allMenus, system.SysMenu{ + SysBaseMenu: baseMenu[i], + AuthorityId: authorityId, + MenuId: baseMenu[i].ID, + Parameters: baseMenu[i].Parameters, + }) + } + + err = global.GVA_DB.Where("authority_id = ?", authorityId).Preload("SysBaseMenuBtn").Find(&btns).Error + if err != nil { + return + } + var btnMap = make(map[uint]map[string]uint) + for _, v := range btns { + if btnMap[v.SysMenuID] == nil { + btnMap[v.SysMenuID] = make(map[string]uint) + } + btnMap[v.SysMenuID][v.SysBaseMenuBtn.Name] = authorityId + } + for _, v := range allMenus { + v.Btns = btnMap[v.SysBaseMenu.ID] + treeMap[v.ParentId] = append(treeMap[v.ParentId], v) + } + return treeMap, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetMenuTree +//@description: 获取动态菜单树 +//@param: authorityId string +//@return: menus []system.SysMenu, err error + +func (menuService *MenuService) GetMenuTree(authorityId uint) (menus []system.SysMenu, err error) { + menuTree, err := menuService.getMenuTreeMap(authorityId) + menus = menuTree[0] + for i := 0; i < len(menus); i++ { + err = menuService.getChildrenList(&menus[i], menuTree) + } + return menus, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: getChildrenList +//@description: 获取子菜单 +//@param: menu *model.SysMenu, treeMap map[string][]model.SysMenu +//@return: err error + +func (menuService *MenuService) getChildrenList(menu *system.SysMenu, treeMap map[uint][]system.SysMenu) (err error) { + menu.Children = treeMap[menu.MenuId] + for i := 0; i < len(menu.Children); i++ { + err = menuService.getChildrenList(&menu.Children[i], treeMap) + } + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetInfoList +//@description: 获取路由分页 +//@return: list interface{}, total int64,err error + +func (menuService *MenuService) GetInfoList(authorityID uint) (list interface{}, err error) { + var menuList []system.SysBaseMenu + treeMap, err := menuService.getBaseMenuTreeMap(authorityID) + menuList = treeMap[0] + for i := 0; i < len(menuList); i++ { + err = menuService.getBaseChildrenList(&menuList[i], treeMap) + } + return menuList, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: getBaseChildrenList +//@description: 获取菜单的子菜单 +//@param: menu *model.SysBaseMenu, treeMap map[string][]model.SysBaseMenu +//@return: err error + +func (menuService *MenuService) getBaseChildrenList(menu *system.SysBaseMenu, treeMap map[uint][]system.SysBaseMenu) (err error) { + menu.Children = treeMap[menu.ID] + for i := 0; i < len(menu.Children); i++ { + err = menuService.getBaseChildrenList(&menu.Children[i], treeMap) + } + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: AddBaseMenu +//@description: 添加基础路由 +//@param: menu model.SysBaseMenu +//@return: error + +func (menuService *MenuService) AddBaseMenu(menu system.SysBaseMenu) error { + if !errors.Is(global.GVA_DB.Where("name = ?", menu.Name).First(&system.SysBaseMenu{}).Error, gorm.ErrRecordNotFound) { + return errors.New("存在重复name,请修改name") + } + return global.GVA_DB.Create(&menu).Error +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: getBaseMenuTreeMap +//@description: 获取路由总树map +//@return: treeMap map[string][]system.SysBaseMenu, err error + +func (menuService *MenuService) getBaseMenuTreeMap(authorityID uint) (treeMap map[uint][]system.SysBaseMenu, err error) { + parentAuthorityID, err := AuthorityServiceApp.GetParentAuthorityID(authorityID) + if err != nil { + return nil, err + } + + var allMenus []system.SysBaseMenu + treeMap = make(map[uint][]system.SysBaseMenu) + db := global.GVA_DB.Order("sort").Preload("MenuBtn").Preload("Parameters") + + // 当开启了严格的树角色并且父角色不为0时需要进行菜单筛选 + if global.GVA_CONFIG.System.UseStrictAuth && parentAuthorityID != 0 { + var authorityMenus []system.SysAuthorityMenu + err = global.GVA_DB.Where("sys_authority_authority_id = ?", authorityID).Find(&authorityMenus).Error + if err != nil { + return nil, err + } + var menuIds []string + for i := range authorityMenus { + menuIds = append(menuIds, authorityMenus[i].MenuId) + } + db = db.Where("id in (?)", menuIds) + } + + err = db.Find(&allMenus).Error + for _, v := range allMenus { + treeMap[v.ParentId] = append(treeMap[v.ParentId], v) + } + return treeMap, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetBaseMenuTree +//@description: 获取基础路由树 +//@return: menus []system.SysBaseMenu, err error + +func (menuService *MenuService) GetBaseMenuTree(authorityID uint) (menus []system.SysBaseMenu, err error) { + treeMap, err := menuService.getBaseMenuTreeMap(authorityID) + menus = treeMap[0] + for i := 0; i < len(menus); i++ { + err = menuService.getBaseChildrenList(&menus[i], treeMap) + } + return menus, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: AddMenuAuthority +//@description: 为角色增加menu树 +//@param: menus []model.SysBaseMenu, authorityId string +//@return: err error + +func (menuService *MenuService) AddMenuAuthority(menus []system.SysBaseMenu, adminAuthorityID, authorityId uint) (err error) { + var auth system.SysAuthority + auth.AuthorityId = authorityId + auth.SysBaseMenus = menus + + err = AuthorityServiceApp.CheckAuthorityIDAuth(adminAuthorityID, authorityId) + if err != nil { + return err + } + + var authority system.SysAuthority + _ = global.GVA_DB.First(&authority, "authority_id = ?", adminAuthorityID).Error + var menuIds []string + + // 当开启了严格的树角色并且父角色不为0时需要进行菜单筛选 + if global.GVA_CONFIG.System.UseStrictAuth && *authority.ParentId != 0 { + var authorityMenus []system.SysAuthorityMenu + err = global.GVA_DB.Where("sys_authority_authority_id = ?", adminAuthorityID).Find(&authorityMenus).Error + if err != nil { + return err + } + for i := range authorityMenus { + menuIds = append(menuIds, authorityMenus[i].MenuId) + } + + for i := range menus { + hasMenu := false + for j := range menuIds { + idStr := strconv.Itoa(int(menus[i].ID)) + if idStr == menuIds[j] { + hasMenu = true + } + } + if !hasMenu { + return errors.New("添加失败,请勿跨级操作") + } + } + } + + err = AuthorityServiceApp.SetMenuAuthority(&auth) + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetMenuAuthority +//@description: 查看当前角色树 +//@param: info *request.GetAuthorityId +//@return: menus []system.SysMenu, err error + +func (menuService *MenuService) GetMenuAuthority(info *request.GetAuthorityId) (menus []system.SysMenu, err error) { + var baseMenu []system.SysBaseMenu + var SysAuthorityMenus []system.SysAuthorityMenu + err = global.GVA_DB.Where("sys_authority_authority_id = ?", info.AuthorityId).Find(&SysAuthorityMenus).Error + if err != nil { + return + } + + var MenuIds []string + + for i := range SysAuthorityMenus { + MenuIds = append(MenuIds, SysAuthorityMenus[i].MenuId) + } + + err = global.GVA_DB.Where("id in (?) ", MenuIds).Order("sort").Find(&baseMenu).Error + + for i := range baseMenu { + menus = append(menus, system.SysMenu{ + SysBaseMenu: baseMenu[i], + AuthorityId: info.AuthorityId, + MenuId: baseMenu[i].ID, + Parameters: baseMenu[i].Parameters, + }) + } + return menus, err +} + +// UserAuthorityDefaultRouter 用户角色默认路由检查 +// +// Author [SliverHorn](https://github.com/SliverHorn) +func (menuService *MenuService) UserAuthorityDefaultRouter(user *system.SysUser) { + var menuIds []string + err := global.GVA_DB.Model(&system.SysAuthorityMenu{}).Where("sys_authority_authority_id = ?", user.AuthorityId).Pluck("sys_base_menu_id", &menuIds).Error + if err != nil { + return + } + var am system.SysBaseMenu + err = global.GVA_DB.First(&am, "name = ? and id in (?)", user.Authority.DefaultRouter, menuIds).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + user.Authority.DefaultRouter = "404" + } +} diff --git a/admin/server/service/system/sys_operation_record.go b/admin/server/service/system/sys_operation_record.go new file mode 100644 index 000000000..adfc25efd --- /dev/null +++ b/admin/server/service/system/sys_operation_record.go @@ -0,0 +1,88 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" +) + +//@author: [granty1](https://github.com/granty1) +//@function: CreateSysOperationRecord +//@description: 创建记录 +//@param: sysOperationRecord model.SysOperationRecord +//@return: err error + +type OperationRecordService struct{} + +var OperationRecordServiceApp = new(OperationRecordService) + +func (operationRecordService *OperationRecordService) CreateSysOperationRecord(sysOperationRecord system.SysOperationRecord) (err error) { + err = global.GVA_DB.Create(&sysOperationRecord).Error + return err +} + +//@author: [granty1](https://github.com/granty1) +//@author: [piexlmax](https://github.com/piexlmax) +//@function: DeleteSysOperationRecordByIds +//@description: 批量删除记录 +//@param: ids request.IdsReq +//@return: err error + +func (operationRecordService *OperationRecordService) DeleteSysOperationRecordByIds(ids request.IdsReq) (err error) { + err = global.GVA_DB.Delete(&[]system.SysOperationRecord{}, "id in (?)", ids.Ids).Error + return err +} + +//@author: [granty1](https://github.com/granty1) +//@function: DeleteSysOperationRecord +//@description: 删除操作记录 +//@param: sysOperationRecord model.SysOperationRecord +//@return: err error + +func (operationRecordService *OperationRecordService) DeleteSysOperationRecord(sysOperationRecord system.SysOperationRecord) (err error) { + err = global.GVA_DB.Delete(&sysOperationRecord).Error + return err +} + +//@author: [granty1](https://github.com/granty1) +//@function: GetSysOperationRecord +//@description: 根据id获取单条操作记录 +//@param: id uint +//@return: sysOperationRecord system.SysOperationRecord, err error + +func (operationRecordService *OperationRecordService) GetSysOperationRecord(id uint) (sysOperationRecord system.SysOperationRecord, err error) { + err = global.GVA_DB.Where("id = ?", id).First(&sysOperationRecord).Error + return +} + +//@author: [granty1](https://github.com/granty1) +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetSysOperationRecordInfoList +//@description: 分页获取操作记录列表 +//@param: info systemReq.SysOperationRecordSearch +//@return: list interface{}, total int64, err error + +func (operationRecordService *OperationRecordService) GetSysOperationRecordInfoList(info systemReq.SysOperationRecordSearch) (list interface{}, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := global.GVA_DB.Model(&system.SysOperationRecord{}) + var sysOperationRecords []system.SysOperationRecord + // 如果有条件搜索 下方会自动创建搜索语句 + if info.Method != "" { + db = db.Where("method = ?", info.Method) + } + if info.Path != "" { + db = db.Where("path LIKE ?", "%"+info.Path+"%") + } + if info.Status != 0 { + db = db.Where("status = ?", info.Status) + } + err = db.Count(&total).Error + if err != nil { + return + } + err = db.Order("id desc").Limit(limit).Offset(offset).Preload("User").Find(&sysOperationRecords).Error + return sysOperationRecords, total, err +} diff --git a/admin/server/service/system/sys_params.go b/admin/server/service/system/sys_params.go new file mode 100644 index 000000000..7391ec016 --- /dev/null +++ b/admin/server/service/system/sys_params.go @@ -0,0 +1,82 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" +) + +type SysParamsService struct{} + +// CreateSysParams 创建参数记录 +// Author [Mr.奇淼](https://github.com/pixelmaxQm) +func (sysParamsService *SysParamsService) CreateSysParams(sysParams *system.SysParams) (err error) { + err = global.GVA_DB.Create(sysParams).Error + return err +} + +// DeleteSysParams 删除参数记录 +// Author [Mr.奇淼](https://github.com/pixelmaxQm) +func (sysParamsService *SysParamsService) DeleteSysParams(ID string) (err error) { + err = global.GVA_DB.Delete(&system.SysParams{}, "id = ?", ID).Error + return err +} + +// DeleteSysParamsByIds 批量删除参数记录 +// Author [Mr.奇淼](https://github.com/pixelmaxQm) +func (sysParamsService *SysParamsService) DeleteSysParamsByIds(IDs []string) (err error) { + err = global.GVA_DB.Delete(&[]system.SysParams{}, "id in ?", IDs).Error + return err +} + +// UpdateSysParams 更新参数记录 +// Author [Mr.奇淼](https://github.com/pixelmaxQm) +func (sysParamsService *SysParamsService) UpdateSysParams(sysParams system.SysParams) (err error) { + err = global.GVA_DB.Model(&system.SysParams{}).Where("id = ?", sysParams.ID).Updates(&sysParams).Error + return err +} + +// GetSysParams 根据ID获取参数记录 +// Author [Mr.奇淼](https://github.com/pixelmaxQm) +func (sysParamsService *SysParamsService) GetSysParams(ID string) (sysParams system.SysParams, err error) { + err = global.GVA_DB.Where("id = ?", ID).First(&sysParams).Error + return +} + +// GetSysParamsInfoList 分页获取参数记录 +// Author [Mr.奇淼](https://github.com/pixelmaxQm) +func (sysParamsService *SysParamsService) GetSysParamsInfoList(info systemReq.SysParamsSearch) (list []system.SysParams, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := global.GVA_DB.Model(&system.SysParams{}) + var sysParamss []system.SysParams + // 如果有条件搜索 下方会自动创建搜索语句 + if info.StartCreatedAt != nil && info.EndCreatedAt != nil { + db = db.Where("created_at BETWEEN ? AND ?", info.StartCreatedAt, info.EndCreatedAt) + } + if info.Name != "" { + db = db.Where("name LIKE ?", "%"+info.Name+"%") + } + if info.Key != "" { + db = db.Where("key LIKE ?", "%"+info.Key+"%") + } + err = db.Count(&total).Error + if err != nil { + return + } + + if limit != 0 { + db = db.Limit(limit).Offset(offset) + } + + err = db.Find(&sysParamss).Error + return sysParamss, total, err +} + +// GetSysParam 根据key获取参数value +// Author [Mr.奇淼](https://github.com/pixelmaxQm) +func (sysParamsService *SysParamsService) GetSysParam(key string) (param system.SysParams, err error) { + err = global.GVA_DB.Where(system.SysParams{Key: key}).First(¶m).Error + return +} diff --git a/admin/server/service/system/sys_system.go b/admin/server/service/system/sys_system.go new file mode 100644 index 000000000..a4415f041 --- /dev/null +++ b/admin/server/service/system/sys_system.go @@ -0,0 +1,62 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/config" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "go.uber.org/zap" +) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetSystemConfig +//@description: 读取配置文件 +//@return: conf config.Server, err error + +type SystemConfigService struct{} + +var SystemConfigServiceApp = new(SystemConfigService) + +func (systemConfigService *SystemConfigService) GetSystemConfig() (conf config.Server, err error) { + return global.GVA_CONFIG, nil +} + +// @description set system config, +//@author: [piexlmax](https://github.com/piexlmax) +//@function: SetSystemConfig +//@description: 设置配置文件 +//@param: system model.System +//@return: err error + +func (systemConfigService *SystemConfigService) SetSystemConfig(system system.System) (err error) { + cs := utils.StructToMap(system.Config) + for k, v := range cs { + global.GVA_VP.Set(k, v) + } + err = global.GVA_VP.WriteConfig() + return err +} + +//@author: [SliverHorn](https://github.com/SliverHorn) +//@function: GetServerInfo +//@description: 获取服务器信息 +//@return: server *utils.Server, err error + +func (systemConfigService *SystemConfigService) GetServerInfo() (server *utils.Server, err error) { + var s utils.Server + s.Os = utils.InitOS() + if s.Cpu, err = utils.InitCPU(); err != nil { + global.GVA_LOG.Error("func utils.InitCPU() Failed", zap.String("err", err.Error())) + return &s, err + } + if s.Ram, err = utils.InitRAM(); err != nil { + global.GVA_LOG.Error("func utils.InitRAM() Failed", zap.String("err", err.Error())) + return &s, err + } + if s.Disk, err = utils.InitDisk(); err != nil { + global.GVA_LOG.Error("func utils.InitDisk() Failed", zap.String("err", err.Error())) + return &s, err + } + + return &s, nil +} diff --git a/admin/server/service/system/sys_user.go b/admin/server/service/system/sys_user.go new file mode 100644 index 000000000..194ec2685 --- /dev/null +++ b/admin/server/service/system/sys_user.go @@ -0,0 +1,456 @@ +package system + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/model/common" + "github.com/flipped-aurora/gin-vue-admin/server/model/gaia" + systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + serviceGaia "github.com/flipped-aurora/gin-vue-admin/server/service/gaia" + "go.uber.org/zap" + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/gofrs/uuid/v5" + "gorm.io/gorm" +) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: Register +//@description: 用户注册 +//@param: u model.SysUser +//@return: userInter system.SysUser, err error + +type UserService struct{} + +var UserServiceApp = new(UserService) + +// Register +// @author: [piexlmax](https://github.com/piexlmax) +// @author: [SliverHorn](https://github.com/SliverHorn) +// @function: Register +// @description: 用户注册 +// @param: u *model.SysUser +// @return: err error, userInter *model.SysUser +func (userService *UserService) Register(u system.SysUser, token string) (userInter system.SysUser, err error) { + var user system.SysUser + if !errors.Is(global.GVA_DB.Where("email = ?", u.Email).First(&user).Error, gorm.ErrRecordNotFound) { + return userInter, errors.New("用户名已注册") + } + global.GVA_LOG.Debug("注册用户信息:", zap.Any("1", 1)) + + // Extend Start: Gaia Register User + if err = serviceGaia.RegisterUser(u, token); err != nil { + return userInter, errors.New("gaia注册失败:" + err.Error()) + } + // Extend Stop: Gaia Register User + + // 否则 附加uuid 密码hash加密 注册 + u.Password = utils.BcryptHash(u.Password) + u.UUID = uuid.Must(uuid.NewV4()) + err = global.GVA_DB.Create(&u).Error + return u, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@author: [SliverHorn](https://github.com/SliverHorn) +//@function: Login +//@description: 用户登录 +//@param: u *model.SysUser +//@return: err error, userInter *model.SysUser + +func (userService *UserService) Login(u *system.SysUser) (userInter *system.SysUser, err error) { + if nil == global.GVA_DB { + return nil, fmt.Errorf("db not init") + } + + var user system.SysUser + err = global.GVA_DB.Where("username = ? or email = ?", u.Username, u.Username).Preload("Authorities").Preload("Authority").First(&user).Error + if err == nil { + // Extend: Start 用户账号密码登录修改 + var ok bool + var account gaia.Account + var pwd = serviceGaia.PasswdEncode{} + if account, err = user.GetAccount(); err != nil { + return nil, errors.New("无法在Gaia中找到相关用户, 请联系管理员到用户列表执行刷新操作") + } + // 判断密码是否正确 + if ok, err = pwd.ComparePassword(u.Password, account.Password, account.PasswordSalt); err != nil || !ok { + return nil, errors.New("密码错误") + } + // Extend: Stop 用户账号密码登录修改 + MenuServiceApp.UserAuthorityDefaultRouter(&user) + } + return &user, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: ChangePassword +//@description: 修改用户密码 +//@param: u *model.SysUser, newPassword string +//@return: userInter *model.SysUser,err error + +func (userService *UserService) ChangePassword(u *system.SysUser, newPassword string) (userInter *system.SysUser, err error) { + var user system.SysUser + if err = global.GVA_DB.Where("id = ?", u.ID).First(&user).Error; err != nil { + return nil, err + } + if ok := utils.BcryptCheck(u.Password, user.Password); !ok { + return nil, errors.New("原密码错误") + } + user.Password = utils.BcryptHash(newPassword) + err = global.GVA_DB.Save(&user).Error + return &user, err + +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetUserInfoList +//@description: 分页获取数据 +//@param: info request.PageInfo +//@return: err error, list interface{}, total int64 + +func (userService *UserService) GetUserInfoList(info systemReq.GetUserList) ( + list []map[string]interface{}, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + db := global.GVA_DB.Model(&system.SysUser{}) + var userList []system.SysUser + + if info.NickName != "" { + db = db.Where("nick_name LIKE ?", "%"+info.NickName+"%") + } + if info.Phone != "" { + db = db.Where("phone LIKE ?", "%"+info.Phone+"%") + } + if info.Username != "" { + db = db.Where("username LIKE ?", "%"+info.Username+"%") + } + if info.Email != "" { + db = db.Where("email LIKE ?", "%"+info.Email+"%") + } + + err = db.Count(&total).Error + if err != nil { + return + } + err = db.Limit(limit).Offset(offset).Preload("Authorities").Preload( + "Authority").Order("id desc").Find(&userList).Error + if err != nil { + return + } + + // Extend Start: global code + + // 获取用户关联的控制名称 + var idList []uint + var globalCode []system.SysUserGlobalCode + if err = global.GVA_DB.Find(&globalCode).Error; err == nil { + for _, v := range globalCode { + idList = append(idList, v.UserID) + } + } + + // Extend Stop: global code + + // Extend Start: Loop through the user list to see if it is disabled + for i, v := range userList { + if len(v.Email) == 0 { + continue + } + // Check if the user is disabled + if count, iErr := global.GVA_REDIS.Get(context.Background(), fmt.Sprintf( + "login_error_rate_limit:%s", v.Email)).Int(); iErr == nil && count >= global.GVA_CONFIG.Gaia.LoginMaxErrorLimit { + userList[i].Enable = system.UserDeactivate + } + // encode + var userByte []byte + var userDick map[string]interface{} + if userByte, err = json.Marshal(&userList[i]); err == nil { + err = json.Unmarshal(userByte, &userDick) + } + list = append(list, userDick) + if utils.InUintArray(v.ID, idList) { + list[i]["global_code"] = true + } + } + // Extend Stop: Loop through the user list to see if it is disabled + return list, total, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: SetUserAuthority +//@description: 设置一个用户的权限 +//@param: uuid uuid.UUID, authorityId string +//@return: err error + +func (userService *UserService) SetUserAuthority(id uint, authorityId uint) (err error) { + + assignErr := global.GVA_DB.Where("sys_user_id = ? AND sys_authority_authority_id = ?", id, authorityId).First(&system.SysUserAuthority{}).Error + if errors.Is(assignErr, gorm.ErrRecordNotFound) { + return errors.New("该用户无此角色") + } + + var authority system.SysAuthority + err = global.GVA_DB.Where("authority_id = ?", authorityId).First(&authority).Error + if err != nil { + return err + } + var authorityMenu []system.SysAuthorityMenu + var authorityMenuIDs []string + err = global.GVA_DB.Where("sys_authority_authority_id = ?", authorityId).Find(&authorityMenu).Error + if err != nil { + return err + } + + for i := range authorityMenu { + authorityMenuIDs = append(authorityMenuIDs, authorityMenu[i].MenuId) + } + + var authorityMenus []system.SysBaseMenu + err = global.GVA_DB.Preload("Parameters").Where("id in (?)", authorityMenuIDs).Find(&authorityMenus).Error + if err != nil { + return err + } + hasMenu := false + for i := range authorityMenus { + if authorityMenus[i].Name == authority.DefaultRouter { + hasMenu = true + break + } + } + if !hasMenu { + return errors.New("找不到默认路由,无法切换本角色") + } + + err = global.GVA_DB.Model(&system.SysUser{}).Where("id = ?", id).Update("authority_id", authorityId).Error + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: SetUserAuthorities +//@description: 设置一个用户的权限 +//@param: id uint, authorityIds []string +//@return: err error + +func (userService *UserService) SetUserAuthorities(adminAuthorityID, id uint, authorityIds []uint) (err error) { + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + var user system.SysUser + TxErr := tx.Where("id = ?", id).First(&user).Error + if TxErr != nil { + global.GVA_LOG.Debug(TxErr.Error()) + return errors.New("查询用户数据失败") + } + TxErr = tx.Delete(&[]system.SysUserAuthority{}, "sys_user_id = ?", id).Error + if TxErr != nil { + return TxErr + } + var useAuthority []system.SysUserAuthority + for _, v := range authorityIds { + e := AuthorityServiceApp.CheckAuthorityIDAuth(adminAuthorityID, v) + if e != nil { + return e + } + useAuthority = append(useAuthority, system.SysUserAuthority{ + SysUserId: id, SysAuthorityAuthorityId: v, + }) + } + TxErr = tx.Create(&useAuthority).Error + if TxErr != nil { + return TxErr + } + TxErr = tx.Model(&user).Update("authority_id", authorityIds[0]).Error + if TxErr != nil { + return TxErr + } + // 返回 nil 提交事务 + return nil + }) +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: DeleteUser +//@description: 删除用户 +//@param: id float64 +//@return: err error + +func (userService *UserService) DeleteUser(id int) (err error) { + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + if err := tx.Where("id = ?", id).Delete(&system.SysUser{}).Error; err != nil { + return err + } + if err := tx.Delete(&[]system.SysUserAuthority{}, "sys_user_id = ?", id).Error; err != nil { + return err + } + return nil + }) +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: SetUserInfo +//@description: 设置用户信息 +//@param: req system.SysUser, globalCodeType bool +//@return: err error, user model.SysUser + +func (userService *UserService) SetUserInfo(req system.SysUser, globalCodeType bool) error { + + // Extend Start: synchronize gaia user information + + var err error + var user system.SysUser + if err = global.GVA_DB.Where("id = ?", req.ID).First(&user).Error; err != nil { + return errors.New("SetUserInfo : No relevant users found" + err.Error()) + } + var account gaia.Account + if account, err = user.GetAccount(); err != nil { + return errors.New("SetUserInfo : No corresponding user can be found in Gaia" + err.Error()) + } + // determine the switching user status + status := gaia.UserBanned + if req.Enable == system.UserActive { + status = gaia.UserActive + } + // switch user status + user.SyncGaiaStatus(req.Enable) + // modify Gaia user information + if err = global.GVA_DB.Model(&gaia.Account{}).Where("id=?", account.ID).Updates(map[string]interface{}{ + "updated_at": time.Now(), + "name": req.NickName, + "avatar": req.HeaderImg, + "email": req.Email, + "status": status, + }).Error; err != nil { + return errors.New("SetUserInfo : gaia.Account update error: " + err.Error()) + } + + // Extend Stop: synchronize gaia user information + + // Extend Start global code + globalCode := system.SysUserGlobalCode{UserID: user.ID} + + // 这里假设 gorn 提供了类似 gorm 的 FirstOrCreate 方法 + if globalCodeType { + global.GVA_DB.FirstOrCreate(&globalCode, global.GVA_DB.Where("user_id = ?", user.ID)) + } + + // 如果需要根据 req.GlobalCode 决定是否创建记录 + if !globalCodeType { + // 如果不需要 GlobalCode,可以在这里删除记录 + global.GVA_DB.Where("user_id = ?", user.ID).Delete(&globalCode) + } + // 修改 + go serviceGaia.SyncExecuteCode() + // Extend Start global code + + return global.GVA_DB.Model(&system.SysUser{}). + Select("updated_at", "nick_name", "header_img", "phone", "email", "enable"). + Where("id=?", req.ID). + Updates(map[string]interface{}{ + "updated_at": time.Now(), + "nick_name": req.NickName, + "header_img": req.HeaderImg, + "phone": req.Phone, + "email": req.Email, + "enable": req.Enable, + }).Error +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: SetSelfInfo +//@description: 设置用户信息 +//@param: reqUser model.SysUser +//@return: err error, user model.SysUser + +func (userService *UserService) SetSelfInfo(req system.SysUser) error { + return global.GVA_DB.Model(&system.SysUser{}). + Where("id=?", req.ID). + Updates(req).Error +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: SetSelfSetting +//@description: 设置用户配置 +//@param: req datatypes.JSON, uid uint +//@return: err error + +func (userService *UserService) SetSelfSetting(req common.JSONMap, uid uint) error { + return global.GVA_DB.Model(&system.SysUser{}).Where("id = ?", uid).Update("origin_setting", req).Error +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@author: [SliverHorn](https://github.com/SliverHorn) +//@function: GetUserInfo +//@description: 获取用户信息 +//@param: uuid uuid.UUID +//@return: err error, user system.SysUser + +func (userService *UserService) GetUserInfo(uuid uuid.UUID) (user system.SysUser, err error) { + var reqUser system.SysUser + err = global.GVA_DB.Preload("Authorities").Preload("Authority").First(&reqUser, "uuid = ?", uuid).Error + if err != nil { + return reqUser, err + } + MenuServiceApp.UserAuthorityDefaultRouter(&reqUser) + return reqUser, err +} + +//@author: [SliverHorn](https://github.com/SliverHorn) +//@function: FindUserById +//@description: 通过id获取用户信息 +//@param: id int +//@return: err error, user *model.SysUser + +func (userService *UserService) FindUserById(id int) (user *system.SysUser, err error) { + var u system.SysUser + err = global.GVA_DB.Where("id = ?", id).First(&u).Error + return &u, err +} + +//@author: [SliverHorn](https://github.com/SliverHorn) +//@function: FindUserByUuid +//@description: 通过uuid获取用户信息 +//@param: uuid string +//@return: err error, user *model.SysUser + +func (userService *UserService) FindUserByUuid(uuid string) (user *system.SysUser, err error) { + var u system.SysUser + if err = global.GVA_DB.Where("uuid = ?", uuid).First(&u).Error; err != nil { + return &u, errors.New("用户不存在") + } + return &u, nil +} + +// Extend Start: update password + +// ResetPassword +// @author: [piexlmax](https://github.com/piexlmax) +// @function: ResetPassword +// @description: 修改用户密码 +// @param: ID uint +// @return: err error +func (userService *UserService) ResetPassword(id uint, passwd string) (err error) { + var s serviceGaia.PasswdEncode + var user system.SysUser + if err = global.GVA_DB.Where("id = ?", id).First(&user).Error; err == nil { + var passwordHashed, salt string + global.GVA_DB.Model(&system.SysUser{}).Where("id = ?", id).Updates(&map[string]string{ + "password": utils.BcryptHash(passwd), + }) + if passwordHashed, salt, err = s.EncodePassword(passwd); err != nil { + return + } + var account gaia.Account + if account, err = user.GetAccount(); err == nil { + account.PasswordSalt = salt + account.Password = passwordHashed + err = global.GVA_DB.Save(&account).Error + } + } + return err +} + +// Extend Stop: update password diff --git a/admin/server/service/system/sys_user_extend.go b/admin/server/service/system/sys_user_extend.go new file mode 100644 index 000000000..5b6f8331b --- /dev/null +++ b/admin/server/service/system/sys_user_extend.go @@ -0,0 +1,156 @@ +package system + +import ( + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/gaia" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/gofrs/uuid/v5" + "strings" +) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: Register +//@description: 用户注册 +//@param: u model.SysUser +//@return: userInter system.SysUser, err error + +type UserExtendService struct{} + +//@author: [piexlmax](https://github.com/piexlmax) +//@author: [SliverHorn](https://github.com/SliverHorn) +//@function: OaLogin +//@description: 用户登录(不检查密码) +//@param: u *model.SysUser +//@return: err error, userInter *model.SysUser + +func (userService *UserExtendService) OaLogin(u *system.SysUser) (userInter *system.SysUser, err error) { + if nil == global.GVA_DB { + return nil, fmt.Errorf("db not init") + } + + var user system.SysUser + err = global.GVA_DB.Where("username = ?", u.Username).Preload("Authorities").Preload("Authority").First(&user).Error + if err == nil { + MenuServiceApp.UserAuthorityDefaultRouter(&user) + } + return &user, err +} + +// SyncUser +// @author: [piexlmax](https://github.com/piexlmax) +// @author: [SliverHorn](https://github.com/SliverHorn) +// @function: SyncUser +// @description: 用户同步 +// @param: u *model.SysUser +// @return: err error, userInter *model.SysUser +func (userService *UserExtendService) SyncUser() { + // init + var err error + var isInit = true + var user []system.SysUser + var accountList []gaia.Account + var mailDick = make(map[string]string) + if global.GVA_DB == nil { + global.GVA_LOG.Info("数据库未初始化,同步用户失败") + return + } + // 遍历后台用户表 + if err = global.GVA_DB.Select("email", "username").Find(&user).Error; err != nil { + global.GVA_LOG.Error("SyncUser gaia表查询失败,原因: " + err.Error()) + return + } + // 循环用户列表 + if len(user) > 0 { + isInit = false + } + var emailList []string + for _, v := range user { + emailList = append(emailList, v.Email) + mailDick[v.Email] = v.Username + } + // 查询gaia用户表 + db := global.GVA_DB + if len(emailList) > 0 { + db = db.Where("email NOT IN (?)", emailList) + } + if err = db.Order("created_at ASC").Find(&accountList).Error; err != nil { + global.GVA_LOG.Error("SyncUser gaia表查询失败,原因: " + err.Error()) + return + } + var adminAuthorities, userAuthorities []system.SysAuthority + userAuthorities = append(userAuthorities, system.SysAuthority{ + AuthorityId: system.DefaultGroupID, + }) + adminAuthorities = append(adminAuthorities, system.SysAuthority{ + AuthorityId: system.AdminGroupID, + }) + // 循环结果 + for i, v := range accountList { + // 创建相关用户 + var integrate gaia.AccountIntegrate + if username, ok := mailDick[v.Email]; ok { + if integrate, err = v.GetAccount(username); err != nil { + global.GVA_LOG.Debug("SyncUser GetAccount: " + err.Error()) + } + } + // 是否配置了多余信息获取接口 + var name = v.Name + var phone, ding string + if mailList := strings.Split(v.Email, "@"); len(integrate.OpenID) == 0 && len(mailList) == 2 { + integrate.OpenID = mailList[0] + } + + // 创建用户 + s := UserService{} + uuidStr, _ := uuid.NewV1() + if len(ding) > 0 { + if err = global.GVA_DB.Create(gaia.AccountDingTalkExtend{ + ID: v.ID, + DingTalk: ding, + }).Error; err != nil { + global.GVA_LOG.Error("SyncUser Create AccountDingTalkExtend: " + err.Error()) + } + } + // 注册 + AuthorityId := system.DefaultGroupID + authorities := userAuthorities + if isInit && i == 0 { + // admin + AuthorityId = system.AdminGroupID + authorities = adminAuthorities + + // 并设置管理员配置 + if global.GVA_CONFIG.Gaia.SuperAdminTenantId == "" || global.GVA_CONFIG.Gaia.SuperAdminAccountId == "" { + global.GVA_CONFIG.Gaia.SuperAdminAccountId = v.ID.String() + // TODO 需要查询默认空间的id + var Tenant gaia.Tenants + global.GVA_CONFIG.Gaia.SuperAdminTenantId = Tenant.GetSuperAdminTenantId() + cs := utils.StructToMap(global.GVA_CONFIG) + for k, v := range cs { + global.GVA_VP.Set(k, v) + } + err = global.GVA_VP.WriteConfig() + if err != nil { + return + } + } + } + // Register + if _, err = s.Register(system.SysUser{ + HeaderImg: "", + UUID: v.ID, + NickName: name, + Phone: phone, + Email: v.Email, + AuthorityId: AuthorityId, + Authorities: authorities, + Username: integrate.OpenID, + Password: uuidStr.String(), + Enable: system.UserActive, + }, ""); err != nil { + global.GVA_LOG.Error("SyncUser Register system.SysUser: " + err.Error()) + } + } +} diff --git a/admin/server/source/example/file_upload_download.go b/admin/server/source/example/file_upload_download.go new file mode 100644 index 000000000..f39dcf2e5 --- /dev/null +++ b/admin/server/source/example/file_upload_download.go @@ -0,0 +1,65 @@ +package example + +import ( + "context" + "github.com/flipped-aurora/gin-vue-admin/server/model/example" + "github.com/flipped-aurora/gin-vue-admin/server/service/system" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +const initOrderExaFile = system.InitOrderInternal + 1 + +type initExaFileMysql struct{} + +// auto run +func init() { + system.RegisterInit(initOrderExaFile, &initExaFileMysql{}) +} + +func (i *initExaFileMysql) MigrateTable(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + return ctx, db.AutoMigrate(&example.ExaFileUploadAndDownload{}) +} + +func (i *initExaFileMysql) TableCreated(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + return db.Migrator().HasTable(&example.ExaFileUploadAndDownload{}) +} + +func (i initExaFileMysql) InitializerName() string { + return example.ExaFileUploadAndDownload{}.TableName() +} + +func (i *initExaFileMysql) InitializeData(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + entities := []example.ExaFileUploadAndDownload{ + {Name: "10.png", Url: "https://qmplusimg.henrongyi.top/gvalogo.png", Tag: "png", Key: "158787308910.png"}, + {Name: "logo.png", Url: "https://qmplusimg.henrongyi.top/1576554439myAvatar.png", Tag: "png", Key: "1587973709logo.png"}, + } + if err := db.Create(&entities).Error; err != nil { + return ctx, errors.Wrap(err, example.ExaFileUploadAndDownload{}.TableName()+"表数据初始化失败!") + } + return ctx, nil +} + +func (i *initExaFileMysql) DataInserted(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + lookup := example.ExaFileUploadAndDownload{Name: "logo.png", Key: "1587973709logo.png"} + if errors.Is(db.First(&lookup, &lookup).Error, gorm.ErrRecordNotFound) { + return false + } + return true +} diff --git a/admin/server/source/system/api.go b/admin/server/source/system/api.go new file mode 100644 index 000000000..54fb74f1a --- /dev/null +++ b/admin/server/source/system/api.go @@ -0,0 +1,223 @@ +package system + +import ( + "context" + sysModel "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/service/system" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type initApi struct{} + +const initOrderApi = system.InitOrderSystem + 1 + +// auto run +func init() { + system.RegisterInit(initOrderApi, &initApi{}) +} + +func (i initApi) InitializerName() string { + return sysModel.SysApi{}.TableName() +} + +func (i *initApi) MigrateTable(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + return ctx, db.AutoMigrate(&sysModel.SysApi{}) +} + +func (i *initApi) TableCreated(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + return db.Migrator().HasTable(&sysModel.SysApi{}) +} + +func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + entities := []sysModel.SysApi{ + {ApiGroup: "jwt", Method: "POST", Path: "/jwt/jsonInBlacklist", Description: "jwt加入黑名单(退出,必选)"}, + + {ApiGroup: "系统用户", Method: "DELETE", Path: "/user/deleteUser", Description: "删除用户"}, + {ApiGroup: "系统用户", Method: "POST", Path: "/user/admin_register", Description: "用户注册"}, + {ApiGroup: "系统用户", Method: "POST", Path: "/user/getUserList", Description: "获取用户列表"}, + {ApiGroup: "系统用户", Method: "PUT", Path: "/user/setUserInfo", Description: "设置用户信息"}, + {ApiGroup: "系统用户", Method: "PUT", Path: "/user/setSelfInfo", Description: "设置自身信息(必选)"}, + {ApiGroup: "系统用户", Method: "GET", Path: "/user/getUserInfo", Description: "获取自身信息(必选)"}, + {ApiGroup: "系统用户", Method: "POST", Path: "/user/setUserAuthorities", Description: "设置权限组"}, + {ApiGroup: "系统用户", Method: "POST", Path: "/user/changePassword", Description: "修改密码(建议选择)"}, + {ApiGroup: "系统用户", Method: "POST", Path: "/user/setUserAuthority", Description: "修改用户角色(必选)"}, + {ApiGroup: "系统用户", Method: "POST", Path: "/user/resetPassword", Description: "重置用户密码"}, + {ApiGroup: "系统用户", Method: "PUT", Path: "/user/setSelfSetting", Description: "用户界面配置"}, + + {ApiGroup: "api", Method: "POST", Path: "/api/createApi", Description: "创建api"}, + {ApiGroup: "api", Method: "POST", Path: "/api/deleteApi", Description: "删除Api"}, + {ApiGroup: "api", Method: "POST", Path: "/api/updateApi", Description: "更新Api"}, + {ApiGroup: "api", Method: "POST", Path: "/api/getApiList", Description: "获取api列表"}, + {ApiGroup: "api", Method: "POST", Path: "/api/getAllApis", Description: "获取所有api"}, + {ApiGroup: "api", Method: "POST", Path: "/api/getApiById", Description: "获取api详细信息"}, + {ApiGroup: "api", Method: "DELETE", Path: "/api/deleteApisByIds", Description: "批量删除api"}, + {ApiGroup: "api", Method: "GET", Path: "/api/syncApi", Description: "获取待同步API"}, + {ApiGroup: "api", Method: "GET", Path: "/api/getApiGroups", Description: "获取路由组"}, + {ApiGroup: "api", Method: "POST", Path: "/api/enterSyncApi", Description: "确认同步API"}, + {ApiGroup: "api", Method: "POST", Path: "/api/ignoreApi", Description: "忽略API"}, + + {ApiGroup: "角色", Method: "POST", Path: "/authority/copyAuthority", Description: "拷贝角色"}, + {ApiGroup: "角色", Method: "POST", Path: "/authority/createAuthority", Description: "创建角色"}, + {ApiGroup: "角色", Method: "POST", Path: "/authority/deleteAuthority", Description: "删除角色"}, + {ApiGroup: "角色", Method: "PUT", Path: "/authority/updateAuthority", Description: "更新角色信息"}, + {ApiGroup: "角色", Method: "POST", Path: "/authority/getAuthorityList", Description: "获取角色列表"}, + {ApiGroup: "角色", Method: "POST", Path: "/authority/setDataAuthority", Description: "设置角色资源权限"}, + + {ApiGroup: "casbin", Method: "POST", Path: "/casbin/updateCasbin", Description: "更改角色api权限"}, + {ApiGroup: "casbin", Method: "POST", Path: "/casbin/getPolicyPathByAuthorityId", Description: "获取权限列表"}, + + {ApiGroup: "菜单", Method: "POST", Path: "/menu/addBaseMenu", Description: "新增菜单"}, + {ApiGroup: "菜单", Method: "POST", Path: "/menu/getMenu", Description: "获取菜单树(必选)"}, + {ApiGroup: "菜单", Method: "POST", Path: "/menu/deleteBaseMenu", Description: "删除菜单"}, + {ApiGroup: "菜单", Method: "POST", Path: "/menu/updateBaseMenu", Description: "更新菜单"}, + {ApiGroup: "菜单", Method: "POST", Path: "/menu/getBaseMenuById", Description: "根据id获取菜单"}, + {ApiGroup: "菜单", Method: "POST", Path: "/menu/getMenuList", Description: "分页获取基础menu列表"}, + {ApiGroup: "菜单", Method: "POST", Path: "/menu/getBaseMenuTree", Description: "获取用户动态路由"}, + {ApiGroup: "菜单", Method: "POST", Path: "/menu/getMenuAuthority", Description: "获取指定角色menu"}, + {ApiGroup: "菜单", Method: "POST", Path: "/menu/addMenuAuthority", Description: "增加menu和角色关联关系"}, + + {ApiGroup: "分片上传", Method: "GET", Path: "/fileUploadAndDownload/findFile", Description: "寻找目标文件(秒传)"}, + {ApiGroup: "分片上传", Method: "POST", Path: "/fileUploadAndDownload/breakpointContinue", Description: "断点续传"}, + {ApiGroup: "分片上传", Method: "POST", Path: "/fileUploadAndDownload/breakpointContinueFinish", Description: "断点续传完成"}, + {ApiGroup: "分片上传", Method: "POST", Path: "/fileUploadAndDownload/removeChunk", Description: "上传完成移除文件"}, + + {ApiGroup: "文件上传与下载", Method: "POST", Path: "/fileUploadAndDownload/upload", Description: "文件上传(建议选择)"}, + {ApiGroup: "文件上传与下载", Method: "POST", Path: "/fileUploadAndDownload/deleteFile", Description: "删除文件"}, + {ApiGroup: "文件上传与下载", Method: "POST", Path: "/fileUploadAndDownload/editFileName", Description: "文件名或者备注编辑"}, + {ApiGroup: "文件上传与下载", Method: "POST", Path: "/fileUploadAndDownload/getFileList", Description: "获取上传文件列表"}, + {ApiGroup: "文件上传与下载", Method: "POST", Path: "/fileUploadAndDownload/importURL", Description: "导入URL"}, + + {ApiGroup: "系统服务", Method: "POST", Path: "/system/getServerInfo", Description: "获取服务器信息"}, + {ApiGroup: "系统服务", Method: "POST", Path: "/system/getSystemConfig", Description: "获取配置文件内容"}, + {ApiGroup: "系统服务", Method: "POST", Path: "/system/setSystemConfig", Description: "设置配置文件内容"}, + + {ApiGroup: "客户", Method: "PUT", Path: "/customer/customer", Description: "更新客户"}, + {ApiGroup: "客户", Method: "POST", Path: "/customer/customer", Description: "创建客户"}, + {ApiGroup: "客户", Method: "DELETE", Path: "/customer/customer", Description: "删除客户"}, + {ApiGroup: "客户", Method: "GET", Path: "/customer/customer", Description: "获取单一客户"}, + {ApiGroup: "客户", Method: "GET", Path: "/customer/customerList", Description: "获取客户列表"}, + + {ApiGroup: "代码生成器", Method: "GET", Path: "/autoCode/getDB", Description: "获取所有数据库"}, + {ApiGroup: "代码生成器", Method: "GET", Path: "/autoCode/getTables", Description: "获取数据库表"}, + {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/createTemp", Description: "自动化代码"}, + {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/preview", Description: "预览自动化代码"}, + {ApiGroup: "代码生成器", Method: "GET", Path: "/autoCode/getColumn", Description: "获取所选table的所有字段"}, + {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/installPlugin", Description: "安装插件"}, + {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/pubPlug", Description: "打包插件"}, + + {ApiGroup: "模板配置", Method: "POST", Path: "/autoCode/createPackage", Description: "配置模板"}, + {ApiGroup: "模板配置", Method: "GET", Path: "/autoCode/getTemplates", Description: "获取模板文件"}, + {ApiGroup: "模板配置", Method: "POST", Path: "/autoCode/getPackage", Description: "获取所有模板"}, + {ApiGroup: "模板配置", Method: "POST", Path: "/autoCode/delPackage", Description: "删除模板"}, + + {ApiGroup: "代码生成器历史", Method: "POST", Path: "/autoCode/getMeta", Description: "获取meta信息"}, + {ApiGroup: "代码生成器历史", Method: "POST", Path: "/autoCode/rollback", Description: "回滚自动生成代码"}, + {ApiGroup: "代码生成器历史", Method: "POST", Path: "/autoCode/getSysHistory", Description: "查询回滚记录"}, + {ApiGroup: "代码生成器历史", Method: "POST", Path: "/autoCode/delSysHistory", Description: "删除回滚记录"}, + {ApiGroup: "代码生成器历史", Method: "POST", Path: "/autoCode/addFunc", Description: "增加模板方法"}, + + {ApiGroup: "系统字典详情", Method: "PUT", Path: "/sysDictionaryDetail/updateSysDictionaryDetail", Description: "更新字典内容"}, + {ApiGroup: "系统字典详情", Method: "POST", Path: "/sysDictionaryDetail/createSysDictionaryDetail", Description: "新增字典内容"}, + {ApiGroup: "系统字典详情", Method: "DELETE", Path: "/sysDictionaryDetail/deleteSysDictionaryDetail", Description: "删除字典内容"}, + {ApiGroup: "系统字典详情", Method: "GET", Path: "/sysDictionaryDetail/findSysDictionaryDetail", Description: "根据ID获取字典内容"}, + {ApiGroup: "系统字典详情", Method: "GET", Path: "/sysDictionaryDetail/getSysDictionaryDetailList", Description: "获取字典内容列表"}, + + {ApiGroup: "系统字典", Method: "POST", Path: "/sysDictionary/createSysDictionary", Description: "新增字典"}, + {ApiGroup: "系统字典", Method: "DELETE", Path: "/sysDictionary/deleteSysDictionary", Description: "删除字典"}, + {ApiGroup: "系统字典", Method: "PUT", Path: "/sysDictionary/updateSysDictionary", Description: "更新字典"}, + {ApiGroup: "系统字典", Method: "GET", Path: "/sysDictionary/findSysDictionary", Description: "根据ID获取字典(建议选择)"}, + {ApiGroup: "系统字典", Method: "GET", Path: "/sysDictionary/getSysDictionaryList", Description: "获取字典列表"}, + + {ApiGroup: "操作记录", Method: "POST", Path: "/sysOperationRecord/createSysOperationRecord", Description: "新增操作记录"}, + {ApiGroup: "操作记录", Method: "GET", Path: "/sysOperationRecord/findSysOperationRecord", Description: "根据ID获取操作记录"}, + {ApiGroup: "操作记录", Method: "GET", Path: "/sysOperationRecord/getSysOperationRecordList", Description: "获取操作记录列表"}, + {ApiGroup: "操作记录", Method: "DELETE", Path: "/sysOperationRecord/deleteSysOperationRecord", Description: "删除操作记录"}, + {ApiGroup: "操作记录", Method: "DELETE", Path: "/sysOperationRecord/deleteSysOperationRecordByIds", Description: "批量删除操作历史"}, + + {ApiGroup: "断点续传(插件版)", Method: "POST", Path: "/simpleUploader/upload", Description: "插件版分片上传"}, + {ApiGroup: "断点续传(插件版)", Method: "GET", Path: "/simpleUploader/checkFileMd5", Description: "文件完整度验证"}, + {ApiGroup: "断点续传(插件版)", Method: "GET", Path: "/simpleUploader/mergeFileMd5", Description: "上传完成合并文件"}, + + {ApiGroup: "email", Method: "POST", Path: "/email/emailTest", Description: "发送测试邮件"}, + {ApiGroup: "email", Method: "POST", Path: "/email/sendEmail", Description: "发送邮件"}, + + {ApiGroup: "按钮权限", Method: "POST", Path: "/authorityBtn/setAuthorityBtn", Description: "设置按钮权限"}, + {ApiGroup: "按钮权限", Method: "POST", Path: "/authorityBtn/getAuthorityBtn", Description: "获取已有按钮权限"}, + {ApiGroup: "按钮权限", Method: "POST", Path: "/authorityBtn/canRemoveAuthorityBtn", Description: "删除按钮"}, + + {ApiGroup: "导出模板", Method: "POST", Path: "/sysExportTemplate/createSysExportTemplate", Description: "新增导出模板"}, + {ApiGroup: "导出模板", Method: "DELETE", Path: "/sysExportTemplate/deleteSysExportTemplate", Description: "删除导出模板"}, + {ApiGroup: "导出模板", Method: "DELETE", Path: "/sysExportTemplate/deleteSysExportTemplateByIds", Description: "批量删除导出模板"}, + {ApiGroup: "导出模板", Method: "PUT", Path: "/sysExportTemplate/updateSysExportTemplate", Description: "更新导出模板"}, + {ApiGroup: "导出模板", Method: "GET", Path: "/sysExportTemplate/findSysExportTemplate", Description: "根据ID获取导出模板"}, + {ApiGroup: "导出模板", Method: "GET", Path: "/sysExportTemplate/getSysExportTemplateList", Description: "获取导出模板列表"}, + {ApiGroup: "导出模板", Method: "GET", Path: "/sysExportTemplate/exportExcel", Description: "导出Excel"}, + {ApiGroup: "导出模板", Method: "GET", Path: "/sysExportTemplate/exportTemplate", Description: "下载模板"}, + {ApiGroup: "导出模板", Method: "POST", Path: "/sysExportTemplate/importExcel", Description: "导入Excel"}, + + {ApiGroup: "公告", Method: "POST", Path: "/info/createInfo", Description: "新建公告"}, + {ApiGroup: "公告", Method: "DELETE", Path: "/info/deleteInfo", Description: "删除公告"}, + {ApiGroup: "公告", Method: "DELETE", Path: "/info/deleteInfoByIds", Description: "批量删除公告"}, + {ApiGroup: "公告", Method: "PUT", Path: "/info/updateInfo", Description: "更新公告"}, + {ApiGroup: "公告", Method: "GET", Path: "/info/findInfo", Description: "根据ID获取公告"}, + {ApiGroup: "公告", Method: "GET", Path: "/info/getInfoList", Description: "获取公告列表"}, + + {ApiGroup: "参数管理", Method: "POST", Path: "/sysParams/createSysParams", Description: "新建参数"}, + {ApiGroup: "参数管理", Method: "DELETE", Path: "/sysParams/deleteSysParams", Description: "删除参数"}, + {ApiGroup: "参数管理", Method: "DELETE", Path: "/sysParams/deleteSysParamsByIds", Description: "批量删除参数"}, + {ApiGroup: "参数管理", Method: "PUT", Path: "/sysParams/updateSysParams", Description: "更新参数"}, + {ApiGroup: "参数管理", Method: "GET", Path: "/sysParams/findSysParams", Description: "根据ID获取参数"}, + {ApiGroup: "参数管理", Method: "GET", Path: "/sysParams/getSysParamsList", Description: "获取参数列表"}, + {ApiGroup: "参数管理", Method: "GET", Path: "/sysParams/getSysParam", Description: "获取参数列表"}, + + {ApiGroup: "额度", Method: "GET", Path: "/gaia/quota/getManagementList", Description: "额度管理列表"}, + {ApiGroup: "额度", Method: "POST", Path: "/gaia/quota/setUserQuota", Description: "设置用户额度"}, + {ApiGroup: "测试", Method: "POST", Path: "/gaia/test/sync/database", Description: "同步数据库表数据"}, + {ApiGroup: "测试", Method: "GET", Path: "/gaia/test/app/request/batch", Description: "gaia应用请求测试批次列表"}, + {ApiGroup: "测试", Method: "POST", Path: "/gaia/test/app/request", Description: "发起gaia应用请求测试"}, + {ApiGroup: "测试", Method: "GET", Path: "/gaia/test/app/request/list", Description: "gaia应用请求测试结果列表"}, + {ApiGroup: "tenants表", Method: "GET", Path: "/tenants/getAllTenants", Description: "获取所有工作区"}, + {ApiGroup: "tenants表", Method: "GET", Path: "/tenants/getTenantsList", Description: "获取tenants表列表"}, + {ApiGroup: "tenants表", Method: "GET", Path: "/tenants/findTenants", Description: "根据ID获取tenants表"}, + {ApiGroup: "盖亚报表", Method: "GET", Path: "/gaia/dashboard/getAppTokenDailyQuotaData", Description: "获取每天密钥花费数据列表"}, + {ApiGroup: "盖亚报表", Method: "GET", Path: "/gaia/dashboard/getAppTokenQuotaRankingData", Description: "分页获取【应用密钥】配额排名数据列表"}, + {ApiGroup: "盖亚报表", Method: "GET", Path: "/gaia/dashboard/getAppQuotaRankingData", Description: "分页获取【应用】配额排名数据"}, + {ApiGroup: "盖亚报表", Method: "GET", Path: "/gaia/dashboard/getAccountQuotaRankingData", Description: "获取账户配额排名数据"}, + {ApiGroup: "系统用户", Method: "POST", Path: "/user/sync", Description: "同步用户列表"}, + + // Extend Start: system integration + {ApiGroup: "应用集成配置", Method: "GET", Path: "/gaia/system/dingtalk", Description: "获取钉钉系统配置"}, + {ApiGroup: "应用集成配置", Method: "POST", Path: "/gaia/system/dingtalk", Description: "设置钉钉系统配置"}, + // Extend Stop: system integration + } + if err := db.Create(&entities).Error; err != nil { + return ctx, errors.Wrap(err, sysModel.SysApi{}.TableName()+"表数据初始化失败!") + } + next := context.WithValue(ctx, i.InitializerName(), entities) + return next, nil +} + +func (i *initApi) DataInserted(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + if errors.Is(db.Where("path = ? AND method = ?", "/authorityBtn/canRemoveAuthorityBtn", "POST"). + First(&sysModel.SysApi{}).Error, gorm.ErrRecordNotFound) { + return false + } + return true +} diff --git a/admin/server/source/system/api_ignore.go b/admin/server/source/system/api_ignore.go new file mode 100644 index 000000000..284a1cc0c --- /dev/null +++ b/admin/server/source/system/api_ignore.go @@ -0,0 +1,77 @@ +package system + +import ( + "context" + sysModel "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/service/system" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type initApiIgnore struct{} + +const initOrderApiIgnore = initOrderApi + 1 + +// auto run +func init() { + system.RegisterInit(initOrderApiIgnore, &initApiIgnore{}) +} + +func (i initApiIgnore) InitializerName() string { + return sysModel.SysIgnoreApi{}.TableName() +} + +func (i *initApiIgnore) MigrateTable(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + return ctx, db.AutoMigrate(&sysModel.SysIgnoreApi{}) +} + +func (i *initApiIgnore) TableCreated(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + return db.Migrator().HasTable(&sysModel.SysIgnoreApi{}) +} + +func (i *initApiIgnore) InitializeData(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + entities := []sysModel.SysIgnoreApi{ + {Method: "GET", Path: "/swagger/*any"}, + {Method: "GET", Path: "/api/freshCasbin"}, + {Method: "GET", Path: "/uploads/file/*filepath"}, + {Method: "GET", Path: "/health"}, + {Method: "HEAD", Path: "/uploads/file/*filepath"}, + {Method: "POST", Path: "/autoCode/llmAuto"}, + {Method: "POST", Path: "/system/reloadSystem"}, + {Method: "POST", Path: "/base/login"}, + {Method: "POST", Path: "/base/captcha"}, + {Method: "POST", Path: "/init/initdb"}, + {Method: "POST", Path: "/init/checkdb"}, + {Method: "GET", Path: "/info/getInfoDataSource"}, + {Method: "GET", Path: "/info/getInfoPublic"}, + } + if err := db.Create(&entities).Error; err != nil { + return ctx, errors.Wrap(err, sysModel.SysIgnoreApi{}.TableName()+"表数据初始化失败!") + } + next := context.WithValue(ctx, i.InitializerName(), entities) + return next, nil +} + +func (i *initApiIgnore) DataInserted(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + if errors.Is(db.Where("path = ? AND method = ?", "/swagger/*any", "GET"). + First(&sysModel.SysIgnoreApi{}).Error, gorm.ErrRecordNotFound) { + return false + } + return true +} diff --git a/admin/server/source/system/authorities_menus.go b/admin/server/source/system/authorities_menus.go new file mode 100644 index 000000000..14f2ffd01 --- /dev/null +++ b/admin/server/source/system/authorities_menus.go @@ -0,0 +1,89 @@ +package system + +import ( + "context" + + sysModel "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/service/system" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +const initOrderMenuAuthority = initOrderMenu + initOrderAuthority + +type initMenuAuthority struct{} + +// auto run +func init() { + system.RegisterInit(initOrderMenuAuthority, &initMenuAuthority{}) +} + +func (i *initMenuAuthority) MigrateTable(ctx context.Context) (context.Context, error) { + return ctx, nil // do nothing +} + +func (i *initMenuAuthority) TableCreated(ctx context.Context) bool { + return false // always replace +} + +func (i initMenuAuthority) InitializerName() string { + return "sys_menu_authorities" +} + +func (i *initMenuAuthority) InitializeData(ctx context.Context) (next context.Context, err error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + authorities, ok := ctx.Value(initAuthority{}.InitializerName()).([]sysModel.SysAuthority) + if !ok { + return ctx, errors.Wrap(system.ErrMissingDependentContext, "创建 [菜单-权限] 关联失败, 未找到权限表初始化数据") + } + menus, ok := ctx.Value(initMenu{}.InitializerName()).([]sysModel.SysBaseMenu) + if !ok { + return next, errors.Wrap(errors.New(""), "创建 [菜单-权限] 关联失败, 未找到菜单表初始化数据") + } + next = ctx + // 888 + if err = db.Model(&authorities[0]).Association("SysBaseMenus").Replace(menus[31:38]); err != nil { + return next, err + } + if err = db.Model(&authorities[0]).Association("SysBaseMenus").Append(menus[2:5]); err != nil { + return next, err + } + if err = db.Model(&authorities[0]).Association("SysBaseMenus").Append(menus[8:9]); err != nil { + return next, err + } + + // 8881 + menu8881 := menus[:2] + menu8881 = append(menu8881, menus[7]) + if err = db.Model(&authorities[1]).Association("SysBaseMenus").Replace(menu8881); err != nil { + return next, err + } + + // 9528 + if err = db.Model(&authorities[2]).Association("SysBaseMenus").Replace(menus[:11]); err != nil { + return next, err + } + if err = db.Model(&authorities[2]).Association("SysBaseMenus").Append(menus[12:17]); err != nil { + return next, err + } + return next, nil +} + +func (i *initMenuAuthority) DataInserted(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + auth := &sysModel.SysAuthority{} + if ret := db.Model(auth). + Where("authority_id = ?", 9528).Preload("SysBaseMenus").Find(auth); ret != nil { + if ret.Error != nil { + return false + } + return len(auth.SysBaseMenus) > 0 + } + return false +} diff --git a/admin/server/source/system/authority.go b/admin/server/source/system/authority.go new file mode 100644 index 000000000..aac60d8c0 --- /dev/null +++ b/admin/server/source/system/authority.go @@ -0,0 +1,89 @@ +package system + +import ( + "context" + sysModel "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/service/system" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +const initOrderAuthority = initOrderCasbin + 1 + +type initAuthority struct{} + +// auto run +func init() { + system.RegisterInit(initOrderAuthority, &initAuthority{}) +} + +func (i *initAuthority) MigrateTable(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + return ctx, db.AutoMigrate(&sysModel.SysAuthority{}) +} + +func (i *initAuthority) TableCreated(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + return db.Migrator().HasTable(&sysModel.SysAuthority{}) +} + +func (i initAuthority) InitializerName() string { + return sysModel.SysAuthority{}.TableName() +} + +func (i *initAuthority) InitializeData(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + entities := []sysModel.SysAuthority{ + {AuthorityId: 888, AuthorityName: "超级管理员", ParentId: utils.Pointer[uint](0), DefaultRouter: "gaiaDashboard"}, + {AuthorityId: 1, AuthorityName: "普通用户", ParentId: utils.Pointer[uint](0), DefaultRouter: "dashboard"}, + {AuthorityId: 9528, AuthorityName: "测试角色", ParentId: utils.Pointer[uint](0), DefaultRouter: "dashboard"}, + {AuthorityId: 8881, AuthorityName: "超级管理员子角色", ParentId: utils.Pointer[uint](888), DefaultRouter: "dashboard"}, + } + + if err := db.Create(&entities).Error; err != nil { + return ctx, errors.Wrapf(err, "%s表数据初始化失败!", sysModel.SysAuthority{}.TableName()) + } + // data authority + if err := db.Model(&entities[0]).Association("DataAuthorityId").Replace( + []*sysModel.SysAuthority{ + {AuthorityId: 888}, + {AuthorityId: 9528}, + {AuthorityId: 8881}, + }); err != nil { + return ctx, errors.Wrapf(err, "%s表数据初始化失败!", + db.Model(&entities[0]).Association("DataAuthorityId").Relationship.JoinTable.Name) + } + if err := db.Model(&entities[1]).Association("DataAuthorityId").Replace( + []*sysModel.SysAuthority{ + {AuthorityId: 9528}, + {AuthorityId: 8881}, + }); err != nil { + return ctx, errors.Wrapf(err, "%s表数据初始化失败!", + db.Model(&entities[1]).Association("DataAuthorityId").Relationship.JoinTable.Name) + } + + next := context.WithValue(ctx, i.InitializerName(), entities) + return next, nil +} + +func (i *initAuthority) DataInserted(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + if errors.Is(db.Where("authority_id = ?", "8881"). + First(&sysModel.SysAuthority{}).Error, gorm.ErrRecordNotFound) { // 判断是否存在数据 + return false + } + return true +} diff --git a/admin/server/source/system/casbin.go b/admin/server/source/system/casbin.go new file mode 100644 index 000000000..5ff4269fe --- /dev/null +++ b/admin/server/source/system/casbin.go @@ -0,0 +1,309 @@ +package system + +import ( + "context" + + adapter "github.com/casbin/gorm-adapter/v3" + "github.com/flipped-aurora/gin-vue-admin/server/service/system" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +const initOrderCasbin = initOrderApiIgnore + 1 + +type initCasbin struct{} + +// auto run +func init() { + system.RegisterInit(initOrderCasbin, &initCasbin{}) +} + +func (i *initCasbin) MigrateTable(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + return ctx, db.AutoMigrate(&adapter.CasbinRule{}) +} + +func (i *initCasbin) TableCreated(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + return db.Migrator().HasTable(&adapter.CasbinRule{}) +} + +func (i initCasbin) InitializerName() string { + var entity adapter.CasbinRule + return entity.TableName() +} + +func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + entities := []adapter.CasbinRule{ + {Ptype: "p", V0: "888", V1: "/user/admin_register", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/api/createApi", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/api/getApiList", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/api/getApiById", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/api/deleteApi", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/api/updateApi", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/api/getAllApis", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/api/deleteApisByIds", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/api/syncApi", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/api/getApiGroups", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/api/enterSyncApi", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/api/ignoreApi", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/authority/copyAuthority", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/authority/updateAuthority", V2: "PUT"}, + {Ptype: "p", V0: "888", V1: "/authority/createAuthority", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/authority/deleteAuthority", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/authority/getAuthorityList", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/authority/setDataAuthority", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/menu/getMenu", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/menu/getMenuList", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/menu/addBaseMenu", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/menu/getBaseMenuTree", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/menu/addMenuAuthority", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/menu/getMenuAuthority", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/menu/deleteBaseMenu", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/menu/updateBaseMenu", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/menu/getBaseMenuById", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/user/getUserInfo", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/user/setUserInfo", V2: "PUT"}, + {Ptype: "p", V0: "888", V1: "/user/setSelfInfo", V2: "PUT"}, + {Ptype: "p", V0: "888", V1: "/user/getUserList", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/user/deleteUser", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/user/changePassword", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/user/setUserAuthority", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/user/setUserAuthorities", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/user/resetPassword", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/user/setSelfSetting", V2: "PUT"}, + + {Ptype: "p", V0: "888", V1: "/fileUploadAndDownload/findFile", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/fileUploadAndDownload/breakpointContinueFinish", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/fileUploadAndDownload/breakpointContinue", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/fileUploadAndDownload/removeChunk", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/fileUploadAndDownload/upload", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/fileUploadAndDownload/deleteFile", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/fileUploadAndDownload/editFileName", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/fileUploadAndDownload/getFileList", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/fileUploadAndDownload/importURL", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/casbin/updateCasbin", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/casbin/getPolicyPathByAuthorityId", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/jwt/jsonInBlacklist", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/system/getSystemConfig", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/system/setSystemConfig", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/system/getServerInfo", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/customer/customer", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/customer/customer", V2: "PUT"}, + {Ptype: "p", V0: "888", V1: "/customer/customer", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/customer/customer", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/customer/customerList", V2: "GET"}, + + {Ptype: "p", V0: "888", V1: "/autoCode/getDB", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/autoCode/getMeta", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/preview", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/getTables", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/autoCode/getColumn", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/autoCode/rollback", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/createTemp", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/delSysHistory", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/getSysHistory", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/createPackage", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/getTemplates", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/autoCode/getPackage", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/delPackage", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/createPlug", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/installPlugin", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/pubPlug", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/addFunc", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/findSysDictionaryDetail", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/updateSysDictionaryDetail", V2: "PUT"}, + {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/createSysDictionaryDetail", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/getSysDictionaryDetailList", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/deleteSysDictionaryDetail", V2: "DELETE"}, + + {Ptype: "p", V0: "888", V1: "/sysDictionary/findSysDictionary", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysDictionary/updateSysDictionary", V2: "PUT"}, + {Ptype: "p", V0: "888", V1: "/sysDictionary/getSysDictionaryList", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysDictionary/createSysDictionary", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/sysDictionary/deleteSysDictionary", V2: "DELETE"}, + + {Ptype: "p", V0: "888", V1: "/sysOperationRecord/findSysOperationRecord", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysOperationRecord/updateSysOperationRecord", V2: "PUT"}, + {Ptype: "p", V0: "888", V1: "/sysOperationRecord/createSysOperationRecord", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/sysOperationRecord/getSysOperationRecordList", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysOperationRecord/deleteSysOperationRecord", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/sysOperationRecord/deleteSysOperationRecordByIds", V2: "DELETE"}, + + {Ptype: "p", V0: "888", V1: "/email/emailTest", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/email/sendEmail", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/simpleUploader/upload", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/simpleUploader/checkFileMd5", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/simpleUploader/mergeFileMd5", V2: "GET"}, + + {Ptype: "p", V0: "888", V1: "/authorityBtn/setAuthorityBtn", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/authorityBtn/getAuthorityBtn", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/authorityBtn/canRemoveAuthorityBtn", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/sysExportTemplate/createSysExportTemplate", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/sysExportTemplate/deleteSysExportTemplate", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/sysExportTemplate/deleteSysExportTemplateByIds", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/sysExportTemplate/updateSysExportTemplate", V2: "PUT"}, + {Ptype: "p", V0: "888", V1: "/sysExportTemplate/findSysExportTemplate", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysExportTemplate/getSysExportTemplateList", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysExportTemplate/exportExcel", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysExportTemplate/exportTemplate", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysExportTemplate/importExcel", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/info/createInfo", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/info/deleteInfo", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/info/deleteInfoByIds", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/info/updateInfo", V2: "PUT"}, + {Ptype: "p", V0: "888", V1: "/info/findInfo", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/info/getInfoList", V2: "GET"}, + + {Ptype: "p", V0: "888", V1: "/sysParams/createSysParams", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/sysParams/deleteSysParams", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/sysParams/deleteSysParamsByIds", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/sysParams/updateSysParams", V2: "PUT"}, + {Ptype: "p", V0: "888", V1: "/sysParams/findSysParams", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysParams/getSysParamsList", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysParams/getSysParam", V2: "GET"}, + + {Ptype: "p", V0: "888", V1: "/gaia/quota/getManagementList", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/gaia/quota/setUserQuota", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/gaia/test/sync/database", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/gaia/test/app/request/batch", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/gaia/test/app/request", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/gaia/test/app/request/list", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/tenants/getAllTenants", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/tenants/getTenantsList", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/tenants/findTenants", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/gaia/dashboard/getAppTokenDailyQuotaData", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/gaia/dashboard/getAppTokenQuotaRankingData", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/gaia/dashboard/getAppQuotaRankingData", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/gaia/dashboard/getAccountQuotaRankingData", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/user/sync", V2: "POST"}, + + {Ptype: "p", V0: "8881", V1: "/user/admin_register", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/api/createApi", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/api/getApiList", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/api/getApiById", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/api/deleteApi", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/api/updateApi", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/api/getAllApis", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/authority/createAuthority", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/authority/deleteAuthority", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/authority/getAuthorityList", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/authority/setDataAuthority", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/menu/getMenu", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/menu/getMenuList", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/menu/addBaseMenu", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/menu/getBaseMenuTree", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/menu/addMenuAuthority", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/menu/getMenuAuthority", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/menu/deleteBaseMenu", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/menu/updateBaseMenu", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/menu/getBaseMenuById", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/user/changePassword", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/user/getUserList", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/user/setUserAuthority", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/fileUploadAndDownload/upload", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/fileUploadAndDownload/getFileList", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/fileUploadAndDownload/deleteFile", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/fileUploadAndDownload/editFileName", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/fileUploadAndDownload/importURL", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/casbin/updateCasbin", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/casbin/getPolicyPathByAuthorityId", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/jwt/jsonInBlacklist", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/system/getSystemConfig", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/system/setSystemConfig", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/customer/customer", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/customer/customer", V2: "PUT"}, + {Ptype: "p", V0: "8881", V1: "/customer/customer", V2: "DELETE"}, + {Ptype: "p", V0: "8881", V1: "/customer/customer", V2: "GET"}, + {Ptype: "p", V0: "8881", V1: "/customer/customerList", V2: "GET"}, + {Ptype: "p", V0: "8881", V1: "/user/getUserInfo", V2: "GET"}, + + {Ptype: "p", V0: "9528", V1: "/user/admin_register", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/api/createApi", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/api/getApiList", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/api/getApiById", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/api/deleteApi", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/api/updateApi", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/api/getAllApis", V2: "POST"}, + + {Ptype: "p", V0: "9528", V1: "/authority/createAuthority", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/authority/deleteAuthority", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/authority/getAuthorityList", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/authority/setDataAuthority", V2: "POST"}, + + {Ptype: "p", V0: "9528", V1: "/menu/getMenu", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/menu/getMenuList", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/menu/addBaseMenu", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/menu/getBaseMenuTree", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/menu/addMenuAuthority", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/menu/getMenuAuthority", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/menu/deleteBaseMenu", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/menu/updateBaseMenu", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/menu/getBaseMenuById", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/user/changePassword", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/user/getUserList", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/user/setUserAuthority", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/fileUploadAndDownload/upload", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/fileUploadAndDownload/getFileList", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/fileUploadAndDownload/deleteFile", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/fileUploadAndDownload/editFileName", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/fileUploadAndDownload/importURL", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/casbin/updateCasbin", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/casbin/getPolicyPathByAuthorityId", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/jwt/jsonInBlacklist", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/system/getSystemConfig", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/system/setSystemConfig", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/customer/customer", V2: "PUT"}, + {Ptype: "p", V0: "9528", V1: "/customer/customer", V2: "GET"}, + {Ptype: "p", V0: "9528", V1: "/customer/customer", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/customer/customer", V2: "DELETE"}, + {Ptype: "p", V0: "9528", V1: "/customer/customerList", V2: "GET"}, + {Ptype: "p", V0: "9528", V1: "/autoCode/createTemp", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/user/getUserInfo", V2: "GET"}, + + // Extend Start: system integration + {Ptype: "p", V0: "888", V1: "/gaia/system/dingtalk", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/gaia/system/dingtalk", V2: "POST"}, + // Extend Stop: system integration + } + if err := db.Create(&entities).Error; err != nil { + return ctx, errors.Wrap(err, "Casbin 表 ("+i.InitializerName()+") 数据初始化失败!") + } + next := context.WithValue(ctx, i.InitializerName(), entities) + return next, nil +} + +func (i *initCasbin) DataInserted(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + if errors.Is(db.Where(adapter.CasbinRule{Ptype: "p", V0: "9528", V1: "/user/getUserInfo", V2: "GET"}). + First(&adapter.CasbinRule{}).Error, gorm.ErrRecordNotFound) { // 判断是否存在数据 + return false + } + return true +} diff --git a/admin/server/source/system/dictionary.go b/admin/server/source/system/dictionary.go new file mode 100644 index 000000000..001496327 --- /dev/null +++ b/admin/server/source/system/dictionary.go @@ -0,0 +1,71 @@ +package system + +import ( + "context" + sysModel "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/service/system" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +const initOrderDict = initOrderCasbin + 1 + +type initDict struct{} + +// auto run +func init() { + system.RegisterInit(initOrderDict, &initDict{}) +} + +func (i *initDict) MigrateTable(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + return ctx, db.AutoMigrate(&sysModel.SysDictionary{}) +} + +func (i *initDict) TableCreated(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + return db.Migrator().HasTable(&sysModel.SysDictionary{}) +} + +func (i initDict) InitializerName() string { + return sysModel.SysDictionary{}.TableName() +} + +func (i *initDict) InitializeData(ctx context.Context) (next context.Context, err error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + True := true + entities := []sysModel.SysDictionary{ + {Name: "性别", Type: "gender", Status: &True, Desc: "性别字典"}, + {Name: "数据库int类型", Type: "int", Status: &True, Desc: "int类型对应的数据库类型"}, + {Name: "数据库时间日期类型", Type: "time.Time", Status: &True, Desc: "数据库时间日期类型"}, + {Name: "数据库浮点型", Type: "float64", Status: &True, Desc: "数据库浮点型"}, + {Name: "数据库字符串", Type: "string", Status: &True, Desc: "数据库字符串"}, + {Name: "数据库bool类型", Type: "bool", Status: &True, Desc: "数据库bool类型"}, + } + + if err = db.Create(&entities).Error; err != nil { + return ctx, errors.Wrap(err, sysModel.SysDictionary{}.TableName()+"表数据初始化失败!") + } + next = context.WithValue(ctx, i.InitializerName(), entities) + return next, nil +} + +func (i *initDict) DataInserted(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + if errors.Is(db.Where("type = ?", "bool").First(&sysModel.SysDictionary{}).Error, gorm.ErrRecordNotFound) { // 判断是否存在数据 + return false + } + return true +} diff --git a/admin/server/source/system/dictionary_detail.go b/admin/server/source/system/dictionary_detail.go new file mode 100644 index 000000000..3dea8b70f --- /dev/null +++ b/admin/server/source/system/dictionary_detail.go @@ -0,0 +1,121 @@ +package system + +import ( + "context" + "fmt" + sysModel "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/service/system" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +const initOrderDictDetail = initOrderDict + 1 + +type initDictDetail struct{} + +// auto run +func init() { + system.RegisterInit(initOrderDictDetail, &initDictDetail{}) +} + +func (i *initDictDetail) MigrateTable(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + return ctx, db.AutoMigrate(&sysModel.SysDictionaryDetail{}) +} + +func (i *initDictDetail) TableCreated(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + return db.Migrator().HasTable(&sysModel.SysDictionaryDetail{}) +} + +func (i initDictDetail) InitializerName() string { + return sysModel.SysDictionaryDetail{}.TableName() +} + +func (i *initDictDetail) InitializeData(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + dicts, ok := ctx.Value(initDict{}.InitializerName()).([]sysModel.SysDictionary) + if !ok { + return ctx, errors.Wrap(system.ErrMissingDependentContext, + fmt.Sprintf("未找到 %s 表初始化数据", sysModel.SysDictionary{}.TableName())) + } + True := true + dicts[0].SysDictionaryDetails = []sysModel.SysDictionaryDetail{ + {Label: "男", Value: "1", Status: &True, Sort: 1}, + {Label: "女", Value: "2", Status: &True, Sort: 2}, + } + + dicts[1].SysDictionaryDetails = []sysModel.SysDictionaryDetail{ + {Label: "smallint", Value: "1", Status: &True, Extend: "mysql", Sort: 1}, + {Label: "mediumint", Value: "2", Status: &True, Extend: "mysql", Sort: 2}, + {Label: "int", Value: "3", Status: &True, Extend: "mysql", Sort: 3}, + {Label: "bigint", Value: "4", Status: &True, Extend: "mysql", Sort: 4}, + {Label: "int2", Value: "5", Status: &True, Extend: "pgsql", Sort: 5}, + {Label: "int4", Value: "6", Status: &True, Extend: "pgsql", Sort: 6}, + {Label: "int6", Value: "7", Status: &True, Extend: "pgsql", Sort: 7}, + {Label: "int8", Value: "8", Status: &True, Extend: "pgsql", Sort: 8}, + } + + dicts[2].SysDictionaryDetails = []sysModel.SysDictionaryDetail{ + {Label: "date", Status: &True}, + {Label: "time", Value: "1", Status: &True, Extend: "mysql", Sort: 1}, + {Label: "year", Value: "2", Status: &True, Extend: "mysql", Sort: 2}, + {Label: "datetime", Value: "3", Status: &True, Extend: "mysql", Sort: 3}, + {Label: "timestamp", Value: "5", Status: &True, Extend: "mysql", Sort: 5}, + {Label: "timestamptz", Value: "6", Status: &True, Extend: "pgsql", Sort: 5}, + } + dicts[3].SysDictionaryDetails = []sysModel.SysDictionaryDetail{ + {Label: "float", Status: &True}, + {Label: "double", Value: "1", Status: &True, Extend: "mysql", Sort: 1}, + {Label: "decimal", Value: "2", Status: &True, Extend: "mysql", Sort: 2}, + {Label: "numeric", Value: "3", Status: &True, Extend: "pgsql", Sort: 3}, + {Label: "smallserial", Value: "4", Status: &True, Extend: "pgsql", Sort: 4}, + } + + dicts[4].SysDictionaryDetails = []sysModel.SysDictionaryDetail{ + {Label: "char", Status: &True}, + {Label: "varchar", Value: "1", Status: &True, Extend: "mysql", Sort: 1}, + {Label: "tinyblob", Value: "2", Status: &True, Extend: "mysql", Sort: 2}, + {Label: "tinytext", Value: "3", Status: &True, Extend: "mysql", Sort: 3}, + {Label: "text", Value: "4", Status: &True, Extend: "mysql", Sort: 4}, + {Label: "blob", Value: "5", Status: &True, Extend: "mysql", Sort: 5}, + {Label: "mediumblob", Value: "6", Status: &True, Extend: "mysql", Sort: 6}, + {Label: "mediumtext", Value: "7", Status: &True, Extend: "mysql", Sort: 7}, + {Label: "longblob", Value: "8", Status: &True, Extend: "mysql", Sort: 8}, + {Label: "longtext", Value: "9", Status: &True, Extend: "mysql", Sort: 9}, + } + + dicts[5].SysDictionaryDetails = []sysModel.SysDictionaryDetail{ + {Label: "tinyint", Value: "1", Extend: "mysql", Status: &True}, + {Label: "bool", Value: "2", Extend: "pgsql", Status: &True}, + } + for _, dict := range dicts { + if err := db.Model(&dict).Association("SysDictionaryDetails"). + Replace(dict.SysDictionaryDetails); err != nil { + return ctx, errors.Wrap(err, sysModel.SysDictionaryDetail{}.TableName()+"表数据初始化失败!") + } + } + return ctx, nil +} + +func (i *initDictDetail) DataInserted(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + var dict sysModel.SysDictionary + if err := db.Preload("SysDictionaryDetails"). + First(&dict, &sysModel.SysDictionary{Name: "数据库bool类型"}).Error; err != nil { + return false + } + return len(dict.SysDictionaryDetails) > 0 && dict.SysDictionaryDetails[0].Label == "tinyint" +} diff --git a/admin/server/source/system/excel_template.go b/admin/server/source/system/excel_template.go new file mode 100644 index 000000000..00f3ed5f1 --- /dev/null +++ b/admin/server/source/system/excel_template.go @@ -0,0 +1,75 @@ +package system + +import ( + "context" + sysModel "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/service/system" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type initExcelTemplate struct{} + +const initOrderExcelTemplate = initOrderDictDetail + 1 + +// auto run +func init() { + system.RegisterInit(initOrderExcelTemplate, &initExcelTemplate{}) +} + +func (i initExcelTemplate) InitializerName() string { + return "sys_export_templates" +} + +func (i *initExcelTemplate) MigrateTable(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + return ctx, db.AutoMigrate(&sysModel.SysExportTemplate{}) +} + +func (i *initExcelTemplate) TableCreated(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + return db.Migrator().HasTable(&sysModel.SysExportTemplate{}) +} + +func (i *initExcelTemplate) InitializeData(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + + entities := []sysModel.SysExportTemplate{ + { + Name: "api", + TableName: "sys_apis", + TemplateID: "api", + TemplateInfo: `{ +"path":"路径", +"method":"方法(大写)", +"description":"方法介绍", +"api_group":"方法分组" +}`, + }, + } + if err := db.Create(&entities).Error; err != nil { + return ctx, errors.Wrap(err, "sys_export_templates"+"表数据初始化失败!") + } + next := context.WithValue(ctx, i.InitializerName(), entities) + return next, nil +} + +func (i *initExcelTemplate) DataInserted(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + if errors.Is(db.First(&sysModel.SysExportTemplate{}).Error, gorm.ErrRecordNotFound) { + return false + } + return true +} diff --git a/admin/server/source/system/menu.go b/admin/server/source/system/menu.go new file mode 100644 index 000000000..9876f108c --- /dev/null +++ b/admin/server/source/system/menu.go @@ -0,0 +1,115 @@ +package system + +import ( + "context" + + . "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/service/system" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +const initOrderMenu = initOrderAuthority + 1 + +type initMenu struct{} + +// auto run +func init() { + system.RegisterInit(initOrderMenu, &initMenu{}) +} + +func (i initMenu) InitializerName() string { + return SysBaseMenu{}.TableName() +} + +func (i *initMenu) MigrateTable(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + return ctx, db.AutoMigrate( + &SysBaseMenu{}, + &SysBaseMenuParameter{}, + &SysBaseMenuBtn{}, + ) +} + +func (i *initMenu) TableCreated(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + m := db.Migrator() + return m.HasTable(&SysBaseMenu{}) && + m.HasTable(&SysBaseMenuParameter{}) && + m.HasTable(&SysBaseMenuBtn{}) +} + +func (i *initMenu) InitializeData(ctx context.Context) (next context.Context, err error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + entities := []SysBaseMenu{ + {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "dashboard", Name: "dashboard", Component: "view/dashboard/index.vue", Sort: 1, Meta: Meta{Title: "仪表盘", Icon: "odometer"}}, + {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "about", Name: "about", Component: "view/about/index.vue", Sort: 9, Meta: Meta{Title: "关于我们", Icon: "info-filled"}}, + {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "admin", Name: "superAdmin", Component: "view/superAdmin/index.vue", Sort: 3, Meta: Meta{Title: "超级管理员", Icon: "user"}}, + {MenuLevel: 0, Hidden: false, ParentId: 3, Path: "authority", Name: "authority", Component: "view/superAdmin/authority/authority.vue", Sort: 1, Meta: Meta{Title: "角色管理", Icon: "avatar"}}, + {MenuLevel: 0, Hidden: false, ParentId: 3, Path: "menu", Name: "menu", Component: "view/superAdmin/menu/menu.vue", Sort: 2, Meta: Meta{Title: "菜单管理", Icon: "tickets", KeepAlive: true}}, + {MenuLevel: 0, Hidden: false, ParentId: 3, Path: "api", Name: "api", Component: "view/superAdmin/api/api.vue", Sort: 3, Meta: Meta{Title: "api管理", Icon: "platform", KeepAlive: true}}, + {MenuLevel: 0, Hidden: false, ParentId: 3, Path: "user", Name: "user", Component: "view/superAdmin/user/user.vue", Sort: 4, Meta: Meta{Title: "用户管理", Icon: "coordinate"}}, + {MenuLevel: 0, Hidden: false, ParentId: 3, Path: "dictionary", Name: "dictionary", Component: "view/superAdmin/dictionary/sysDictionary.vue", Sort: 5, Meta: Meta{Title: "字典管理", Icon: "notebook"}}, + {MenuLevel: 0, Hidden: false, ParentId: 3, Path: "operation", Name: "operation", Component: "view/superAdmin/operation/sysOperationRecord.vue", Sort: 6, Meta: Meta{Title: "操作历史", Icon: "pie-chart"}}, + {MenuLevel: 0, Hidden: true, ParentId: 0, Path: "person", Name: "person", Component: "view/person/person.vue", Sort: 4, Meta: Meta{Title: "个人信息", Icon: "message"}}, + {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "example", Name: "example", Component: "view/example/index.vue", Sort: 7, Meta: Meta{Title: "示例文件", Icon: "management"}}, + {MenuLevel: 0, Hidden: false, ParentId: 11, Path: "upload", Name: "upload", Component: "view/example/upload/upload.vue", Sort: 5, Meta: Meta{Title: "媒体库(上传下载)", Icon: "upload"}}, + {MenuLevel: 0, Hidden: false, ParentId: 11, Path: "breakpoint", Name: "breakpoint", Component: "view/example/breakpoint/breakpoint.vue", Sort: 6, Meta: Meta{Title: "断点续传", Icon: "upload-filled"}}, + {MenuLevel: 0, Hidden: false, ParentId: 11, Path: "customer", Name: "customer", Component: "view/example/customer/customer.vue", Sort: 7, Meta: Meta{Title: "客户列表(资源示例)", Icon: "avatar"}}, + {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "systemTools", Name: "systemTools", Component: "view/systemTools/index.vue", Sort: 5, Meta: Meta{Title: "系统工具", Icon: "tools"}}, + {MenuLevel: 0, Hidden: false, ParentId: 15, Path: "autoCode", Name: "autoCode", Component: "view/systemTools/autoCode/index.vue", Sort: 1, Meta: Meta{Title: "代码生成器", Icon: "cpu", KeepAlive: true}}, + {MenuLevel: 0, Hidden: false, ParentId: 15, Path: "formCreate", Name: "formCreate", Component: "view/systemTools/formCreate/index.vue", Sort: 3, Meta: Meta{Title: "表单生成器", Icon: "magic-stick", KeepAlive: true}}, + {MenuLevel: 0, Hidden: false, ParentId: 15, Path: "system", Name: "system", Component: "view/systemTools/system/system.vue", Sort: 4, Meta: Meta{Title: "系统配置", Icon: "operation"}}, + {MenuLevel: 0, Hidden: false, ParentId: 15, Path: "autoCodeAdmin", Name: "autoCodeAdmin", Component: "view/systemTools/autoCodeAdmin/index.vue", Sort: 2, Meta: Meta{Title: "自动化代码管理", Icon: "magic-stick"}}, + {MenuLevel: 0, Hidden: true, ParentId: 15, Path: "autoCodeEdit/:id", Name: "autoCodeEdit", Component: "view/systemTools/autoCode/index.vue", Sort: 0, Meta: Meta{Title: "自动化代码-${id}", Icon: "magic-stick"}}, + {MenuLevel: 0, Hidden: false, ParentId: 15, Path: "autoPkg", Name: "autoPkg", Component: "view/systemTools/autoPkg/autoPkg.vue", Sort: 0, Meta: Meta{Title: "模板配置", Icon: "folder"}}, + {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "https://www.gin-vue-admin.com", Name: "https://www.gin-vue-admin.com", Component: "/", Sort: 0, Meta: Meta{Title: "官方网站", Icon: "customer-gva"}}, + {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "state", Name: "state", Component: "view/system/state.vue", Sort: 8, Meta: Meta{Title: "服务器状态", Icon: "cloudy"}}, + {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "plugin", Name: "plugin", Component: "view/routerHolder.vue", Sort: 6, Meta: Meta{Title: "插件系统", Icon: "cherry"}}, + {MenuLevel: 0, Hidden: false, ParentId: 24, Path: "https://plugin.gin-vue-admin.com/", Name: "https://plugin.gin-vue-admin.com/", Component: "https://plugin.gin-vue-admin.com/", Sort: 0, Meta: Meta{Title: "插件市场", Icon: "shop"}}, + {MenuLevel: 0, Hidden: false, ParentId: 24, Path: "installPlugin", Name: "installPlugin", Component: "view/systemTools/installPlugin/index.vue", Sort: 1, Meta: Meta{Title: "插件安装", Icon: "box"}}, + {MenuLevel: 0, Hidden: false, ParentId: 24, Path: "pubPlug", Name: "pubPlug", Component: "view/systemTools/pubPlug/pubPlug.vue", Sort: 3, Meta: Meta{Title: "打包插件", Icon: "files"}}, + {MenuLevel: 0, Hidden: false, ParentId: 24, Path: "plugin-email", Name: "plugin-email", Component: "plugin/email/view/index.vue", Sort: 4, Meta: Meta{Title: "邮件插件", Icon: "message"}}, + {MenuLevel: 0, Hidden: false, ParentId: 15, Path: "exportTemplate", Name: "exportTemplate", Component: "view/systemTools/exportTemplate/exportTemplate.vue", Sort: 5, Meta: Meta{Title: "导出模板", Icon: "reading"}}, + {MenuLevel: 0, Hidden: false, ParentId: 24, Path: "anInfo", Name: "anInfo", Component: "plugin/announcement/view/info.vue", Sort: 5, Meta: Meta{Title: "公告管理[示例]", Icon: "scaleToOriginal"}}, + {MenuLevel: 0, Hidden: false, ParentId: 3, Path: "sysParams", Name: "sysParams", Component: "view/superAdmin/params/sysParams.vue", Sort: 7, Meta: Meta{Title: "参数管理", Icon: "compass"}}, + // 二开部分 + {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "gaiaDashboard", Name: "gaiaDashboard", Component: "view/gaia/dashboard/index.vue", Sort: 0, Meta: Meta{Title: "费用报表", Icon: "odometer"}}, + {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "UserList", Name: "UserList", Component: "view/user/index.vue", Sort: 1, Meta: Meta{Title: "用户列表", Icon: "user"}}, + {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "QuotaList", Name: "QuotaList", Component: "view/quota/index.vue", Sort: 2, Meta: Meta{Title: "额度管理", Icon: "wallet"}}, + {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "SuperTest", Name: "SuperTest", Component: "view/test/index.vue", Sort: 2, Meta: Meta{Title: "测试管理", Icon: "management"}}, + {MenuLevel: 0, Hidden: false, ParentId: 36, Path: "AppRequestTestBatch", Name: "AppRequestTestBatch", Component: "view/test/appRequest/index.vue", Sort: 1, Meta: Meta{Title: "测试批次", Icon: "list"}}, + {MenuLevel: 0, Hidden: true, ParentId: 36, Path: "AppRequestTestList", Name: "AppRequestTestList", Component: "view/test/appRequest/list.vue", Sort: 1, Meta: Meta{Title: "测试列表", Icon: "list"}}, + // Extend Start: system integration + {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "SystemIntegrated", Name: "SystemIntegrated", Component: "view/systemIntegrated/index.vue", Sort: 1, Meta: Meta{Title: "系统集成", Icon: "box"}}, + {MenuLevel: 0, Hidden: false, ParentId: 39, Path: "IntegratedDingTalk", Name: "IntegratedDingTalk", Component: "view/systemIntegrated/dingTalk/index.vue", Sort: 1, Meta: Meta{Title: "钉钉", Icon: "turn-off"}}, + // Extend Stop: system integration + + // 二开部分 + } + if err = db.Create(&entities).Error; err != nil { + return ctx, errors.Wrap(err, SysBaseMenu{}.TableName()+"表数据初始化失败!") + } + next = context.WithValue(ctx, i.InitializerName(), entities) + return next, nil +} + +func (i *initMenu) DataInserted(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + if errors.Is(db.Where("path = ?", "autoPkg").First(&SysBaseMenu{}).Error, gorm.ErrRecordNotFound) { // 判断是否存在数据 + return false + } + return true +} diff --git a/admin/server/source/system/user.go b/admin/server/source/system/user.go new file mode 100644 index 000000000..a01b2447e --- /dev/null +++ b/admin/server/source/system/user.go @@ -0,0 +1,108 @@ +package system + +import ( + "context" + sysModel "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/service/system" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/gofrs/uuid/v5" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +const initOrderUser = initOrderAuthority + 1 + +type initUser struct{} + +// auto run +func init() { + system.RegisterInit(initOrderUser, &initUser{}) +} + +func (i *initUser) MigrateTable(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + return ctx, db.AutoMigrate(&sysModel.SysUser{}) +} + +func (i *initUser) TableCreated(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + return db.Migrator().HasTable(&sysModel.SysUser{}) +} + +func (i initUser) InitializerName() string { + return sysModel.SysUser{}.TableName() +} + +func (i *initUser) InitializeData(ctx context.Context) (next context.Context, err error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + + ap := ctx.Value("adminPassword") + apStr, ok := ap.(string) + if !ok { + apStr = "123456" + } + + password := utils.BcryptHash(apStr) + adminPassword := utils.BcryptHash(apStr) + + entities := []sysModel.SysUser{ + { + UUID: uuid.Must(uuid.NewV4()), + Username: "admin", + Password: adminPassword, + NickName: "Mr.奇淼", + HeaderImg: "https://qmplusimg.henrongyi.top/gva_header.jpg", + AuthorityId: 888, + Phone: "17611111111", + Email: "333333333@qq.com", + }, + { + UUID: uuid.Must(uuid.NewV4()), + Username: "a303176530", + Password: password, + NickName: "用户1", + HeaderImg: "https:///qmplusimg.henrongyi.top/1572075907logo.png", + AuthorityId: 9528, + Phone: "17611111111", + Email: "333333333@qq.com"}, + } + next = context.WithValue(ctx, i.InitializerName(), entities) + return + if err = db.Create(&entities).Error; err != nil { + return ctx, errors.Wrap(err, sysModel.SysUser{}.TableName()+"表数据初始化失败!") + } + next = context.WithValue(ctx, i.InitializerName(), entities) + authorityEntities, ok := ctx.Value(initAuthority{}.InitializerName()).([]sysModel.SysAuthority) + if !ok { + return next, errors.Wrap(system.ErrMissingDependentContext, "创建 [用户-权限] 关联失败, 未找到权限表初始化数据") + } + if err = db.Model(&entities[0]).Association("Authorities").Replace(authorityEntities); err != nil { + return next, err + } + if err = db.Model(&entities[1]).Association("Authorities").Replace(authorityEntities[:1]); err != nil { + return next, err + } + return next, err +} + +func (i *initUser) DataInserted(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + var record sysModel.SysUser + if errors.Is(db.Where("username = ?", "a303176530"). + Preload("Authorities").First(&record).Error, gorm.ErrRecordNotFound) { // 判断是否存在数据 + return false + } + return len(record.Authorities) > 0 && record.Authorities[0].AuthorityId == 888 +} diff --git a/admin/server/task/clearTable.go b/admin/server/task/clearTable.go new file mode 100644 index 000000000..8d1e2f2f6 --- /dev/null +++ b/admin/server/task/clearTable.go @@ -0,0 +1,51 @@ +package task + +import ( + "errors" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/model/common" + "time" + + "gorm.io/gorm" +) + +//@author: [songzhibin97](https://github.com/songzhibin97) +//@function: ClearTable +//@description: 清理数据库表数据 +//@param: db(数据库对象) *gorm.DB, tableName(表名) string, compareField(比较字段) string, interval(间隔) string +//@return: error + +func ClearTable(db *gorm.DB) error { + var ClearTableDetail []common.ClearDB + + ClearTableDetail = append(ClearTableDetail, common.ClearDB{ + TableName: "sys_operation_records", + CompareField: "created_at", + Interval: "2160h", + }) + + ClearTableDetail = append(ClearTableDetail, common.ClearDB{ + TableName: "jwt_blacklists", + CompareField: "created_at", + Interval: "168h", + }) + + if db == nil { + return errors.New("db Cannot be empty") + } + + for _, detail := range ClearTableDetail { + duration, err := time.ParseDuration(detail.Interval) + if err != nil { + return err + } + if duration < 0 { + return errors.New("parse duration < 0") + } + err = db.Debug().Exec(fmt.Sprintf("DELETE FROM %s WHERE %s < ?", detail.TableName, detail.CompareField), time.Now().Add(-duration)).Error + if err != nil { + return err + } + } + return nil +} diff --git a/admin/server/utils/ast/ast.go b/admin/server/utils/ast/ast.go new file mode 100644 index 000000000..b6f85d6d0 --- /dev/null +++ b/admin/server/utils/ast/ast.go @@ -0,0 +1,231 @@ +package ast + +import ( + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "go/ast" + "go/parser" + "go/token" + "log" +) + +// AddImport 增加 import 方法 +func AddImport(astNode ast.Node, imp string) { + impStr := fmt.Sprintf("\"%s\"", imp) + ast.Inspect(astNode, func(node ast.Node) bool { + if genDecl, ok := node.(*ast.GenDecl); ok { + if genDecl.Tok == token.IMPORT { + for i := range genDecl.Specs { + if impNode, ok := genDecl.Specs[i].(*ast.ImportSpec); ok { + if impNode.Path.Value == impStr { + return false + } + } + } + genDecl.Specs = append(genDecl.Specs, &ast.ImportSpec{ + Path: &ast.BasicLit{ + Kind: token.STRING, + Value: impStr, + }, + }) + } + } + return true + }) +} + +// FindFunction 查询特定function方法 +func FindFunction(astNode ast.Node, FunctionName string) *ast.FuncDecl { + var funcDeclP *ast.FuncDecl + ast.Inspect(astNode, func(node ast.Node) bool { + if funcDecl, ok := node.(*ast.FuncDecl); ok { + if funcDecl.Name.String() == FunctionName { + funcDeclP = funcDecl + return false + } + } + return true + }) + return funcDeclP +} + +// FindArray 查询特定数组方法 +func FindArray(astNode ast.Node, identName, selectorExprName string) *ast.CompositeLit { + var assignStmt *ast.CompositeLit + ast.Inspect(astNode, func(n ast.Node) bool { + switch node := n.(type) { + case *ast.AssignStmt: + for _, expr := range node.Rhs { + if exprType, ok := expr.(*ast.CompositeLit); ok { + if arrayType, ok := exprType.Type.(*ast.ArrayType); ok { + sel, ok1 := arrayType.Elt.(*ast.SelectorExpr) + x, ok2 := sel.X.(*ast.Ident) + if ok1 && ok2 && x.Name == identName && sel.Sel.Name == selectorExprName { + assignStmt = exprType + return false + } + } + } + } + } + return true + }) + return assignStmt +} + +func CreateMenuStructAst(menus []system.SysBaseMenu) *[]ast.Expr { + var menuElts []ast.Expr + for i := range menus { + elts := []ast.Expr{ // 结构体的字段 + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "ParentId"}, + Value: &ast.BasicLit{Kind: token.INT, Value: "0"}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Path"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", menus[i].Path)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Name"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", menus[i].Name)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Hidden"}, + Value: &ast.Ident{Name: "false"}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Component"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", menus[i].Component)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Sort"}, + Value: &ast.BasicLit{Kind: token.INT, Value: fmt.Sprintf("%d", menus[i].Sort)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Meta"}, + Value: &ast.CompositeLit{ + Type: &ast.SelectorExpr{ + X: &ast.Ident{Name: "model"}, + Sel: &ast.Ident{Name: "Meta"}, + }, + Elts: []ast.Expr{ + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Title"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", menus[i].Title)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Icon"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", menus[i].Icon)}, + }, + }, + }, + }, + } + menuElts = append(menuElts, &ast.CompositeLit{ + Type: nil, + Elts: elts, + }) + } + return &menuElts +} + +func CreateApiStructAst(apis []system.SysApi) *[]ast.Expr { + var apiElts []ast.Expr + for i := range apis { + elts := []ast.Expr{ // 结构体的字段 + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Path"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", apis[i].Path)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Description"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", apis[i].Description)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "ApiGroup"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", apis[i].ApiGroup)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Method"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", apis[i].Method)}, + }, + } + apiElts = append(apiElts, &ast.CompositeLit{ + Type: nil, + Elts: elts, + }) + } + return &apiElts +} + +// 检查是否存在Import +func CheckImport(file *ast.File, importPath string) bool { + for _, imp := range file.Imports { + // Remove quotes around the import path + path := imp.Path.Value[1 : len(imp.Path.Value)-1] + + if path == importPath { + return true + } + } + + return false +} + +func clearPosition(astNode ast.Node) { + ast.Inspect(astNode, func(n ast.Node) bool { + switch node := n.(type) { + case *ast.Ident: + // 清除位置信息 + node.NamePos = token.NoPos + case *ast.CallExpr: + // 清除位置信息 + node.Lparen = token.NoPos + node.Rparen = token.NoPos + case *ast.BasicLit: + // 清除位置信息 + node.ValuePos = token.NoPos + case *ast.SelectorExpr: + // 清除位置信息 + node.Sel.NamePos = token.NoPos + case *ast.BinaryExpr: + node.OpPos = token.NoPos + case *ast.UnaryExpr: + node.OpPos = token.NoPos + case *ast.StarExpr: + node.Star = token.NoPos + } + return true + }) +} + +func CreateStmt(statement string) *ast.ExprStmt { + expr, err := parser.ParseExpr(statement) + if err != nil { + log.Fatal(err) + } + clearPosition(expr) + return &ast.ExprStmt{X: expr} +} + +func IsBlockStmt(node ast.Node) bool { + _, ok := node.(*ast.BlockStmt) + return ok +} + +func VariableExistsInBlock(block *ast.BlockStmt, varName string) bool { + exists := false + ast.Inspect(block, func(n ast.Node) bool { + switch node := n.(type) { + case *ast.AssignStmt: + for _, expr := range node.Lhs { + if ident, ok := expr.(*ast.Ident); ok && ident.Name == varName { + exists = true + return false + } + } + } + return true + }) + return exists +} diff --git a/admin/server/utils/ast/ast_auto_enter.go b/admin/server/utils/ast/ast_auto_enter.go new file mode 100644 index 000000000..382f554e8 --- /dev/null +++ b/admin/server/utils/ast/ast_auto_enter.go @@ -0,0 +1,47 @@ +package ast + +import ( + "bytes" + "fmt" + "go/ast" + "go/parser" + "go/printer" + "go/token" + "os" +) + +func ImportForAutoEnter(path string, funcName string, code string) { + src, err := os.ReadFile(path) + if err != nil { + fmt.Println(err) + } + fileSet := token.NewFileSet() + astFile, err := parser.ParseFile(fileSet, "", src, 0) + ast.Inspect(astFile, func(node ast.Node) bool { + if typeSpec, ok := node.(*ast.TypeSpec); ok { + if typeSpec.Name.Name == funcName { + if st, ok := typeSpec.Type.(*ast.StructType); ok { + for i := range st.Fields.List { + if t, ok := st.Fields.List[i].Type.(*ast.Ident); ok { + if t.Name == code { + return false + } + } + } + sn := &ast.Field{ + Type: &ast.Ident{Name: code}, + } + st.Fields.List = append(st.Fields.List, sn) + } + } + } + return true + }) + var out []byte + bf := bytes.NewBuffer(out) + err = printer.Fprint(bf, fileSet, astFile) + if err != nil { + return + } + _ = os.WriteFile(path, bf.Bytes(), 0666) +} diff --git a/admin/server/utils/ast/ast_enter.go b/admin/server/utils/ast/ast_enter.go new file mode 100644 index 000000000..7a5c72745 --- /dev/null +++ b/admin/server/utils/ast/ast_enter.go @@ -0,0 +1,181 @@ +package ast + +import ( + "bytes" + "go/ast" + "go/format" + "go/parser" + "go/token" + "golang.org/x/text/cases" + "golang.org/x/text/language" + "log" + "os" + "strconv" + "strings" +) + +type Visitor struct { + ImportCode string + StructName string + PackageName string + GroupName string +} + +func (vi *Visitor) Visit(node ast.Node) ast.Visitor { + switch n := node.(type) { + case *ast.GenDecl: + // 查找有没有import context包 + // Notice:没有考虑没有import任何包的情况 + if n.Tok == token.IMPORT && vi.ImportCode != "" { + vi.addImport(n) + // 不需要再遍历子树 + return nil + } + if n.Tok == token.TYPE && vi.StructName != "" && vi.PackageName != "" && vi.GroupName != "" { + vi.addStruct(n) + return nil + } + case *ast.FuncDecl: + if n.Name.Name == "Routers" { + vi.addFuncBodyVar(n) + return nil + } + + } + return vi +} + +func (vi *Visitor) addStruct(genDecl *ast.GenDecl) ast.Visitor { + for i := range genDecl.Specs { + switch n := genDecl.Specs[i].(type) { + case *ast.TypeSpec: + if strings.Index(n.Name.Name, "Group") > -1 { + switch t := n.Type.(type) { + case *ast.StructType: + f := &ast.Field{ + Names: []*ast.Ident{ + { + Name: vi.StructName, + Obj: &ast.Object{ + Kind: ast.Var, + Name: vi.StructName, + }, + }, + }, + Type: &ast.SelectorExpr{ + X: &ast.Ident{ + Name: vi.PackageName, + }, + Sel: &ast.Ident{ + Name: vi.GroupName, + }, + }, + } + t.Fields.List = append(t.Fields.List, f) + } + } + } + } + return vi +} + +func (vi *Visitor) addImport(genDecl *ast.GenDecl) ast.Visitor { + // 是否已经import + hasImported := false + for _, v := range genDecl.Specs { + importSpec := v.(*ast.ImportSpec) + // 如果已经包含 + if importSpec.Path.Value == strconv.Quote(vi.ImportCode) { + hasImported = true + } + } + if !hasImported { + genDecl.Specs = append(genDecl.Specs, &ast.ImportSpec{ + Path: &ast.BasicLit{ + Kind: token.STRING, + Value: strconv.Quote(vi.ImportCode), + }, + }) + } + return vi +} + +func (vi *Visitor) addFuncBodyVar(funDecl *ast.FuncDecl) ast.Visitor { + hasVar := false + for _, v := range funDecl.Body.List { + switch varSpec := v.(type) { + case *ast.AssignStmt: + for i := range varSpec.Lhs { + switch nn := varSpec.Lhs[i].(type) { + case *ast.Ident: + if nn.Name == vi.PackageName+"Router" { + hasVar = true + } + } + } + } + } + if !hasVar { + assignStmt := &ast.AssignStmt{ + Lhs: []ast.Expr{ + &ast.Ident{ + Name: vi.PackageName + "Router", + Obj: &ast.Object{ + Kind: ast.Var, + Name: vi.PackageName + "Router", + }, + }, + }, + Tok: token.DEFINE, + Rhs: []ast.Expr{ + &ast.SelectorExpr{ + X: &ast.SelectorExpr{ + X: &ast.Ident{ + Name: "router", + }, + Sel: &ast.Ident{ + Name: "RouterGroupApp", + }, + }, + Sel: &ast.Ident{ + Name: cases.Title(language.English).String(vi.PackageName), + }, + }, + }, + } + funDecl.Body.List = append(funDecl.Body.List, funDecl.Body.List[1]) + index := 1 + copy(funDecl.Body.List[index+1:], funDecl.Body.List[index:]) + funDecl.Body.List[index] = assignStmt + } + return vi +} + +func ImportReference(filepath, importCode, structName, packageName, groupName string) error { + fSet := token.NewFileSet() + fParser, err := parser.ParseFile(fSet, filepath, nil, parser.ParseComments) + if err != nil { + return err + } + importCode = strings.TrimSpace(importCode) + v := &Visitor{ + ImportCode: importCode, + StructName: structName, + PackageName: packageName, + GroupName: groupName, + } + if importCode == "" { + ast.Print(fSet, fParser) + } + + ast.Walk(v, fParser) + + var output []byte + buffer := bytes.NewBuffer(output) + err = format.Node(buffer, fSet, fParser) + if err != nil { + log.Fatal(err) + } + // 写回数据 + return os.WriteFile(filepath, buffer.Bytes(), 0o600) +} diff --git a/admin/server/utils/ast/ast_gorm.go b/admin/server/utils/ast/ast_gorm.go new file mode 100644 index 000000000..d9c1beb3e --- /dev/null +++ b/admin/server/utils/ast/ast_gorm.go @@ -0,0 +1,166 @@ +package ast + +import ( + "bytes" + "fmt" + "go/ast" + "go/parser" + "go/printer" + "go/token" + "os" +) + +// 自动为 gorm.go 注册一个自动迁移 +func AddRegisterTablesAst(path, funcName, pk, varName, dbName, model string) { + modelPk := fmt.Sprintf("github.com/flipped-aurora/gin-vue-admin/server/model/%s", pk) + src, err := os.ReadFile(path) + if err != nil { + fmt.Println(err) + } + fileSet := token.NewFileSet() + astFile, err := parser.ParseFile(fileSet, "", src, 0) + if err != nil { + fmt.Println(err) + } + AddImport(astFile, modelPk) + FuncNode := FindFunction(astFile, funcName) + if FuncNode != nil { + ast.Print(fileSet, FuncNode) + } + addDBVar(FuncNode.Body, varName, dbName) + addAutoMigrate(FuncNode.Body, varName, pk, model) + var out []byte + bf := bytes.NewBuffer(out) + printer.Fprint(bf, fileSet, astFile) + + os.WriteFile(path, bf.Bytes(), 0666) +} + +// 增加一个 db库变量 +func addDBVar(astBody *ast.BlockStmt, varName, dbName string) { + if dbName == "" { + return + } + dbStr := fmt.Sprintf("\"%s\"", dbName) + + for i := range astBody.List { + if assignStmt, ok := astBody.List[i].(*ast.AssignStmt); ok { + if ident, ok := assignStmt.Lhs[0].(*ast.Ident); ok { + if ident.Name == varName { + return + } + } + } + } + assignNode := &ast.AssignStmt{ + Lhs: []ast.Expr{ + &ast.Ident{ + Name: varName, + }, + }, + Tok: token.DEFINE, + Rhs: []ast.Expr{ + &ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: &ast.Ident{ + Name: "global", + }, + Sel: &ast.Ident{ + Name: "GetGlobalDBByDBName", + }, + }, + Args: []ast.Expr{ + &ast.BasicLit{ + Kind: token.STRING, + Value: dbStr, + }, + }, + }, + }, + } + astBody.List = append([]ast.Stmt{assignNode}, astBody.List...) +} + +// 为db库变量增加 AutoMigrate 方法 +func addAutoMigrate(astBody *ast.BlockStmt, dbname string, pk string, model string) { + if dbname == "" { + dbname = "db" + } + flag := true + ast.Inspect(astBody, func(node ast.Node) bool { + // 首先判断需要加入的方法调用语句是否存在 不存在则直接走到下方逻辑 + switch n := node.(type) { + case *ast.CallExpr: + // 判断是否找到了AutoMigrate语句 + if s, ok := n.Fun.(*ast.SelectorExpr); ok { + if x, ok := s.X.(*ast.Ident); ok { + if s.Sel.Name == "AutoMigrate" && x.Name == dbname { + flag = false + if !NeedAppendModel(n, pk, model) { + return false + } + // 判断已经找到了AutoMigrate语句 + n.Args = append(n.Args, &ast.CompositeLit{ + Type: &ast.SelectorExpr{ + X: &ast.Ident{ + Name: pk, + }, + Sel: &ast.Ident{ + Name: model, + }, + }, + }) + return false + } + } + } + } + return true + //然后判断 pk.model是否存在 如果存在直接跳出 如果不存在 则向已经找到的方法调用语句的node里面push一条 + }) + + if flag { + exprStmt := &ast.ExprStmt{ + X: &ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: &ast.Ident{ + Name: dbname, + }, + Sel: &ast.Ident{ + Name: "AutoMigrate", + }, + }, + Args: []ast.Expr{ + &ast.CompositeLit{ + Type: &ast.SelectorExpr{ + X: &ast.Ident{ + Name: pk, + }, + Sel: &ast.Ident{ + Name: model, + }, + }, + }, + }, + }} + astBody.List = append(astBody.List, exprStmt) + } +} + +// 为automigrate增加实参 +func NeedAppendModel(callNode ast.Node, pk string, model string) bool { + flag := true + ast.Inspect(callNode, func(node ast.Node) bool { + switch n := node.(type) { + case *ast.SelectorExpr: + if x, ok := n.X.(*ast.Ident); ok { + if n.Sel.Name == model && x.Name == pk { + flag = false + return false + } + } + } + return true + }) + return flag +} diff --git a/admin/server/utils/ast/ast_init_test.go b/admin/server/utils/ast/ast_init_test.go new file mode 100644 index 000000000..ec6bd9b5e --- /dev/null +++ b/admin/server/utils/ast/ast_init_test.go @@ -0,0 +1,11 @@ +package ast + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "path/filepath" +) + +func init() { + global.GVA_CONFIG.AutoCode.Root, _ = filepath.Abs("../../../") + global.GVA_CONFIG.AutoCode.Server = "server" +} diff --git a/admin/server/utils/ast/ast_rollback.go b/admin/server/utils/ast/ast_rollback.go new file mode 100644 index 000000000..daa84226f --- /dev/null +++ b/admin/server/utils/ast/ast_rollback.go @@ -0,0 +1,173 @@ +package ast + +import ( + "bytes" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "go/ast" + "go/parser" + "go/printer" + "go/token" + "os" + "path/filepath" +) + +func RollBackAst(pk, model string) { + RollGormBack(pk, model) + RollRouterBack(pk, model) +} + +func RollGormBack(pk, model string) { + + // 首先分析存在多少个ttt作为调用方的node块 + // 如果多个 仅仅删除对应块即可 + // 如果单个 那么还需要剔除import + path := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go") + src, err := os.ReadFile(path) + if err != nil { + fmt.Println(err) + } + fileSet := token.NewFileSet() + astFile, err := parser.ParseFile(fileSet, "", src, 0) + if err != nil { + fmt.Println(err) + } + var n *ast.CallExpr + var k int = -1 + var pkNum = 0 + ast.Inspect(astFile, func(node ast.Node) bool { + if node, ok := node.(*ast.CallExpr); ok { + for i := range node.Args { + pkOK := false + modelOK := false + ast.Inspect(node.Args[i], func(item ast.Node) bool { + if ii, ok := item.(*ast.Ident); ok { + if ii.Name == pk { + pkOK = true + pkNum++ + } + if ii.Name == model { + modelOK = true + } + } + if pkOK && modelOK { + n = node + k = i + } + return true + }) + } + } + return true + }) + if k > -1 { + n.Args = append(append([]ast.Expr{}, n.Args[:k]...), n.Args[k+1:]...) + } + if pkNum == 1 { + var imI int = -1 + var gp *ast.GenDecl + ast.Inspect(astFile, func(node ast.Node) bool { + if gen, ok := node.(*ast.GenDecl); ok { + for i := range gen.Specs { + if imspec, ok := gen.Specs[i].(*ast.ImportSpec); ok { + if imspec.Path.Value == "\"github.com/flipped-aurora/gin-vue-admin/server/model/"+pk+"\"" { + gp = gen + imI = i + return false + } + } + } + } + return true + }) + + if imI > -1 { + gp.Specs = append(append([]ast.Spec{}, gp.Specs[:imI]...), gp.Specs[imI+1:]...) + } + } + + var out []byte + bf := bytes.NewBuffer(out) + printer.Fprint(bf, fileSet, astFile) + os.Remove(path) + os.WriteFile(path, bf.Bytes(), 0666) + +} + +func RollRouterBack(pk, model string) { + + // 首先抓到所有的代码块结构 {} + // 分析结构中是否存在一个变量叫做 pk+Router + // 然后获取到代码块指针 对内部需要回滚的代码进行剔除 + path := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "router_biz.go") + src, err := os.ReadFile(path) + if err != nil { + fmt.Println(err) + } + fileSet := token.NewFileSet() + astFile, err := parser.ParseFile(fileSet, "", src, 0) + if err != nil { + fmt.Println(err) + } + + var block *ast.BlockStmt + var routerStmt *ast.FuncDecl + + ast.Inspect(astFile, func(node ast.Node) bool { + if n, ok := node.(*ast.FuncDecl); ok { + if n.Name.Name == "initBizRouter" { + routerStmt = n + } + } + + if n, ok := node.(*ast.BlockStmt); ok { + ast.Inspect(n, func(bNode ast.Node) bool { + if in, ok := bNode.(*ast.Ident); ok { + if in.Name == pk+"Router" { + block = n + return false + } + } + return true + }) + return true + } + return true + }) + var k int + for i := range block.List { + if stmtNode, ok := block.List[i].(*ast.ExprStmt); ok { + ast.Inspect(stmtNode, func(node ast.Node) bool { + if n, ok := node.(*ast.Ident); ok { + if n.Name == "Init"+model+"Router" { + k = i + return false + } + } + return true + }) + } + } + + block.List = append(append([]ast.Stmt{}, block.List[:k]...), block.List[k+1:]...) + + if len(block.List) == 1 { + // 说明这个块就没任何意义了 + block.List = nil + } + + for i, n := range routerStmt.Body.List { + if n, ok := n.(*ast.BlockStmt); ok { + if n.List == nil { + routerStmt.Body.List = append(append([]ast.Stmt{}, routerStmt.Body.List[:i]...), routerStmt.Body.List[i+1:]...) + i-- + } + } + } + + var out []byte + bf := bytes.NewBuffer(out) + printer.Fprint(bf, fileSet, astFile) + os.Remove(path) + os.WriteFile(path, bf.Bytes(), 0666) +} diff --git a/admin/server/utils/ast/ast_router.go b/admin/server/utils/ast/ast_router.go new file mode 100644 index 000000000..86356b819 --- /dev/null +++ b/admin/server/utils/ast/ast_router.go @@ -0,0 +1,135 @@ +package ast + +import ( + "bytes" + "fmt" + "go/ast" + "go/parser" + "go/printer" + "go/token" + "os" + "strings" +) + +func AppendNodeToList(stmts []ast.Stmt, stmt ast.Stmt, index int) []ast.Stmt { + return append(stmts[:index], append([]ast.Stmt{stmt}, stmts[index:]...)...) +} + +func AddRouterCode(path, funcName, pk, model string) { + src, err := os.ReadFile(path) + if err != nil { + fmt.Println(err) + } + fileSet := token.NewFileSet() + astFile, err := parser.ParseFile(fileSet, "", src, parser.ParseComments) + + if err != nil { + fmt.Println(err) + } + + FuncNode := FindFunction(astFile, funcName) + + pkName := strings.ToUpper(pk[:1]) + pk[1:] + routerName := fmt.Sprintf("%sRouter", pk) + modelName := fmt.Sprintf("Init%sRouter", model) + var bloctPre *ast.BlockStmt + for i := len(FuncNode.Body.List) - 1; i >= 0; i-- { + if block, ok := FuncNode.Body.List[i].(*ast.BlockStmt); ok { + bloctPre = block + } + } + ast.Print(fileSet, FuncNode) + if ok, b := needAppendRouter(FuncNode, pk); ok { + routerNode := + &ast.BlockStmt{ + List: []ast.Stmt{ + &ast.AssignStmt{ + Lhs: []ast.Expr{ + &ast.Ident{Name: routerName}, + }, + Tok: token.DEFINE, + Rhs: []ast.Expr{ + &ast.SelectorExpr{ + X: &ast.SelectorExpr{ + X: &ast.Ident{Name: "router"}, + Sel: &ast.Ident{Name: "RouterGroupApp"}, + }, + Sel: &ast.Ident{Name: pkName}, + }, + }, + }, + }, + } + + FuncNode.Body.List = AppendNodeToList(FuncNode.Body.List, routerNode, len(FuncNode.Body.List)-1) + bloctPre = routerNode + } else { + bloctPre = b + } + + if needAppendInit(FuncNode, routerName, modelName) { + bloctPre.List = append(bloctPre.List, + &ast.ExprStmt{ + X: &ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: &ast.Ident{Name: routerName}, + Sel: &ast.Ident{Name: modelName}, + }, + Args: []ast.Expr{ + &ast.Ident{ + Name: "privateGroup", + }, + &ast.Ident{ + Name: "publicGroup", + }, + }, + }, + }) + } + var out []byte + bf := bytes.NewBuffer(out) + printer.Fprint(bf, fileSet, astFile) + os.WriteFile(path, bf.Bytes(), 0666) +} + +func needAppendRouter(funcNode ast.Node, pk string) (bool, *ast.BlockStmt) { + flag := true + var block *ast.BlockStmt + ast.Inspect(funcNode, func(node ast.Node) bool { + switch n := node.(type) { + case *ast.BlockStmt: + for i := range n.List { + if assignNode, ok := n.List[i].(*ast.AssignStmt); ok { + if identNode, ok := assignNode.Lhs[0].(*ast.Ident); ok { + if identNode.Name == fmt.Sprintf("%sRouter", pk) { + flag = false + block = n + return false + } + } + } + } + + } + return true + }) + return flag, block +} + +func needAppendInit(funcNode ast.Node, routerName string, modelName string) bool { + flag := true + ast.Inspect(funcNode, func(node ast.Node) bool { + switch n := funcNode.(type) { + case *ast.CallExpr: + if selectNode, ok := n.Fun.(*ast.SelectorExpr); ok { + x, xok := selectNode.X.(*ast.Ident) + if xok && x.Name == routerName && selectNode.Sel.Name == modelName { + flag = false + return false + } + } + } + return true + }) + return flag +} diff --git a/admin/server/utils/ast/ast_test.go b/admin/server/utils/ast/ast_test.go new file mode 100644 index 000000000..001f530f9 --- /dev/null +++ b/admin/server/utils/ast/ast_test.go @@ -0,0 +1,32 @@ +package ast + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "go/ast" + "go/parser" + "go/printer" + "go/token" + "os" + "path/filepath" + "testing" +) + +func TestAst(t *testing.T) { + filename := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "plugin.go") + fileSet := token.NewFileSet() + file, err := parser.ParseFile(fileSet, filename, nil, parser.ParseComments) + if err != nil { + t.Error(err) + return + } + err = ast.Print(fileSet, file) + if err != nil { + t.Error(err) + return + } + err = printer.Fprint(os.Stdout, token.NewFileSet(), file) + if err != nil { + panic(err) + } + +} diff --git a/admin/server/utils/ast/ast_type.go b/admin/server/utils/ast/ast_type.go new file mode 100644 index 000000000..c4e905eda --- /dev/null +++ b/admin/server/utils/ast/ast_type.go @@ -0,0 +1,53 @@ +package ast + +type Type string + +func (r Type) String() string { + return string(r) +} + +func (r Type) Group() string { + switch r { + case TypePackageApiEnter: + return "ApiGroup" + case TypePackageRouterEnter: + return "RouterGroup" + case TypePackageServiceEnter: + return "ServiceGroup" + case TypePackageApiModuleEnter: + return "ApiGroup" + case TypePackageRouterModuleEnter: + return "RouterGroup" + case TypePackageServiceModuleEnter: + return "ServiceGroup" + case TypePluginApiEnter: + return "api" + case TypePluginRouterEnter: + return "router" + case TypePluginServiceEnter: + return "service" + default: + return "" + } +} + +const ( + TypePackageApiEnter = "PackageApiEnter" // server/api/v1/enter.go + TypePackageRouterEnter = "PackageRouterEnter" // server/router/enter.go + TypePackageServiceEnter = "PackageServiceEnter" // server/service/enter.go + TypePackageApiModuleEnter = "PackageApiModuleEnter" // server/api/v1/{package}/enter.go + TypePackageRouterModuleEnter = "PackageRouterModuleEnter" // server/router/{package}/enter.go + TypePackageServiceModuleEnter = "PackageServiceModuleEnter" // server/service/{package}/enter.go + TypePackageInitializeGorm = "PackageInitializeGorm" // server/initialize/gorm_biz.go + TypePackageInitializeRouter = "PackageInitializeRouter" // server/initialize/router_biz.go + TypePluginGen = "PluginGen" // server/plugin/{package}/gen/main.go + TypePluginApiEnter = "PluginApiEnter" // server/plugin/{package}/enter.go + TypePluginInitializeV1 = "PluginInitializeV1" // server/initialize/plugin_biz_v1.go + TypePluginInitializeV2 = "PluginInitializeV2" // server/initialize/plugin_biz_v2.go + TypePluginRouterEnter = "PluginRouterEnter" // server/plugin/{package}/enter.go + TypePluginServiceEnter = "PluginServiceEnter" // server/plugin/{package}/enter.go + TypePluginInitializeApi = "PluginInitializeApi" // server/plugin/{package}/initialize/api.go + TypePluginInitializeGorm = "PluginInitializeGorm" // server/plugin/{package}/initialize/gorm.go + TypePluginInitializeMenu = "PluginInitializeMenu" // server/plugin/{package}/initialize/menu.go + TypePluginInitializeRouter = "PluginInitializeRouter" // server/plugin/{package}/initialize/router.go +) diff --git a/admin/server/utils/ast/import.go b/admin/server/utils/ast/import.go new file mode 100644 index 000000000..5de18a317 --- /dev/null +++ b/admin/server/utils/ast/import.go @@ -0,0 +1,94 @@ +package ast + +import ( + "go/ast" + "go/token" + "io" + "strings" +) + +type Import struct { + Base + ImportPath string // 导包路径 +} + +func NewImport(importPath string) *Import { + return &Import{ImportPath: importPath} +} + +func (a *Import) Parse(filename string, writer io.Writer) (file *ast.File, err error) { + return a.Base.Parse(filename, writer) +} + +func (a *Import) Rollback(file *ast.File) error { + if a.ImportPath == "" { + return nil + } + for i := 0; i < len(file.Decls); i++ { + v1, o1 := file.Decls[i].(*ast.GenDecl) + if o1 { + if v1.Tok != token.IMPORT { + break + } + for j := 0; j < len(v1.Specs); j++ { + v2, o2 := v1.Specs[j].(*ast.ImportSpec) + if o2 && strings.HasSuffix(a.ImportPath, v2.Path.Value) { + v1.Specs = append(v1.Specs[:j], v1.Specs[j+1:]...) + if len(v1.Specs) == 0 { + file.Decls = append(file.Decls[:i], file.Decls[i+1:]...) + } // 如果没有import声明,就删除, 如果不删除则会出现import() + break + } + } + } + } + return nil +} + +func (a *Import) Injection(file *ast.File) error { + if a.ImportPath == "" { + return nil + } + var has bool + for i := 0; i < len(file.Decls); i++ { + v1, o1 := file.Decls[i].(*ast.GenDecl) + if o1 { + if v1.Tok != token.IMPORT { + break + } + for j := 0; j < len(v1.Specs); j++ { + v2, o2 := v1.Specs[j].(*ast.ImportSpec) + if o2 && strings.HasSuffix(a.ImportPath, v2.Path.Value) { + has = true + break + } + } + if !has { + spec := &ast.ImportSpec{ + Path: &ast.BasicLit{Kind: token.STRING, Value: a.ImportPath}, + } + v1.Specs = append(v1.Specs, spec) + return nil + } + } + } + if !has { + decls := file.Decls + file.Decls = make([]ast.Decl, 0, len(file.Decls)+1) + decl := &ast.GenDecl{ + Tok: token.IMPORT, + Specs: []ast.Spec{ + &ast.ImportSpec{ + Path: &ast.BasicLit{Kind: token.STRING, Value: a.ImportPath}, + }, + }, + } + file.Decls = append(file.Decls, decl) + file.Decls = append(file.Decls, decls...) + } // 如果没有import声明,就创建一个, 主要要放在第一个 + return nil +} + +func (a *Import) Format(filename string, writer io.Writer, file *ast.File) error { + return a.Base.Format(filename, writer, file) +} diff --git a/admin/server/utils/ast/interfaces.go b/admin/server/utils/ast/interfaces.go new file mode 100644 index 000000000..33ecc4723 --- /dev/null +++ b/admin/server/utils/ast/interfaces.go @@ -0,0 +1,17 @@ +package ast + +import ( + "go/ast" + "io" +) + +type Ast interface { + // Parse 解析文件/代码 + Parse(filename string, writer io.Writer) (file *ast.File, err error) + // Rollback 回滚 + Rollback(file *ast.File) error + // Injection 注入 + Injection(file *ast.File) error + // Format 格式化输出 + Format(filename string, writer io.Writer, file *ast.File) error +} diff --git a/admin/server/utils/ast/interfaces_base.go b/admin/server/utils/ast/interfaces_base.go new file mode 100644 index 000000000..05cc7f779 --- /dev/null +++ b/admin/server/utils/ast/interfaces_base.go @@ -0,0 +1,76 @@ +package ast + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/pkg/errors" + "go/ast" + "go/format" + "go/parser" + "go/token" + "io" + "os" + "path" + "path/filepath" + "strings" +) + +type Base struct{} + +func (a *Base) Parse(filename string, writer io.Writer) (file *ast.File, err error) { + fileSet := token.NewFileSet() + if writer != nil { + file, err = parser.ParseFile(fileSet, filename, nil, parser.ParseComments) + } else { + file, err = parser.ParseFile(fileSet, filename, writer, parser.ParseComments) + } + if err != nil { + return nil, errors.Wrapf(err, "[filepath:%s]打开/解析文件失败!", filename) + } + return file, nil +} + +func (a *Base) Rollback(file *ast.File) error { + return nil +} + +func (a *Base) Injection(file *ast.File) error { + return nil +} + +func (a *Base) Format(filename string, writer io.Writer, file *ast.File) error { + fileSet := token.NewFileSet() + if writer == nil { + open, err := os.OpenFile(filename, os.O_WRONLY|os.O_TRUNC, 0666) + defer open.Close() + if err != nil { + return errors.Wrapf(err, "[filepath:%s]打开文件失败!", filename) + } + writer = open + } + err := format.Node(writer, fileSet, file) + if err != nil { + return errors.Wrapf(err, "[filepath:%s]注入失败!", filename) + } + return nil +} + +// RelativePath 绝对路径转相对路径 +func (a *Base) RelativePath(filePath string) string { + server := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server) + hasServer := strings.Index(filePath, server) + if hasServer != -1 { + filePath = strings.TrimPrefix(filePath, server) + keys := strings.Split(filePath, string(filepath.Separator)) + filePath = path.Join(keys...) + } + return filePath +} + +// AbsolutePath 相对路径转绝对路径 +func (a *Base) AbsolutePath(filePath string) string { + server := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server) + keys := strings.Split(filePath, "/") + filePath = filepath.Join(keys...) + filePath = filepath.Join(server, filePath) + return filePath +} diff --git a/admin/server/utils/ast/package_enter.go b/admin/server/utils/ast/package_enter.go new file mode 100644 index 000000000..f4b6305f9 --- /dev/null +++ b/admin/server/utils/ast/package_enter.go @@ -0,0 +1,85 @@ +package ast + +import ( + "go/ast" + "go/token" + "io" +) + +// PackageEnter 模块化入口 +type PackageEnter struct { + Base + Type Type // 类型 + Path string // 文件路径 + ImportPath string // 导包路径 + StructName string // 结构体名称 + PackageName string // 包名 + RelativePath string // 相对路径 + PackageStructName string // 包结构体名称 +} + +func (a *PackageEnter) Parse(filename string, writer io.Writer) (file *ast.File, err error) { + if filename == "" { + if a.RelativePath == "" { + filename = a.Path + a.RelativePath = a.Base.RelativePath(a.Path) + return a.Base.Parse(filename, writer) + } + a.Path = a.Base.AbsolutePath(a.RelativePath) + filename = a.Path + } + return a.Base.Parse(filename, writer) +} + +func (a *PackageEnter) Rollback(file *ast.File) error { + // 无需回滚 + return nil +} + +func (a *PackageEnter) Injection(file *ast.File) error { + _ = NewImport(a.ImportPath).Injection(file) + ast.Inspect(file, func(n ast.Node) bool { + genDecl, ok := n.(*ast.GenDecl) + if !ok || genDecl.Tok != token.TYPE { + return true + } + + for _, spec := range genDecl.Specs { + typeSpec, specok := spec.(*ast.TypeSpec) + if !specok || typeSpec.Name.Name != a.Type.Group() { + continue + } + + structType, structTypeOK := typeSpec.Type.(*ast.StructType) + if !structTypeOK { + continue + } + + for _, field := range structType.Fields.List { + if len(field.Names) == 1 && field.Names[0].Name == a.StructName { + return true + } + } + + field := &ast.Field{ + Names: []*ast.Ident{{Name: a.StructName}}, + Type: &ast.SelectorExpr{ + X: &ast.Ident{Name: a.PackageName}, + Sel: &ast.Ident{Name: a.PackageStructName}, + }, + } + structType.Fields.List = append(structType.Fields.List, field) + return false + } + + return true + }) + return nil +} + +func (a *PackageEnter) Format(filename string, writer io.Writer, file *ast.File) error { + if filename == "" { + filename = a.Path + } + return a.Base.Format(filename, writer, file) +} diff --git a/admin/server/utils/ast/package_enter_test.go b/admin/server/utils/ast/package_enter_test.go new file mode 100644 index 000000000..3cf4ab459 --- /dev/null +++ b/admin/server/utils/ast/package_enter_test.go @@ -0,0 +1,154 @@ +package ast + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "path/filepath" + "testing" +) + +func TestPackageEnter_Rollback(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + StructName string + PackageName string + PackageStructName string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试ExampleApiGroup回滚", + fields: fields{ + Type: TypePackageApiEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", "enter.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/api/v1/example"`, + StructName: "ExampleApiGroup", + PackageName: "example", + PackageStructName: "ApiGroup", + }, + wantErr: false, + }, + { + name: "测试ExampleRouterGroup回滚", + fields: fields{ + Type: TypePackageRouterEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", "enter.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/router/example"`, + StructName: "Example", + PackageName: "example", + PackageStructName: "RouterGroup", + }, + wantErr: false, + }, + { + name: "测试ExampleServiceGroup回滚", + fields: fields{ + Type: TypePackageServiceEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", "enter.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/service/example"`, + StructName: "ExampleServiceGroup", + PackageName: "example", + PackageStructName: "ServiceGroup", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PackageEnter{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + StructName: tt.fields.StructName, + PackageName: tt.fields.PackageName, + PackageStructName: tt.fields.PackageStructName, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Rollback(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestPackageEnter_Injection(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + StructName string + PackageName string + PackageStructName string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试ExampleApiGroup注入", + fields: fields{ + Type: TypePackageApiEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", "enter.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/api/v1/example"`, + StructName: "ExampleApiGroup", + PackageName: "example", + PackageStructName: "ApiGroup", + }, + }, + { + name: "测试ExampleRouterGroup注入", + fields: fields{ + Type: TypePackageRouterEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", "enter.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/router/example"`, + StructName: "Example", + PackageName: "example", + PackageStructName: "RouterGroup", + }, + wantErr: false, + }, + { + name: "测试ExampleServiceGroup注入", + fields: fields{ + Type: TypePackageServiceEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", "enter.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/service/example"`, + StructName: "ExampleServiceGroup", + PackageName: "example", + PackageStructName: "ServiceGroup", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PackageEnter{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + StructName: tt.fields.StructName, + PackageName: tt.fields.PackageName, + PackageStructName: tt.fields.PackageStructName, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Injection(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Format() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/admin/server/utils/ast/package_initialize_gorm.go b/admin/server/utils/ast/package_initialize_gorm.go new file mode 100644 index 000000000..594f71490 --- /dev/null +++ b/admin/server/utils/ast/package_initialize_gorm.go @@ -0,0 +1,196 @@ +package ast + +import ( + "fmt" + "go/ast" + "go/token" + "io" +) + +// PackageInitializeGorm 包初始化gorm +type PackageInitializeGorm struct { + Base + Type Type // 类型 + Path string // 文件路径 + ImportPath string // 导包路径 + Business string // 业务库 gva => gva, 不要传"gva" + StructName string // 结构体名称 + PackageName string // 包名 + RelativePath string // 相对路径 + IsNew bool // 是否使用new关键字 true: new(PackageName.StructName) false: &PackageName.StructName{} +} + +func (a *PackageInitializeGorm) Parse(filename string, writer io.Writer) (file *ast.File, err error) { + if filename == "" { + if a.RelativePath == "" { + filename = a.Path + a.RelativePath = a.Base.RelativePath(a.Path) + return a.Base.Parse(filename, writer) + } + a.Path = a.Base.AbsolutePath(a.RelativePath) + filename = a.Path + } + return a.Base.Parse(filename, writer) +} + +func (a *PackageInitializeGorm) Rollback(file *ast.File) error { + packageNameNum := 0 + // 寻找目标结构 + ast.Inspect(file, func(n ast.Node) bool { + // 总调用的db变量根据business来决定 + varDB := a.Business + "Db" + + if a.Business == "" { + varDB = "db" + } + + callExpr, ok := n.(*ast.CallExpr) + if !ok { + return true + } + + // 检查是不是 db.AutoMigrate() 方法 + selExpr, ok := callExpr.Fun.(*ast.SelectorExpr) + if !ok || selExpr.Sel.Name != "AutoMigrate" { + return true + } + + // 检查调用方是不是 db + ident, ok := selExpr.X.(*ast.Ident) + if !ok || ident.Name != varDB { + return true + } + + // 删除结构体参数 + for i := 0; i < len(callExpr.Args); i++ { + if com, comok := callExpr.Args[i].(*ast.CompositeLit); comok { + if selector, exprok := com.Type.(*ast.SelectorExpr); exprok { + if x, identok := selector.X.(*ast.Ident); identok { + if x.Name == a.PackageName { + packageNameNum++ + if selector.Sel.Name == a.StructName { + callExpr.Args = append(callExpr.Args[:i], callExpr.Args[i+1:]...) + i-- + } + } + } + } + } + } + return true + }) + + if packageNameNum == 1 { + _ = NewImport(a.ImportPath).Rollback(file) + } + return nil +} + +func (a *PackageInitializeGorm) Injection(file *ast.File) error { + _ = NewImport(a.ImportPath).Injection(file) + bizModelDecl := FindFunction(file, "bizModel") + if bizModelDecl != nil { + a.addDbVar(bizModelDecl.Body) + } + // 寻找目标结构 + ast.Inspect(file, func(n ast.Node) bool { + // 总调用的db变量根据business来决定 + varDB := a.Business + "Db" + + if a.Business == "" { + varDB = "db" + } + + callExpr, ok := n.(*ast.CallExpr) + if !ok { + return true + } + + // 检查是不是 db.AutoMigrate() 方法 + selExpr, ok := callExpr.Fun.(*ast.SelectorExpr) + if !ok || selExpr.Sel.Name != "AutoMigrate" { + return true + } + + // 检查调用方是不是 db + ident, ok := selExpr.X.(*ast.Ident) + if !ok || ident.Name != varDB { + return true + } + + // 添加结构体参数 + callExpr.Args = append(callExpr.Args, &ast.CompositeLit{ + Type: &ast.SelectorExpr{ + X: ast.NewIdent(a.PackageName), + Sel: ast.NewIdent(a.StructName), + }, + }) + return true + }) + return nil +} + +func (a *PackageInitializeGorm) Format(filename string, writer io.Writer, file *ast.File) error { + if filename == "" { + filename = a.Path + } + return a.Base.Format(filename, writer, file) +} + +// 创建businessDB变量 +func (a *PackageInitializeGorm) addDbVar(astBody *ast.BlockStmt) { + for i := range astBody.List { + if assignStmt, ok := astBody.List[i].(*ast.AssignStmt); ok { + if ident, ok := assignStmt.Lhs[0].(*ast.Ident); ok { + if (a.Business == "" && ident.Name == "db") || ident.Name == a.Business+"Db" { + return + } + } + } + } + + // 添加 businessDb := global.GetGlobalDBByDBName("business") 变量 + assignNode := &ast.AssignStmt{ + Lhs: []ast.Expr{ + &ast.Ident{ + Name: a.Business + "Db", + }, + }, + Tok: token.DEFINE, + Rhs: []ast.Expr{ + &ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: &ast.Ident{ + Name: "global", + }, + Sel: &ast.Ident{ + Name: "GetGlobalDBByDBName", + }, + }, + Args: []ast.Expr{ + &ast.BasicLit{ + Kind: token.STRING, + Value: fmt.Sprintf("\"%s\"", a.Business), + }, + }, + }, + }, + } + + // 添加 businessDb.AutoMigrate() 方法 + autoMigrateCall := &ast.ExprStmt{ + X: &ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: &ast.Ident{ + Name: a.Business + "Db", + }, + Sel: &ast.Ident{ + Name: "AutoMigrate", + }, + }, + }, + } + + returnNode := astBody.List[len(astBody.List)-1] + astBody.List = append(astBody.List[:len(astBody.List)-1], assignNode, autoMigrateCall, returnNode) +} diff --git a/admin/server/utils/ast/package_initialize_gorm_test.go b/admin/server/utils/ast/package_initialize_gorm_test.go new file mode 100644 index 000000000..af5cef9e8 --- /dev/null +++ b/admin/server/utils/ast/package_initialize_gorm_test.go @@ -0,0 +1,171 @@ +package ast + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "path/filepath" + "testing" +) + +func TestPackageInitializeGorm_Injection(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + StructName string + PackageName string + IsNew bool + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 &example.ExaFileUploadAndDownload{} 注入", + fields: fields{ + Type: TypePackageInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/model/example"`, + StructName: "ExaFileUploadAndDownload", + PackageName: "example", + IsNew: false, + }, + }, + { + name: "测试 &example.ExaCustomer{} 注入", + fields: fields{ + Type: TypePackageInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/model/example"`, + StructName: "ExaCustomer", + PackageName: "example", + IsNew: false, + }, + }, + { + name: "测试 new(example.ExaFileUploadAndDownload) 注入", + fields: fields{ + Type: TypePackageInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/model/example"`, + StructName: "ExaFileUploadAndDownload", + PackageName: "example", + IsNew: true, + }, + }, + { + name: "测试 new(example.ExaCustomer) 注入", + fields: fields{ + Type: TypePackageInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/model/example"`, + StructName: "ExaCustomer", + PackageName: "example", + IsNew: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PackageInitializeGorm{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + StructName: tt.fields.StructName, + PackageName: tt.fields.PackageName, + IsNew: tt.fields.IsNew, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Injection(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestPackageInitializeGorm_Rollback(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + StructName string + PackageName string + IsNew bool + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 &example.ExaFileUploadAndDownload{} 回滚", + fields: fields{ + Type: TypePackageInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/model/example"`, + StructName: "ExaFileUploadAndDownload", + PackageName: "example", + IsNew: false, + }, + }, + { + name: "测试 &example.ExaCustomer{} 回滚", + fields: fields{ + Type: TypePackageInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/model/example"`, + StructName: "ExaCustomer", + PackageName: "example", + IsNew: false, + }, + }, + { + name: "测试 new(example.ExaFileUploadAndDownload) 回滚", + fields: fields{ + Type: TypePackageInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/model/example"`, + StructName: "ExaFileUploadAndDownload", + PackageName: "example", + IsNew: true, + }, + }, + { + name: "测试 new(example.ExaCustomer) 回滚", + fields: fields{ + Type: TypePackageInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/model/example"`, + StructName: "ExaCustomer", + PackageName: "example", + IsNew: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PackageInitializeGorm{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + StructName: tt.fields.StructName, + PackageName: tt.fields.PackageName, + IsNew: tt.fields.IsNew, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Rollback(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/admin/server/utils/ast/package_initialize_router.go b/admin/server/utils/ast/package_initialize_router.go new file mode 100644 index 000000000..9fe4429db --- /dev/null +++ b/admin/server/utils/ast/package_initialize_router.go @@ -0,0 +1,150 @@ +package ast + +import ( + "fmt" + "go/ast" + "go/token" + "io" +) + +// PackageInitializeRouter 包初始化路由 +// ModuleName := PackageName.AppName.GroupName +// ModuleName.FunctionName(RouterGroupName) +type PackageInitializeRouter struct { + Base + Type Type // 类型 + Path string // 文件路径 + ImportPath string // 导包路径 + RelativePath string // 相对路径 + AppName string // 应用名称 + GroupName string // 分组名称 + ModuleName string // 模块名称 + PackageName string // 包名 + FunctionName string // 函数名 + RouterGroupName string // 路由分组名称 + LeftRouterGroupName string // 左路由分组名称 + RightRouterGroupName string // 右路由分组名称 +} + +func (a *PackageInitializeRouter) Parse(filename string, writer io.Writer) (file *ast.File, err error) { + if filename == "" { + if a.RelativePath == "" { + filename = a.Path + a.RelativePath = a.Base.RelativePath(a.Path) + return a.Base.Parse(filename, writer) + } + a.Path = a.Base.AbsolutePath(a.RelativePath) + filename = a.Path + } + return a.Base.Parse(filename, writer) +} + +func (a *PackageInitializeRouter) Rollback(file *ast.File) error { + funcDecl := FindFunction(file, "initBizRouter") + exprNum := 0 + for i := range funcDecl.Body.List { + if IsBlockStmt(funcDecl.Body.List[i]) { + if VariableExistsInBlock(funcDecl.Body.List[i].(*ast.BlockStmt), a.ModuleName) { + for ii, stmt := range funcDecl.Body.List[i].(*ast.BlockStmt).List { + // 检查语句是否为 *ast.ExprStmt + exprStmt, ok := stmt.(*ast.ExprStmt) + if !ok { + continue + } + // 检查表达式是否为 *ast.CallExpr + callExpr, ok := exprStmt.X.(*ast.CallExpr) + if !ok { + continue + } + // 检查是否调用了我们正在寻找的函数 + selExpr, ok := callExpr.Fun.(*ast.SelectorExpr) + if !ok { + continue + } + // 检查调用的函数是否为 systemRouter.InitApiRouter + ident, ok := selExpr.X.(*ast.Ident) + //只要存在调用则+1 + if ok && ident.Name == a.ModuleName { + exprNum++ + } + //判断是否为目标结构 + if !ok || ident.Name != a.ModuleName || selExpr.Sel.Name != a.FunctionName { + continue + } + exprNum-- + // 从语句列表中移除。 + funcDecl.Body.List[i].(*ast.BlockStmt).List = append(funcDecl.Body.List[i].(*ast.BlockStmt).List[:ii], funcDecl.Body.List[i].(*ast.BlockStmt).List[ii+1:]...) + // 如果不再存在任何调用,则删除导入和变量。 + if exprNum == 0 { + funcDecl.Body.List = append(funcDecl.Body.List[:i], funcDecl.Body.List[i+1:]...) + } + break + } + break + } + } + } + + return nil +} + +func (a *PackageInitializeRouter) Injection(file *ast.File) error { + funcDecl := FindFunction(file, "initBizRouter") + hasRouter := false + var varBlock *ast.BlockStmt + for i := range funcDecl.Body.List { + if IsBlockStmt(funcDecl.Body.List[i]) { + if VariableExistsInBlock(funcDecl.Body.List[i].(*ast.BlockStmt), a.ModuleName) { + hasRouter = true + varBlock = funcDecl.Body.List[i].(*ast.BlockStmt) + break + } + } + } + if !hasRouter { + stmt := a.CreateAssignStmt() + varBlock = &ast.BlockStmt{ + List: []ast.Stmt{ + stmt, + }, + } + } + routerStmt := CreateStmt(fmt.Sprintf("%s.%s(%s,%s)", a.ModuleName, a.FunctionName, a.LeftRouterGroupName, a.RightRouterGroupName)) + varBlock.List = append(varBlock.List, routerStmt) + if !hasRouter { + funcDecl.Body.List = append(funcDecl.Body.List, varBlock) + } + return nil +} + +func (a *PackageInitializeRouter) Format(filename string, writer io.Writer, file *ast.File) error { + if filename == "" { + filename = a.Path + } + return a.Base.Format(filename, writer, file) +} + +func (a *PackageInitializeRouter) CreateAssignStmt() *ast.AssignStmt { + //创建左侧变量 + ident := &ast.Ident{ + Name: a.ModuleName, + } + + //创建右侧的赋值语句 + selector := &ast.SelectorExpr{ + X: &ast.SelectorExpr{ + X: &ast.Ident{Name: a.PackageName}, + Sel: &ast.Ident{Name: a.AppName}, + }, + Sel: &ast.Ident{Name: a.GroupName}, + } + + // 创建一个组合的赋值语句 + stmt := &ast.AssignStmt{ + Lhs: []ast.Expr{ident}, + Tok: token.DEFINE, + Rhs: []ast.Expr{selector}, + } + + return stmt +} diff --git a/admin/server/utils/ast/package_initialize_router_test.go b/admin/server/utils/ast/package_initialize_router_test.go new file mode 100644 index 000000000..5a23dbb34 --- /dev/null +++ b/admin/server/utils/ast/package_initialize_router_test.go @@ -0,0 +1,158 @@ +package ast + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "path/filepath" + "testing" +) + +func TestPackageInitializeRouter_Injection(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + AppName string + GroupName string + ModuleName string + PackageName string + FunctionName string + RouterGroupName string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 InitCustomerRouter 注入", + fields: fields{ + Type: TypePackageInitializeRouter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "router_biz.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/router"`, + AppName: "RouterGroupApp", + GroupName: "Example", + ModuleName: "exampleRouter", + PackageName: "router", + FunctionName: "InitCustomerRouter", + RouterGroupName: "privateGroup", + }, + wantErr: false, + }, + { + name: "测试 InitFileUploadAndDownloadRouter 注入", + fields: fields{ + Type: TypePackageInitializeRouter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "router_biz.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/router"`, + AppName: "RouterGroupApp", + GroupName: "Example", + ModuleName: "exampleRouter", + PackageName: "router", + FunctionName: "InitFileUploadAndDownloadRouter", + RouterGroupName: "privateGroup", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PackageInitializeRouter{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + AppName: tt.fields.AppName, + GroupName: tt.fields.GroupName, + ModuleName: tt.fields.ModuleName, + PackageName: tt.fields.PackageName, + FunctionName: tt.fields.FunctionName, + RouterGroupName: tt.fields.RouterGroupName, + LeftRouterGroupName: "privateGroup", + RightRouterGroupName: "publicGroup", + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Injection(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestPackageInitializeRouter_Rollback(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + AppName string + GroupName string + ModuleName string + PackageName string + FunctionName string + RouterGroupName string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + + { + name: "测试 InitCustomerRouter 回滚", + fields: fields{ + Type: TypePackageInitializeRouter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "router_biz.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/router"`, + AppName: "RouterGroupApp", + GroupName: "Example", + ModuleName: "exampleRouter", + PackageName: "router", + FunctionName: "InitCustomerRouter", + RouterGroupName: "privateGroup", + }, + wantErr: false, + }, + { + name: "测试 InitFileUploadAndDownloadRouter 回滚", + fields: fields{ + Type: TypePackageInitializeRouter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "router_biz.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/router"`, + AppName: "RouterGroupApp", + GroupName: "Example", + ModuleName: "exampleRouter", + PackageName: "router", + FunctionName: "InitFileUploadAndDownloadRouter", + RouterGroupName: "privateGroup", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PackageInitializeRouter{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + AppName: tt.fields.AppName, + GroupName: tt.fields.GroupName, + ModuleName: tt.fields.ModuleName, + PackageName: tt.fields.PackageName, + FunctionName: tt.fields.FunctionName, + RouterGroupName: tt.fields.RouterGroupName, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Rollback(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/admin/server/utils/ast/package_module_enter.go b/admin/server/utils/ast/package_module_enter.go new file mode 100644 index 000000000..881fb3ff7 --- /dev/null +++ b/admin/server/utils/ast/package_module_enter.go @@ -0,0 +1,180 @@ +package ast + +import ( + "go/ast" + "go/token" + "io" +) + +// PackageModuleEnter 模块化入口 +// ModuleName := PackageName.AppName.GroupName.ServiceName +type PackageModuleEnter struct { + Base + Type Type // 类型 + Path string // 文件路径 + ImportPath string // 导包路径 + RelativePath string // 相对路径 + StructName string // 结构体名称 + AppName string // 应用名称 + GroupName string // 分组名称 + ModuleName string // 模块名称 + PackageName string // 包名 + ServiceName string // 服务名称 +} + +func (a *PackageModuleEnter) Parse(filename string, writer io.Writer) (file *ast.File, err error) { + if filename == "" { + if a.RelativePath == "" { + filename = a.Path + a.RelativePath = a.Base.RelativePath(a.Path) + return a.Base.Parse(filename, writer) + } + a.Path = a.Base.AbsolutePath(a.RelativePath) + filename = a.Path + } + return a.Base.Parse(filename, writer) +} + +func (a *PackageModuleEnter) Rollback(file *ast.File) error { + for i := 0; i < len(file.Decls); i++ { + v1, o1 := file.Decls[i].(*ast.GenDecl) + if o1 { + for j := 0; j < len(v1.Specs); j++ { + v2, o2 := v1.Specs[j].(*ast.TypeSpec) + if o2 { + if v2.Name.Name != a.Type.Group() { + continue + } + v3, o3 := v2.Type.(*ast.StructType) + if o3 { + for k := 0; k < len(v3.Fields.List); k++ { + v4, o4 := v3.Fields.List[k].Type.(*ast.Ident) + if o4 && v4.Name == a.StructName { + v3.Fields.List = append(v3.Fields.List[:k], v3.Fields.List[k+1:]...) + } + } + } + continue + } + if a.Type == TypePackageServiceModuleEnter { + continue + } + v3, o3 := v1.Specs[j].(*ast.ValueSpec) + if o3 { + if len(v3.Names) == 1 && v3.Names[0].Name == a.ModuleName { + v1.Specs = append(v1.Specs[:j], v1.Specs[j+1:]...) + } + } + if v1.Tok == token.VAR && len(v1.Specs) == 0 { + _ = NewImport(a.ImportPath).Rollback(file) + if i == len(file.Decls) { + file.Decls = append(file.Decls[:i-1]) + break + } // 空的var(), 如果不删除则会影响的注入变量, 因为识别不到*ast.ValueSpec + file.Decls = append(file.Decls[:i], file.Decls[i+1:]...) + } + } + } + } + return nil +} + +func (a *PackageModuleEnter) Injection(file *ast.File) error { + _ = NewImport(a.ImportPath).Injection(file) + var hasValue bool + var hasVariables bool + for i := 0; i < len(file.Decls); i++ { + v1, o1 := file.Decls[i].(*ast.GenDecl) + if o1 { + if v1.Tok == token.VAR { + hasVariables = true + } + for j := 0; j < len(v1.Specs); j++ { + if a.Type == TypePackageServiceModuleEnter { + hasValue = true + } + v2, o2 := v1.Specs[j].(*ast.TypeSpec) + if o2 { + if v2.Name.Name != a.Type.Group() { + continue + } + v3, o3 := v2.Type.(*ast.StructType) + if o3 { + var hasStruct bool + for k := 0; k < len(v3.Fields.List); k++ { + v4, o4 := v3.Fields.List[k].Type.(*ast.Ident) + if o4 && v4.Name == a.StructName { + hasStruct = true + } + } + if !hasStruct { + field := &ast.Field{Type: &ast.Ident{Name: a.StructName}} + v3.Fields.List = append(v3.Fields.List, field) + } + } + continue + } + v3, o3 := v1.Specs[j].(*ast.ValueSpec) + if o3 { + hasVariables = true + if len(v3.Names) == 1 && v3.Names[0].Name == a.ModuleName { + hasValue = true + } + } + if v1.Tok == token.VAR && len(v1.Specs) == 0 { + hasVariables = false + } // 说明是空var() + if hasVariables && !hasValue { + spec := &ast.ValueSpec{ + Names: []*ast.Ident{{Name: a.ModuleName}}, + Values: []ast.Expr{ + &ast.SelectorExpr{ + X: &ast.SelectorExpr{ + X: &ast.SelectorExpr{ + X: &ast.Ident{Name: a.PackageName}, + Sel: &ast.Ident{Name: a.AppName}, + }, + Sel: &ast.Ident{Name: a.GroupName}, + }, + Sel: &ast.Ident{Name: a.ServiceName}, + }, + }, + } + v1.Specs = append(v1.Specs, spec) + hasValue = true + } + } + } + } + if !hasValue && !hasVariables { + decl := &ast.GenDecl{ + Tok: token.VAR, + Specs: []ast.Spec{ + &ast.ValueSpec{ + Names: []*ast.Ident{{Name: a.ModuleName}}, + Values: []ast.Expr{ + &ast.SelectorExpr{ + X: &ast.SelectorExpr{ + X: &ast.SelectorExpr{ + X: &ast.Ident{Name: a.PackageName}, + Sel: &ast.Ident{Name: a.AppName}, + }, + Sel: &ast.Ident{Name: a.GroupName}, + }, + Sel: &ast.Ident{Name: a.ServiceName}, + }, + }, + }, + }, + } + file.Decls = append(file.Decls, decl) + } + return nil +} + +func (a *PackageModuleEnter) Format(filename string, writer io.Writer, file *ast.File) error { + if filename == "" { + filename = a.Path + } + return a.Base.Format(filename, writer, file) +} diff --git a/admin/server/utils/ast/package_module_enter_test.go b/admin/server/utils/ast/package_module_enter_test.go new file mode 100644 index 000000000..0015e3588 --- /dev/null +++ b/admin/server/utils/ast/package_module_enter_test.go @@ -0,0 +1,185 @@ +package ast + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "path/filepath" + "testing" +) + +func TestPackageModuleEnter_Rollback(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + StructName string + AppName string + GroupName string + ModuleName string + PackageName string + ServiceName string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 FileUploadAndDownloadRouter 回滚", + fields: fields{ + Type: TypePackageRouterModuleEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", "example", "enter.go"), + ImportPath: `api "github.com/flipped-aurora/gin-vue-admin/server/api/v1"`, + StructName: "FileUploadAndDownloadRouter", + AppName: "ApiGroupApp", + GroupName: "ExampleApiGroup", + ModuleName: "exaFileUploadAndDownloadApi", + PackageName: "api", + ServiceName: "FileUploadAndDownloadApi", + }, + wantErr: false, + }, + { + name: "测试 FileUploadAndDownloadApi 回滚", + fields: fields{ + Type: TypePackageApiModuleEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", "example", "enter.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/service"`, + StructName: "FileUploadAndDownloadApi", + AppName: "ServiceGroupApp", + GroupName: "ExampleServiceGroup", + ModuleName: "fileUploadAndDownloadService", + PackageName: "service", + ServiceName: "FileUploadAndDownloadService", + }, + wantErr: false, + }, + { + name: "测试 FileUploadAndDownloadService 回滚", + fields: fields{ + Type: TypePackageServiceModuleEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", "example", "enter.go"), + ImportPath: ``, + StructName: "FileUploadAndDownloadService", + AppName: "", + GroupName: "", + ModuleName: "", + PackageName: "", + ServiceName: "", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PackageModuleEnter{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + StructName: tt.fields.StructName, + AppName: tt.fields.AppName, + GroupName: tt.fields.GroupName, + ModuleName: tt.fields.ModuleName, + PackageName: tt.fields.PackageName, + ServiceName: tt.fields.ServiceName, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Rollback(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestPackageModuleEnter_Injection(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + StructName string + AppName string + GroupName string + ModuleName string + PackageName string + ServiceName string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 FileUploadAndDownloadRouter 注入", + fields: fields{ + Type: TypePackageRouterModuleEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", "example", "enter.go"), + ImportPath: `api "github.com/flipped-aurora/gin-vue-admin/server/api/v1"`, + StructName: "FileUploadAndDownloadRouter", + AppName: "ApiGroupApp", + GroupName: "ExampleApiGroup", + ModuleName: "exaFileUploadAndDownloadApi", + PackageName: "api", + ServiceName: "FileUploadAndDownloadApi", + }, + wantErr: false, + }, + { + name: "测试 FileUploadAndDownloadApi 注入", + fields: fields{ + Type: TypePackageApiModuleEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", "example", "enter.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/service"`, + StructName: "FileUploadAndDownloadApi", + AppName: "ServiceGroupApp", + GroupName: "ExampleServiceGroup", + ModuleName: "fileUploadAndDownloadService", + PackageName: "service", + ServiceName: "FileUploadAndDownloadService", + }, + wantErr: false, + }, + { + name: "测试 FileUploadAndDownloadService 注入", + fields: fields{ + Type: TypePackageServiceModuleEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", "example", "enter.go"), + ImportPath: ``, + StructName: "FileUploadAndDownloadService", + AppName: "", + GroupName: "", + ModuleName: "", + PackageName: "", + ServiceName: "", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PackageModuleEnter{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + StructName: tt.fields.StructName, + AppName: tt.fields.AppName, + GroupName: tt.fields.GroupName, + ModuleName: tt.fields.ModuleName, + PackageName: tt.fields.PackageName, + ServiceName: tt.fields.ServiceName, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Injection(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/admin/server/utils/ast/plugin_enter.go b/admin/server/utils/ast/plugin_enter.go new file mode 100644 index 000000000..df5bba4d1 --- /dev/null +++ b/admin/server/utils/ast/plugin_enter.go @@ -0,0 +1,167 @@ +package ast + +import ( + "go/ast" + "go/token" + "io" +) + +// PluginEnter 插件化入口 +// ModuleName := PackageName.GroupName.ServiceName +type PluginEnter struct { + Base + Type Type // 类型 + Path string // 文件路径 + ImportPath string // 导包路径 + RelativePath string // 相对路径 + StructName string // 结构体名称 + StructCamelName string // 结构体小驼峰名称 + ModuleName string // 模块名称 + GroupName string // 分组名称 + PackageName string // 包名 + ServiceName string // 服务名称 +} + +func (a *PluginEnter) Parse(filename string, writer io.Writer) (file *ast.File, err error) { + if filename == "" { + if a.RelativePath == "" { + filename = a.Path + a.RelativePath = a.Base.RelativePath(a.Path) + return a.Base.Parse(filename, writer) + } + a.Path = a.Base.AbsolutePath(a.RelativePath) + filename = a.Path + } + return a.Base.Parse(filename, writer) +} + +func (a *PluginEnter) Rollback(file *ast.File) error { + //回滚结构体内内容 + var structType *ast.StructType + ast.Inspect(file, func(n ast.Node) bool { + switch x := n.(type) { + case *ast.TypeSpec: + if s, ok := x.Type.(*ast.StructType); ok { + structType = s + for i, field := range x.Type.(*ast.StructType).Fields.List { + if len(field.Names) > 0 && field.Names[0].Name == a.StructName { + s.Fields.List = append(s.Fields.List[:i], s.Fields.List[i+1:]...) + return false + } + } + } + } + return true + }) + + if len(structType.Fields.List) == 0 { + _ = NewImport(a.ImportPath).Rollback(file) + } + + if a.Type == TypePluginServiceEnter { + return nil + } + + //回滚变量内容 + ast.Inspect(file, func(n ast.Node) bool { + genDecl, ok := n.(*ast.GenDecl) + if ok && genDecl.Tok == token.VAR { + for i, spec := range genDecl.Specs { + valueSpec, vsok := spec.(*ast.ValueSpec) + if vsok { + for _, name := range valueSpec.Names { + if name.Name == a.ModuleName { + genDecl.Specs = append(genDecl.Specs[:i], genDecl.Specs[i+1:]...) + return false + } + } + } + } + } + return true + }) + + return nil +} + +func (a *PluginEnter) Injection(file *ast.File) error { + _ = NewImport(a.ImportPath).Injection(file) + + has := false + hasVar := false + var firstStruct *ast.StructType + var varSpec *ast.GenDecl + //寻找是否存在结构且定位 + ast.Inspect(file, func(n ast.Node) bool { + switch x := n.(type) { + case *ast.TypeSpec: + if s, ok := x.Type.(*ast.StructType); ok { + firstStruct = s + for _, field := range x.Type.(*ast.StructType).Fields.List { + if len(field.Names) > 0 && field.Names[0].Name == a.StructName { + has = true + return false + } + } + } + } + return true + }) + + if !has { + field := &ast.Field{ + Names: []*ast.Ident{{Name: a.StructName}}, + Type: &ast.Ident{Name: a.StructCamelName}, + } + firstStruct.Fields.List = append(firstStruct.Fields.List, field) + } + + if a.Type == TypePluginServiceEnter { + return nil + } + + //寻找是否存在变量且定位 + ast.Inspect(file, func(n ast.Node) bool { + genDecl, ok := n.(*ast.GenDecl) + if ok && genDecl.Tok == token.VAR { + for _, spec := range genDecl.Specs { + valueSpec, vsok := spec.(*ast.ValueSpec) + if vsok { + varSpec = genDecl + for _, name := range valueSpec.Names { + if name.Name == a.ModuleName { + hasVar = true + return false + } + } + } + } + } + return true + }) + + if !hasVar { + spec := &ast.ValueSpec{ + Names: []*ast.Ident{{Name: a.ModuleName}}, + Values: []ast.Expr{ + &ast.SelectorExpr{ + X: &ast.SelectorExpr{ + X: &ast.Ident{Name: a.PackageName}, + Sel: &ast.Ident{Name: a.GroupName}, + }, + Sel: &ast.Ident{Name: a.ServiceName}, + }, + }, + } + varSpec.Specs = append(varSpec.Specs, spec) + } + + return nil +} + +func (a *PluginEnter) Format(filename string, writer io.Writer, file *ast.File) error { + if filename == "" { + filename = a.Path + } + return a.Base.Format(filename, writer, file) +} diff --git a/admin/server/utils/ast/plugin_enter_test.go b/admin/server/utils/ast/plugin_enter_test.go new file mode 100644 index 000000000..60b8dfca6 --- /dev/null +++ b/admin/server/utils/ast/plugin_enter_test.go @@ -0,0 +1,200 @@ +package ast + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "path/filepath" + "testing" +) + +func TestPluginEnter_Injection(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + StructName string + StructCamelName string + ModuleName string + GroupName string + PackageName string + ServiceName string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 Gva插件UserApi 注入", + fields: fields{ + Type: TypePluginApiEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "api", "enter.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/service"`, + StructName: "User", + StructCamelName: "user", + ModuleName: "serviceUser", + GroupName: "Service", + PackageName: "service", + ServiceName: "User", + }, + wantErr: false, + }, + { + name: "测试 Gva插件UserRouter 注入", + fields: fields{ + Type: TypePluginRouterEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "router", "enter.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/api"`, + StructName: "User", + StructCamelName: "user", + ModuleName: "userApi", + GroupName: "Api", + PackageName: "api", + ServiceName: "User", + }, + wantErr: false, + }, + { + name: "测试 Gva插件UserService 注入", + fields: fields{ + Type: TypePluginServiceEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "service", "enter.go"), + ImportPath: "", + StructName: "User", + StructCamelName: "user", + ModuleName: "", + GroupName: "", + PackageName: "", + ServiceName: "", + }, + wantErr: false, + }, + { + name: "测试 gva的User 注入", + fields: fields{ + Type: TypePluginServiceEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "service", "enter.go"), + ImportPath: "", + StructName: "User", + StructCamelName: "user", + ModuleName: "", + GroupName: "", + PackageName: "", + ServiceName: "", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PluginEnter{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + StructName: tt.fields.StructName, + StructCamelName: tt.fields.StructCamelName, + ModuleName: tt.fields.ModuleName, + GroupName: tt.fields.GroupName, + PackageName: tt.fields.PackageName, + ServiceName: tt.fields.ServiceName, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Injection(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestPluginEnter_Rollback(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + StructName string + StructCamelName string + ModuleName string + GroupName string + PackageName string + ServiceName string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 Gva插件UserRouter 回滚", + fields: fields{ + Type: TypePluginRouterEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "router", "enter.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/api"`, + StructName: "User", + StructCamelName: "user", + ModuleName: "userApi", + GroupName: "Api", + PackageName: "api", + ServiceName: "User", + }, + wantErr: false, + }, + { + name: "测试 Gva插件UserApi 回滚", + fields: fields{ + Type: TypePluginApiEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "api", "enter.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/service"`, + StructName: "User", + StructCamelName: "user", + ModuleName: "serviceUser", + GroupName: "Service", + PackageName: "service", + ServiceName: "User", + }, + wantErr: false, + }, + { + name: "测试 Gva插件UserService 回滚", + fields: fields{ + Type: TypePluginServiceEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "service", "enter.go"), + ImportPath: "", + StructName: "User", + StructCamelName: "user", + ModuleName: "", + GroupName: "", + PackageName: "", + ServiceName: "", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PluginEnter{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + StructName: tt.fields.StructName, + StructCamelName: tt.fields.StructCamelName, + ModuleName: tt.fields.ModuleName, + GroupName: tt.fields.GroupName, + PackageName: tt.fields.PackageName, + ServiceName: tt.fields.ServiceName, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Rollback(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/admin/server/utils/ast/plugin_gen.go b/admin/server/utils/ast/plugin_gen.go new file mode 100644 index 000000000..ed7d04fd9 --- /dev/null +++ b/admin/server/utils/ast/plugin_gen.go @@ -0,0 +1,189 @@ +package ast + +import ( + "go/ast" + "go/token" + "io" +) + +type PluginGen struct { + Base + Type Type // 类型 + Path string // 文件路径 + ImportPath string // 导包路径 + RelativePath string // 相对路径 + StructName string // 结构体名称 + PackageName string // 包名 + IsNew bool // 是否使用new关键字 +} + +func (a *PluginGen) Parse(filename string, writer io.Writer) (file *ast.File, err error) { + if filename == "" { + if a.RelativePath == "" { + filename = a.Path + a.RelativePath = a.Base.RelativePath(a.Path) + return a.Base.Parse(filename, writer) + } + a.Path = a.Base.AbsolutePath(a.RelativePath) + filename = a.Path + } + return a.Base.Parse(filename, writer) +} +func (a *PluginGen) Rollback(file *ast.File) error { + for i := 0; i < len(file.Decls); i++ { + v1, o1 := file.Decls[i].(*ast.FuncDecl) + if o1 { + for j := 0; j < len(v1.Body.List); j++ { + v2, o2 := v1.Body.List[j].(*ast.ExprStmt) + if o2 { + v3, o3 := v2.X.(*ast.CallExpr) + if o3 { + v4, o4 := v3.Fun.(*ast.SelectorExpr) + if o4 { + if v4.Sel.Name != "ApplyBasic" { + continue + } + for k := 0; k < len(v3.Args); k++ { + v5, o5 := v3.Args[k].(*ast.CallExpr) + if o5 { + v6, o6 := v5.Fun.(*ast.Ident) + if o6 { + if v6.Name != "new" { + continue + } + for l := 0; l < len(v5.Args); l++ { + v7, o7 := v5.Args[l].(*ast.SelectorExpr) + if o7 { + v8, o8 := v7.X.(*ast.Ident) + if o8 { + if v8.Name == a.PackageName && v7.Sel.Name == a.StructName { + v3.Args = append(v3.Args[:k], v3.Args[k+1:]...) + continue + } + } + } + } + } + } + if k >= len(v3.Args) { + break + } + v6, o6 := v3.Args[k].(*ast.CompositeLit) + if o6 { + v7, o7 := v6.Type.(*ast.SelectorExpr) + if o7 { + v8, o8 := v7.X.(*ast.Ident) + if o8 { + if v8.Name == a.PackageName && v7.Sel.Name == a.StructName { + v3.Args = append(v3.Args[:k], v3.Args[k+1:]...) + continue + } + } + } + } + } + if len(v3.Args) == 0 { + _ = NewImport(a.ImportPath).Rollback(file) + } + } + } + } + } + } + } + return nil +} + +func (a *PluginGen) Injection(file *ast.File) error { + _ = NewImport(a.ImportPath).Injection(file) + for i := 0; i < len(file.Decls); i++ { + v1, o1 := file.Decls[i].(*ast.FuncDecl) + if o1 { + for j := 0; j < len(v1.Body.List); j++ { + v2, o2 := v1.Body.List[j].(*ast.ExprStmt) + if o2 { + v3, o3 := v2.X.(*ast.CallExpr) + if o3 { + v4, o4 := v3.Fun.(*ast.SelectorExpr) + if o4 { + if v4.Sel.Name != "ApplyBasic" { + continue + } + var has bool + for k := 0; k < len(v3.Args); k++ { + v5, o5 := v3.Args[k].(*ast.CallExpr) + if o5 { + v6, o6 := v5.Fun.(*ast.Ident) + if o6 { + if v6.Name != "new" { + continue + } + for l := 0; l < len(v5.Args); l++ { + v7, o7 := v5.Args[l].(*ast.SelectorExpr) + if o7 { + v8, o8 := v7.X.(*ast.Ident) + if o8 { + if v8.Name == a.PackageName && v7.Sel.Name == a.StructName { + has = true + break + } + } + } + } + } + } + v6, o6 := v3.Args[k].(*ast.CompositeLit) + if o6 { + v7, o7 := v6.Type.(*ast.SelectorExpr) + if o7 { + v8, o8 := v7.X.(*ast.Ident) + if o8 { + if v8.Name == a.PackageName && v7.Sel.Name == a.StructName { + has = true + break + } + } + } + } + } + if !has { + if a.IsNew { + arg := &ast.CallExpr{ + Fun: &ast.Ident{Name: "\n\t\tnew"}, + Args: []ast.Expr{ + &ast.SelectorExpr{ + X: &ast.Ident{Name: a.PackageName}, + Sel: &ast.Ident{Name: a.StructName}, + }, + }, + } + v3.Args = append(v3.Args, arg) + v3.Args = append(v3.Args, &ast.BasicLit{ + Kind: token.STRING, + Value: "\n", + }) + break + } + arg := &ast.CompositeLit{ + Type: &ast.SelectorExpr{ + X: &ast.Ident{Name: a.PackageName}, + Sel: &ast.Ident{Name: a.StructName}, + }, + } + v3.Args = append(v3.Args, arg) + } + } + } + } + } + } + } + return nil +} + +func (a *PluginGen) Format(filename string, writer io.Writer, file *ast.File) error { + if filename == "" { + filename = a.Path + } + return a.Base.Format(filename, writer, file) +} diff --git a/admin/server/utils/ast/plugin_gen_test.go b/admin/server/utils/ast/plugin_gen_test.go new file mode 100644 index 000000000..1b9c790ef --- /dev/null +++ b/admin/server/utils/ast/plugin_gen_test.go @@ -0,0 +1,127 @@ +package ast + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "path/filepath" + "testing" +) + +func TestPluginGenModel_Injection(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + PackageName string + StructName string + IsNew bool + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 GvaUser 结构体注入", + fields: fields{ + Type: TypePluginGen, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "gen", "main.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/model"`, + PackageName: "model", + StructName: "User", + IsNew: false, + }, + }, + { + name: "测试 GvaUser 结构体注入", + fields: fields{ + Type: TypePluginGen, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "gen", "main.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/model"`, + PackageName: "model", + StructName: "User", + IsNew: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PluginGen{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + PackageName: tt.fields.PackageName, + StructName: tt.fields.StructName, + IsNew: tt.fields.IsNew, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Injection(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestPluginGenModel_Rollback(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + PackageName string + StructName string + IsNew bool + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 GvaUser 回滚", + fields: fields{ + Type: TypePluginGen, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "gen", "main.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/model"`, + PackageName: "model", + StructName: "User", + IsNew: false, + }, + }, + { + name: "测试 GvaUser 回滚", + fields: fields{ + Type: TypePluginGen, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "gen", "main.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/model"`, + PackageName: "model", + StructName: "User", + IsNew: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PluginGen{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + PackageName: tt.fields.PackageName, + StructName: tt.fields.StructName, + IsNew: tt.fields.IsNew, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Rollback(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/admin/server/utils/ast/plugin_initialize_gorm.go b/admin/server/utils/ast/plugin_initialize_gorm.go new file mode 100644 index 000000000..e3422518c --- /dev/null +++ b/admin/server/utils/ast/plugin_initialize_gorm.go @@ -0,0 +1,111 @@ +package ast + +import ( + "go/ast" + "io" +) + +type PluginInitializeGorm struct { + Base + Type Type // 类型 + Path string // 文件路径 + ImportPath string // 导包路径 + RelativePath string // 相对路径 + StructName string // 结构体名称 + PackageName string // 包名 + IsNew bool // 是否使用new关键字 true: new(PackageName.StructName) false: &PackageName.StructName{} +} + +func (a *PluginInitializeGorm) Parse(filename string, writer io.Writer) (file *ast.File, err error) { + if filename == "" { + if a.RelativePath == "" { + filename = a.Path + a.RelativePath = a.Base.RelativePath(a.Path) + return a.Base.Parse(filename, writer) + } + a.Path = a.Base.AbsolutePath(a.RelativePath) + filename = a.Path + } + return a.Base.Parse(filename, writer) +} + +func (a *PluginInitializeGorm) Rollback(file *ast.File) error { + var needRollBackImport bool + ast.Inspect(file, func(n ast.Node) bool { + callExpr, ok := n.(*ast.CallExpr) + if !ok { + return true + } + + selExpr, seok := callExpr.Fun.(*ast.SelectorExpr) + if !seok || selExpr.Sel.Name != "AutoMigrate" { + return true + } + if len(callExpr.Args) <= 1 { + needRollBackImport = true + } + // 删除指定的参数 + for i, arg := range callExpr.Args { + compLit, cok := arg.(*ast.CompositeLit) + if !cok { + continue + } + + cselExpr, sok := compLit.Type.(*ast.SelectorExpr) + if !sok { + continue + } + + ident, idok := cselExpr.X.(*ast.Ident) + if idok && ident.Name == a.PackageName && cselExpr.Sel.Name == a.StructName { + // 删除参数 + callExpr.Args = append(callExpr.Args[:i], callExpr.Args[i+1:]...) + break + } + } + + return true + }) + + if needRollBackImport { + _ = NewImport(a.ImportPath).Rollback(file) + } + + return nil +} + +func (a *PluginInitializeGorm) Injection(file *ast.File) error { + _ = NewImport(a.ImportPath).Injection(file) + var call *ast.CallExpr + ast.Inspect(file, func(n ast.Node) bool { + callExpr, ok := n.(*ast.CallExpr) + if !ok { + return true + } + + selExpr, ok := callExpr.Fun.(*ast.SelectorExpr) + if ok && selExpr.Sel.Name == "AutoMigrate" { + call = callExpr + return false + } + + return true + }) + + arg := &ast.CompositeLit{ + Type: &ast.SelectorExpr{ + X: &ast.Ident{Name: a.PackageName}, + Sel: &ast.Ident{Name: a.StructName}, + }, + } + + call.Args = append(call.Args, arg) + return nil +} + +func (a *PluginInitializeGorm) Format(filename string, writer io.Writer, file *ast.File) error { + if filename == "" { + filename = a.Path + } + return a.Base.Format(filename, writer, file) +} diff --git a/admin/server/utils/ast/plugin_initialize_gorm_test.go b/admin/server/utils/ast/plugin_initialize_gorm_test.go new file mode 100644 index 000000000..ebfc12a12 --- /dev/null +++ b/admin/server/utils/ast/plugin_initialize_gorm_test.go @@ -0,0 +1,138 @@ +package ast + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "path/filepath" + "testing" +) + +func TestPluginInitializeGorm_Injection(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + StructName string + PackageName string + IsNew bool + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 &model.User{} 注入", + fields: fields{ + Type: TypePluginInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "gorm.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/model"`, + StructName: "User", + PackageName: "model", + IsNew: false, + }, + }, + { + name: "测试 new(model.ExaCustomer) 注入", + fields: fields{ + Type: TypePluginInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "gorm.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/model"`, + StructName: "User", + PackageName: "model", + IsNew: true, + }, + }, + { + name: "测试 new(model.SysUsers) 注入", + fields: fields{ + Type: TypePluginInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "gorm.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/model"`, + StructName: "SysUser", + PackageName: "model", + IsNew: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PluginInitializeGorm{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + StructName: tt.fields.StructName, + PackageName: tt.fields.PackageName, + IsNew: tt.fields.IsNew, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Injection(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestPluginInitializeGorm_Rollback(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + StructName string + PackageName string + IsNew bool + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 &model.User{} 回滚", + fields: fields{ + Type: TypePluginInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "gorm.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/model"`, + StructName: "User", + PackageName: "model", + IsNew: false, + }, + }, + { + name: "测试 new(model.ExaCustomer) 回滚", + fields: fields{ + Type: TypePluginInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "gorm.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/model"`, + StructName: "User", + PackageName: "model", + IsNew: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PluginInitializeGorm{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + StructName: tt.fields.StructName, + PackageName: tt.fields.PackageName, + IsNew: tt.fields.IsNew, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Rollback(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/admin/server/utils/ast/plugin_initialize_router.go b/admin/server/utils/ast/plugin_initialize_router.go new file mode 100644 index 000000000..6550789ed --- /dev/null +++ b/admin/server/utils/ast/plugin_initialize_router.go @@ -0,0 +1,124 @@ +package ast + +import ( + "fmt" + "go/ast" + "io" +) + +// PluginInitializeRouter 插件初始化路由 +// PackageName.AppName.GroupName.FunctionName() +type PluginInitializeRouter struct { + Base + Type Type // 类型 + Path string // 文件路径 + ImportPath string // 导包路径 + ImportGlobalPath string // 导包全局变量路径 + ImportMiddlewarePath string // 导包中间件路径 + RelativePath string // 相对路径 + AppName string // 应用名称 + GroupName string // 分组名称 + PackageName string // 包名 + FunctionName string // 函数名 + LeftRouterGroupName string // 左路由分组名称 + RightRouterGroupName string // 右路由分组名称 +} + +func (a *PluginInitializeRouter) Parse(filename string, writer io.Writer) (file *ast.File, err error) { + if filename == "" { + if a.RelativePath == "" { + filename = a.Path + a.RelativePath = a.Base.RelativePath(a.Path) + return a.Base.Parse(filename, writer) + } + a.Path = a.Base.AbsolutePath(a.RelativePath) + filename = a.Path + } + return a.Base.Parse(filename, writer) +} + +func (a *PluginInitializeRouter) Rollback(file *ast.File) error { + funcDecl := FindFunction(file, "Router") + delI := 0 + routerNum := 0 + for i := len(funcDecl.Body.List) - 1; i >= 0; i-- { + stmt, ok := funcDecl.Body.List[i].(*ast.ExprStmt) + if !ok { + continue + } + + callExpr, ok := stmt.X.(*ast.CallExpr) + if !ok { + continue + } + + selExpr, ok := callExpr.Fun.(*ast.SelectorExpr) + if !ok { + continue + } + + ident, ok := selExpr.X.(*ast.SelectorExpr) + + if ok { + if iExpr, ieok := ident.X.(*ast.SelectorExpr); ieok { + if iden, idok := iExpr.X.(*ast.Ident); idok { + if iden.Name == "router" { + routerNum++ + } + } + } + if ident.Sel.Name == a.GroupName && selExpr.Sel.Name == a.FunctionName { + // 删除语句 + delI = i + } + } + } + + funcDecl.Body.List = append(funcDecl.Body.List[:delI], funcDecl.Body.List[delI+1:]...) + + if routerNum <= 1 { + _ = NewImport(a.ImportPath).Rollback(file) + } + + return nil +} + +func (a *PluginInitializeRouter) Injection(file *ast.File) error { + _ = NewImport(a.ImportPath).Injection(file) + funcDecl := FindFunction(file, "Router") + + var exists bool + + ast.Inspect(funcDecl, func(n ast.Node) bool { + callExpr, ok := n.(*ast.CallExpr) + if !ok { + return true + } + + selExpr, ok := callExpr.Fun.(*ast.SelectorExpr) + if !ok { + return true + } + + ident, ok := selExpr.X.(*ast.SelectorExpr) + if ok && ident.Sel.Name == a.GroupName && selExpr.Sel.Name == a.FunctionName { + exists = true + return false + } + return true + }) + + if !exists { + stmtStr := fmt.Sprintf("%s.%s.%s.%s(%s, %s)", a.PackageName, a.AppName, a.GroupName, a.FunctionName, a.LeftRouterGroupName, a.RightRouterGroupName) + stmt := CreateStmt(stmtStr) + funcDecl.Body.List = append(funcDecl.Body.List, stmt) + } + return nil +} + +func (a *PluginInitializeRouter) Format(filename string, writer io.Writer, file *ast.File) error { + if filename == "" { + filename = a.Path + } + return a.Base.Format(filename, writer, file) +} diff --git a/admin/server/utils/ast/plugin_initialize_router_test.go b/admin/server/utils/ast/plugin_initialize_router_test.go new file mode 100644 index 000000000..4dffd7fc4 --- /dev/null +++ b/admin/server/utils/ast/plugin_initialize_router_test.go @@ -0,0 +1,155 @@ +package ast + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "path/filepath" + "testing" +) + +func TestPluginInitializeRouter_Injection(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + AppName string + GroupName string + PackageName string + FunctionName string + LeftRouterGroupName string + RightRouterGroupName string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 Gva插件User 注入", + fields: fields{ + Type: TypePluginInitializeRouter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "router.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/router"`, + AppName: "Router", + GroupName: "User", + PackageName: "router", + FunctionName: "Init", + LeftRouterGroupName: "public", + RightRouterGroupName: "private", + }, + wantErr: false, + }, + { + name: "测试 中文 注入", + fields: fields{ + Type: TypePluginInitializeRouter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "router.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/router"`, + AppName: "Router", + GroupName: "U中文", + PackageName: "router", + FunctionName: "Init", + LeftRouterGroupName: "public", + RightRouterGroupName: "private", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PluginInitializeRouter{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + AppName: tt.fields.AppName, + GroupName: tt.fields.GroupName, + PackageName: tt.fields.PackageName, + FunctionName: tt.fields.FunctionName, + LeftRouterGroupName: tt.fields.LeftRouterGroupName, + RightRouterGroupName: tt.fields.RightRouterGroupName, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Injection(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestPluginInitializeRouter_Rollback(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + AppName string + GroupName string + PackageName string + FunctionName string + LeftRouterGroupName string + RightRouterGroupName string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 Gva插件User 回滚", + fields: fields{ + Type: TypePluginInitializeRouter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "router.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/router"`, + AppName: "Router", + GroupName: "User", + PackageName: "router", + FunctionName: "Init", + LeftRouterGroupName: "public", + RightRouterGroupName: "private", + }, + wantErr: false, + }, + { + name: "测试 中文 注入", + fields: fields{ + Type: TypePluginInitializeRouter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "router.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/router"`, + AppName: "Router", + GroupName: "U中文", + PackageName: "router", + FunctionName: "Init", + LeftRouterGroupName: "public", + RightRouterGroupName: "private", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PluginInitializeRouter{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + AppName: tt.fields.AppName, + GroupName: tt.fields.GroupName, + PackageName: tt.fields.PackageName, + FunctionName: tt.fields.FunctionName, + LeftRouterGroupName: tt.fields.LeftRouterGroupName, + RightRouterGroupName: tt.fields.RightRouterGroupName, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Rollback(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/admin/server/utils/ast/plugin_initialize_v2.go b/admin/server/utils/ast/plugin_initialize_v2.go new file mode 100644 index 000000000..1befdc657 --- /dev/null +++ b/admin/server/utils/ast/plugin_initialize_v2.go @@ -0,0 +1,52 @@ +package ast + +import ( + "fmt" + "go/ast" + "io" +) + +type PluginInitializeV2 struct { + Base + Type Type // 类型 + Path string // 文件路径 + PluginPath string // 插件路径 + RelativePath string // 相对路径 + ImportPath string // 导包路径 + StructName string // 结构体名称 + PackageName string // 包名 +} + +func (a *PluginInitializeV2) Parse(filename string, writer io.Writer) (file *ast.File, err error) { + if filename == "" { + if a.RelativePath == "" { + filename = a.PluginPath + a.RelativePath = a.Base.RelativePath(a.PluginPath) + return a.Base.Parse(filename, writer) + } + a.PluginPath = a.Base.AbsolutePath(a.RelativePath) + filename = a.PluginPath + } + return a.Base.Parse(filename, writer) +} + +func (a *PluginInitializeV2) Injection(file *ast.File) error { + if !CheckImport(file, a.ImportPath) { + NewImport(a.ImportPath).Injection(file) + funcDecl := FindFunction(file, "bizPluginV2") + stmt := CreateStmt(fmt.Sprintf("PluginInitV2(engine, %s.Plugin)", a.PackageName)) + funcDecl.Body.List = append(funcDecl.Body.List, stmt) + } + return nil +} + +func (a *PluginInitializeV2) Rollback(file *ast.File) error { + return nil +} + +func (a *PluginInitializeV2) Format(filename string, writer io.Writer, file *ast.File) error { + if filename == "" { + filename = a.PluginPath + } + return a.Base.Format(filename, writer, file) +} diff --git a/admin/server/utils/ast/plugin_initialize_v2_test.go b/admin/server/utils/ast/plugin_initialize_v2_test.go new file mode 100644 index 000000000..4e99c6dae --- /dev/null +++ b/admin/server/utils/ast/plugin_initialize_v2_test.go @@ -0,0 +1,100 @@ +package ast + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "path/filepath" + "testing" +) + +func TestPluginInitialize_Injection(t *testing.T) { + type fields struct { + Type Type + Path string + PluginPath string + ImportPath string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 Gva插件 注册注入", + fields: fields{ + Type: TypePluginInitializeV2, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "plugin_biz_v2.go"), + PluginPath: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "plugin.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva"`, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := PluginInitializeV2{ + Type: tt.fields.Type, + Path: tt.fields.Path, + PluginPath: tt.fields.PluginPath, + ImportPath: tt.fields.ImportPath, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Injection(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestPluginInitialize_Rollback(t *testing.T) { + type fields struct { + Type Type + Path string + PluginPath string + ImportPath string + PluginName string + StructName string + PackageName string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 Gva插件 回滚", + fields: fields{ + Type: TypePluginInitializeV2, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "plugin_biz_v2.go"), + PluginPath: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "plugin.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva"`, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := PluginInitializeV2{ + Type: tt.fields.Type, + Path: tt.fields.Path, + PluginPath: tt.fields.PluginPath, + ImportPath: tt.fields.ImportPath, + StructName: "Plugin", + PackageName: "gva", + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Rollback(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/admin/server/utils/breakpoint_continue.go b/admin/server/utils/breakpoint_continue.go new file mode 100644 index 000000000..c0baee57c --- /dev/null +++ b/admin/server/utils/breakpoint_continue.go @@ -0,0 +1,112 @@ +package utils + +import ( + "errors" + "os" + "strconv" + "strings" +) + +// 前端传来文件片与当前片为什么文件的第几片 +// 后端拿到以后比较次分片是否上传 或者是否为不完全片 +// 前端发送每片多大 +// 前端告知是否为最后一片且是否完成 + +const ( + breakpointDir = "./breakpointDir/" + finishDir = "./fileDir/" +) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: BreakPointContinue +//@description: 断点续传 +//@param: content []byte, fileName string, contentNumber int, contentTotal int, fileMd5 string +//@return: error, string + +func BreakPointContinue(content []byte, fileName string, contentNumber int, contentTotal int, fileMd5 string) (string, error) { + path := breakpointDir + fileMd5 + "/" + err := os.MkdirAll(path, os.ModePerm) + if err != nil { + return path, err + } + pathC, err := makeFileContent(content, fileName, path, contentNumber) + return pathC, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: CheckMd5 +//@description: 检查Md5 +//@param: content []byte, chunkMd5 string +//@return: CanUpload bool + +func CheckMd5(content []byte, chunkMd5 string) (CanUpload bool) { + fileMd5 := MD5V(content) + if fileMd5 == chunkMd5 { + return true // 可以继续上传 + } else { + return false // 切片不完整,废弃 + } +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: makeFileContent +//@description: 创建切片内容 +//@param: content []byte, fileName string, FileDir string, contentNumber int +//@return: string, error + +func makeFileContent(content []byte, fileName string, FileDir string, contentNumber int) (string, error) { + if strings.Index(fileName, "..") > -1 || strings.Index(FileDir, "..") > -1 { + return "", errors.New("文件名或路径不合法") + } + path := FileDir + fileName + "_" + strconv.Itoa(contentNumber) + f, err := os.Create(path) + if err != nil { + return path, err + } else { + _, err = f.Write(content) + if err != nil { + return path, err + } + } + defer f.Close() + return path, nil +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: makeFileContent +//@description: 创建切片文件 +//@param: fileName string, FileMd5 string +//@return: error, string + +func MakeFile(fileName string, FileMd5 string) (string, error) { + rd, err := os.ReadDir(breakpointDir + FileMd5) + if err != nil { + return finishDir + fileName, err + } + _ = os.MkdirAll(finishDir, os.ModePerm) + fd, err := os.OpenFile(finishDir+fileName, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o644) + if err != nil { + return finishDir + fileName, err + } + defer fd.Close() + for k := range rd { + content, _ := os.ReadFile(breakpointDir + FileMd5 + "/" + fileName + "_" + strconv.Itoa(k)) + _, err = fd.Write(content) + if err != nil { + _ = os.Remove(finishDir + fileName) + return finishDir + fileName, err + } + } + return finishDir + fileName, nil +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: RemoveChunk +//@description: 移除切片 +//@param: FileMd5 string +//@return: error + +func RemoveChunk(FileMd5 string) error { + err := os.RemoveAll(breakpointDir + FileMd5) + return err +} diff --git a/admin/server/utils/captcha/redis.go b/admin/server/utils/captcha/redis.go new file mode 100644 index 000000000..a13b7cc11 --- /dev/null +++ b/admin/server/utils/captcha/redis.go @@ -0,0 +1,60 @@ +package captcha + +import ( + "context" + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/mojocn/base64Captcha" + "go.uber.org/zap" +) + +func NewDefaultRedisStore() *RedisStore { + return &RedisStore{ + Expiration: time.Second * 180, + PreKey: "CAPTCHA_", + Context: context.TODO(), + } +} + +type RedisStore struct { + Expiration time.Duration + PreKey string + Context context.Context +} + +func (rs *RedisStore) UseWithCtx(ctx context.Context) base64Captcha.Store { + rs.Context = ctx + return rs +} + +func (rs *RedisStore) Set(id string, value string) error { + err := global.GVA_REDIS.Set(rs.Context, rs.PreKey+id, value, rs.Expiration).Err() + if err != nil { + global.GVA_LOG.Error("RedisStoreSetError!", zap.Error(err)) + return err + } + return nil +} + +func (rs *RedisStore) Get(key string, clear bool) string { + val, err := global.GVA_REDIS.Get(rs.Context, key).Result() + if err != nil { + global.GVA_LOG.Error("RedisStoreGetError!", zap.Error(err)) + return "" + } + if clear { + err := global.GVA_REDIS.Del(rs.Context, key).Err() + if err != nil { + global.GVA_LOG.Error("RedisStoreClearError!", zap.Error(err)) + return "" + } + } + return val +} + +func (rs *RedisStore) Verify(id, answer string, clear bool) bool { + key := rs.PreKey + id + v := rs.Get(key, clear) + return v == answer +} diff --git a/admin/server/utils/claims.go b/admin/server/utils/claims.go new file mode 100644 index 000000000..e2e5de9f8 --- /dev/null +++ b/admin/server/utils/claims.go @@ -0,0 +1,169 @@ +package utils + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/gaia" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/gin-gonic/gin" + "github.com/gofrs/uuid/v5" + "net" + "time" +) + +func ClearToken(c *gin.Context) { + // 增加cookie x-token 向来源的web添加 + host, _, err := net.SplitHostPort(c.Request.Host) + if err != nil { + host = c.Request.Host + } + + if net.ParseIP(host) != nil { + c.SetCookie("x-token", "", -1, "/", "", false, false) + } else { + c.SetCookie("x-token", "", -1, "/", host, false, false) + } +} + +func SetToken(c *gin.Context, token string, maxAge int) { + // 增加cookie x-token 向来源的web添加 + host, _, err := net.SplitHostPort(c.Request.Host) + if err != nil { + host = c.Request.Host + } + + if net.ParseIP(host) != nil { + c.SetCookie("x-token", token, maxAge, "/", "", false, false) + c.Request.Header.Set("console_token", token) // Extend: add token + } else { + c.SetCookie("x-token", token, maxAge, "/", host, false, false) + c.Request.Header.Set("console_token", token) // Extend: add token + } +} + +func GetToken(c *gin.Context) string { + // Extend Start: Admin and Gaia JWT + token, _ := c.Cookie("x-token") + if token == "" { + j := NewJWT() + token, _ = c.Cookie("x-token") + claims, err := j.ParseToken(token) + if err != nil { + global.GVA_LOG.Error("重新写入cookie token失败,未能成功解析token,请检查请求头是否存在x-token且claims是否为规定结构") + return token + } + SetToken(c, token, int((claims.ExpiresAt.Unix()-time.Now().Unix())/60)) + } + // Extend Stop: Admin and Gaia JWT + return token +} + +func GetClaims(c *gin.Context) (*systemReq.CustomClaims, error) { + token := GetToken(c) + j := NewJWT() + claims, err := j.ParseToken(token) + if err != nil { + global.GVA_LOG.Error("从Gin的Context中获取从jwt解析信息失败, 请检查请求头是否存在x-token且claims是否为规定结构") + } + return claims, err +} + +// GetUserID 从Gin的Context中获取从jwt解析出来的用户ID +func GetUserID(c *gin.Context) uint { + if claims, exists := c.Get("claims"); !exists { + if cl, err := GetClaims(c); err != nil { + return 0 + } else { + return cl.BaseClaims.ID + } + } else { + waitUse := claims.(*systemReq.CustomClaims) + return waitUse.BaseClaims.ID + } +} + +// GetUserUuid 从Gin的Context中获取从jwt解析出来的用户UUID +func GetUserUuid(c *gin.Context) uuid.UUID { + if claims, exists := c.Get("claims"); !exists { + if cl, err := GetClaims(c); err != nil { + return uuid.UUID{} + } else { + return cl.UUID + } + } else { + waitUse := claims.(*systemReq.CustomClaims) + return waitUse.UUID + } +} + +// GetUserAuthorityId 从Gin的Context中获取从jwt解析出来的用户角色id +func GetUserAuthorityId(c *gin.Context) uint { + if claims, exists := c.Get("claims"); !exists { + if cl, err := GetClaims(c); err != nil { + return 0 + } else { + return cl.AuthorityId + } + } else { + waitUse := claims.(*systemReq.CustomClaims) + return waitUse.AuthorityId + } +} + +// GetUserInfo 从Gin的Context中获取从jwt解析出来的用户角色id +func GetUserInfo(c *gin.Context) *systemReq.CustomClaims { + if claims, exists := c.Get("claims"); !exists { + if cl, err := GetClaims(c); err != nil { + return nil + } else { + return cl + } + } else { + waitUse := claims.(*systemReq.CustomClaims) + return waitUse + } +} + +// GetUserName 从Gin的Context中获取从jwt解析出来的用户名 +func GetUserName(c *gin.Context) string { + if claims, exists := c.Get("claims"); !exists { + if cl, err := GetClaims(c); err != nil { + return "" + } else { + return cl.Username + } + } else { + waitUse := claims.(*systemReq.CustomClaims) + return waitUse.Username + } +} + +func LoginToken(user system.Login) (token string, claims systemReq.CustomClaims, err error) { + var account gaia.Account + dr, err := ParseDuration(global.GVA_CONFIG.JWT.BufferTime) + if err != nil { + return token, claims, err + } + j := &JWT{SigningKey: []byte(global.GVA_CONFIG.JWT.SigningKey)} // 唯一签名 + if err = global.GVA_DB.Where("email=?", user.GetUserEmail()).First(&account).Error; err != nil { + return token, claims, err + } + claims = j.CreateClaims(systemReq.BaseClaims{ + UUID: user.GetUUID(), + ID: user.GetUserId(), + NickName: user.GetNickname(), + Username: user.GetUsername(), + AuthorityId: user.GetAuthorityId(), + // Extend Start: add gaia token + UserId: account.ID.String(), + Exp: time.Now().Add(dr).Unix(), + Sub: "Console API Passport", + Email: account.Email, + // Extend Start: add gaia token + }) + token, err = j.CreateToken(claims) + if err != nil { + return + } + return +} diff --git a/admin/server/utils/directory.go b/admin/server/utils/directory.go new file mode 100644 index 000000000..d419feefd --- /dev/null +++ b/admin/server/utils/directory.go @@ -0,0 +1,124 @@ +package utils + +import ( + "errors" + "os" + "path/filepath" + "reflect" + "strings" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "go.uber.org/zap" +) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: PathExists +//@description: 文件目录是否存在 +//@param: path string +//@return: bool, error + +func PathExists(path string) (bool, error) { + fi, err := os.Stat(path) + if err == nil { + if fi.IsDir() { + return true, nil + } + return false, errors.New("存在同名文件") + } + if os.IsNotExist(err) { + return false, nil + } + return false, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: CreateDir +//@description: 批量创建文件夹 +//@param: dirs ...string +//@return: err error + +func CreateDir(dirs ...string) (err error) { + for _, v := range dirs { + exist, err := PathExists(v) + if err != nil { + return err + } + if !exist { + global.GVA_LOG.Debug("create directory" + v) + if err := os.MkdirAll(v, os.ModePerm); err != nil { + global.GVA_LOG.Error("create directory"+v, zap.Any(" error:", err)) + return err + } + } + } + return err +} + +//@author: [songzhibin97](https://github.com/songzhibin97) +//@function: FileMove +//@description: 文件移动供外部调用 +//@param: src string, dst string(src: 源位置,绝对路径or相对路径, dst: 目标位置,绝对路径or相对路径,必须为文件夹) +//@return: err error + +func FileMove(src string, dst string) (err error) { + if dst == "" { + return nil + } + src, err = filepath.Abs(src) + if err != nil { + return err + } + dst, err = filepath.Abs(dst) + if err != nil { + return err + } + revoke := false + dir := filepath.Dir(dst) +Redirect: + _, err = os.Stat(dir) + if err != nil { + err = os.MkdirAll(dir, 0o755) + if err != nil { + return err + } + if !revoke { + revoke = true + goto Redirect + } + } + return os.Rename(src, dst) +} + +func DeLFile(filePath string) error { + return os.RemoveAll(filePath) +} + +//@author: [songzhibin97](https://github.com/songzhibin97) +//@function: TrimSpace +//@description: 去除结构体空格 +//@param: target interface (target: 目标结构体,传入必须是指针类型) +//@return: null + +func TrimSpace(target interface{}) { + t := reflect.TypeOf(target) + if t.Kind() != reflect.Ptr { + return + } + t = t.Elem() + v := reflect.ValueOf(target).Elem() + for i := 0; i < t.NumField(); i++ { + switch v.Field(i).Kind() { + case reflect.String: + v.Field(i).SetString(strings.TrimSpace(v.Field(i).String())) + } + } +} + +// FileExist 判断文件是否存在 +func FileExist(path string) bool { + fi, err := os.Lstat(path) + if err == nil { + return !fi.IsDir() + } + return !os.IsNotExist(err) +} diff --git a/admin/server/utils/encode.go b/admin/server/utils/encode.go new file mode 100644 index 000000000..2bcd2daed --- /dev/null +++ b/admin/server/utils/encode.go @@ -0,0 +1,82 @@ +package utils + +import ( + "bytes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "golang.org/x/crypto/blowfish" + "io" +) + +// PKCS#7填充 +func pkcs7Padding(data []byte, blockSize int) []byte { + padding := blockSize - len(data)%blockSize + padtext := bytes.Repeat([]byte{byte(padding)}, padding) + return append(data, padtext...) +} + +// 去除PKCS#7填充 +func pkcs7UnPadding(data []byte) []byte { + length := len(data) + unpadding := int(data[length-1]) + return data[:(length - unpadding)] +} + +// EncryptBlowfish Blowfish加密函数 +func EncryptBlowfish(plaintext []byte, key string) (string, error) { + block, err := blowfish.NewCipher([]byte(key)) + if err != nil { + return "", err + } + + // 创建初始向量 + iv := make([]byte, blowfish.BlockSize) + if _, err = io.ReadFull(rand.Reader, iv); err != nil { + return "", err + } + + // 对明文进行填充 + plaintext = pkcs7Padding(plaintext, blowfish.BlockSize) + + // 加密 + mode := cipher.NewCBCEncrypter(block, iv) + ciphertext := make([]byte, len(plaintext)) + mode.CryptBlocks(ciphertext, plaintext) + + // 将IV与密文拼接 + result := make([]byte, len(iv)+len(ciphertext)) + copy(result, iv) + copy(result[len(iv):], ciphertext) + + return base64.StdEncoding.EncodeToString(result), nil +} + +// DecryptBlowfish Blowfish解密函数 +func DecryptBlowfish(text string, key string) (string, error) { + if len(text) < blowfish.BlockSize { + return text, nil + } + ciphertext, err := base64.StdEncoding.DecodeString(text) + if err != nil { + return "", err + } + block, err := blowfish.NewCipher([]byte(key)) + if err != nil { + return "", err + } + + // 从密文中提取IV + iv := ciphertext[:blowfish.BlockSize] + ciphertext = ciphertext[blowfish.BlockSize:] + + // 解密 + mode := cipher.NewCBCDecrypter(block, iv) + plaintext := make([]byte, len(ciphertext)) + mode.CryptBlocks(plaintext, ciphertext) + + // 去除填充 + plaintext = pkcs7UnPadding(plaintext) + + return string(plaintext), nil +} diff --git a/admin/server/utils/fmt_plus.go b/admin/server/utils/fmt_plus.go new file mode 100644 index 000000000..8b77d4f26 --- /dev/null +++ b/admin/server/utils/fmt_plus.go @@ -0,0 +1,82 @@ +package utils + +import ( + "fmt" + "math/rand" + "reflect" + "strings" +) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: StructToMap +//@description: 利用反射将结构体转化为map +//@param: obj interface{} +//@return: map[string]interface{} + +func StructToMap(obj interface{}) map[string]interface{} { + obj1 := reflect.TypeOf(obj) + obj2 := reflect.ValueOf(obj) + + data := make(map[string]interface{}) + for i := 0; i < obj1.NumField(); i++ { + if obj1.Field(i).Tag.Get("mapstructure") != "" { + data[obj1.Field(i).Tag.Get("mapstructure")] = obj2.Field(i).Interface() + } else { + data[obj1.Field(i).Name] = obj2.Field(i).Interface() + } + } + return data +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: ArrayToString +//@description: 将数组格式化为字符串 +//@param: array []interface{} +//@return: string + +func ArrayToString(array []interface{}) string { + return strings.Replace(strings.Trim(fmt.Sprint(array), "[]"), " ", ",", -1) +} + +func Pointer[T any](in T) (out *T) { + return &in +} + +func FirstUpper(s string) string { + if s == "" { + return "" + } + return strings.ToUpper(s[:1]) + s[1:] +} + +func FirstLower(s string) string { + if s == "" { + return "" + } + return strings.ToLower(s[:1]) + s[1:] +} + +// MaheHump 将字符串转换为驼峰命名 +func MaheHump(s string) string { + words := strings.Split(s, "-") + + for i := 1; i < len(words); i++ { + words[i] = strings.Title(words[i]) + } + + return strings.Join(words, "") +} + +// 随机字符串 +func RandomString(n int) string { + var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + b := make([]rune, n) + for i := range b { + b[i] = letters[RandomInt(0, len(letters))] + } + return string(b) +} + +func RandomInt(min, max int) int { + return min + rand.Intn(max-min) +} diff --git a/admin/server/utils/fun.go b/admin/server/utils/fun.go new file mode 100644 index 000000000..130753f93 --- /dev/null +++ b/admin/server/utils/fun.go @@ -0,0 +1,71 @@ +package utils + +import "math" + +// InArray @author: [Fantasia](https://www.npc0.com) +// @function: InArray +// @description: 判断是否在数组中 +// @return: err error, conf config.Server +func InArray(value interface{}, array []interface{}) (isIn bool) { + // 判断array是否数组 + for _, item := range array { + if value == item { + isIn = true + return + } + } + return false +} + +// InUintArray @author: [Fantasia](https://www.npc0.com) +// @function: InUintArray +// @description: 判断是否在uint数组中 +// @return: err error, conf config.Server +func InUintArray(value uint, array []uint) (isIn bool) { + // 判断array是否数组 + for _, item := range array { + if value == item { + isIn = true + return + } + } + return false +} + +// InStringArray @author: [Fantasia](https://www.npc0.com) +// @function: InStringArray +// @description: 判断是否在字符串数组中 +// @return: err error, conf config.Server +func InStringArray(value string, array []string) (isIn bool) { + // 判断array是否数组 + for _, item := range array { + if value == item { + isIn = true + return + } + } + return false +} + +// AddAsteriskToString @author: [Fantasia](https://www.npc0.com) +// @function: AddAsteriskToString +// @description: 字符串加星号 +// @return: err error, conf config.Server +func AddAsteriskToString(s string) string { + // 计算要插入的星号数量 + num := 0 + stars := "" + // 计算插入位置 + insertPos := len(s) / 2 + numStars := int(math.Ceil(float64(len(s)) / 5)) + for i := 0; i < numStars; i++ { + if num > 8 { + continue + } + stars += "*" + num += 1 + } + // 插入星号 + result := s[:insertPos] + stars + s[insertPos:] + return result +} diff --git a/admin/server/utils/hash.go b/admin/server/utils/hash.go new file mode 100644 index 000000000..9c3564b49 --- /dev/null +++ b/admin/server/utils/hash.go @@ -0,0 +1,31 @@ +package utils + +import ( + "crypto/md5" + "encoding/hex" + "golang.org/x/crypto/bcrypt" +) + +// BcryptHash 使用 bcrypt 对密码进行加密 +func BcryptHash(password string) string { + bytes, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(bytes) +} + +// BcryptCheck 对比明文密码和数据库的哈希值 +func BcryptCheck(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: MD5V +//@description: md5加密 +//@param: str []byte +//@return: string + +func MD5V(str []byte, b ...byte) string { + h := md5.New() + h.Write(str) + return hex.EncodeToString(h.Sum(b)) +} diff --git a/admin/server/utils/human_duration.go b/admin/server/utils/human_duration.go new file mode 100644 index 000000000..0cdb055c6 --- /dev/null +++ b/admin/server/utils/human_duration.go @@ -0,0 +1,29 @@ +package utils + +import ( + "strconv" + "strings" + "time" +) + +func ParseDuration(d string) (time.Duration, error) { + d = strings.TrimSpace(d) + dr, err := time.ParseDuration(d) + if err == nil { + return dr, nil + } + if strings.Contains(d, "d") { + index := strings.Index(d, "d") + + hour, _ := strconv.Atoi(d[:index]) + dr = time.Hour * 24 * time.Duration(hour) + ndr, err := time.ParseDuration(d[index+1:]) + if err != nil { + return dr, nil + } + return dr + ndr, nil + } + + dv, err := strconv.ParseInt(d, 10, 64) + return time.Duration(dv), err +} diff --git a/admin/server/utils/human_duration_test.go b/admin/server/utils/human_duration_test.go new file mode 100644 index 000000000..8a5294b29 --- /dev/null +++ b/admin/server/utils/human_duration_test.go @@ -0,0 +1,49 @@ +package utils + +import ( + "testing" + "time" +) + +func TestParseDuration(t *testing.T) { + type args struct { + d string + } + tests := []struct { + name string + args args + want time.Duration + wantErr bool + }{ + { + name: "5h20m", + args: args{"5h20m"}, + want: time.Hour*5 + 20*time.Minute, + wantErr: false, + }, + { + name: "1d5h20m", + args: args{"1d5h20m"}, + want: 24*time.Hour + time.Hour*5 + 20*time.Minute, + wantErr: false, + }, + { + name: "1d", + args: args{"1d"}, + want: 24 * time.Hour, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseDuration(tt.args.d) + if (err != nil) != tt.wantErr { + t.Errorf("ParseDuration() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("ParseDuration() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/admin/server/utils/json.go b/admin/server/utils/json.go new file mode 100644 index 000000000..8c4118c7b --- /dev/null +++ b/admin/server/utils/json.go @@ -0,0 +1,34 @@ +package utils + +import ( + "encoding/json" + "strings" +) + +func GetJSONKeys(jsonStr string) (keys []string, err error) { + // 使用json.Decoder,以便在解析过程中记录键的顺序 + dec := json.NewDecoder(strings.NewReader(jsonStr)) + t, err := dec.Token() + if err != nil { + return nil, err + } + // 确保数据是一个对象 + if t != json.Delim('{') { + return nil, err + } + for dec.More() { + t, err = dec.Token() + if err != nil { + return nil, err + } + keys = append(keys, t.(string)) + + // 解析值 + var value interface{} + err = dec.Decode(&value) + if err != nil { + return nil, err + } + } + return keys, nil +} diff --git a/admin/server/utils/json_test.go b/admin/server/utils/json_test.go new file mode 100644 index 000000000..f21a67922 --- /dev/null +++ b/admin/server/utils/json_test.go @@ -0,0 +1,53 @@ +package utils + +import ( + "fmt" + "testing" +) + +func TestGetJSONKeys(t *testing.T) { + var jsonStr = ` + { + "Name": "test", + "TableName": "test", + "TemplateID": "test", + "TemplateInfo": "test", + "Limit": 0 +}` + keys, err := GetJSONKeys(jsonStr) + if err != nil { + t.Errorf("GetJSONKeys failed" + err.Error()) + return + } + if len(keys) != 5 { + t.Errorf("GetJSONKeys failed" + err.Error()) + return + } + if keys[0] != "Name" { + t.Errorf("GetJSONKeys failed" + err.Error()) + + return + } + if keys[1] != "TableName" { + t.Errorf("GetJSONKeys failed" + err.Error()) + + return + } + if keys[2] != "TemplateID" { + t.Errorf("GetJSONKeys failed" + err.Error()) + + return + } + if keys[3] != "TemplateInfo" { + t.Errorf("GetJSONKeys failed" + err.Error()) + + return + } + if keys[4] != "Limit" { + t.Errorf("GetJSONKeys failed" + err.Error()) + + return + } + + fmt.Println(keys) +} diff --git a/admin/server/utils/jwt.go b/admin/server/utils/jwt.go new file mode 100644 index 000000000..998443eae --- /dev/null +++ b/admin/server/utils/jwt.go @@ -0,0 +1,93 @@ +package utils + +import ( + "errors" + "time" + + jwt "github.com/golang-jwt/jwt/v4" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" +) + +type JWT struct { + SigningKey []byte +} + +var ( + TokenExpired = errors.New("Token is expired") + TokenNotValidYet = errors.New("Token not active yet") + TokenMalformed = errors.New("That's not even a token") + TokenInvalid = errors.New("Couldn't handle this token:") +) + +func NewJWT() *JWT { + return &JWT{ + []byte(global.GVA_CONFIG.JWT.SigningKey), + } +} + +func (j *JWT) CreateClaims(baseClaims request.BaseClaims) request.CustomClaims { + bf, _ := ParseDuration(global.GVA_CONFIG.JWT.BufferTime) + ep, _ := ParseDuration(global.GVA_CONFIG.JWT.ExpiresTime) + claims := request.CustomClaims{ + BaseClaims: baseClaims, + // Extend Start: add gaia token + UserId: baseClaims.UserId, + Exp: baseClaims.Exp, + Sub: baseClaims.Sub, + Email: baseClaims.Email, + BufferTime: int64(bf / time.Second), // 缓冲时间1天 缓冲时间内会获得新的token刷新令牌 此时一个用户会存在两个有效令牌 但是前端只留一个 另一个会丢失 + RegisteredClaims: jwt.RegisteredClaims{ + //Audience: jwt.ClaimStrings{"GVA"}, // Extend: Gaia 受众移除 + NotBefore: jwt.NewNumericDate(time.Now().Add(-1000)), // 签名生效时间 + ExpiresAt: jwt.NewNumericDate(time.Now().Add(ep)), // 过期时间 1天 配置文件 + Issuer: global.GVA_CONFIG.JWT.Issuer, // 签名的发行者 + }, + } + return claims +} + +// 创建一个token +func (j *JWT) CreateToken(claims request.CustomClaims) (string, error) { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(j.SigningKey) +} + +// CreateTokenByOldToken 旧token 换新token 使用归并回源避免并发问题 +func (j *JWT) CreateTokenByOldToken(oldToken string, claims request.CustomClaims) (string, error) { + v, err, _ := global.GVA_Concurrency_Control.Do("JWT:"+oldToken, func() (interface{}, error) { + return j.CreateToken(claims) + }) + return v.(string), err +} + +// 解析 token +func (j *JWT) ParseToken(tokenString string) (*request.CustomClaims, error) { + token, err := jwt.ParseWithClaims(tokenString, &request.CustomClaims{}, func(token *jwt.Token) (i interface{}, e error) { + return j.SigningKey, nil + }) + if err != nil { + if ve, ok := err.(*jwt.ValidationError); ok { + if ve.Errors&jwt.ValidationErrorMalformed != 0 { + return nil, TokenMalformed + } else if ve.Errors&jwt.ValidationErrorExpired != 0 { + // Token is expired + return nil, TokenExpired + } else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 { + return nil, TokenNotValidYet + } else { + return nil, TokenInvalid + } + } + } + if token != nil { + if claims, ok := token.Claims.(*request.CustomClaims); ok && token.Valid { + return claims, nil + } + return nil, TokenInvalid + + } else { + return nil, TokenInvalid + } +} diff --git a/admin/server/utils/plugin/plugin.go b/admin/server/utils/plugin/plugin.go new file mode 100644 index 000000000..a59d5b52a --- /dev/null +++ b/admin/server/utils/plugin/plugin.go @@ -0,0 +1,18 @@ +package plugin + +import ( + "github.com/gin-gonic/gin" +) + +const ( + OnlyFuncName = "Plugin" +) + +// Plugin 插件模式接口化 +type Plugin interface { + // Register 注册路由 + Register(group *gin.RouterGroup) + + // RouterPath 用户返回注册路由 + RouterPath() string +} diff --git a/admin/server/utils/plugin/v2/plugin.go b/admin/server/utils/plugin/v2/plugin.go new file mode 100644 index 000000000..4dac0ab1a --- /dev/null +++ b/admin/server/utils/plugin/v2/plugin.go @@ -0,0 +1,11 @@ +package plugin + +import ( + "github.com/gin-gonic/gin" +) + +// Plugin 插件模式接口化v2 +type Plugin interface { + // Register 注册路由 + Register(group *gin.Engine) +} diff --git a/admin/server/utils/reload.go b/admin/server/utils/reload.go new file mode 100644 index 000000000..de5499bf3 --- /dev/null +++ b/admin/server/utils/reload.go @@ -0,0 +1,18 @@ +package utils + +import ( + "errors" + "os" + "os/exec" + "runtime" + "strconv" +) + +func Reload() error { + if runtime.GOOS == "windows" { + return errors.New("系统不支持") + } + pid := os.Getpid() + cmd := exec.Command("kill", "-1", strconv.Itoa(pid)) + return cmd.Run() +} diff --git a/admin/server/utils/request/http.go b/admin/server/utils/request/http.go new file mode 100644 index 000000000..86d0d1509 --- /dev/null +++ b/admin/server/utils/request/http.go @@ -0,0 +1,62 @@ +package request + +import ( + "bytes" + "encoding/json" + "net/http" + "net/url" +) + +func HttpRequest( + urlStr string, + method string, + headers map[string]string, + params map[string]string, + data any) (*http.Response, error) { + // 创建URL + u, err := url.Parse(urlStr) + if err != nil { + return nil, err + } + + // 添加查询参数 + query := u.Query() + for k, v := range params { + query.Set(k, v) + } + u.RawQuery = query.Encode() + + // 将数据编码为JSON + buf := new(bytes.Buffer) + if data != nil { + b, err := json.Marshal(data) + if err != nil { + return nil, err + } + buf = bytes.NewBuffer(b) + } + + // 创建请求 + req, err := http.NewRequest(method, u.String(), buf) + + if err != nil { + return nil, err + } + + for k, v := range headers { + req.Header.Set(k, v) + } + + if data != nil { + req.Header.Set("Content-Type", "application/json") + } + + // 发送请求 + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + + // 返回响应,让调用者处理 + return resp, nil +} diff --git a/admin/server/utils/server.go b/admin/server/utils/server.go new file mode 100644 index 000000000..8c14cd79d --- /dev/null +++ b/admin/server/utils/server.go @@ -0,0 +1,126 @@ +package utils + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "runtime" + "time" + + "github.com/shirou/gopsutil/v3/cpu" + "github.com/shirou/gopsutil/v3/disk" + "github.com/shirou/gopsutil/v3/mem" +) + +const ( + B = 1 + KB = 1024 * B + MB = 1024 * KB + GB = 1024 * MB +) + +type Server struct { + Os Os `json:"os"` + Cpu Cpu `json:"cpu"` + Ram Ram `json:"ram"` + Disk []Disk `json:"disk"` +} + +type Os struct { + GOOS string `json:"goos"` + NumCPU int `json:"numCpu"` + Compiler string `json:"compiler"` + GoVersion string `json:"goVersion"` + NumGoroutine int `json:"numGoroutine"` +} + +type Cpu struct { + Cpus []float64 `json:"cpus"` + Cores int `json:"cores"` +} + +type Ram struct { + UsedMB int `json:"usedMb"` + TotalMB int `json:"totalMb"` + UsedPercent int `json:"usedPercent"` +} + +type Disk struct { + MountPoint string `json:"mountPoint"` + UsedMB int `json:"usedMb"` + UsedGB int `json:"usedGb"` + TotalMB int `json:"totalMb"` + TotalGB int `json:"totalGb"` + UsedPercent int `json:"usedPercent"` +} + +//@author: [SliverHorn](https://github.com/SliverHorn) +//@function: InitCPU +//@description: OS信息 +//@return: o Os, err error + +func InitOS() (o Os) { + o.GOOS = runtime.GOOS + o.NumCPU = runtime.NumCPU() + o.Compiler = runtime.Compiler + o.GoVersion = runtime.Version() + o.NumGoroutine = runtime.NumGoroutine() + return o +} + +//@author: [SliverHorn](https://github.com/SliverHorn) +//@function: InitCPU +//@description: CPU信息 +//@return: c Cpu, err error + +func InitCPU() (c Cpu, err error) { + if cores, err := cpu.Counts(false); err != nil { + return c, err + } else { + c.Cores = cores + } + if cpus, err := cpu.Percent(time.Duration(200)*time.Millisecond, true); err != nil { + return c, err + } else { + c.Cpus = cpus + } + return c, nil +} + +//@author: [SliverHorn](https://github.com/SliverHorn) +//@function: InitRAM +//@description: RAM信息 +//@return: r Ram, err error + +func InitRAM() (r Ram, err error) { + if u, err := mem.VirtualMemory(); err != nil { + return r, err + } else { + r.UsedMB = int(u.Used) / MB + r.TotalMB = int(u.Total) / MB + r.UsedPercent = int(u.UsedPercent) + } + return r, nil +} + +//@author: [SliverHorn](https://github.com/SliverHorn) +//@function: InitDisk +//@description: 硬盘信息 +//@return: d Disk, err error + +func InitDisk() (d []Disk, err error) { + for i := range global.GVA_CONFIG.DiskList { + mp := global.GVA_CONFIG.DiskList[i].MountPoint + if u, err := disk.Usage(mp); err != nil { + return d, err + } else { + d = append(d, Disk{ + MountPoint: mp, + UsedMB: int(u.Used) / MB, + UsedGB: int(u.Used) / GB, + TotalMB: int(u.Total) / MB, + TotalGB: int(u.Total) / GB, + UsedPercent: int(u.UsedPercent), + }) + } + } + return d, nil +} diff --git a/admin/server/utils/timer/timed_task.go b/admin/server/utils/timer/timed_task.go new file mode 100644 index 000000000..9f761436f --- /dev/null +++ b/admin/server/utils/timer/timed_task.go @@ -0,0 +1,229 @@ +package timer + +import ( + "github.com/robfig/cron/v3" + "sync" +) + +type Timer interface { + // 寻找所有Cron + FindCronList() map[string]*taskManager + // 添加Task 方法形式以秒的形式加入 + AddTaskByFuncWithSecond(cronName string, spec string, fun func(), taskName string, option ...cron.Option) (cron.EntryID, error) // 添加Task Func以秒的形式加入 + // 添加Task 接口形式以秒的形式加入 + AddTaskByJobWithSeconds(cronName string, spec string, job interface{ Run() }, taskName string, option ...cron.Option) (cron.EntryID, error) + // 通过函数的方法添加任务 + AddTaskByFunc(cronName string, spec string, task func(), taskName string, option ...cron.Option) (cron.EntryID, error) + // 通过接口的方法添加任务 要实现一个带有 Run方法的接口触发 + AddTaskByJob(cronName string, spec string, job interface{ Run() }, taskName string, option ...cron.Option) (cron.EntryID, error) + // 获取对应taskName的cron 可能会为空 + FindCron(cronName string) (*taskManager, bool) + // 指定cron开始执行 + StartCron(cronName string) + // 指定cron停止执行 + StopCron(cronName string) + // 查找指定cron下的指定task + FindTask(cronName string, taskName string) (*task, bool) + // 根据id删除指定cron下的指定task + RemoveTask(cronName string, id int) + // 根据taskName删除指定cron下的指定task + RemoveTaskByName(cronName string, taskName string) + // 清理掉指定cronName + Clear(cronName string) + // 停止所有的cron + Close() +} + +type task struct { + EntryID cron.EntryID + Spec string + TaskName string +} + +type taskManager struct { + corn *cron.Cron + tasks map[cron.EntryID]*task +} + +// timer 定时任务管理 +type timer struct { + cronList map[string]*taskManager + sync.Mutex +} + +// AddTaskByFunc 通过函数的方法添加任务 +func (t *timer) AddTaskByFunc(cronName string, spec string, fun func(), taskName string, option ...cron.Option) (cron.EntryID, error) { + t.Lock() + defer t.Unlock() + if _, ok := t.cronList[cronName]; !ok { + tasks := make(map[cron.EntryID]*task) + t.cronList[cronName] = &taskManager{ + corn: cron.New(option...), + tasks: tasks, + } + } + id, err := t.cronList[cronName].corn.AddFunc(spec, fun) + t.cronList[cronName].corn.Start() + t.cronList[cronName].tasks[id] = &task{ + EntryID: id, + Spec: spec, + TaskName: taskName, + } + return id, err +} + +// AddTaskByFuncWithSeconds 通过函数的方法使用WithSeconds添加任务 +func (t *timer) AddTaskByFuncWithSecond(cronName string, spec string, fun func(), taskName string, option ...cron.Option) (cron.EntryID, error) { + t.Lock() + defer t.Unlock() + option = append(option, cron.WithSeconds()) + if _, ok := t.cronList[cronName]; !ok { + tasks := make(map[cron.EntryID]*task) + t.cronList[cronName] = &taskManager{ + corn: cron.New(option...), + tasks: tasks, + } + } + id, err := t.cronList[cronName].corn.AddFunc(spec, fun) + t.cronList[cronName].corn.Start() + t.cronList[cronName].tasks[id] = &task{ + EntryID: id, + Spec: spec, + TaskName: taskName, + } + return id, err +} + +// AddTaskByJob 通过接口的方法添加任务 +func (t *timer) AddTaskByJob(cronName string, spec string, job interface{ Run() }, taskName string, option ...cron.Option) (cron.EntryID, error) { + t.Lock() + defer t.Unlock() + if _, ok := t.cronList[cronName]; !ok { + tasks := make(map[cron.EntryID]*task) + t.cronList[cronName] = &taskManager{ + corn: cron.New(option...), + tasks: tasks, + } + } + id, err := t.cronList[cronName].corn.AddJob(spec, job) + t.cronList[cronName].corn.Start() + t.cronList[cronName].tasks[id] = &task{ + EntryID: id, + Spec: spec, + TaskName: taskName, + } + return id, err +} + +// AddTaskByJobWithSeconds 通过接口的方法添加任务 +func (t *timer) AddTaskByJobWithSeconds(cronName string, spec string, job interface{ Run() }, taskName string, option ...cron.Option) (cron.EntryID, error) { + t.Lock() + defer t.Unlock() + option = append(option, cron.WithSeconds()) + if _, ok := t.cronList[cronName]; !ok { + tasks := make(map[cron.EntryID]*task) + t.cronList[cronName] = &taskManager{ + corn: cron.New(option...), + tasks: tasks, + } + } + id, err := t.cronList[cronName].corn.AddJob(spec, job) + t.cronList[cronName].corn.Start() + t.cronList[cronName].tasks[id] = &task{ + EntryID: id, + Spec: spec, + TaskName: taskName, + } + return id, err +} + +// FindCron 获取对应cronName的cron 可能会为空 +func (t *timer) FindCron(cronName string) (*taskManager, bool) { + t.Lock() + defer t.Unlock() + v, ok := t.cronList[cronName] + return v, ok +} + +// FindTask 获取对应cronName的cron 可能会为空 +func (t *timer) FindTask(cronName string, taskName string) (*task, bool) { + t.Lock() + defer t.Unlock() + v, ok := t.cronList[cronName] + if !ok { + return nil, ok + } + for _, t2 := range v.tasks { + if t2.TaskName == taskName { + return t2, true + } + } + return nil, false +} + +// FindCronList 获取所有的任务列表 +func (t *timer) FindCronList() map[string]*taskManager { + t.Lock() + defer t.Unlock() + return t.cronList +} + +// StartCron 开始任务 +func (t *timer) StartCron(cronName string) { + t.Lock() + defer t.Unlock() + if v, ok := t.cronList[cronName]; ok { + v.corn.Start() + } +} + +// StopCron 停止任务 +func (t *timer) StopCron(cronName string) { + t.Lock() + defer t.Unlock() + if v, ok := t.cronList[cronName]; ok { + v.corn.Stop() + } +} + +// Remove 从cronName 删除指定任务 +func (t *timer) RemoveTask(cronName string, id int) { + t.Lock() + defer t.Unlock() + if v, ok := t.cronList[cronName]; ok { + v.corn.Remove(cron.EntryID(id)) + delete(v.tasks, cron.EntryID(id)) + } +} + +// RemoveTaskByName 从cronName 使用taskName 删除指定任务 +func (t *timer) RemoveTaskByName(cronName string, taskName string) { + fTask, ok := t.FindTask(cronName, taskName) + if !ok { + return + } + t.RemoveTask(cronName, int(fTask.EntryID)) +} + +// Clear 清除任务 +func (t *timer) Clear(cronName string) { + t.Lock() + defer t.Unlock() + if v, ok := t.cronList[cronName]; ok { + v.corn.Stop() + delete(t.cronList, cronName) + } +} + +// Close 释放资源 +func (t *timer) Close() { + t.Lock() + defer t.Unlock() + for _, v := range t.cronList { + v.corn.Stop() + } +} + +func NewTimerTask() Timer { + return &timer{cronList: make(map[string]*taskManager)} +} diff --git a/admin/server/utils/timer/timed_task_test.go b/admin/server/utils/timer/timed_task_test.go new file mode 100644 index 000000000..9f2c02c0b --- /dev/null +++ b/admin/server/utils/timer/timed_task_test.go @@ -0,0 +1,72 @@ +package timer + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +var job = mockJob{} + +type mockJob struct{} + +func (job mockJob) Run() { + mockFunc() +} + +func mockFunc() { + time.Sleep(time.Second) + fmt.Println("1s...") +} + +func TestNewTimerTask(t *testing.T) { + tm := NewTimerTask() + _tm := tm.(*timer) + + { + _, err := tm.AddTaskByFunc("func", "@every 1s", mockFunc, "测试mockfunc") + assert.Nil(t, err) + _, ok := _tm.cronList["func"] + if !ok { + t.Error("no find func") + } + } + + { + _, err := tm.AddTaskByJob("job", "@every 1s", job, "测试job mockfunc") + assert.Nil(t, err) + _, ok := _tm.cronList["job"] + if !ok { + t.Error("no find job") + } + } + + { + _, ok := tm.FindCron("func") + if !ok { + t.Error("no find func") + } + _, ok = tm.FindCron("job") + if !ok { + t.Error("no find job") + } + _, ok = tm.FindCron("none") + if ok { + t.Error("find none") + } + } + { + tm.Clear("func") + _, ok := tm.FindCron("func") + if ok { + t.Error("find func") + } + } + { + a := tm.FindCronList() + b, c := tm.FindCron("job") + fmt.Println(a, b, c) + } +} diff --git a/admin/server/utils/upload/aliyun_oss.go b/admin/server/utils/upload/aliyun_oss.go new file mode 100644 index 000000000..cf7410161 --- /dev/null +++ b/admin/server/utils/upload/aliyun_oss.go @@ -0,0 +1,75 @@ +package upload + +import ( + "errors" + "mime/multipart" + "time" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "go.uber.org/zap" +) + +type AliyunOSS struct{} + +func (*AliyunOSS) UploadFile(file *multipart.FileHeader) (string, string, error) { + bucket, err := NewBucket() + if err != nil { + global.GVA_LOG.Error("function AliyunOSS.NewBucket() Failed", zap.Any("err", err.Error())) + return "", "", errors.New("function AliyunOSS.NewBucket() Failed, err:" + err.Error()) + } + + // 读取本地文件。 + f, openError := file.Open() + if openError != nil { + global.GVA_LOG.Error("function file.Open() Failed", zap.Any("err", openError.Error())) + return "", "", errors.New("function file.Open() Failed, err:" + openError.Error()) + } + defer f.Close() // 创建文件 defer 关闭 + // 上传阿里云路径 文件名格式 自己可以改 建议保证唯一性 + // yunFileTmpPath := filepath.Join("uploads", time.Now().Format("2006-01-02")) + "/" + file.Filename + yunFileTmpPath := global.GVA_CONFIG.AliyunOSS.BasePath + "/" + "uploads" + "/" + time.Now().Format("2006-01-02") + "/" + file.Filename + + // 上传文件流。 + err = bucket.PutObject(yunFileTmpPath, f) + if err != nil { + global.GVA_LOG.Error("function formUploader.Put() Failed", zap.Any("err", err.Error())) + return "", "", errors.New("function formUploader.Put() Failed, err:" + err.Error()) + } + + return global.GVA_CONFIG.AliyunOSS.BucketUrl + "/" + yunFileTmpPath, yunFileTmpPath, nil +} + +func (*AliyunOSS) DeleteFile(key string) error { + bucket, err := NewBucket() + if err != nil { + global.GVA_LOG.Error("function AliyunOSS.NewBucket() Failed", zap.Any("err", err.Error())) + return errors.New("function AliyunOSS.NewBucket() Failed, err:" + err.Error()) + } + + // 删除单个文件。objectName表示删除OSS文件时需要指定包含文件后缀在内的完整路径,例如abc/efg/123.jpg。 + // 如需删除文件夹,请将objectName设置为对应的文件夹名称。如果文件夹非空,则需要将文件夹下的所有object删除后才能删除该文件夹。 + err = bucket.DeleteObject(key) + if err != nil { + global.GVA_LOG.Error("function bucketManager.Delete() failed", zap.Any("err", err.Error())) + return errors.New("function bucketManager.Delete() failed, err:" + err.Error()) + } + + return nil +} + +func NewBucket() (*oss.Bucket, error) { + // 创建OSSClient实例。 + client, err := oss.New(global.GVA_CONFIG.AliyunOSS.Endpoint, global.GVA_CONFIG.AliyunOSS.AccessKeyId, global.GVA_CONFIG.AliyunOSS.AccessKeySecret) + if err != nil { + return nil, err + } + + // 获取存储空间。 + bucket, err := client.Bucket(global.GVA_CONFIG.AliyunOSS.BucketName) + if err != nil { + return nil, err + } + + return bucket, nil +} diff --git a/admin/server/utils/upload/aws_s3.go b/admin/server/utils/upload/aws_s3.go new file mode 100644 index 000000000..342f9b8b4 --- /dev/null +++ b/admin/server/utils/upload/aws_s3.go @@ -0,0 +1,97 @@ +package upload + +import ( + "errors" + "fmt" + "mime/multipart" + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/aws/aws-sdk-go/service/s3/s3manager" + "go.uber.org/zap" +) + +type AwsS3 struct{} + +//@author: [WqyJh](https://github.com/WqyJh) +//@object: *AwsS3 +//@function: UploadFile +//@description: Upload file to Aws S3 using aws-sdk-go. See https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/s3-example-basic-bucket-operations.html#s3-examples-bucket-ops-upload-file-to-bucket +//@param: file *multipart.FileHeader +//@return: string, string, error + +func (*AwsS3) UploadFile(file *multipart.FileHeader) (string, string, error) { + session := newSession() + uploader := s3manager.NewUploader(session) + + fileKey := fmt.Sprintf("%d%s", time.Now().Unix(), file.Filename) + filename := global.GVA_CONFIG.AwsS3.PathPrefix + "/" + fileKey + f, openError := file.Open() + if openError != nil { + global.GVA_LOG.Error("function file.Open() failed", zap.Any("err", openError.Error())) + return "", "", errors.New("function file.Open() failed, err:" + openError.Error()) + } + defer f.Close() // 创建文件 defer 关闭 + + _, err := uploader.Upload(&s3manager.UploadInput{ + Bucket: aws.String(global.GVA_CONFIG.AwsS3.Bucket), + Key: aws.String(filename), + Body: f, + }) + if err != nil { + global.GVA_LOG.Error("function uploader.Upload() failed", zap.Any("err", err.Error())) + return "", "", err + } + + return global.GVA_CONFIG.AwsS3.BaseURL + "/" + filename, fileKey, nil +} + +//@author: [WqyJh](https://github.com/WqyJh) +//@object: *AwsS3 +//@function: DeleteFile +//@description: Delete file from Aws S3 using aws-sdk-go. See https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/s3-example-basic-bucket-operations.html#s3-examples-bucket-ops-delete-bucket-item +//@param: file *multipart.FileHeader +//@return: string, string, error + +func (*AwsS3) DeleteFile(key string) error { + session := newSession() + svc := s3.New(session) + filename := global.GVA_CONFIG.AwsS3.PathPrefix + "/" + key + bucket := global.GVA_CONFIG.AwsS3.Bucket + + _, err := svc.DeleteObject(&s3.DeleteObjectInput{ + Bucket: aws.String(bucket), + Key: aws.String(filename), + }) + if err != nil { + global.GVA_LOG.Error("function svc.DeleteObject() failed", zap.Any("err", err.Error())) + return errors.New("function svc.DeleteObject() failed, err:" + err.Error()) + } + + _ = svc.WaitUntilObjectNotExists(&s3.HeadObjectInput{ + Bucket: aws.String(bucket), + Key: aws.String(filename), + }) + return nil +} + +// newSession Create S3 session +func newSession() *session.Session { + sess, _ := session.NewSession(&aws.Config{ + Region: aws.String(global.GVA_CONFIG.AwsS3.Region), + Endpoint: aws.String(global.GVA_CONFIG.AwsS3.Endpoint), //minio在这里设置地址,可以兼容 + S3ForcePathStyle: aws.Bool(global.GVA_CONFIG.AwsS3.S3ForcePathStyle), + DisableSSL: aws.Bool(global.GVA_CONFIG.AwsS3.DisableSSL), + Credentials: credentials.NewStaticCredentials( + global.GVA_CONFIG.AwsS3.SecretID, + global.GVA_CONFIG.AwsS3.SecretKey, + "", + ), + }) + return sess +} diff --git a/admin/server/utils/upload/cloudflare_r2.go b/admin/server/utils/upload/cloudflare_r2.go new file mode 100644 index 000000000..a68d212bc --- /dev/null +++ b/admin/server/utils/upload/cloudflare_r2.go @@ -0,0 +1,85 @@ +package upload + +import ( + "errors" + "fmt" + "mime/multipart" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/aws/aws-sdk-go/service/s3/s3manager" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "go.uber.org/zap" +) + +type CloudflareR2 struct{} + +func (c *CloudflareR2) UploadFile(file *multipart.FileHeader) (fileUrl string, fileName string, err error) { + session := c.newSession() + client := s3manager.NewUploader(session) + + fileKey := fmt.Sprintf("%d_%s", time.Now().Unix(), file.Filename) + fileName = fmt.Sprintf("%s/%s", global.GVA_CONFIG.CloudflareR2.Path, fileKey) + f, openError := file.Open() + if openError != nil { + global.GVA_LOG.Error("function file.Open() failed", zap.Any("err", openError.Error())) + return "", "", errors.New("function file.Open() failed, err:" + openError.Error()) + } + defer f.Close() // 创建文件 defer 关闭 + + input := &s3manager.UploadInput{ + Bucket: aws.String(global.GVA_CONFIG.CloudflareR2.Bucket), + Key: aws.String(fileName), + Body: f, + } + + _, err = client.Upload(input) + if err != nil { + global.GVA_LOG.Error("function uploader.Upload() failed", zap.Any("err", err.Error())) + return "", "", err + } + + return fmt.Sprintf("%s/%s", global.GVA_CONFIG.CloudflareR2.BaseURL, + fileName), + fileKey, + nil +} + +func (c *CloudflareR2) DeleteFile(key string) error { + session := newSession() + svc := s3.New(session) + filename := global.GVA_CONFIG.CloudflareR2.Path + "/" + key + bucket := global.GVA_CONFIG.CloudflareR2.Bucket + + _, err := svc.DeleteObject(&s3.DeleteObjectInput{ + Bucket: aws.String(bucket), + Key: aws.String(filename), + }) + if err != nil { + global.GVA_LOG.Error("function svc.DeleteObject() failed", zap.Any("err", err.Error())) + return errors.New("function svc.DeleteObject() failed, err:" + err.Error()) + } + + _ = svc.WaitUntilObjectNotExists(&s3.HeadObjectInput{ + Bucket: aws.String(bucket), + Key: aws.String(filename), + }) + return nil +} + +func (*CloudflareR2) newSession() *session.Session { + endpoint := fmt.Sprintf("%s.r2.cloudflarestorage.com", global.GVA_CONFIG.CloudflareR2.AccountID) + + return session.Must(session.NewSession(&aws.Config{ + Region: aws.String("auto"), + Endpoint: aws.String(endpoint), + Credentials: credentials.NewStaticCredentials( + global.GVA_CONFIG.CloudflareR2.AccessKeyID, + global.GVA_CONFIG.CloudflareR2.SecretAccessKey, + "", + ), + })) +} diff --git a/admin/server/utils/upload/local.go b/admin/server/utils/upload/local.go new file mode 100644 index 000000000..bb0784995 --- /dev/null +++ b/admin/server/utils/upload/local.go @@ -0,0 +1,109 @@ +package upload + +import ( + "errors" + "io" + "mime/multipart" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "go.uber.org/zap" +) + +var mu sync.Mutex + +type Local struct{} + +//@author: [piexlmax](https://github.com/piexlmax) +//@author: [ccfish86](https://github.com/ccfish86) +//@author: [SliverHorn](https://github.com/SliverHorn) +//@object: *Local +//@function: UploadFile +//@description: 上传文件 +//@param: file *multipart.FileHeader +//@return: string, string, error + +func (*Local) UploadFile(file *multipart.FileHeader) (string, string, error) { + // 读取文件后缀 + ext := filepath.Ext(file.Filename) + // 读取文件名并加密 + name := strings.TrimSuffix(file.Filename, ext) + name = utils.MD5V([]byte(name)) + // 拼接新文件名 + filename := name + "_" + time.Now().Format("20060102150405") + ext + // 尝试创建此路径 + mkdirErr := os.MkdirAll(global.GVA_CONFIG.Local.StorePath, os.ModePerm) + if mkdirErr != nil { + global.GVA_LOG.Error("function os.MkdirAll() failed", zap.Any("err", mkdirErr.Error())) + return "", "", errors.New("function os.MkdirAll() failed, err:" + mkdirErr.Error()) + } + // 拼接路径和文件名 + p := global.GVA_CONFIG.Local.StorePath + "/" + filename + filepath := global.GVA_CONFIG.Local.Path + "/" + filename + + f, openError := file.Open() // 读取文件 + if openError != nil { + global.GVA_LOG.Error("function file.Open() failed", zap.Any("err", openError.Error())) + return "", "", errors.New("function file.Open() failed, err:" + openError.Error()) + } + defer f.Close() // 创建文件 defer 关闭 + + out, createErr := os.Create(p) + if createErr != nil { + global.GVA_LOG.Error("function os.Create() failed", zap.Any("err", createErr.Error())) + + return "", "", errors.New("function os.Create() failed, err:" + createErr.Error()) + } + defer out.Close() // 创建文件 defer 关闭 + + _, copyErr := io.Copy(out, f) // 传输(拷贝)文件 + if copyErr != nil { + global.GVA_LOG.Error("function io.Copy() failed", zap.Any("err", copyErr.Error())) + return "", "", errors.New("function io.Copy() failed, err:" + copyErr.Error()) + } + return filepath, filename, nil +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@author: [ccfish86](https://github.com/ccfish86) +//@author: [SliverHorn](https://github.com/SliverHorn) +//@object: *Local +//@function: DeleteFile +//@description: 删除文件 +//@param: key string +//@return: error + +func (*Local) DeleteFile(key string) error { + // 检查 key 是否为空 + if key == "" { + return errors.New("key不能为空") + } + + // 验证 key 是否包含非法字符或尝试访问存储路径之外的文件 + if strings.Contains(key, "..") || strings.ContainsAny(key, `\/:*?"<>|`) { + return errors.New("非法的key") + } + + p := filepath.Join(global.GVA_CONFIG.Local.StorePath, key) + + // 检查文件是否存在 + if _, err := os.Stat(p); os.IsNotExist(err) { + return errors.New("文件不存在") + } + + // 使用文件锁防止并发删除 + mu.Lock() + defer mu.Unlock() + + err := os.Remove(p) + if err != nil { + return errors.New("文件删除失败: " + err.Error()) + } + + return nil +} diff --git a/admin/server/utils/upload/minio_oss.go b/admin/server/utils/upload/minio_oss.go new file mode 100644 index 000000000..3a6af72ce --- /dev/null +++ b/admin/server/utils/upload/minio_oss.go @@ -0,0 +1,98 @@ +package upload + +import ( + "bytes" + "context" + "errors" + "io" + "mime/multipart" + "path/filepath" + "strings" + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" + "go.uber.org/zap" +) + +var MinioClient *Minio // 优化性能,但是不支持动态配置 + +type Minio struct { + Client *minio.Client + bucket string +} + +func GetMinio(endpoint, accessKeyID, secretAccessKey, bucketName string, useSSL bool) (*Minio, error) { + if MinioClient != nil { + return MinioClient, nil + } + // Initialize minio client object. + minioClient, err := minio.New(endpoint, &minio.Options{ + Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""), + Secure: useSSL, // Set to true if using https + }) + if err != nil { + return nil, err + } + // 尝试创建bucket + err = minioClient.MakeBucket(context.Background(), bucketName, minio.MakeBucketOptions{}) + if err != nil { + // Check to see if we already own this bucket (which happens if you run this twice) + exists, errBucketExists := minioClient.BucketExists(context.Background(), bucketName) + if errBucketExists == nil && exists { + // log.Printf("We already own %s\n", bucketName) + } else { + return nil, err + } + } + MinioClient = &Minio{Client: minioClient, bucket: bucketName} + return MinioClient, nil +} + +func (m *Minio) UploadFile(file *multipart.FileHeader) (filePathres, key string, uploadErr error) { + f, openError := file.Open() + // mutipart.File to os.File + if openError != nil { + global.GVA_LOG.Error("function file.Open() Failed", zap.Any("err", openError.Error())) + return "", "", errors.New("function file.Open() Failed, err:" + openError.Error()) + } + + filecontent := bytes.Buffer{} + _, err := io.Copy(&filecontent, f) + if err != nil { + global.GVA_LOG.Error("读取文件失败", zap.Any("err", err.Error())) + return "", "", errors.New("读取文件失败, err:" + err.Error()) + } + f.Close() // 创建文件 defer 关闭 + + + // 对文件名进行加密存储 + ext := filepath.Ext(file.Filename) + filename := utils.MD5V([]byte(strings.TrimSuffix(file.Filename, ext))) + ext + if global.GVA_CONFIG.Minio.BasePath == "" { + filePathres = "uploads" + "/" + time.Now().Format("2006-01-02") + "/" + filename + } else { + filePathres = global.GVA_CONFIG.Minio.BasePath + "/" + time.Now().Format("2006-01-02") + "/" + filename + } + + // 设置超时10分钟 + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*10) + defer cancel() + + // Upload the file with PutObject 大文件自动切换为分片上传 + info, err := m.Client.PutObject(ctx, global.GVA_CONFIG.Minio.BucketName, filePathres, &filecontent, file.Size, minio.PutObjectOptions{ContentType: "application/octet-stream"}) + if err != nil { + global.GVA_LOG.Error("上传文件到minio失败", zap.Any("err", err.Error())) + return "", "", errors.New("上传文件到minio失败, err:" + err.Error()) + } + return global.GVA_CONFIG.Minio.BucketUrl + "/" + info.Key, filePathres, nil +} + +func (m *Minio) DeleteFile(key string) error { + // Delete the object from MinIO + ctx, _ := context.WithTimeout(context.Background(), time.Second*5) + err := m.Client.RemoveObject(ctx, m.bucket, key, minio.RemoveObjectOptions{}) + return err +} diff --git a/admin/server/utils/upload/obs.go b/admin/server/utils/upload/obs.go new file mode 100644 index 000000000..70ff42e56 --- /dev/null +++ b/admin/server/utils/upload/obs.go @@ -0,0 +1,69 @@ +package upload + +import ( + "mime/multipart" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/huaweicloud/huaweicloud-sdk-go-obs/obs" + "github.com/pkg/errors" +) + +var HuaWeiObs = new(Obs) + +type Obs struct{} + +func NewHuaWeiObsClient() (client *obs.ObsClient, err error) { + return obs.New(global.GVA_CONFIG.HuaWeiObs.AccessKey, global.GVA_CONFIG.HuaWeiObs.SecretKey, global.GVA_CONFIG.HuaWeiObs.Endpoint) +} + +func (o *Obs) UploadFile(file *multipart.FileHeader) (string, string, error) { + // var open multipart.File + open, err := file.Open() + if err != nil { + return "", "", err + } + defer open.Close() + filename := file.Filename + input := &obs.PutObjectInput{ + PutObjectBasicInput: obs.PutObjectBasicInput{ + ObjectOperationInput: obs.ObjectOperationInput{ + Bucket: global.GVA_CONFIG.HuaWeiObs.Bucket, + Key: filename, + }, + HttpHeader: obs.HttpHeader{ + ContentType: file.Header.Get("content-type"), + }, + }, + Body: open, + } + + var client *obs.ObsClient + client, err = NewHuaWeiObsClient() + if err != nil { + return "", "", errors.Wrap(err, "获取华为对象存储对象失败!") + } + + _, err = client.PutObject(input) + if err != nil { + return "", "", errors.Wrap(err, "文件上传失败!") + } + filepath := global.GVA_CONFIG.HuaWeiObs.Path + "/" + filename + return filepath, filename, err +} + +func (o *Obs) DeleteFile(key string) error { + client, err := NewHuaWeiObsClient() + if err != nil { + return errors.Wrap(err, "获取华为对象存储对象失败!") + } + input := &obs.DeleteObjectInput{ + Bucket: global.GVA_CONFIG.HuaWeiObs.Bucket, + Key: key, + } + var output *obs.DeleteObjectOutput + output, err = client.DeleteObject(input) + if err != nil { + return errors.Wrapf(err, "删除对象(%s)失败!, output: %v", key, output) + } + return nil +} diff --git a/admin/server/utils/upload/qiniu.go b/admin/server/utils/upload/qiniu.go new file mode 100644 index 000000000..f4287d590 --- /dev/null +++ b/admin/server/utils/upload/qiniu.go @@ -0,0 +1,96 @@ +package upload + +import ( + "context" + "errors" + "fmt" + "mime/multipart" + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/qiniu/go-sdk/v7/auth/qbox" + "github.com/qiniu/go-sdk/v7/storage" + "go.uber.org/zap" +) + +type Qiniu struct{} + +//@author: [piexlmax](https://github.com/piexlmax) +//@author: [ccfish86](https://github.com/ccfish86) +//@author: [SliverHorn](https://github.com/SliverHorn) +//@object: *Qiniu +//@function: UploadFile +//@description: 上传文件 +//@param: file *multipart.FileHeader +//@return: string, string, error + +func (*Qiniu) UploadFile(file *multipart.FileHeader) (string, string, error) { + putPolicy := storage.PutPolicy{Scope: global.GVA_CONFIG.Qiniu.Bucket} + mac := qbox.NewMac(global.GVA_CONFIG.Qiniu.AccessKey, global.GVA_CONFIG.Qiniu.SecretKey) + upToken := putPolicy.UploadToken(mac) + cfg := qiniuConfig() + formUploader := storage.NewFormUploader(cfg) + ret := storage.PutRet{} + putExtra := storage.PutExtra{Params: map[string]string{"x:name": "github logo"}} + + f, openError := file.Open() + if openError != nil { + global.GVA_LOG.Error("function file.Open() failed", zap.Any("err", openError.Error())) + + return "", "", errors.New("function file.Open() failed, err:" + openError.Error()) + } + defer f.Close() // 创建文件 defer 关闭 + fileKey := fmt.Sprintf("%d%s", time.Now().Unix(), file.Filename) // 文件名格式 自己可以改 建议保证唯一性 + putErr := formUploader.Put(context.Background(), &ret, upToken, fileKey, f, file.Size, &putExtra) + if putErr != nil { + global.GVA_LOG.Error("function formUploader.Put() failed", zap.Any("err", putErr.Error())) + return "", "", errors.New("function formUploader.Put() failed, err:" + putErr.Error()) + } + return global.GVA_CONFIG.Qiniu.ImgPath + "/" + ret.Key, ret.Key, nil +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@author: [ccfish86](https://github.com/ccfish86) +//@author: [SliverHorn](https://github.com/SliverHorn) +//@object: *Qiniu +//@function: DeleteFile +//@description: 删除文件 +//@param: key string +//@return: error + +func (*Qiniu) DeleteFile(key string) error { + mac := qbox.NewMac(global.GVA_CONFIG.Qiniu.AccessKey, global.GVA_CONFIG.Qiniu.SecretKey) + cfg := qiniuConfig() + bucketManager := storage.NewBucketManager(mac, cfg) + if err := bucketManager.Delete(global.GVA_CONFIG.Qiniu.Bucket, key); err != nil { + global.GVA_LOG.Error("function bucketManager.Delete() failed", zap.Any("err", err.Error())) + return errors.New("function bucketManager.Delete() failed, err:" + err.Error()) + } + return nil +} + +//@author: [SliverHorn](https://github.com/SliverHorn) +//@object: *Qiniu +//@function: qiniuConfig +//@description: 根据配置文件进行返回七牛云的配置 +//@return: *storage.Config + +func qiniuConfig() *storage.Config { + cfg := storage.Config{ + UseHTTPS: global.GVA_CONFIG.Qiniu.UseHTTPS, + UseCdnDomains: global.GVA_CONFIG.Qiniu.UseCdnDomains, + } + switch global.GVA_CONFIG.Qiniu.Zone { // 根据配置文件进行初始化空间对应的机房 + case "ZoneHuadong": + cfg.Zone = &storage.ZoneHuadong + case "ZoneHuabei": + cfg.Zone = &storage.ZoneHuabei + case "ZoneHuanan": + cfg.Zone = &storage.ZoneHuanan + case "ZoneBeimei": + cfg.Zone = &storage.ZoneBeimei + case "ZoneXinjiapo": + cfg.Zone = &storage.ZoneXinjiapo + } + return &cfg +} diff --git a/admin/server/utils/upload/tencent_cos.go b/admin/server/utils/upload/tencent_cos.go new file mode 100644 index 000000000..efb99d894 --- /dev/null +++ b/admin/server/utils/upload/tencent_cos.go @@ -0,0 +1,61 @@ +package upload + +import ( + "context" + "errors" + "fmt" + "mime/multipart" + "net/http" + "net/url" + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + + "github.com/tencentyun/cos-go-sdk-v5" + "go.uber.org/zap" +) + +type TencentCOS struct{} + +// UploadFile upload file to COS +func (*TencentCOS) UploadFile(file *multipart.FileHeader) (string, string, error) { + client := NewClient() + f, openError := file.Open() + if openError != nil { + global.GVA_LOG.Error("function file.Open() failed", zap.Any("err", openError.Error())) + return "", "", errors.New("function file.Open() failed, err:" + openError.Error()) + } + defer f.Close() // 创建文件 defer 关闭 + fileKey := fmt.Sprintf("%d%s", time.Now().Unix(), file.Filename) + + _, err := client.Object.Put(context.Background(), global.GVA_CONFIG.TencentCOS.PathPrefix+"/"+fileKey, f, nil) + if err != nil { + panic(err) + } + return global.GVA_CONFIG.TencentCOS.BaseURL + "/" + global.GVA_CONFIG.TencentCOS.PathPrefix + "/" + fileKey, fileKey, nil +} + +// DeleteFile delete file form COS +func (*TencentCOS) DeleteFile(key string) error { + client := NewClient() + name := global.GVA_CONFIG.TencentCOS.PathPrefix + "/" + key + _, err := client.Object.Delete(context.Background(), name) + if err != nil { + global.GVA_LOG.Error("function bucketManager.Delete() failed", zap.Any("err", err.Error())) + return errors.New("function bucketManager.Delete() failed, err:" + err.Error()) + } + return nil +} + +// NewClient init COS client +func NewClient() *cos.Client { + urlStr, _ := url.Parse("https://" + global.GVA_CONFIG.TencentCOS.Bucket + ".cos." + global.GVA_CONFIG.TencentCOS.Region + ".myqcloud.com") + baseURL := &cos.BaseURL{BucketURL: urlStr} + client := cos.NewClient(baseURL, &http.Client{ + Transport: &cos.AuthorizationTransport{ + SecretID: global.GVA_CONFIG.TencentCOS.SecretID, + SecretKey: global.GVA_CONFIG.TencentCOS.SecretKey, + }, + }) + return client +} diff --git a/admin/server/utils/upload/upload.go b/admin/server/utils/upload/upload.go new file mode 100644 index 000000000..28266ab18 --- /dev/null +++ b/admin/server/utils/upload/upload.go @@ -0,0 +1,46 @@ +package upload + +import ( + "mime/multipart" + + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +// OSS 对象存储接口 +// Author [SliverHorn](https://github.com/SliverHorn) +// Author [ccfish86](https://github.com/ccfish86) +type OSS interface { + UploadFile(file *multipart.FileHeader) (string, string, error) + DeleteFile(key string) error +} + +// NewOss OSS的实例化方法 +// Author [SliverHorn](https://github.com/SliverHorn) +// Author [ccfish86](https://github.com/ccfish86) +func NewOss() OSS { + switch global.GVA_CONFIG.System.OssType { + case "local": + return &Local{} + case "qiniu": + return &Qiniu{} + case "tencent-cos": + return &TencentCOS{} + case "aliyun-oss": + return &AliyunOSS{} + case "huawei-obs": + return HuaWeiObs + case "aws-s3": + return &AwsS3{} + case "cloudflare-r2": + return &CloudflareR2{} + case "minio": + minioClient, err := GetMinio(global.GVA_CONFIG.Minio.Endpoint, global.GVA_CONFIG.Minio.AccessKeyId, global.GVA_CONFIG.Minio.AccessKeySecret, global.GVA_CONFIG.Minio.BucketName, global.GVA_CONFIG.Minio.UseSSL) + if err != nil { + global.GVA_LOG.Warn("你配置了使用minio,但是初始化失败,请检查minio可用性或安全配置: " + err.Error()) + panic("minio初始化失败") // 建议这样做,用户自己配置了minio,如果报错了还要把服务开起来,使用起来也很危险 + } + return minioClient + default: + return &Local{} + } +} diff --git a/admin/server/utils/validator.go b/admin/server/utils/validator.go new file mode 100644 index 000000000..a56dac030 --- /dev/null +++ b/admin/server/utils/validator.go @@ -0,0 +1,294 @@ +package utils + +import ( + "errors" + "reflect" + "regexp" + "strconv" + "strings" +) + +type Rules map[string][]string + +type RulesMap map[string]Rules + +var CustomizeMap = make(map[string]Rules) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: RegisterRule +//@description: 注册自定义规则方案建议在路由初始化层即注册 +//@param: key string, rule Rules +//@return: err error + +func RegisterRule(key string, rule Rules) (err error) { + if CustomizeMap[key] != nil { + return errors.New(key + "已注册,无法重复注册") + } else { + CustomizeMap[key] = rule + return nil + } +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: NotEmpty +//@description: 非空 不能为其对应类型的0值 +//@return: string + +func NotEmpty() string { + return "notEmpty" +} + +// @author: [zooqkl](https://github.com/zooqkl) +// @function: RegexpMatch +// @description: 正则校验 校验输入项是否满足正则表达式 +// @param: rule string +// @return: string + +func RegexpMatch(rule string) string { + return "regexp=" + rule +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: Lt +//@description: 小于入参(<) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较 +//@param: mark string +//@return: string + +func Lt(mark string) string { + return "lt=" + mark +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: Le +//@description: 小于等于入参(<=) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较 +//@param: mark string +//@return: string + +func Le(mark string) string { + return "le=" + mark +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: Eq +//@description: 等于入参(==) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较 +//@param: mark string +//@return: string + +func Eq(mark string) string { + return "eq=" + mark +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: Ne +//@description: 不等于入参(!=) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较 +//@param: mark string +//@return: string + +func Ne(mark string) string { + return "ne=" + mark +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: Ge +//@description: 大于等于入参(>=) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较 +//@param: mark string +//@return: string + +func Ge(mark string) string { + return "ge=" + mark +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: Gt +//@description: 大于入参(>) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较 +//@param: mark string +//@return: string + +func Gt(mark string) string { + return "gt=" + mark +} + +// +//@author: [piexlmax](https://github.com/piexlmax) +//@function: Verify +//@description: 校验方法 +//@param: st interface{}, roleMap Rules(入参实例,规则map) +//@return: err error + +func Verify(st interface{}, roleMap Rules) (err error) { + compareMap := map[string]bool{ + "lt": true, + "le": true, + "eq": true, + "ne": true, + "ge": true, + "gt": true, + } + + typ := reflect.TypeOf(st) + val := reflect.ValueOf(st) // 获取reflect.Type类型 + + kd := val.Kind() // 获取到st对应的类别 + if kd != reflect.Struct { + return errors.New("expect struct") + } + num := val.NumField() + // 遍历结构体的所有字段 + for i := 0; i < num; i++ { + tagVal := typ.Field(i) + val := val.Field(i) + if tagVal.Type.Kind() == reflect.Struct { + if err = Verify(val.Interface(), roleMap); err != nil { + return err + } + } + if len(roleMap[tagVal.Name]) > 0 { + for _, v := range roleMap[tagVal.Name] { + switch { + case v == "notEmpty": + if isBlank(val) { + return errors.New(tagVal.Name + "值不能为空") + } + case strings.Split(v, "=")[0] == "regexp": + if !regexpMatch(strings.Split(v, "=")[1], val.String()) { + return errors.New(tagVal.Name + "格式校验不通过") + } + case compareMap[strings.Split(v, "=")[0]]: + if !compareVerify(val, v) { + return errors.New(tagVal.Name + "长度或值不在合法范围," + v) + } + } + } + } + } + return nil +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: compareVerify +//@description: 长度和数字的校验方法 根据类型自动校验 +//@param: value reflect.Value, VerifyStr string +//@return: bool + +func compareVerify(value reflect.Value, VerifyStr string) bool { + switch value.Kind() { + case reflect.String: + return compare(len([]rune(value.String())), VerifyStr) + case reflect.Slice, reflect.Array: + return compare(value.Len(), VerifyStr) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return compare(value.Uint(), VerifyStr) + case reflect.Float32, reflect.Float64: + return compare(value.Float(), VerifyStr) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return compare(value.Int(), VerifyStr) + default: + return false + } +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: isBlank +//@description: 非空校验 +//@param: value reflect.Value +//@return: bool + +func isBlank(value reflect.Value) bool { + switch value.Kind() { + case reflect.String, reflect.Slice: + return value.Len() == 0 + case reflect.Bool: + return !value.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return value.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return value.Uint() == 0 + case reflect.Float32, reflect.Float64: + return value.Float() == 0 + case reflect.Interface, reflect.Ptr: + return value.IsNil() + } + return reflect.DeepEqual(value.Interface(), reflect.Zero(value.Type()).Interface()) +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: compare +//@description: 比较函数 +//@param: value interface{}, VerifyStr string +//@return: bool + +func compare(value interface{}, VerifyStr string) bool { + VerifyStrArr := strings.Split(VerifyStr, "=") + val := reflect.ValueOf(value) + switch val.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + VInt, VErr := strconv.ParseInt(VerifyStrArr[1], 10, 64) + if VErr != nil { + return false + } + switch { + case VerifyStrArr[0] == "lt": + return val.Int() < VInt + case VerifyStrArr[0] == "le": + return val.Int() <= VInt + case VerifyStrArr[0] == "eq": + return val.Int() == VInt + case VerifyStrArr[0] == "ne": + return val.Int() != VInt + case VerifyStrArr[0] == "ge": + return val.Int() >= VInt + case VerifyStrArr[0] == "gt": + return val.Int() > VInt + default: + return false + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + VInt, VErr := strconv.Atoi(VerifyStrArr[1]) + if VErr != nil { + return false + } + switch { + case VerifyStrArr[0] == "lt": + return val.Uint() < uint64(VInt) + case VerifyStrArr[0] == "le": + return val.Uint() <= uint64(VInt) + case VerifyStrArr[0] == "eq": + return val.Uint() == uint64(VInt) + case VerifyStrArr[0] == "ne": + return val.Uint() != uint64(VInt) + case VerifyStrArr[0] == "ge": + return val.Uint() >= uint64(VInt) + case VerifyStrArr[0] == "gt": + return val.Uint() > uint64(VInt) + default: + return false + } + case reflect.Float32, reflect.Float64: + VFloat, VErr := strconv.ParseFloat(VerifyStrArr[1], 64) + if VErr != nil { + return false + } + switch { + case VerifyStrArr[0] == "lt": + return val.Float() < VFloat + case VerifyStrArr[0] == "le": + return val.Float() <= VFloat + case VerifyStrArr[0] == "eq": + return val.Float() == VFloat + case VerifyStrArr[0] == "ne": + return val.Float() != VFloat + case VerifyStrArr[0] == "ge": + return val.Float() >= VFloat + case VerifyStrArr[0] == "gt": + return val.Float() > VFloat + default: + return false + } + default: + return false + } +} + +func regexpMatch(rule, matchStr string) bool { + return regexp.MustCompile(rule).MatchString(matchStr) +} diff --git a/admin/server/utils/validator_test.go b/admin/server/utils/validator_test.go new file mode 100644 index 000000000..bdacb8b35 --- /dev/null +++ b/admin/server/utils/validator_test.go @@ -0,0 +1,37 @@ +package utils + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "testing" +) + +type PageInfoTest struct { + PageInfo request.PageInfo + Name string +} + +func TestVerify(t *testing.T) { + PageInfoVerify := Rules{"Page": {NotEmpty()}, "PageSize": {NotEmpty()}, "Name": {NotEmpty()}} + var testInfo PageInfoTest + testInfo.Name = "test" + testInfo.PageInfo.Page = 0 + testInfo.PageInfo.PageSize = 0 + err := Verify(testInfo, PageInfoVerify) + if err == nil { + t.Error("校验失败,未能捕捉0值") + } + testInfo.Name = "" + testInfo.PageInfo.Page = 1 + testInfo.PageInfo.PageSize = 10 + err = Verify(testInfo, PageInfoVerify) + if err == nil { + t.Error("校验失败,未能正常检测name为空") + } + testInfo.Name = "test" + testInfo.PageInfo.Page = 1 + testInfo.PageInfo.PageSize = 10 + err = Verify(testInfo, PageInfoVerify) + if err != nil { + t.Error("校验失败,未能正常通过检测") + } +} diff --git a/admin/server/utils/verify.go b/admin/server/utils/verify.go new file mode 100644 index 000000000..43a86725f --- /dev/null +++ b/admin/server/utils/verify.go @@ -0,0 +1,19 @@ +package utils + +var ( + IdVerify = Rules{"ID": []string{NotEmpty()}} + ApiVerify = Rules{"Path": {NotEmpty()}, "Description": {NotEmpty()}, "ApiGroup": {NotEmpty()}, "Method": {NotEmpty()}} + MenuVerify = Rules{"Path": {NotEmpty()}, "Name": {NotEmpty()}, "Component": {NotEmpty()}, "Sort": {Ge("0")}} + MenuMetaVerify = Rules{"Title": {NotEmpty()}} + LoginVerify = Rules{"CaptchaId": {NotEmpty()}, "Username": {NotEmpty()}, "Password": {NotEmpty()}} + RegisterVerify = Rules{"Username": {NotEmpty()}, "NickName": {NotEmpty()}, "Password": {NotEmpty()}, "AuthorityId": {NotEmpty()}} + PageInfoVerify = Rules{"Page": {NotEmpty()}, "PageSize": {NotEmpty()}} + CustomerVerify = Rules{"CustomerName": {NotEmpty()}, "CustomerPhoneData": {NotEmpty()}} + AutoCodeVerify = Rules{"Abbreviation": {NotEmpty()}, "StructName": {NotEmpty()}, "PackageName": {NotEmpty()}} + AutoPackageVerify = Rules{"PackageName": {NotEmpty()}} + AuthorityVerify = Rules{"AuthorityId": {NotEmpty()}, "AuthorityName": {NotEmpty()}} + AuthorityIdVerify = Rules{"AuthorityId": {NotEmpty()}} + OldAuthorityVerify = Rules{"OldAuthorityId": {NotEmpty()}} + ChangePasswordVerify = Rules{"Password": {NotEmpty()}, "NewPassword": {NotEmpty()}} + SetUserAuthorityVerify = Rules{"AuthorityId": {NotEmpty()}} +) diff --git a/admin/server/utils/verify_extend.go b/admin/server/utils/verify_extend.go new file mode 100644 index 000000000..1811e1d66 --- /dev/null +++ b/admin/server/utils/verify_extend.go @@ -0,0 +1,5 @@ +package utils + +var ( + OaLoginVerify = Rules{"AuthorizeCode": {NotEmpty()}} // 新增OA登录 +) diff --git a/admin/server/utils/zip.go b/admin/server/utils/zip.go new file mode 100644 index 000000000..bee0a0bf4 --- /dev/null +++ b/admin/server/utils/zip.go @@ -0,0 +1,53 @@ +package utils + +import ( + "archive/zip" + "fmt" + "io" + "os" + "path/filepath" + "strings" +) + +// 解压 +func Unzip(zipFile string, destDir string) ([]string, error) { + zipReader, err := zip.OpenReader(zipFile) + var paths []string + if err != nil { + return []string{}, err + } + defer zipReader.Close() + + for _, f := range zipReader.File { + if strings.Index(f.Name, "..") > -1 { + return []string{}, fmt.Errorf("%s 文件名不合法", f.Name) + } + fpath := filepath.Join(destDir, f.Name) + paths = append(paths, fpath) + if f.FileInfo().IsDir() { + os.MkdirAll(fpath, os.ModePerm) + } else { + if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { + return []string{}, err + } + + inFile, err := f.Open() + if err != nil { + return []string{}, err + } + defer inFile.Close() + + outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + return []string{}, err + } + defer outFile.Close() + + _, err = io.Copy(outFile, inFile) + if err != nil { + return []string{}, err + } + } + } + return paths, nil +} diff --git a/admin/web/.docker-compose/nginx/conf.d/my.conf b/admin/web/.docker-compose/nginx/conf.d/my.conf new file mode 100644 index 000000000..c8568011e --- /dev/null +++ b/admin/web/.docker-compose/nginx/conf.d/my.conf @@ -0,0 +1,27 @@ +server { + listen 8081; + server_name localhost; + + #charset koi8-r; + #access_log logs/host.access.log main; + + location / { + root /usr/share/nginx/html; + add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0'; + #rewrite ^/admin/(.*)$ /$1 break; + try_files $uri $uri/ /index.html; + } + + location /api { + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + rewrite ^/api/(.*)$ /$1 break; #重写 + proxy_pass http://admin-server:8888; # 设置代理服务器的协议和地址 + } + + location /api/swagger/index.html { + proxy_pass http://127.0.0.1:8888/swagger/index.html; + } + } diff --git a/admin/web/.docker-compose/nginx/conf.d/nginx.conf b/admin/web/.docker-compose/nginx/conf.d/nginx.conf new file mode 100644 index 000000000..29f68b81f --- /dev/null +++ b/admin/web/.docker-compose/nginx/conf.d/nginx.conf @@ -0,0 +1,32 @@ +server { + listen 80; + server_name localhost; + + #charset koi8-r; + #access_log logs/host.access.log main; + + location / { + root /usr/share/nginx/html/dist; + add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0'; + try_files $uri $uri/ /index.html; + } + + location /api { + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + rewrite ^/api/(.*)$ /$1 break; #重写 + proxy_pass http://127.0.0.1:8888; # 设置代理服务器的协议和地址 + } + location /form-generator { + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_pass http://127.0.0.1:8888; + } + location /api/swagger/index.html { + proxy_pass http://127.0.0.1:8888/swagger/index.html; + } + } \ No newline at end of file diff --git a/admin/web/.dockerignore b/admin/web/.dockerignore new file mode 100644 index 000000000..40b878db5 --- /dev/null +++ b/admin/web/.dockerignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/admin/web/.env.development b/admin/web/.env.development new file mode 100644 index 000000000..aa7c7c2cb --- /dev/null +++ b/admin/web/.env.development @@ -0,0 +1,15 @@ +ENV = 'development' +VITE_CLI_PORT = 8080 +VITE_SERVER_PORT = 8888 +VITE_BASE_API = /api +VITE_FILE_API = /api +VITE_BASE_PATH = http://127.0.0.1 +VITE_POSITION = close +VITE_EDITOR = vscode +// VITE_EDITOR = webstorm 如果使用webstorm开发且要使用dom定位到代码行功能 请先自定添加 webstorm到环境变量 再将VITE_EDITOR值修改为webstorm +// 如果使用docker-compose开发模式,设置为下面的地址或本机主机IP +//VITE_BASE_PATH = http://177.7.0.12 +# oa-oauth2.0登录客户端ID +VITE_OA_LOGIN_CLINET_ID = +# oa地址 +VITE_OA_URL = diff --git a/admin/web/.env.production b/admin/web/.env.production new file mode 100644 index 000000000..86beeaa38 --- /dev/null +++ b/admin/web/.env.production @@ -0,0 +1,7 @@ +ENV = 'production' + +#下方为上线需要用到的程序代理前缀,一般用于nginx代理转发 +VITE_BASE_API = /api +VITE_FILE_API = /api +#下方修改为你的线上ip(如果需要在线使用表单构建工具时使用,其余情况无需使用以下环境变量) +VITE_BASE_PATH = diff --git a/admin/web/.eslintignore b/admin/web/.eslintignore new file mode 100644 index 000000000..e6529fc09 --- /dev/null +++ b/admin/web/.eslintignore @@ -0,0 +1,4 @@ +build/*.js +src/assets +public +dist diff --git a/admin/web/.eslintrc.js b/admin/web/.eslintrc.js new file mode 100644 index 000000000..0821611b5 --- /dev/null +++ b/admin/web/.eslintrc.js @@ -0,0 +1,17 @@ +module.exports = { + root: true, + parserOptions: { + parser: '@babel/eslint-parser', + sourceType: 'module' + }, + env: { + browser: true, + node: true, + es6: true + }, + extends: ['plugin:vue/recommended', 'eslint:recommended'], + rules: { + "vue/max-attributes-per-line" : 0, + "vue/no-v-model-argument" : 0 + } +} diff --git a/admin/web/.gitignore b/admin/web/.gitignore new file mode 100644 index 000000000..1a4abd90a --- /dev/null +++ b/admin/web/.gitignore @@ -0,0 +1,5 @@ +node_modules/* +package-lock.json +yarn.lock +bun.lockb +config.yaml \ No newline at end of file diff --git a/admin/web/Dockerfile b/admin/web/Dockerfile new file mode 100644 index 000000000..aa9d9f661 --- /dev/null +++ b/admin/web/Dockerfile @@ -0,0 +1,17 @@ +FROM node:20-slim AS base + +WORKDIR /gva_web/ +COPY . . +#RUN sed -i 's/mirrors.aliyun.com/dl-cdn.alpinelinux.org/g' /etc/apk/repositories +RUN yarn config set registry https://registry.npmmirror.com \ + && yarn \ + && yarn build + +FROM nginx:alpine +LABEL MAINTAINER="SliverHorn@sliver_horn@qq.com" + +COPY .docker-compose/nginx/conf.d/my.conf /etc/nginx/conf.d/my.conf +COPY --from=0 /gva_web/dist /usr/share/nginx/html +RUN cat /etc/nginx/nginx.conf +RUN cat /etc/nginx/conf.d/my.conf +RUN ls -al /usr/share/nginx/html diff --git a/admin/web/babel.config.js b/admin/web/babel.config.js new file mode 100644 index 000000000..88029f087 --- /dev/null +++ b/admin/web/babel.config.js @@ -0,0 +1,8 @@ +module.exports = { + presets: [ + + ], + 'plugins': [ + + ] +} diff --git a/admin/web/index.html b/admin/web/index.html new file mode 100644 index 000000000..47b792a7e --- /dev/null +++ b/admin/web/index.html @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + +
+
+
+
+
+
系统正在加载中,请稍候...
+
+
+ + + + diff --git a/admin/web/jsconfig.json b/admin/web/jsconfig.json new file mode 100644 index 000000000..deaa520aa --- /dev/null +++ b/admin/web/jsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "baseUrl": "./", + "paths": { + "@/*": ["src/*"] + } + }, + "exclude": ["node_modules", "dist"], + "include": ["src/**/*"] + } diff --git a/admin/web/limit.js b/admin/web/limit.js new file mode 100644 index 000000000..6ba9d4623 --- /dev/null +++ b/admin/web/limit.js @@ -0,0 +1,37 @@ +// 运行项目前通过node执行此脚本 (此脚本与 node_modules 目录同级) +const fs = require('fs') +const path = require('path') +const wfPath = path.resolve(__dirname, './node_modules/.bin') + +fs.readdir(wfPath, (err, files) => { + if (err) { + console.log(err) + } else { + if (files.length !== 0) { + files.forEach((item) => { + if (item.split('.')[1] === 'cmd') { + replaceStr(`${wfPath}/${item}`, /"%_prog%"/, '%_prog%') + } + }) + } + } +}) + +// 参数:[文件路径、 需要修改的字符串、修改后的字符串] (替换对应文件内字符串的公共函数) +function replaceStr(filePath, sourceRegx, targetSrt) { + fs.readFile(filePath, (err, data) => { + if (err) { + console.log(err) + } else { + let str = data.toString() + str = str.replace(sourceRegx, targetSrt) + fs.writeFile(filePath, str, (err) => { + if (err) { + console.log(err) + } else { + console.log('\x1B[42m%s\x1B[0m', '文件修改成功') + } + }) + } + }) +} diff --git a/admin/web/package.json b/admin/web/package.json new file mode 100644 index 000000000..2d2316d40 --- /dev/null +++ b/admin/web/package.json @@ -0,0 +1,73 @@ +{ + "name": "gaia-admin", + "version": "0.0.1", + "private": true, + "scripts": { + "serve": "vite --host --mode development", + "build": "vite build --mode production", + "build-test": "vite build --mode test", + "limit-build": "npm install increase-memory-limit-fixbug cross-env -g && npm run fix-memory-limit && node ./limit && npm run build", + "preview": "vite preview", + "fix-memory-limit": "cross-env LIMIT=4096 increase-memory-limit" + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.1", + "@form-create/designer": "^3.2.6", + "@form-create/element-ui": "^3.2.10", + "@vue-office/docx": "^1.6.2", + "@vue-office/excel": "^1.7.11", + "@vue-office/pdf": "^2.0.2", + "@vueuse/core": "^11.0.3", + "@wangeditor/editor": "^5.1.23", + "@wangeditor/editor-for-vue": "^5.1.12", + "ace-builds": "^1.36.4", + "axios": "^1.7.7", + "chokidar": "^4.0.0", + "core-js": "^3.38.1", + "default-passive-events": "^2.0.0", + "echarts": "5.5.1", + "element-plus": "^2.8.5", + "highlight.js": "^11.10.0", + "js-cookie": "^3.0.5", + "marked": "14.1.1", + "marked-highlight": "^2.1.4", + "mitt": "^3.0.1", + "nprogress": "^0.2.0", + "path": "^0.12.7", + "pinia": "^2.2.2", + "qs": "^6.13.0", + "screenfull": "^6.0.2", + "sortablejs": "^1.15.3", + "spark-md5": "^3.0.2", + "tailwindcss": "^3.4.10", + "vform3-builds": "^3.0.10", + "vite-auto-import-svg": "^1.1.0", + "vue": "^3.5.7", + "vue-echarts": "^7.0.3", + "vue-router": "^4.4.3", + "vue3-ace-editor": "^2.2.4", + "vuedraggable": "^4.1.0" + }, + "devDependencies": { + "@babel/eslint-parser": "^7.25.1", + "@vitejs/plugin-legacy": "^5.4.2", + "@vitejs/plugin-vue": "^5.1.3", + "@vue/cli-plugin-babel": "~5.0.8", + "@vue/cli-plugin-eslint": "~5.0.8", + "@vue/cli-plugin-router": "~5.0.8", + "@vue/cli-plugin-vuex": "~5.0.8", + "@vue/cli-service": "~5.0.8", + "@vue/compiler-sfc": "^3.5.1", + "babel-plugin-import": "^1.13.8", + "chalk": "^5.3.0", + "dotenv": "^16.4.5", + "eslint": "^9.9.1", + "eslint-plugin-vue": "^9.28.0", + "sass": "^1.78.0", + "terser": "^5.31.6", + "vite": "^5.4.3", + "vite-plugin-banner": "^0.8.0", + "vite-plugin-importer": "^0.2.5", + "vite-plugin-vue-devtools": "^7.4.4" + } +} diff --git a/admin/web/postcss.config.js b/admin/web/postcss.config.js new file mode 100644 index 000000000..33ad091d2 --- /dev/null +++ b/admin/web/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/admin/web/public/favicon.ico b/admin/web/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..f5ab4a4da0287170ab8b2361db29ceb08bd47473 GIT binary patch literal 845 zcmV-T1G4;yP)|AW=QwTIw14;}-+xZd=W~81=ZqK;435gHpfo2d zaH!RPOKI{dbfZFh{*(NxoOX8^Z{B}RU=Q7}v&Zf@nbQv?x}?fQl|FYF%WIUUw`x6G zt94it$p#Y;>?pM{d&+p%fpSqka?2r|x-U6PDrLvTYQ=v2tzy6X5r_}#LDSPFUveZL z6l^=sF?knkuDw?TnM@&_xaSNCF1=9fH{W~2`(J=q`PC;p)B8@LaF*H61Gdp$pnRnIQYw2$I4CYmQjC>|+*XeW6v^a+Y!D zm)cyX9yp!1YdvBBeow&9-H5C2MOecrzWpz0SZ7?UPd(SKy<$i zGCzr^k401ujpK&}na1dZ>@IP}Y67RX`(qaC)$u9cVB!ir@cNNeQZQwcLXrU;^s&iJFd!6h!WkH;sO#oGH}j_j}(_#7ahBzL!7#_ zRh_(0uTGfr9cmLBQG9wc@T3=U*wszEz#St}K@sKq&Po=zH< z(j;r*8|3)3X3-C^-UIL>7r+|_F&-Vs{t1qXS*_DdN^2VvY*L^#V`h^za@UcOyZ_B^ X+S@G?&S|Wu00000NkvXXu0mjfuOx$F literal 0 HcmV?d00001 diff --git a/admin/web/public/logo.png b/admin/web/public/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..87a8e889f26724427dd43a236563b6eacaddd51c GIT binary patch literal 5805 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 +
+ + + +
+ + + + diff --git a/admin/web/src/api/api.js b/admin/web/src/api/api.js new file mode 100644 index 000000000..17147b04e --- /dev/null +++ b/admin/web/src/api/api.js @@ -0,0 +1,179 @@ +import service from '@/utils/request' + +// @Tags api +// @Summary 分页获取角色列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body modelInterface.PageInfo true "分页获取用户列表" +// @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /api/getApiList [post] +// { +// page int +// pageSize int +// } +export const getApiList = (data) => { + return service({ + url: '/api/getApiList', + method: 'post', + data + }) +} + +// @Tags Api +// @Summary 创建基础api +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body api.CreateApiParams true "创建api" +// @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /api/createApi [post] +export const createApi = (data) => { + return service({ + url: '/api/createApi', + method: 'post', + data + }) +} + +// @Tags menu +// @Summary 根据id获取菜单 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body api.GetById true "根据id获取菜单" +// @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /menu/getApiById [post] +export const getApiById = (data) => { + return service({ + url: '/api/getApiById', + method: 'post', + data + }) +} + +// @Tags Api +// @Summary 更新api +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body api.CreateApiParams true "更新api" +// @Success 200 {string} json "{"success":true,"data":{},"msg":"更新成功"}" +// @Router /api/updateApi [post] +export const updateApi = (data) => { + return service({ + url: '/api/updateApi', + method: 'post', + data + }) +} + +// @Tags Api +// @Summary 更新api +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body api.CreateApiParams true "更新api" +// @Success 200 {string} json "{"success":true,"data":{},"msg":"更新成功"}" +// @Router /api/setAuthApi [post] +export const setAuthApi = (data) => { + return service({ + url: '/api/setAuthApi', + method: 'post', + data + }) +} + +// @Tags Api +// @Summary 获取所有的Api 不分页 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /api/getAllApis [post] +export const getAllApis = (data) => { + return service({ + url: '/api/getAllApis', + method: 'post', + data + }) +} + +// @Tags Api +// @Summary 删除指定api +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body dbModel.Api true "删除api" +// @Success 200 {string} json "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /api/deleteApi [post] +export const deleteApi = (data) => { + return service({ + url: '/api/deleteApi', + method: 'post', + data + }) +} + +// @Tags SysApi +// @Summary 删除选中Api +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "ID" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /api/deleteApisByIds [delete] +export const deleteApisByIds = (data) => { + return service({ + url: '/api/deleteApisByIds', + method: 'delete', + data + }) +} + +// FreshCasbin +// @Tags SysApi +// @Summary 刷新casbin缓存 +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "刷新成功" +// @Router /api/freshCasbin [get] +export const freshCasbin = () => { + return service({ + url: '/api/freshCasbin', + method: 'get' + }) +} + + +export const syncApi = () => { + return service({ + url: '/api/syncApi', + method: 'get' + }) +} + + +export const getApiGroups = () => { + return service({ + url: '/api/getApiGroups', + method: 'get' + }) +} + +export const ignoreApi = (data) => { + return service({ + url: '/api/ignoreApi', + method: 'post', + data + }) +} + + +export const enterSyncApi = (data) => { + return service({ + url: '/api/enterSyncApi', + method: 'post', + data + }) +} \ No newline at end of file diff --git a/admin/web/src/api/authority.js b/admin/web/src/api/authority.js new file mode 100644 index 000000000..61b22067a --- /dev/null +++ b/admin/web/src/api/authority.js @@ -0,0 +1,85 @@ +import service from '@/utils/request' +// @Router /authority/getAuthorityList [post] +export const getAuthorityList = (data) => { + return service({ + url: '/authority/getAuthorityList', + method: 'post', + data + }) +} + +// @Summary 删除角色 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body {authorityId uint} true "删除角色" +// @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /authority/deleteAuthority [post] +export const deleteAuthority = (data) => { + return service({ + url: '/authority/deleteAuthority', + method: 'post', + data, + }) +} + +// @Summary 创建角色 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body api.CreateAuthorityPatams true "创建角色" +// @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /authority/createAuthority [post] +export const createAuthority = (data) => { + return service({ + url: '/authority/createAuthority', + method: 'post', + data + }) +} + +// @Tags authority +// @Summary 拷贝角色 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body api.CreateAuthorityPatams true "拷贝角色" +// @Success 200 {string} json "{"success":true,"data":{},"msg":"拷贝成功"}" +// @Router /authority/copyAuthority [post] +export const copyAuthority = (data) => { + return service({ + url: '/authority/copyAuthority', + method: 'post', + data + }) +} + +// @Summary 设置角色资源权限 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body sysModel.SysAuthority true "设置角色资源权限" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"设置成功"}" +// @Router /authority/setDataAuthority [post] +export const setDataAuthority = (data) => { + return service({ + url: '/authority/setDataAuthority', + method: 'post', + data + }) +} + +// @Summary 修改角色 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.SysAuthority true "修改角色" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"设置成功"}" +// @Router /authority/setDataAuthority [post] +export const updateAuthority = (data) => { + return service({ + url: '/authority/updateAuthority', + method: 'put', + data + }) +} diff --git a/admin/web/src/api/authorityBtn.js b/admin/web/src/api/authorityBtn.js new file mode 100644 index 000000000..9fe73bf42 --- /dev/null +++ b/admin/web/src/api/authorityBtn.js @@ -0,0 +1,27 @@ + +import service from '@/utils/request' + +export const getAuthorityBtnApi = (data) => { + return service({ + url: '/authorityBtn/getAuthorityBtn', + method: 'post', + data + }) +} + +export const setAuthorityBtnApi = (data) => { + return service({ + url: '/authorityBtn/setAuthorityBtn', + method: 'post', + data + }) +} + +export const canRemoveAuthorityBtnApi = (params) => { + return service({ + url: '/authorityBtn/canRemoveAuthorityBtn', + method: 'post', + params + }) +} + diff --git a/admin/web/src/api/autoCode.js b/admin/web/src/api/autoCode.js new file mode 100644 index 000000000..544330902 --- /dev/null +++ b/admin/web/src/api/autoCode.js @@ -0,0 +1,189 @@ +import service from '@/utils/request' + +export const preview = (data) => { + return service({ + url: '/autoCode/preview', + method: 'post', + data + }) +} + +export const createTemp = (data) => { + return service({ + url: '/autoCode/createTemp', + method: 'post', + data + }) +} + +// @Tags SysApi +// @Summary 获取当前所有数据库 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" +// @Router /autoCode/getDatabase [get] +export const getDB = (params) => { + return service({ + url: '/autoCode/getDB', + method: 'get', + params + }) +} + +// @Tags SysApi +// @Summary 获取当前数据库所有表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" +// @Router /autoCode/getTables [get] +export const getTable = (params) => { + return service({ + url: '/autoCode/getTables', + method: 'get', + params + }) +} + +// @Tags SysApi +// @Summary 获取当前数据库所有表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" +// @Router /autoCode/getColumn [get] +export const getColumn = (params) => { + return service({ + url: '/autoCode/getColumn', + method: 'get', + params + }) +} + +export const getSysHistory = (data) => { + return service({ + url: '/autoCode/getSysHistory', + method: 'post', + data + }) +} + +export const rollback = (data) => { + return service({ + url: '/autoCode/rollback', + method: 'post', + data + }) +} + +export const getMeta = (data) => { + return service({ + url: '/autoCode/getMeta', + method: 'post', + data + }) +} + +export const delSysHistory = (data) => { + return service({ + url: '/autoCode/delSysHistory', + method: 'post', + data + }) +} + +export const createPackageApi = (data) => { + return service({ + url: '/autoCode/createPackage', + method: 'post', + data + }) +} + +export const getPackageApi = () => { + return service({ + url: '/autoCode/getPackage', + method: 'post' + }) +} + +export const deletePackageApi = (data) => { + return service({ + url: '/autoCode/delPackage', + method: 'post', + data + }) +} + +export const getTemplatesApi = () => { + return service({ + url: '/autoCode/getTemplates', + method: 'get' + }) +} + +export const installPlug = (data) => { + return service({ + url: '/autoCode/installPlug', + method: 'post', + data + }) +} + +export const pubPlug = (params) => { + return service({ + url: '/autoCode/pubPlug', + method: 'post', + params + }) +} + + +export const llmAuto = (data) => { + return service({ + url: '/autoCode/llmAuto', + method: 'post', + data:{...data,mode:'ai'}, + timeout: 1000 * 60 * 10, + loadingOption:{ + lock: true, + fullscreen:true, + text: `小淼正在思考,请稍候...`, + } + }) +} + + +export const butler = (data) => { + return service({ + url: '/autoCode/llmAuto', + method: 'post', + data:{...data,mode:'butler'}, + timeout: 1000 * 60 * 10, + }) +} + +export const addFunc = (data) => { + return service({ + url: '/autoCode/addFunc', + method: 'post', + data + }) +} + +export const initMenu = (data) => { + return service({ + url: '/autoCode/initMenu', + method: 'post', + data + }) +} + +export const initAPI = (data) => { + return service({ + url: '/autoCode/initAPI', + method: 'post', + data + }) +} diff --git a/admin/web/src/api/breakpoint.js b/admin/web/src/api/breakpoint.js new file mode 100644 index 000000000..1dbfba23e --- /dev/null +++ b/admin/web/src/api/breakpoint.js @@ -0,0 +1,43 @@ +import service from '@/utils/request' +// @Summary 设置角色资源权限 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body sysModel.SysAuthority true "设置角色资源权限" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"设置成功"}" +// @Router /authority/setDataAuthority [post] + +export const findFile = (params) => { + return service({ + url: '/fileUploadAndDownload/findFile', + method: 'get', + params + }) +} + +export const breakpointContinue = (data) => { + return service({ + url: '/fileUploadAndDownload/breakpointContinue', + method: 'post', + donNotShowLoading: true, + headers: { 'Content-Type': 'multipart/form-data' }, + data + }) +} + +export const breakpointContinueFinish = (params) => { + return service({ + url: '/fileUploadAndDownload/breakpointContinueFinish', + method: 'post', + params + }) +} + +export const removeChunk = (data, params) => { + return service({ + url: '/fileUploadAndDownload/removeChunk', + method: 'post', + data, + params + }) +} diff --git a/admin/web/src/api/casbin.js b/admin/web/src/api/casbin.js new file mode 100644 index 000000000..802e13006 --- /dev/null +++ b/admin/web/src/api/casbin.js @@ -0,0 +1,32 @@ +import service from '@/utils/request' +// @Tags authority +// @Summary 更改角色api权限 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body api.CreateAuthorityPatams true "更改角色api权限" +// @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /casbin/UpdateCasbin [post] +export const UpdateCasbin = (data) => { + return service({ + url: '/casbin/updateCasbin', + method: 'post', + data + }) +} + +// @Tags casbin +// @Summary 获取权限列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body api.CreateAuthorityPatams true "获取权限列表" +// @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /casbin/getPolicyPathByAuthorityId [post] +export const getPolicyPathByAuthorityId = (data) => { + return service({ + url: '/casbin/getPolicyPathByAuthorityId', + method: 'post', + data + }) +} diff --git a/admin/web/src/api/customer.js b/admin/web/src/api/customer.js new file mode 100644 index 000000000..4776f1c09 --- /dev/null +++ b/admin/web/src/api/customer.js @@ -0,0 +1,80 @@ +import service from '@/utils/request' +// @Tags SysApi +// @Summary 删除客户 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body dbModel.ExaCustomer true "删除客户" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /customer/customer [post] +export const createExaCustomer = (data) => { + return service({ + url: '/customer/customer', + method: 'post', + data + }) +} + +// @Tags SysApi +// @Summary 更新客户信息 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body dbModel.ExaCustomer true "更新客户信息" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /customer/customer [put] +export const updateExaCustomer = (data) => { + return service({ + url: '/customer/customer', + method: 'put', + data + }) +} + +// @Tags SysApi +// @Summary 创建客户 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body dbModel.ExaCustomer true "创建客户" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /customer/customer [delete] +export const deleteExaCustomer = (data) => { + return service({ + url: '/customer/customer', + method: 'delete', + data + }) +} + +// @Tags SysApi +// @Summary 获取单一客户信息 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body dbModel.ExaCustomer true "获取单一客户信息" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /customer/customer [get] +export const getExaCustomer = (params) => { + return service({ + url: '/customer/customer', + method: 'get', + params + }) +} + +// @Tags SysApi +// @Summary 获取权限客户列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body modelInterface.PageInfo true "获取权限客户列表" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /customer/customerList [get] +export const getExaCustomerList = (params) => { + return service({ + url: '/customer/customerList', + method: 'get', + params + }) +} diff --git a/admin/web/src/api/email.js b/admin/web/src/api/email.js new file mode 100644 index 000000000..c2f16f430 --- /dev/null +++ b/admin/web/src/api/email.js @@ -0,0 +1,14 @@ +import service from '@/utils/request' +// @Tags email +// @Summary 发送测试邮件 +// @Security ApiKeyAuth +// @Produce application/json +// @Success 200 {string} string "{"success":true,"data":{},"msg":"返回成功"}" +// @Router /email/emailTest [post] +export const emailTest = (data) => { + return service({ + url: '/email/emailTest', + method: 'post', + data + }) +} diff --git a/admin/web/src/api/exportTemplate.js b/admin/web/src/api/exportTemplate.js new file mode 100644 index 000000000..5b7b27827 --- /dev/null +++ b/admin/web/src/api/exportTemplate.js @@ -0,0 +1,97 @@ +import service from '@/utils/request' + +// @Tags SysExportTemplate +// @Summary 创建导出模板 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.SysExportTemplate true "创建导出模板" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" +// @Router /sysExportTemplate/createSysExportTemplate [post] +export const createSysExportTemplate = (data) => { + return service({ + url: '/sysExportTemplate/createSysExportTemplate', + method: 'post', + data + }) +} + +// @Tags SysExportTemplate +// @Summary 删除导出模板 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.SysExportTemplate true "删除导出模板" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /sysExportTemplate/deleteSysExportTemplate [delete] +export const deleteSysExportTemplate = (data) => { + return service({ + url: '/sysExportTemplate/deleteSysExportTemplate', + method: 'delete', + data + }) +} + +// @Tags SysExportTemplate +// @Summary 批量删除导出模板 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "批量删除导出模板" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /sysExportTemplate/deleteSysExportTemplate [delete] +export const deleteSysExportTemplateByIds = (data) => { + return service({ + url: '/sysExportTemplate/deleteSysExportTemplateByIds', + method: 'delete', + data + }) +} + +// @Tags SysExportTemplate +// @Summary 更新导出模板 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.SysExportTemplate true "更新导出模板" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}" +// @Router /sysExportTemplate/updateSysExportTemplate [put] +export const updateSysExportTemplate = (data) => { + return service({ + url: '/sysExportTemplate/updateSysExportTemplate', + method: 'put', + data + }) +} + +// @Tags SysExportTemplate +// @Summary 用id查询导出模板 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query model.SysExportTemplate true "用id查询导出模板" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" +// @Router /sysExportTemplate/findSysExportTemplate [get] +export const findSysExportTemplate = (params) => { + return service({ + url: '/sysExportTemplate/findSysExportTemplate', + method: 'get', + params + }) +} + +// @Tags SysExportTemplate +// @Summary 分页获取导出模板列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query request.PageInfo true "分页获取导出模板列表" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /sysExportTemplate/getSysExportTemplateList [get] +export const getSysExportTemplateList = (params) => { + return service({ + url: '/sysExportTemplate/getSysExportTemplateList', + method: 'get', + params + }) +} diff --git a/admin/web/src/api/fileUploadAndDownload.js b/admin/web/src/api/fileUploadAndDownload.js new file mode 100644 index 000000000..2bff5bb47 --- /dev/null +++ b/admin/web/src/api/fileUploadAndDownload.js @@ -0,0 +1,57 @@ +import service from '@/utils/request' +// @Tags FileUploadAndDownload +// @Summary 分页文件列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body modelInterface.PageInfo true "分页获取文件户列表" +// @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /fileUploadAndDownload/getFileList [post] +export const getFileList = (data) => { + return service({ + url: '/fileUploadAndDownload/getFileList', + method: 'post', + data + }) +} + +// @Tags FileUploadAndDownload +// @Summary 删除文件 +// @Security ApiKeyAuth +// @Produce application/json +// @Param data body dbModel.FileUploadAndDownload true "传入文件里面id即可" +// @Success 200 {string} json "{"success":true,"data":{},"msg":"返回成功"}" +// @Router /fileUploadAndDownload/deleteFile [post] +export const deleteFile = (data) => { + return service({ + url: '/fileUploadAndDownload/deleteFile', + method: 'post', + data + }) +} + +/** + * 编辑文件名或者备注 + * @param data + * @returns {*} + */ +export const editFileName = (data) => { + return service({ + url: '/fileUploadAndDownload/editFileName', + method: 'post', + data + }) +} + +/** + * 导入URL + * @param data + * @returns {*} + */ +export const importURL = (data) => { + return service({ + url: '/fileUploadAndDownload/importURL', + method: 'post', + data + }) +} diff --git a/admin/web/src/api/gaia/dashboard.js b/admin/web/src/api/gaia/dashboard.js new file mode 100644 index 000000000..ad6543664 --- /dev/null +++ b/admin/web/src/api/gaia/dashboard.js @@ -0,0 +1,84 @@ +import service from '@/utils/request' + +// @Tags dashboard +// @Summary 分页获取账户配额排名数据 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query request.PageInfo true "分页获取账户配额排名数据" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /gaia/dashboard/getAccountQuotaRankingData [get] +export const getAccountQuotaRankingData = (params) => { + return service({ + url: '/gaia/dashboard/getAccountQuotaRankingData', + method: 'get', + params + }) +} + +// @Tags dashboard +// @Summary 分页获取【应用】配额排名数据 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query request.PageInfo true "分页获取【应用】配额排名数据" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /gaia/dashboard/getAppQuotaRankingData [get] +export const getAppQuotaRankingData = (params) => { + return service({ + url: '/gaia/dashboard/getAppQuotaRankingData', + method: 'get', + params + }) +} + +// @Tags dashboard +// @Summary 分页获取【应用密钥】配额排名数据列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query request.PageInfo true "分页获取【应用密钥】配额排名数据列表" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /gaia/dashboard/getAppTokenQuotaRankingData [get] +export const getAppTokenQuotaRankingData = (params) => { + return service({ + url: '/gaia/dashboard/getAppTokenQuotaRankingData', + method: 'get', + params + }) +} + + +// @Tags dashboard +// @Summary 获取每天密钥花费数据列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query request.PageInfo true "获取每天密钥花费数据列表" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /gaia/dashboard/getAppTokenDailyQuotaData [get] +export const getAppTokenDailyQuotaData = (params) => { + return service({ + url: '/gaia/dashboard/getAppTokenDailyQuotaData', + method: 'get', + params + }) +} + + +// @Tags dashboard +// @Summary 获取AI作图花费排行列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query request.PageInfo true "获取AI作图花费排行列表" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /gaia/dashboard/getAiImageQuotaRankingData [get] +export const getAiImageQuotaRankingData = (params) => { + return service({ + url: '/gaia/dashboard/getAiImageQuotaRankingData', + method: 'get', + params + }) +} + diff --git a/admin/web/src/api/gaia/providers.js b/admin/web/src/api/gaia/providers.js new file mode 100644 index 000000000..2b81856fe --- /dev/null +++ b/admin/web/src/api/gaia/providers.js @@ -0,0 +1,49 @@ +import service from '@/utils/request' + +// @Tags Providers +// @Summary 更新providers表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.Providers true "更新providers表" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}" +// @Router /providers/syncProviders [put] +export const syncProviders = (data) => { + return service({ + url: '/providers/syncProviders', + method: 'put', + data + }) +} + +// @Tags Providers +// @Summary 用id查询providers表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query model.Providers true "用id查询providers表" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" +// @Router /providers/findProviders [get] +export const findProviders = (params) => { + return service({ + url: '/providers/findProviders', + method: 'get', + params + }) +} + +// @Tags Providers +// @Summary 分页获取providers表列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query request.PageInfo true "分页获取providers表列表" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /providers/getProvidersList [get] +export const getProvidersList = (params) => { + return service({ + url: '/providers/getProvidersList', + method: 'get', + params + }) +} \ No newline at end of file diff --git a/admin/web/src/api/gaia/system.js b/admin/web/src/api/gaia/system.js new file mode 100644 index 000000000..1f8ecb68e --- /dev/null +++ b/admin/web/src/api/gaia/system.js @@ -0,0 +1,28 @@ +import service from '@/utils/request' + +// @Tags systrm +// @Summary 获取钉钉集成配置 +// @Security ApiKeyAuth +// @Produce application/json +// @Success 200 {string} string "{"success":true,"data":{},"msg":"返回成功"}" +// @Router /gaia/system/dingtalk [get] +export const getSystemDingTalk = () => { + return service({ + url: '/gaia/system/dingtalk', + method: 'get' + }) +} + +// @Tags systrm +// @Summary 修改钉钉集成配置 +// @Security ApiKeyAuth +// @Produce application/json +// @Success 200 {string} string "{"success":true,"data":{},"msg":"返回成功"}" +// @Router /gaia/system/dingtalk [post] +export const setSystemDingTalk = (data) => { + return service({ + url: '/gaia/system/dingtalk', + method: 'post', + data, + }) +} diff --git a/admin/web/src/api/gaia/tenants.js b/admin/web/src/api/gaia/tenants.js new file mode 100644 index 000000000..965a1791b --- /dev/null +++ b/admin/web/src/api/gaia/tenants.js @@ -0,0 +1,49 @@ +import service from '@/utils/request' + +// @Tags Tenants +// @Summary 用id查询tenants表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query model.Tenants true "用id查询tenants表" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" +// @Router /tenants/findTenants [get] +export const findTenants = (params) => { + return service({ + url: '/tenants/findTenants', + method: 'get', + params + }) +} + +// @Tags Tenants +// @Summary 分页获取tenants表列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query request.PageInfo true "分页获取tenants表列表" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /tenants/getTenantsList [get] +export const getTenantsList = (params) => { + return service({ + url: '/tenants/getTenantsList', + method: 'get', + params + }) +} + +// @Tags Tenants +// @Summary 获取所有工作区 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query model.Tenants true "获取所有工作区" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" +// @Router /tenants/getAllTenants [get] +export const getAllTenants = (params) => { + return service({ + url: '/tenants/getAllTenants', + method: 'get', + params + }) +} diff --git a/admin/web/src/api/gaia/test.js b/admin/web/src/api/gaia/test.js new file mode 100644 index 000000000..c4f3123c8 --- /dev/null +++ b/admin/web/src/api/gaia/test.js @@ -0,0 +1,43 @@ +import service from '@/utils/request' + + +// @Summary gaia应用请求测试结果列表 +// @Produce application/json +// @Param data body {username:"string",password:"string",newPassword:"string"} +// @Router /gaia/test/app/request [get] +export const gaiaAppRequestTesList = (data) => { + return service({ + url: '/gaia/test/app/request/list', + method: 'get', + params: data + }) +} + + +// @Summary 发起gaia应用请求测试 +// @Produce application/json +// @Param data body {username:"string",password:"string",newPassword:"string"} +// @Router /gaia/test/app/request [post] +export const gaiaAppRequestTest = (data) => { + return service({ + url: '/gaia/test/app/request', + method: 'post', + data: data + }) +} + + +// @Summary gaia应用请求测试批次列表 +// @Produce application/json +// @Param data body {username:"string",password:"string",newPassword:"string"} +// @Router /gaia/test/app/request/batch [get] +export const gaiaAppRequestBatch = (data) => { + return service({ + url: '/gaia/test/app/request/batch', + method: 'get', + params: data + }) +} + + +// Extend Stop: Sync User diff --git a/admin/web/src/api/initdb.js b/admin/web/src/api/initdb.js new file mode 100644 index 000000000..f1eb2f4ad --- /dev/null +++ b/admin/web/src/api/initdb.js @@ -0,0 +1,27 @@ +import service from '@/utils/request' +// @Tags InitDB +// @Summary 初始化用户数据库 +// @Produce application/json +// @Param data body request.InitDB true "初始化数据库参数" +// @Success 200 {string} string "{"code":0,"data":{},"msg":"自动创建数据库成功"}" +// @Router /init/initdb [post] +export const initDB = (data) => { + return service({ + url: '/init/initdb', + method: 'post', + data, + donNotShowLoading: true + }) +} + +// @Tags CheckDB +// @Summary 初始化用户数据库 +// @Produce application/json +// @Success 200 {string} string "{"code":0,"data":{},"msg":"探测完成"}" +// @Router /init/checkdb [post] +export const checkDB = () => { + return service({ + url: '/init/checkdb', + method: 'post' + }) +} diff --git a/admin/web/src/api/jwt.js b/admin/web/src/api/jwt.js new file mode 100644 index 000000000..811ffc4f4 --- /dev/null +++ b/admin/web/src/api/jwt.js @@ -0,0 +1,14 @@ +import service from '@/utils/request' +// @Tags jwt +// @Summary jwt加入黑名单 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {string} string "{"success":true,"data":{},"msg":"拉黑成功"}" +// @Router /jwt/jsonInBlacklist [post] +export const jsonInBlacklist = () => { + return service({ + url: '/jwt/jsonInBlacklist', + method: 'post' + }) +} diff --git a/admin/web/src/api/menu.js b/admin/web/src/api/menu.js new file mode 100644 index 000000000..163b5a697 --- /dev/null +++ b/admin/web/src/api/menu.js @@ -0,0 +1,113 @@ +import service from '@/utils/request' +// @Summary 用户登录 获取动态路由 +// @Produce application/json +// @Param 可以什么都不填 调一下即可 +// @Router /menu/getMenu [post] +export const asyncMenu = () => { + return service({ + url: '/menu/getMenu', + method: 'post' + }) +} + +// @Summary 获取menu列表 +// @Produce application/json +// @Param { +// page int +// pageSize int +// } +// @Router /menu/getMenuList [post] +export const getMenuList = (data) => { + return service({ + url: '/menu/getMenuList', + method: 'post', + data + }) +} + +// @Summary 新增基础menu +// @Produce application/json +// @Param menu Object +// @Router /menu/getMenuList [post] +export const addBaseMenu = (data) => { + return service({ + url: '/menu/addBaseMenu', + method: 'post', + data + }) +} + +// @Summary 获取基础路由列表 +// @Produce application/json +// @Param 可以什么都不填 调一下即可 +// @Router /menu/getBaseMenuTree [post] +export const getBaseMenuTree = () => { + return service({ + url: '/menu/getBaseMenuTree', + method: 'post' + }) +} + +// @Summary 添加用户menu关联关系 +// @Produce application/json +// @Param menus Object authorityId string +// @Router /menu/getMenuList [post] +export const addMenuAuthority = (data) => { + return service({ + url: '/menu/addMenuAuthority', + method: 'post', + data + }) +} + +// @Summary 获取用户menu关联关系 +// @Produce application/json +// @Param authorityId string +// @Router /menu/getMenuAuthority [post] +export const getMenuAuthority = (data) => { + return service({ + url: '/menu/getMenuAuthority', + method: 'post', + data + }) +} + +// @Summary 删除menu +// @Produce application/json +// @Param ID float64 +// @Router /menu/deleteBaseMenu [post] +export const deleteBaseMenu = (data) => { + return service({ + url: '/menu/deleteBaseMenu', + method: 'post', + data + }) +} + +// @Summary 修改menu列表 +// @Produce application/json +// @Param menu Object +// @Router /menu/updateBaseMenu [post] +export const updateBaseMenu = (data) => { + return service({ + url: '/menu/updateBaseMenu', + method: 'post', + data + }) +} + +// @Tags menu +// @Summary 根据id获取菜单 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body api.GetById true "根据id获取菜单" +// @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /menu/getBaseMenuById [post] +export const getBaseMenuById = (data) => { + return service({ + url: '/menu/getBaseMenuById', + method: 'post', + data + }) +} diff --git a/admin/web/src/api/sysDictionary.js b/admin/web/src/api/sysDictionary.js new file mode 100644 index 000000000..f5d6c8620 --- /dev/null +++ b/admin/web/src/api/sysDictionary.js @@ -0,0 +1,80 @@ +import service from '@/utils/request' +// @Tags SysDictionary +// @Summary 创建SysDictionary +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.SysDictionary true "创建SysDictionary" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /sysDictionary/createSysDictionary [post] +export const createSysDictionary = (data) => { + return service({ + url: '/sysDictionary/createSysDictionary', + method: 'post', + data + }) +} + +// @Tags SysDictionary +// @Summary 删除SysDictionary +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.SysDictionary true "删除SysDictionary" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /sysDictionary/deleteSysDictionary [delete] +export const deleteSysDictionary = (data) => { + return service({ + url: '/sysDictionary/deleteSysDictionary', + method: 'delete', + data + }) +} + +// @Tags SysDictionary +// @Summary 更新SysDictionary +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.SysDictionary true "更新SysDictionary" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}" +// @Router /sysDictionary/updateSysDictionary [put] +export const updateSysDictionary = (data) => { + return service({ + url: '/sysDictionary/updateSysDictionary', + method: 'put', + data + }) +} + +// @Tags SysDictionary +// @Summary 用id查询SysDictionary +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.SysDictionary true "用id查询SysDictionary" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" +// @Router /sysDictionary/findSysDictionary [get] +export const findSysDictionary = (params) => { + return service({ + url: '/sysDictionary/findSysDictionary', + method: 'get', + params + }) +} + +// @Tags SysDictionary +// @Summary 分页获取SysDictionary列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.PageInfo true "分页获取SysDictionary列表" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /sysDictionary/getSysDictionaryList [get] +export const getSysDictionaryList = (params) => { + return service({ + url: '/sysDictionary/getSysDictionaryList', + method: 'get', + params + }) +} diff --git a/admin/web/src/api/sysDictionaryDetail.js b/admin/web/src/api/sysDictionaryDetail.js new file mode 100644 index 000000000..d4f877224 --- /dev/null +++ b/admin/web/src/api/sysDictionaryDetail.js @@ -0,0 +1,80 @@ +import service from '@/utils/request' +// @Tags SysDictionaryDetail +// @Summary 创建SysDictionaryDetail +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.SysDictionaryDetail true "创建SysDictionaryDetail" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /sysDictionaryDetail/createSysDictionaryDetail [post] +export const createSysDictionaryDetail = (data) => { + return service({ + url: '/sysDictionaryDetail/createSysDictionaryDetail', + method: 'post', + data + }) +} + +// @Tags SysDictionaryDetail +// @Summary 删除SysDictionaryDetail +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.SysDictionaryDetail true "删除SysDictionaryDetail" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /sysDictionaryDetail/deleteSysDictionaryDetail [delete] +export const deleteSysDictionaryDetail = (data) => { + return service({ + url: '/sysDictionaryDetail/deleteSysDictionaryDetail', + method: 'delete', + data + }) +} + +// @Tags SysDictionaryDetail +// @Summary 更新SysDictionaryDetail +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.SysDictionaryDetail true "更新SysDictionaryDetail" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}" +// @Router /sysDictionaryDetail/updateSysDictionaryDetail [put] +export const updateSysDictionaryDetail = (data) => { + return service({ + url: '/sysDictionaryDetail/updateSysDictionaryDetail', + method: 'put', + data + }) +} + +// @Tags SysDictionaryDetail +// @Summary 用id查询SysDictionaryDetail +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.SysDictionaryDetail true "用id查询SysDictionaryDetail" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" +// @Router /sysDictionaryDetail/findSysDictionaryDetail [get] +export const findSysDictionaryDetail = (params) => { + return service({ + url: '/sysDictionaryDetail/findSysDictionaryDetail', + method: 'get', + params + }) +} + +// @Tags SysDictionaryDetail +// @Summary 分页获取SysDictionaryDetail列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.PageInfo true "分页获取SysDictionaryDetail列表" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /sysDictionaryDetail/getSysDictionaryDetailList [get] +export const getSysDictionaryDetailList = (params) => { + return service({ + url: '/sysDictionaryDetail/getSysDictionaryDetailList', + method: 'get', + params + }) +} diff --git a/admin/web/src/api/sysOperationRecord.js b/admin/web/src/api/sysOperationRecord.js new file mode 100644 index 000000000..4428c036f --- /dev/null +++ b/admin/web/src/api/sysOperationRecord.js @@ -0,0 +1,48 @@ +import service from '@/utils/request' +// @Tags SysOperationRecord +// @Summary 删除SysOperationRecord +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.SysOperationRecord true "删除SysOperationRecord" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /sysOperationRecord/deleteSysOperationRecord [delete] +export const deleteSysOperationRecord = (data) => { + return service({ + url: '/sysOperationRecord/deleteSysOperationRecord', + method: 'delete', + data + }) +} + +// @Tags SysOperationRecord +// @Summary 删除SysOperationRecord +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "删除SysOperationRecord" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /sysOperationRecord/deleteSysOperationRecord [delete] +export const deleteSysOperationRecordByIds = (data) => { + return service({ + url: '/sysOperationRecord/deleteSysOperationRecordByIds', + method: 'delete', + data + }) +} + +// @Tags SysOperationRecord +// @Summary 分页获取SysOperationRecord列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.PageInfo true "分页获取SysOperationRecord列表" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /sysOperationRecord/getSysOperationRecordList [get] +export const getSysOperationRecordList = (params) => { + return service({ + url: '/sysOperationRecord/getSysOperationRecordList', + method: 'get', + params + }) +} diff --git a/admin/web/src/api/sysParams.js b/admin/web/src/api/sysParams.js new file mode 100644 index 000000000..348f1b5e4 --- /dev/null +++ b/admin/web/src/api/sysParams.js @@ -0,0 +1,111 @@ +import service from '@/utils/request' +// @Tags SysParams +// @Summary 创建参数 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.SysParams true "创建参数" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" +// @Router /sysParams/createSysParams [post] +export const createSysParams = (data) => { + return service({ + url: '/sysParams/createSysParams', + method: 'post', + data + }) +} + +// @Tags SysParams +// @Summary 删除参数 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.SysParams true "删除参数" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /sysParams/deleteSysParams [delete] +export const deleteSysParams = (params) => { + return service({ + url: '/sysParams/deleteSysParams', + method: 'delete', + params + }) +} + +// @Tags SysParams +// @Summary 批量删除参数 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "批量删除参数" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /sysParams/deleteSysParams [delete] +export const deleteSysParamsByIds = (params) => { + return service({ + url: '/sysParams/deleteSysParamsByIds', + method: 'delete', + params + }) +} + +// @Tags SysParams +// @Summary 更新参数 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.SysParams true "更新参数" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}" +// @Router /sysParams/updateSysParams [put] +export const updateSysParams = (data) => { + return service({ + url: '/sysParams/updateSysParams', + method: 'put', + data + }) +} + +// @Tags SysParams +// @Summary 用id查询参数 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query model.SysParams true "用id查询参数" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" +// @Router /sysParams/findSysParams [get] +export const findSysParams = (params) => { + return service({ + url: '/sysParams/findSysParams', + method: 'get', + params + }) +} + +// @Tags SysParams +// @Summary 分页获取参数列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query request.PageInfo true "分页获取参数列表" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /sysParams/getSysParamsList [get] +export const getSysParamsList = (params) => { + return service({ + url: '/sysParams/getSysParamsList', + method: 'get', + params + }) +} + +// @Tags SysParams +// @Summary 不需要鉴权的参数接口 +// @accept application/json +// @Produce application/json +// @Param data query systemReq.SysParamsSearch true "分页获取参数列表" +// @Success 200 {object} response.Response{data=object,msg=string} "获取成功" +// @Router /sysParams/getSysParam [get] +export const getSysParam = (params) => { + return service({ + url: '/sysParams/getSysParam', + method: 'get', + params + }) +} diff --git a/admin/web/src/api/system.js b/admin/web/src/api/system.js new file mode 100644 index 000000000..4dd5eca25 --- /dev/null +++ b/admin/web/src/api/system.js @@ -0,0 +1,56 @@ +import service from '@/utils/request' +// @Tags systrm +// @Summary 获取配置文件内容 +// @Security ApiKeyAuth +// @Produce application/json +// @Success 200 {string} string "{"success":true,"data":{},"msg":"返回成功"}" +// @Router /system/getSystemConfig [post] +export const getSystemConfig = () => { + return service({ + url: '/system/getSystemConfig', + method: 'post' + }) +} + +// @Tags system +// @Summary 设置配置文件内容 +// @Security ApiKeyAuth +// @Produce application/json +// @Param data body sysModel.System true +// @Success 200 {string} string "{"success":true,"data":{},"msg":"返回成功"}" +// @Router /system/setSystemConfig [post] +export const setSystemConfig = (data) => { + return service({ + url: '/system/setSystemConfig', + method: 'post', + data + }) +} + +// @Tags system +// @Summary 获取服务器运行状态 +// @Security ApiKeyAuth +// @Produce application/json +// @Success 200 {string} string "{"success":true,"data":{},"msg":"返回成功"}" +// @Router /system/getServerInfo [post] +export const getSystemState = () => { + return service({ + url: '/system/getServerInfo', + method: 'post', + donNotShowLoading: true + }) +} + +/** + * 重启服务 + * @param data + * @returns {*} + */ +export const reloadSystem = (data) => { + return service({ + url: '/system/reloadSystem', + method: 'post', + data + }) +} + diff --git a/admin/web/src/api/user.js b/admin/web/src/api/user.js new file mode 100644 index 000000000..e9a8f6457 --- /dev/null +++ b/admin/web/src/api/user.js @@ -0,0 +1,223 @@ +import service from '@/utils/request' +// @Summary 用户登录 +// @Produce application/json +// @Param data body {username:"string",password:"string"} +// @Router /base/login [post] +export const login = (data) => { + return service({ + url: '/base/login', + method: 'post', + data: data + }) +} + +// @Summary 获取验证码 +// @Produce application/json +// @Param data body {username:"string",password:"string"} +// @Router /base/captcha [post] +export const captcha = () => { + return service({ + url: '/base/captcha', + method: 'post' + }) +} + +// @Summary 用户注册 +// @Produce application/json +// @Param data body {username:"string",password:"string"} +// @Router /base/resige [post] +export const register = (data) => { + return service({ + url: '/user/admin_register', + method: 'post', + data: data + }) +} + +// @Summary 修改密码 +// @Produce application/json +// @Param data body {username:"string",password:"string",newPassword:"string"} +// @Router /user/changePassword [post] +export const changePassword = (data) => { + return service({ + url: '/user/changePassword', + method: 'post', + data: data + }) +} + +// @Tags User +// @Summary 分页获取用户列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body modelInterface.PageInfo true "分页获取用户列表" +// @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /user/getUserList [post] +export const getUserList = (data) => { + return service({ + url: '/user/getUserList', + method: 'post', + data: data + }) +} + +// @Tags User +// @Summary 设置用户权限 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body api.SetUserAuth true "设置用户权限" +// @Success 200 {string} json "{"success":true,"data":{},"msg":"修改成功"}" +// @Router /user/setUserAuthority [post] +export const setUserAuthority = (data) => { + return service({ + url: '/user/setUserAuthority', + method: 'post', + data: data + }) +} + +// @Tags SysUser +// @Summary 删除用户 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.SetUserAuth true "删除用户" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"修改成功"}" +// @Router /user/deleteUser [delete] +export const deleteUser = (data) => { + return service({ + url: '/user/deleteUser', + method: 'delete', + data: data + }) +} + +// @Tags SysUser +// @Summary 设置用户信息 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.SysUser true "设置用户信息" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"修改成功"}" +// @Router /user/setUserInfo [put] +export const setUserInfo = (data) => { + return service({ + url: '/user/setUserInfo', + method: 'put', + data: data + }) +} + +// @Tags SysUser +// @Summary 设置用户信息 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.SysUser true "设置用户信息" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"修改成功"}" +// @Router /user/setSelfInfo [put] +export const setSelfInfo = (data) => { + return service({ + url: '/user/setSelfInfo', + method: 'put', + data: data + }) +} + +// @Tags SysUser +// @Summary 设置自身界面配置 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.SysUser true "设置自身界面配置" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"修改成功"}" +// @Router /user/setSelfSetting [put] +export const setSelfSetting = (data) => { + return service({ + url: '/user/setSelfSetting', + method: 'put', + data: data + }) +} + +// @Tags User +// @Summary 设置用户权限 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body api.setUserAuthorities true "设置用户权限" +// @Success 200 {string} json "{"success":true,"data":{},"msg":"修改成功"}" +// @Router /user/setUserAuthorities [post] +export const setUserAuthorities = (data) => { + return service({ + url: '/user/setUserAuthorities', + method: 'post', + data: data + }) +} + +// @Tags User +// @Summary 获取用户信息 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /user/getUserInfo [get] +export const getUserInfo = () => { + return service({ + url: '/user/getUserInfo', + method: 'get' + }) +} + +export const resetPassword = (data) => { + return service({ + url: '/user/resetPassword', + method: 'post', + data: data + }) +} + +// Extend Start: Sync User + + +// @Summary 同步用户按钮 +// @Produce application/json +// @Param data body {username:"string",password:"string",newPassword:"string"} +// @Router /user/sync [post] +export const syncUser = () => { + return service({ + url: '/user/sync', + method: 'post' + }) +} + + +// @Summary 获取gaia的额度列表 +// @Produce application/json +// @Param data body {username:"string",password:"string",newPassword:"string"} +// @Router /gaia/quota/getManagementList [get] +export const getManagementList = (data) => { + return service({ + url: '/gaia/quota/getManagementList', + method: 'get', + params: data + }) +} + + +// @Summary 设置用户额度 +// @Produce application/json +// @Param data body {username:"string",password:"string",newPassword:"string"} +// @Router /gaia/quota/setUserQuota [post] +export const setUserQuota = (data) => { + return service({ + url: '/gaia/quota/setUserQuota', + method: 'post', + data: data + }) +} + +// Extend Stop: Sync User diff --git a/admin/web/src/api/user_extend.js b/admin/web/src/api/user_extend.js new file mode 100644 index 000000000..85d7ba6a3 --- /dev/null +++ b/admin/web/src/api/user_extend.js @@ -0,0 +1,13 @@ +import service from '@/utils/request' + +// @Summary 用户OA登录 +// @Produce application/json +// @Param data body {authorize_code:"string"} +// @Router /base/login [post] +export const oaLogin = (data) => { + return service({ + url: '/base/oaLogin', + method: 'post', + data: data + }) +} diff --git a/admin/web/src/assets/404.png b/admin/web/src/assets/404.png new file mode 100644 index 0000000000000000000000000000000000000000..f80372413856425983d078ae25e1aac63272e8ad GIT binary patch literal 43988 zcmcG#2{_f!_cyA!h@4Cr;|N7m=4iq}5*5b~2W8IuB^03&4#!kTri9EKA$$=k=OAOs z5DxM+6(W^6-2FY(|NndMz3=-z_j&HyIajna0hzwm*M|hnJ&&Z2aE*{(iC5dkOrs-9j5f zLWZoY7Ta!7sl(v^KV6%fo1hEOfRAWP`|ZV6FVF&L0N1dURYSvPD)lqH_tvnrHT~l- z=>01adAT3dzz%YPArKc+qwceH)p3=RfA;o&fe zRNmBxQvUe?1VAP5uerAN&mTZ%AoFEW<3LCA+}sEtsIMOcTmY^M>Mpe0$|@Jv?B@>? z;)5bccXTes#`?WP-aVU1jH7+3Y0D_P?^)KG?+2Hqwlx-Q#zZA07Dh%sZON|w7@g$d zQJI+7vflm5*E>XB{*tw2ad7b4{LpM?ZS7z;cMbInL;dWH+!RGcJ5SG;H^iq#Mz+4b zNp&^#!~HF!TV6+wcy@KZTk`e&h*mu+dO zsiyX&x>`{|@qHU@sH$Oerm4T+)%rru>gR$U_nSj~pC(^7^?d06<_@iVirbv&>WocK zeNxq)S`;4xM@E!Sjefj*+3ag_){ieAS{rE%4MU3^_nOIX=P8W~QL&qEUo5_B-B_IX z-O{>Qk+b%_yX$4c+Du1nZQtr>$(b`M8*4w6l}_8+-wh5XM@7M#>#OU(Mt;u^tPDI` zZ1Y+A7`8U?s<5!b)YNtDOa1fbjSnAI#mCo(in^XU^$Zw#$kI|yPHywTgUz*UwP)j$Xs)D z_Cub@&X0##c5P|jSYjCZGHJlTAjqJFQ^DWrpBcOzo-%p;%jBBTm1B?ciHA&FwkSl8 z>pc?OeP1y6P*%zfCih!0T0V9=w+;Uu<*h&A!Y6s<(CbGr)6b;S?X~!{3WT!L1gW-} z+aHHH+E>bIV zmcD*DWKIM%P$zrJE^m37YcGFaNe3d1Ha#RqwrTC8byK@*zB&8VR-pc(snp*ebulZ` zk@`k=_&Vw^!q@$FEgpP&v5)MZeh#{j?a4s;Sz-q-AUt+j7D7oVo9o})cCAskcqu@1Q#MYm3l!BQmo z9j9_hUmjh%E2MIc^%!n9c(9Z}7g@&Ql-wG&vF?0kAF5XdW5}YeZLCOr)zr@{2&_3oFd(G?}^ZEHt^pZ*^Od=F@CFKp;%JcF}WR` z+k-f_kG-4aD>#Zd_#idE{tZaQ-4j#ljmio z@dN(CfI(Dj^mq4mjHb&km;UmqC-+4-4N={Yc6i$v4KTjy(kwHz>!{i64-7_ZMB3Yq zP%UD>3T-cPUC;BR;tL1M-&I_tXenyv0hiF?cQoWmx;D(=g62_n&M2B`*~p2zFm8zxu-BlHwGS|PkX!4=bnuQDkcvUD5Z`U~XaF$vIGHIf%MB#Q` z0Na?vRBuj~ae2rS^;keCLmG8z=L8{dF&pC|qAs@<^#g&y+5`!9aNE^Hz+e zt<4P)&mNU-=f@C`x^WmM>$3;`)z!>7dSqYuh)d#|zpKbszB#)Obmoi`@FMcY0ejqx|t07cz zPp|u$+3GfIXQDG-br`Np@CLA9(5McqbSgp@-}ea2^bCr^b|&HsD}Y_IC=Ma=?kF=l28^&Q%Yf--$BNd1KFuSDI*@8eBsZ;3yTSB2fA(RxqDHPBF*kglD zOfL^dl(`!%jhTSfsHA(O{g`THb*_@a&y!}cAxcOs~kvT!4qAbDe@#69` zbhyk2aS%earmyOSztQ{4*Sq-$xMsYcy zJpNxzRA7{Epea^o8XE>9;uU-V29)n1)5*FqBPx&*ez9rIk@l4jlH6yQI*kb7 zfM42%zI1fbhgrR;=YDZ;p?Vfvy{^iK#`Ft#@J?k)v7dXKAb+ zRCNrLqP%AC2z9{aszE8~Iz*H))sYRl9Rb&>@p9c0HLO>LxM8&=92(_4MJNS5(@JStS=sl#CpIL&SJ3?{ya zsQhC1dhmPk*}DfjK^)9?&AldZ1HQa`F_t5~mpJX@eS|>dsOZ>tgB6M`)gZ_DR$k9T zSVCa}jxc9vq(tJW=sz{;)W?=;!6Njhf!c zJo7?HWFvbqFlpa6$LX2buPyxnrL*7GB-1C}s+`SJp%l9%zD>&IsGz1a-KH*hx(tLllN*xC{R*yUZ$2w5&8V>1>{e2kdl{T~@SDGJAknJUS=G7I z847x9=dseaG&4|KF{Wy@jxll&nQ{N*Oeprqn@*I!-s_j+7j(BJf#2T*@Q`(+o#zLT zAI4D4nit=ZH81;gjM0w|#4gbp9~M4ZT`wn0=tm~yh>Et;rs*FzeQ};Xri787w^u(^ z?{g_wUiO{B#Q%O^HrJZA_oM6W^16;FC$Jq%))YEaP&r(hS{Oc*FZQ@yf{81Cj=G^z}GjtS`gt`XyHjAX+zEN`wO!He{AErn zT#$7S2B@J+(OJo4!jd0PR%Fq-Fk=UM{dgUD#nkz@$uNuXF(Mo+JlpFpqF8rE45@NG z#TGs)83`k|V2+3^j_w!YWIIhWuAERsG=^c~M~J2d?9jr|9L_Ubtqgj0 zZ$lNOEg)wN3s&faCc!u=>*)A}xoC|WEQ$utT#(OWGzr(DvTU!3&aU2f z7-?YG8C~1Pd`egxX}Uw@KfZCFNRme$bEp#rnew*aX|=);d!w7WxCXS-pv|Te9oa?EsJW|5G z$b?jm?F+=mOH@WWOi{JC{`g+36q(56ij|U)fvCl*!4k8tN3<}S*5wC6R4D2kkODiH z?#>B3%}(am*G0KIhYcv0XX}0%maNb`jxY%m^M}8Lh8q$lhsJ`{F`DPR@&JzWI9k#6 z15iRAo6%90_E7lkq|A>#m)S&ih;l={s2FH_2-ADJ{UX8Lj#pX_IxVrEpIx0#r7s6; zUh4ayI(HR{oYz$vjVM8Tze-ImuaCh-62E;r!4pU%2_gMJ;!ge~3#jnvPQ@;{BM7k{ z^+2g$;#xNo*G#D6cc*&FtFSdyM~T^Mn*fBxu<7Fhfy9;oN=o%LQK`Nb#DoC(t;HN< zRp)!t*X?|6Cpco_Xh<*@p3uVS;UljbruZcJ6zvA?Z-*VPZfg;oWb*RK$4W;%$~_Q9 z#gLiM{14}acsaa3e}7KQe|GlrobI0aX%|mw8F!2d*urHXxm4#Tc`&EbyS}F%auSk= zYe^OlzOIwF0tArWbr;gvIVt+W3589Do#7b|og#SgWrHdZeLoz2N8YAk15TK_lo!0*b^Hz$o;DU!ET%W^s59qIuumm5V# zS3p)40G)EWlmASPgTc#LBCn(zhtcvyeR;#|0XHz49sE$tH-8!+N-D%kbrK~!+nUD1 z9N#uf#qxs02Yw;!;vSN^B#8XT^i3gZ%Oe-eWZWwe7u~fC+NmSpxI)agCZ#S5BJMWc zS12NUkX6tJJu>_)2FMPDn-`Z>682%G&icp`;pf6+MOs*x^7~^bn`tW=Ev3#r=29mh zAg0*+Q#}tpWk(T~bWkrkxCTU~yOt5P&mb3umX*Ji-tddA(G*or1_d00PpD^(Q&!W7 z+Cpe|k^{By37yx##x*MSH{iV z&9Ar=hN7ES1hbs|twITM)wx2++z)YNA&d%#%pqqp`eb-!rWQXQ#1XzEx)liZ5c7|7 z7{Nj#5#4QyPsD*AJjJ^FaLZ2J6hyXG)E-9kKni$%g(z`Hs58v5`3)Y(cjtgWrb+m2 z7z%e`$sh295hg8{f}yj2V5~&?!$C<=jJ~p1u=?~Q1xtq}t=zdln2IGZY9_E7BuwPN2cTj|G+B)EgyU$T}Z@QQEo})U;v)?5$btVEUoJ2Si!X)mH3*U6S@cYvr9oVpy6&ktmp*T1P zSibl?b?}oh)pL=ALe10VA`4DB$vWzocpr+SJb<$Jp*Bn656VX!EOB>GTL2aPbP2}636?ysp^Jx?Oi%Qis=p%`U!-;H+SfZB*KQmksi;cX8{ zrh`a_zC_hW!S6U~cJ5$>-hAP{k@%BKitpf_cAP54h>9nqUA{zJ5a5I|PyZTGJLqq` zvMGe@U)Wsl#;%qEx%B<~8tu+KbrB5IeUaL2009>y+z2RJ7Ldfm=bTMy4Xioc0A$}= zwh8TdPUIDTt_KaSW62xDwMM5zhn9mIR(A!O)hRjxeI*$+>z^mSg)H*hgQV+Au?`DH zQ^Ql@rL8&vdX^Wq<`j|oxm^om6hJw7ho^Vsqtb#m{~5II6(Qj@J6vMbSXMhd)8kE4 zG;vrtK+>Kd;yh7AdVg&x*P(m9$&fd%w>itJ#lZM&bFatuyb>^-2InRht%q6Mx@m|! zov0+&QG8CG{HZR}?`N&<+6*XNxki;Zq!&+N$!=;3jL2}L7K7py18(~D@ zla8?lJF^jo99WpXilQG|cstWdt~LuH=P4Ji#{oZo%WN>4-KRib228FdE_09DoG*YW zTFHOSo8T!bM`|ydQNMAZp<#!qyYk}6bx^MIueLGpVCKvt!w&MQ2mkm*(5ou(F;>V} zhgsdg)wK=IwVLm6%|KU>)bD7=IoSh3NKQg&u8wy>^fhYPpQ-=2*86x{c@yY~06hx$ zoHV40Yvs2gNL;{*Eme1!^ooB6dhaFqm-@Ce*`($Zpp-vri{ME+^AjQ}=5f?tk zE=M=dw(J9&96I7{b5H7-Q8sL3vro^FJf~r4X)|b7v!8!8h?#6qWlE&-msoHrzwmfZ z-s1yD4(UN_e^5VpX8gtQH=s@PKwh?V0IKPcnvxQIx;C!7`wnD-BSTNM;ia;MbA=q< zIbD{w{^&Ee*v}V2%y_=wApXEqBTHg-BIw}o8!dWd+2HS+ol?Z{F*)n&tI(vSov;73 z0~KnxUK*}OIdN%t=LwDmNnsHQvIl+Lb7?p5OIr#dPpo1dGmbxgsK-&EhU&o8Ii#q` z#|pKUn`#Gnyc{X%XkS_O0v%$AWy#whVWJ}0R6KL5QvdNfr2o&9u6!Mylam4o^UYgi zp@Ix%{WGZv-Dn`i5q^4>DUX0sPa>0ss&c>|L}99 z{Kd+V3slcP*jj>1!7o9?QBr;o-c6GiY|3-LvU{aaF49e%g=caGIKr>RrW}SS`HL%W zZ&LX&sLN5Oh#q-F_>*M(6(Ip z1CG)x7_QQxGI8-%ptsA^r(;OokM|*ItM$sW%O*q%@48ndGQh>BUR83;bOyI@cpFm; zDyFOp8m&P*qRmej!kWBUujaFefuF=i%X!~{{MsZv&BPV^85AJiyRZ31*bw0Zo`-Xy zfwCumqI8u`+c8|3HjLuif*B+5G!mBOXf97X$o_z|_eNBkXW*{ESWlhOxqpb({NRo1 z3l1?vP*^)}P(v0+AXF{r%ag+-wwr=W0cpf7=>#*zPMfbyfp(2rgGB^=*5w<75T{V# zmkV7-iinEWLMC}Z3Jhrz43l@40m;@)VFIH(SSe2a=`MYZz3f{H!IjC?l9u_MIKt5$ zQg*=sM@5A37)kN-3*P_?^zgfzgV#m&x5*?nC|c6Yxj`OBV1Mk8-KuD~+e1p1qS6C4 zt~bmWu6sisw`KQ#^KnS~{*t(jAP18EH?>T++3hKEi@yw~L3KuDkx#Z(2yvf_#}R_V z+3y#Cfw{X}?y6D72Lt5HhrZ_W@$75Iy~ZfSo~Lk_b3u2*K$pKZt^dN@Ii2zM_P2Oj zwnoC%J^D%JVDflAQ1cqVvqP0WKKUX5qjOXt#J)-nD-|`JtIq*;zb(27ARltdC$@~E zYBgwKcI7DI2$6-H(Ct&H{bRsF(N~iqJc#hCsl%YOK+e`Rpe12h(t87%&W8|Na&@p$ z71OyosG@z{p06*1wrn83ZaO#UZGwV2UkTzUdYDjrM>!wRw^UN6_Yz*lq+SP~z-bG|z&GSDscU?5x{Rbwlkuy2v}n3RRYB zP;B1g=q=NPRT4-I4X{#6=)9J~fD0|m$shnpTaXTJ`Z%zm;7(N9ZwKLQ5Ny=yVvor1 zTL~lZ0vdiCaZw%GL&c$s~O@TopY1Tnsp@}q>;3IA#PhUI%3>G{m#c{#K`qQ{6G~2BZJlVx1x9 z;4?VF87DRfVec6Ulj=)&9pVLYCsNw#xt3vaNq@>-+$f5xzanI%eVSD@Z-Jp>A+Js+BKo) z^fDo3hjrb^J`7i_MOC!L8%^5ff9*lmsq>)yl?4Wx2U_VvuJqu4H0M5%>66aEvEFOn zG2R8NmqE=C=zBB=i`b{vmZ@W3SzGr52SMkHag^DPMc$Tc9;V)Y*L0CubC@2-dLN## z62wZ`PkZWRF|)tTMfq`rYp>G6h)e@KDeTiC!l6cg@H%eBzf%QdmE}{M5RiXTB zAmzHjilYQMW$K14J$jfB=1bqtxFm~Qg%H&VHwRi0@(4IC3Ro#KQ86*wR7Ab@$ZUxb zox8Z-g%3yB;TUtF8SYd){ukGd5%X%dG*%2(7Blu; z7W|xEu*GmaUk_Hr5u%k2J^F9%^Xpj%E2QFHf1aTskpl|qr9+tojs!d2G7HO7MZo$& z%f0$wA5FB4z?B@4D*kIp2T~^(;+Zoe7@`u50;mt9kXh=%#7=2*fI&R|c?Q2Xh!|VP zf-wN9d~|Uf^k>iWKWM%)3pq6aGG^VVb3kw<5L`H%>OVf*x6zv%5QF))6Ne+%FMbhD zLN4IZc!4d=?;sM1OszhC*7^i@bQu#u!ihxq_0e8Ud1{>dgbrpG)Mt+ah}%}2VXVh} zl$ccqO8pv_IE;2{F)qM&>9`rdWe-ht;XOC{KAxusV*Q zNL!e$8a^ZehWVKH9R zWp|}byAJ~xSOuKiO45SEl#d{So3teok?FxM-ejkoH)wfmY{*|t7%R0`z-f2$Eum=? z^IIXz5k&a-45x;!d>0;Fs!i3pR)A=?`Jj6VINkwmY6BZIa0J$JMlhCx2Wpp<^0%LW z?cVBLAG20_gvz-i#SPAu*E6NuOVTF7e6LVc$YtDo7)a>*O$XzhjNBlO?mGG2#SN^! zYbk(82t_!08!vMt5oY++l>Q=|LmJ(ne!MtB?)^?Aw$z?3IeauDsNj}5Ma7^0_$e$> z;u>=lNN=x%jc|NpkCTA2Z!sktk#@~J?0!lP#VvZD^Um`)LQf&P zdK$O5H3&g`WA$F!4q>IH4N2HWeYPAp2Fv1Z8?t3TCf<`*_KI6WDHjx^0P?veu10+f zlu%*FcJgVWJ*rcPFe9SoJ6*N3CJxDHoWV6v#7dXwq~Z-8*U&6)8S>bZqBJEaQ87p^qSG z5!|;rxCpl4a=Ea>IJO{KE2>Zdi`5|zhA?gX!et$3!_i9sHXI*j@PwWRok0E%K_|bk z(eD~!;OB2L8dyZyxkAvr@w_&=q-^=6K?}Im;*}W70>7<$Z2z$I@e9lNiz0$fyCNQ< zQ$%$C^cz;601)38_!X_JVPXQXlXfJ57BS>J&5adfv}}d}?*cF(M61VE*KxGaVGf9u zyxwpg1byMRBqT2dq;Z14+q!J>1^()AkEliwLa@^KCT*ZID~PDLWD-(x6{?d2@m2^B zw?5gOgNXW@f=4-Ol!q@3+?&)PNi=Pwk3@QRPDl(u{fAa6jOhiK2N1%Cl@bjMN=AcN zYyb6d90s0-Y8k_C(sUoF;MxCYvRBJ`fse95&#%9X zjzZUjfig4QG~%j}@Y^}y>8~W|5`;O}R?uL6wHtNDP({5H=1uCTJBVM(2G)0pIKbHL zCd7q$XFdz+o!Ce`pTq-m6mY?oQ=jglgIGO}CZU5|S@)xRy(E`f8qeM{2qPOxRZ3d` z&RAt*d7e^+u6*m*YJUuLwZeRFkVC;=#3%OaGE27sg2O zXL|8|=^LdO;D3;>$=S46^ zoaH>p5iE|riYlAldIJ;YlpJ4dfzo87UZSd#Ernkq2dJ_u0*%n73Ay>|dEsy_&yc$>XJpO*_+7Oed0bxn!1v>tnS0MunGMcd?tDe$ZM_O2w!D+7*G-;- zXaJSfHzMIVAT3!Mp4wfRu2kMM&5G5fstCZM;1J=LFYQ@&UJj#hM~K^Qx@y_GlZ~T| z1Q*l5bUe3-SwBE^-SVmbey}o>c8|z=D*iIXZ28&K`t78RY?30!u9I#Q`d;PSR%h}& zEW4yxllFd|DKsHx#j(he^TlmY`UMufV0b_PgwqOTpv=*yMx+iTMcIEU@MCsIs#5iG ztXW|QZ(d(SFHm1{tOU{JIW4rU{0~P~pAm`A)jrbcg=K9@s-pZF{PxS|?+Ia>&?H-` zDm%pNt43AXiOz-V?zm2^$(WNBQwqn^SDV+Kk16j}tLIR1dG`1SzNgN#-7nk z_Z&RxZq{_Za?fk4J(q;SV4V1@j_~1^#!({}0Hkl?C%4Rw9eY#rl#qtz2OpP_pZ1fy z0~`u^?u_%laN5Ey>A-gQo7|x6u;q)(d^dJdBdlAK@HJ$zgSp2d+CbnkztqRbQOO8+ zo~A(<2@f012w?eGG3-0HOUQrlVEg)FH9E`tLs@2$dhSv)%f`jQM2VW`C-nxo+^ROJ zkqbLNSKb4~+9Jf9{Y0lG?bzF2KS^A}`{t)N>}M+iGCD!hb-%3emC4xe@fs%sF!Y*w zaDK|3QwEcq<(-e%?%M4l6-L4%_rbmv3gfZua^P?#=wv_AjkU`kgdKbQ#T?d*OW(Ib*4nff-4jy(T<)ILARWF%^ihjjfLjG#z7~a zK29f>T*z0y@RUi>b3G`?LD=Qh+gRgm`jA!JL{&2_42ytjfK-PJL=II0shfAd5$Vp0 zPi#!x8nlGfBCgz9J&)f}}2Gj;T*RH8Nu9+!kotX&igcp;IEE7=Y@%{-ABu}@V`oi=H` zNMYs#TIf^)}`PXSm{`)hy-{WJ=gU~@-1tkowg|h=iSq;wr$?&RZ~2Q zMs4irL~wk9?`RMzP-TN-1ujYYNj>WlHe$xnjJL)F+T|y!{=SxT7%>_5)Di}eu?dsw zI^F{pQB#V_W=YAjy>wL)qwrywSy*K5z)92YpqpIn;hgHp!0ptF#b#eT1Y(H>xxc7= zd5f_&ClEA_E;ze`pap6difk3VO+XfoXU-)8UzvS0uq8J$n^iLmOYR28wjOl8&lBJj z4Un%3>fQ3rAFI5(uURL_X6l;SJ1)_77EX0SOAW{XfOS4xq_GHt%$N1u*}mhoU$jjn zt1ZV`u}0y0pPJ5ON@B7BBDbU9HoC0+0VN~21>fcpufDlrkM6;+M-iZ?PcejC#d$#< zm571X!o$JQ_0VtY`Y&I37GvdBXL@dJ?Qlskw`PRW%TH?3<)zJqPRmMTk*yDBVwgob zb|e?xVn}NyN^o48bTqqLaNvemUM+_@rL2h?Z1ey-Tf`@VE_T&g?VZP#Nr~a@dd`%% zWzR_s-hft5_ycOcbYcr0R0v$q#pNqXU$<_DmaXeQ_w_HvcC$m-yl4B|vVNaU2iBf1 z{$s!P-&l`t7S@z+d9^D4yJ$7-;0nZV{yULC<`e z&!w#PT62*V_1}TvFz*md)?_yET@%u4C%4;0VmC8VWy@PLqX~jpVPaRtOy1 z$p@+J;L!BX_IhPx-S$0igBq-nlH2X6^?tPfzhZ4CbAYv{_v>Pc69hdblZ*@&tSj+>^ z;zpwa1oD=o%Q?%b#9#vMYMC^8jQ8j+a#e=q)ym(7L#t9kQBywAT1kin9&f70?(= zgxwa%OkzlLs&h^sZEd`j5(G+aB6~on6GD(OrQCuiIhvvB9s0oMiu*RX#r8Mndw96U z^X5!yAA>yJ^;Onc5-6G9m_hXbm3B)~vRp4TV;5W8(2-@q1fufe*YRh}zpj4n3RV0| z^2wR$2ojbt8DkHFjqtx_bI+6wPZ80BLS>VQEf=UYN3=nt17SJa<~q1x`*ks6ZA2sN zBoS6wK+8vHPo>U1bwaTs?Q(qzj|7;~cr*9kAr5kXPF#@D8U&b z>d*@7H$GbbKo5lU!k~Tb;B^h4hEKt2en5xrUeWc;3VB51=D;gZwo5^ej94l87VzMp z9F*3>+qeNNN_>Vt;y_`VYRE!AIx++YP=SjmM2}_c~^tmel(LZf`D|HE+$MW`6rT!i}7VwK7MIHd;Gyuv9u_l?`_yEuwRry5^tpMT( zWW}dE$yYKBZ%fVQ5cB26)2~{BD@c$MOGIXe!LL>ZGr(h~MCDx+fY(&4nrqH_68GS){{@gkstQI z8?j^+i;aib0@x3>$3X$9X};@y_~Dlh7k>k$sm7(6yiv2%ZYnH(NOrL zRDkxZZ=uC!+_M&AuxWn0aFzY>7C%E9>L9qI|Dh=}9Gelffgc?k!l;2d=S5xl?Y72G z4Zc#m)d_0(xxKZ=j@2xZTi;bgptaT~$xgdlVI^&FUqSob-Q{?(aeRRDzxY&>(HWD+ zaJ0CuirIYb1%sEti26}Ln?25kfpcN7PaJ%F#gIGRXn;8gY?OsP>M9spxs?*4;}_%l zQ-%g+U^^1lFP}<}4xcLd1rF_T7CJRkXSm}v`8B}s&w6%eMDKm~r{`qPafMNr&QoaO z_7F22bme>l{;ChyGC5O-Nhe3YbM(a=tKJJr$(KC=*Da+ouVGe5i_h^%3{o66bQMAbP?3Cg)deJ;e znz8AHK)$QgtexghUMOnAk__H|*>~^qiBcMg-Zj_&s`W6yxCs`>_eQ-^`xe$^;z8g* zKw$5%-2q_SmL%pa^EVv^{g*)JtZ`r=bkhCPVF2?J1>UzK-G#EC0@3YW@TLc*eR5<6 z)l#PzdB$L)iVvVXYP9B%C)dsQ=?2zt1{9r z%ndI1C+L{;c2t9iws>^{Pa{u3 zZWYMSHA^dJd(G;A$B8Il>~!P=PivgR5k3=^rpd$0ps<$-AWd{IuG)K-2!Af*JP-J5 zk@P6|?5gaI9z5M`{RXx_s`elK`8&gdwir*5)hK=J zA&$t_Up`VABl{-RX=<7No!%QGngJ^@srcE-OCPW4bb~{eqElGC93)PT-N?hv`n2g# zX~S|i<<6_cg(`#Qo*Mo z5xcS4hf?ThfNd*k2_`+j}^rLR2CZ&pa2IueZG;JQUI^J?+CQIxBcop@r68Rh$OHTx0m z`2IqfEk=ZZ`xu3jD=9`6C0RT%UV#U(?OBS6*Km%R#?Z0E(CVq|uTuFY`fah_8qVO7 z(YfNN<06QtSl!Ql?H;@^^!if>vGfRwLU<1R8%OA`9*y)@KV_p{QM?ahr_YC~Uijm^ zrbkUu>)FNZB{1u{jWg(l;K9euhZMrW%krY_%IvAhKZ~BC% z^EE)OhK;3Sac!ggV)ng#n0Q|EA$P?%)sY)&zPW`Es?GJ}C7va4(^;$`ZWAA`Z$QYe zXaF-ysSiGlBlyGClo33huyP}*w;*>SoUS7B2os`TIpbOHTiS~UQ1@?DjsUm1e0^DQ ze7fq?tNIZ^|4nxVip#vEyI|m$r{ncL2i(bG;=`>UYgQzhLt@PHZ5H}9)QXDSMQ(8W zd%FZLc`~^17n1W!&?uP>()DR-sa79{=jWN7YUYB&7=o3t-!Q#D^T4DvkCo#bv@-QxDX9 zs=~qhs_9(C-srVTMUIM3iBo>5S@Dp?{JIWU(m?Bv#kw*ld+%1lGX=T+s~;T65!&?{ zqu@$E!0-l8@yVQxnm z0Bo$*!W6j-sTAazo0_DEpY(8-J87Q-Q0Wr?YOT#DP9Bl#Ha+W{m$5Xc6z9%z3f~yW z44zz=h^Ca(KZa+37cmMtI_7zp%x10$_$`!~`md(U?XF^EIq(&{Md&UyRJNf;+v-6= zYt7oJqrgW7I~ionA!sD>^UL)jr?29VJh!pRqTLj64o-Bm`6Xo&`b+crLUhmYEyxjKeQDhgYQOx#JQzraP2e zLcD#Iz|re2plbjuOuskBM)ntCt`yc@8+&jLHT&PjL{H`Z_m);a4zm_HmbkV5nTW^B zd%yVgHNjd(r;CCYvLKyT_Np=yLBG#O&&4C<;cE}@60^?_F^!(4!xXcuQu^;L00e!K zd~WhC$yDXG8nwGZZuSbw1TI!OgI~>0JPs?BLlbcnijU$V-KtUt_7oyR?A8mF<0AhE zcXEY4L$=;|n-(etzRQSS2BmukK6c=CbqSgb4{O;dgl?$Q0-j9MCG(-`Z0DGKpvHh% zB2{jn4^y#`h65RyCN_A?j;qkIyyBg0*`(_q22w8f#XEQJtDG(B?->rAzxVJ>aoQ29x|O8OjblTx*Y-F^`f>X zlbb8+ZFQHgg%%dbe}9HTPlTJC_Cswq4YvoK-lcHda3bnOq_*ss`F`|FrOELaJoHt{ z3{=&Htui<^*jwO3HESPxT?%hQsORGdPCa&qkoLX1CYSNC-OHx0l6YQ(?GYQ*ONgl+ zFVl29e-iizE7W-vE!yp#d;WEWiA%Tos{-e!n!%mdb-C3av?`5UYvyX>>3;L@O)hMY zZLiLK!;||Xs^lFk>o}XOe{$(dM%0jk!OdeeH-F{Ob^fwY$PTrIDdy*<325=>ua+kV z6qWtR3NWYmvA^!64RNuLz5s7oTX2LJtjlHQal0d)H|_16?3*6t5nG<8>;$jkj`|pp z;0auEch@;ym!Z($&Nuhw^JDVZ>V?6#^Crb?wuQrHD4pzM5}bvj<#c!P$Vzqb0-2~f#HxUAn^dqg zO&;$G|KvhYqfo7qh}z0XF#DAzQm&eqLo!_Y%t4f#xyaiC9x;9~7en+SVXLsir|Nq* zh`RU{WpW!eLZ@To&hm(6#pX{l8JD{C#i%oPjMzfqEH`~VY&$6RYac}j`?=Ed#q>4v zEtu-O&M%xD@|--K8tc|^+Iz^W_nSkzyF+sovm+4(UxouOzwpBCBG49|oKdeQY(_@c z>Q=REyf0M{>NJ{bLnauX*D0XdV89bMy$Hodo~=U|qmOS8M?0a7Uoj3enK<`-Uk~k{ zL?519G`W2ayjJum5IQ%yAQ@ya$hq`g%%FO$ePE;SIbZ11luUsTauJ}9!?qU(Gnfqy z$zL9DtzK-XA586!4gtlZW<}EzAzRIKB z5&>s#PzJq+Jgcb&JZ`UZf{CPe;N6@a1$=?vdtaB*(5ur|KCX08IgO~h7y2wO1Lu(A zs4$^qvY|@k+`HN(vA*<4ObcFXuRbF!qRBsF-)+a!uF-FfuH zcji%st@FQbd-@QS!NoiM=Zwy3#YShyEAMq6!Z}CTpc^=#p^#F~xqFumK_zK~rmH4Y zfFz&DFR+N-Y0{nuGA_G0_^om4li;t>Ym!2m2I%u0Qp@UNb=v&THz&PE-u5~84+e1J zwrJAMnbqBvZ3Bn>nQJz2>bK8wV7L+t3d7*vO4%XzWj~##Fn2F~H*{um_pbDN(7fs! zegkMRb5#u4?`6I(h!3hy0Ch7qcdw=^ohnwqcclhQSfPw;efQsQgFLcLby%+I>XeSo zc0sY8lxP5;c4NP#|3w1qR^Dwmn*=0dJ^M7Dn!#YD8Od86Mwjd9OwUWkuA=^S=G58ThTCP9%6B;(U-d`0EM z`Uj7Rjt6xQB;&_R_|fOfPKF7gGMLFb4yP65Ep+OTjFB5<~sP$@TOR|l$goD`k%@!cdT{aa?GtO zYK5Nu`V2w5Uf%`i>C8FMr>}5Ec5akkS^my=!$Y>wZGk^Gi{8l-?la|!OF3w+hT8*! zJ4v${#cvsp$*+Xd^wQ4A%9>1-27DfsJyh}{4@viWkBVytP~|i=BE-a)}msQ z>h)QPJ-@Q|#rH8Sb;UQ^>N`l}FGv?Y#7V@${sDP;YPl@9maKlcqx13Q1*4 zXsoHMSwdtTRFZ7NkbRk3im^wbvL#u@zRrw2&DgS)HD-{pOa@~o`+I)sz8~hpoaNci zbDnd4=R5}-{1weUa1u@LNO9b#(>s)=Z=z?7@#$Em+?^Y~>Sr>*t0yW-^y4^`)njZt zUEwpXB6gyXy-`XvY5!g7QerEKHDjpu?6=zGtZ1c$c@qgg8cvx=XEFvamcPvPxW97y z;)&w+1JY3QFAC>FpIIvhA}#W?$QyM6W#Q&|+A6VPcD3 z=x_lf3xHw`Km3+cs{x z&|mgk8*N)oYZt7`cqq^}^5{D7A5BVvcah?G@1CUuI^O!6@#~t&I?T;$P5UjDmw9(fqY_B#~h=?=&)Sbcsj4YQpoh4%xa7XyH1!`ly!1x7{ z%VJzhEYBCukD3KyMEX-`R}YqJ9Z)rTnVV&MlbQaz3Gf2GH)NR3=*=U*{D0rS(fw|RiUG_zWz1&Ex_rTotPQqe|8guY-&Pq?Sv*aE(O?aZ430bc9*cQY75GHWp z-N@$l9Q|Q9we52bi;)ez!&)`T8EvZLY*m|4=PN%lVwE)eqVENjaNOF|<67_N0mwJV zqrywy48H5_=Crunw%yUNK8#t6$9UO^J-Bg zg~fG!s?_+`YE%i$0Usam7{~zk`=Gn@PN^7ivj2g@rEV7q1bzOdnV0BeVv~er@8_4= z7ic@CWYsy%XgHWAxkI=`Gbb6K1!i?;E7)&tUAwegd_4Be%gxx}`b|hodDg-dfUw)i@d8N_$sLKkOaxaJIQc`klXPIe7zBsym1n_zlgZjbH7l5{D!;#} z0&6dy*F(1s4$4t4QzZY24~M{1oTc$q7+FovDIKvfqTPguxxoKU^ViY`m*q=Q{8R znk26tMIdF~H!)0=+8N6f57xQ$fSEhx<|o`_Tw zjHvvA@32c^*oZZPB2fP5i<05(dLY8pI|1e=-R|b+RoXny1sNn>o~L1T=I+4lRFI}7 zb$4gkUcJFOIyc`fF4;))u1A9JV#d7Rtfe2QAguuNdSoTSJppF2c4W(gX6RbKfVfq$ zwe*!oDoC6^tLw^>1zxlsQtK0Dqt4>Eo2`kla;&(iff^ZnJ#`#9rXdS3C?$z~hp$l; z5{_SCck{Bd4Nns0ev>i%FTXAs{x-WXoIBoG&P>VDUx10zzU83S9A|E}E%Euq0DJ!< z%@b(VM`qP9#D}5Jl^glKk;8CKt>*1g6m;U|1HSPU&REZV(($4mxmH_ zDLGggx;I}jf%rT{$Ra6M>kmIOF_ftF+9k~)wJwhDjLcyQ2l%~K0tzs9#2~p+Y1PY! zL5o0A%|`yyP~sGOZ_}_=7A=M7y|5NoH^q%rQ3?HhE2 z!Z2bguMdUj>vl&6YG?b#B9^Tl< z`>5?DP+-r$mS&aqn#2p_3HGaSNmk-+J}b)~N2fbEhv93hRKbHw;yVG0p-oK8C7^(4mo5Fsi9Z*b)v)d)U|4@5vQ=M>8LeXb-w@HER+T}Im z=hA4U)E}xy2LIa2&crVKsc))0-|kYi62eWDU;%n-pKV~#NS6XXJA8v>a?s|+XKVk3 z7n`Y0KOQ^0Qej%2k+q$X`VcDH)?g1z?S<{ABdoLC*JV1nsn8|XZ+@YK&{A`^lNWv0 zn}6!%l8=oA_+sU_Npr(kQ?XM7|IO8BZk1l19!knJA+rNs;!2A;FTUrj+J|_hRGF5Z zQ(>Y~5w`i4--pV6c(uk!RKJ-T7{0_HFlsE&GAo1Bftt0>+xVt4ezZ}RGl8_>roz-a z#I@v0*1GxH5BTZ(vZ{j)05f>9f%nO-XV_Z-Bh*V#QdryFYBMawe*#x~wjpE@`c zZk`U(e)&3VITe@Y7D2mvNFjWv?3Cyk*+S51DDs@C~@4(mhf9RKXoSG8M!!~ z%OLy}C=-olM|gf_*fWO=N@9jFPJOjRx}9f@($Y8$5u#|d{)DuT^Oey^L{gI>?x9KQ zs16yPw&Ez0)Z6{PZRfW)eJ3~aC}SBZ!ClDpOGCB2`7}LgQ1&sV7CU5>AX@+Hb zE}tea(!}|;#BcsV<#6GZ@xa!m+d{+o>}XX7T~ri;r*(Ll(waGPXP$J}V0mlBgplHw zyFr*eQ^4bMF@!p20(a}Us^21>&;UdemoNs~$JY@?s)~E|c%zoMDG~zCwa~1xPx^P( zxfaWDB*#>3@~0}Y&s=^DHID|(k4mW0Iz<}3PB^*)?Vy)RVOiI9JR|b6~F(uZbsAYD&DaD65(4^fk7}Tk_shW||*Y-S#Ld*UnBPbB{ z2GN**zc>Pa=p8rmEU>C8*xfo)Hn@%N}W-SodmvXPVAr@yT*R! zMoECt_nGA#rM|FULzBAlwoO*?UjYx#(`GQ38rrFk&9Y>q`0_Q`d!2<^)5L{F1;8&T|M*(N7dC}A6%R1hB#bCVf2;+dG0 z!GclFF?_MN_(Xd%g3ZphIkjCoNtol>r}>?&0GGKovu6Q$UxNLvaKpS77?`E0m!%)H zeE3F=qg7Gc2MROft6$n|#AM&f)4izHwq&KaVr=4}3QAkUedw~mTcO?z zJgH(staYan90PdOO*cvn5PpmGQ{;xNJ?l(zC_nvKl_zGJb@B_b3>D(;%@2+&0wRj$ ze>~&Kb(C9~LmJ~ z%sSHl&eQG<1b@GsVXg#gtg%dvjXYR!Qc_DC}w8^)xz(;f(MOmHqV~Gj-HqE8mFCZvPC$_UOB7R+C1g-iE$CquM-kbgDi7 zwDNL{VU1v;IGm;XYJS_8bkY>zQ}Pz6D;LKW?_HM$*Lx^L+v&5r4XdsZqTN#;S)9qo zMfKHjGaD0mteS(&I2&p(0WvGIQ-~5P?<>Vvzq2CnHQh6EGOn#yY!LVWR3+tUPc^j+ zxufjf4NFz(*=}GD{6xR8%bHrbpOJ`O4+UWRsT1uxD$+DUxd|S-;RGBJI8$Qt1R0P+Zo0ZHu zc~X}=VXL&l!dft!ET^@h;|5DeCgB94xOM}VPnCkkeO3M*ttU{#FTLHJBUEEwy=D&O zA$Qu~dJjM`*XDH3?@B$^&y)<$ZVwiPv$(H_S-f3%UbT?PeXXkSGwCU>WgwB>pfcC4 zXgj}3HH2B~6^#~b36}ow^8eBEF#RuWa{Xtu-V8HcV&~BOK%&AX$NHkzuYr^^by8k3 zE9+C)eslTf1ew!sLy3j!)i`IRa*8WZN1ASKNxUl}aC8ZACYzxe5FJWXTwe!!)o(?H zQZ!3O3zA+pKdW0s8zu%F5P`E~%nVeD3m)=XpZ{FTwZmdpl{({Np&u}pT#>~!d`p_f z*QrC^ejA-QI3r^eDtqA-x+-x!jP%2GB6lRG&k=m%tzG@RBIequL+B7pxEV&|%&7}z zTBcr=Daevfy+nKO#3p9Zn>0mCvk}(HPw87(ntFd`rXrW9F{$Xb@hRi70P}@i?G9P6 zv(96xOTk5HectA-BI`m*=R5O?ITP}Y{X>q%2p`^+FC;UM5c+-Pes>eJNxa^OFKT0W zgdT(?(o^28d0bonz8St1e`X^a*Bco08XKgwan)IO>27aYOsGRaAlOVP-ksJuFr<(y zuO~!xHR*eNV)c6HzOv2{i}f}CZVlc1qud`_%zpGKme4ZF6n=2SPPMv7- zWg;uiFP{wz z);SOSJMu4w@AsoGa|XQ)3UbWI;bBj&$B5QT#rJ#_IrcI3NGKJ1V1?`Axo{;_9kzd- z#h%T*b#g&X=laj$H_TAKaoD!~bvEl*p+&(JrH+~rxwW0ZNc*0T2J8vvQkMGV){?sY zx`JT;OvJ+BEFNaNc9?~xzq|}~Q_ogqv6^8;W>5_3v?=8o0{M%R`?jb%C)y>Nh5KMj zI!xdw$h4z;sHk|udS`k!AUdS<-D;*}O?DN|J&S9@m!yHz_z|q2h^Ox2JFc-^J@%C{ z-L5Tgl*{bp<|}2F9Wn#6Q;69b8QC1o%dD}{kzk-0PX{{Wj@6FlDExiKK3lqKo?c-6 zZvNS=TjU!-R@>cmzq}#?1=p;1vXpn&Fz;r2Uw5AR7G9YskIA_&8ibv0FCWc`%eWR^ zMO%A!zS`;9arD>cFy-wtJL8?Z&X)brTXf8>24*2kE^kBGSrx)Z#RV(%iYB#{j-mpn z;GoFoU!fJ6TZJaPC8}{z|z~PQf{A4kK{b^Q*B5$ znX;&ubc=MCKp%9~7H_Dy|NN1n{qf`IV-M|_y)w$%Q+m5IojXn&yE{|wuKJ@kU;b6| zz6xhkI@I0&R+l_`sLz$cs3bH0&bs_~rsG#0${jG4LGQ7b@#h?@Ds(@vhV1a5fCT0T9|PCqEEXPJpC7f^0HbZ zVq!m#gGip<6(=I&r)n;&pOD@=YeDCF9E$*?d}Q_#A}1QCy0gi!ywGo=f(0vUh6-Hk zBl!Mwi+`4j761|&pVVv4CkPL}v6!Ccqbs9cZwuP46WH8EDRe1=#mwBUA^_} zZl5Nke_JEmmM;CKV4l9e261!AO8OAgE?s>=pweh+Yr!D*goVQ5g^J-5Fs1U|-X7&U zktAHzmS?vjCkSgb-@G8+l&>uE+_>%ERqQ1?ovY5Uj-d2$&;0XZ$jE#id+R#+M#-== z77}xry8cQsb+{bSiXf`9^{m-aLo-2U$*o&dbQP}bhfgVBeF~8U%|3B zBCcpy667w>>~RmCzgb!KxT?gf>8Z0)g&MVO_j8mpYyba*@g8LxzT~KL(K1`pD@NOn zhXPGgq}iZ8D!y6GRk-l0@?KAz6Dgp;j$Y!CA9t4WfS=w2(=w{w3+@UGc>bDz-(wFv5 zrSm*rVNl$fP!95|t3rOAh1l@7-N0SkS$;EEocKmq2L_VwKf!d z#Fstjux;Ux^{WbJ$tphab4v5d*d+^LL#3X3LgKzDL&woCQ#gBXk9rwuaE`P{`nb6~ zi*`=dtQz${0ezmo#~-EcX(&=YXjf7wQv7^+$ZD*vA?`I-hZeGDSdtfLr=9d5>wRs7 zrE;oHA6^FcoZ_3&hpo{F`y8<<83l{FZ{msizK{z3{&7Ar@g(S1firh{p z3)BO575ES!d9G$#`h%?bDjX#Nr2acu8=m{blb4yhuD3f|-xH|ke}c1AqYYeBKE4$7 z{o)qNp|cC_b!9^;r-oNJ{+FGe#AqJ5Vb*pDsG>|!(DQIzqP%=XgwjQSSby>C__GY6 z$jHtkuV;0_wV~mI;phJW%JW;D;%qx8a5Q$Y`uoKk8|BH)mWfL!={T&y zd0HOXc3MX288+y3o{#k&9&@E4BzYzCIC|vd4@3h3ZD3)i1zc`yQ`zkWFT>p4j@k_F zg2D%gUacf-;%KiJK)OIPzUT?6@>{8rv0rXd4dC=dqLY#{)bBXwcKiciy1YtncM@SE zkSz@O4!;H#C#2_?d}SuHgL z^Necl0CRq@$#shp<0qQu!T^f1#(3Mh;f>_EJA}scLGgNbTS5Q zj?86-$?BoNQ6Db~S1DlYp(5(GNJ+XUNVO2@-SiO>zRTNgRR9MXk&`~McgZ(u`)a;D zF9h5QA@+y7c`8y|lmz1yOe1gUy|B$1$V zVecW0a1z4Pu=sfY|6BlO=#Js>gqchM8rS@@Luw#5fQTD8S=;_IpZhHyfdZ6^%jGoa ztp{V8fLW-*BLw>hxwqXNgpA-|PI#ka6Iao36M{y&@b7{dZw8QE9@rK@c+{+`0%%LR zNa;s^XRnbTb{^G3-52UW<$%lpc0%I$uV9q(!or0SgSu*DvANUTp`cut_F>~f0l`I{ zc76>imrJ|^5)yN2prJxu^S*Jq-hZJRE$6odiJr`g!pJ5Eedj$~= z#%A`G?_yIaE)~E}8Q4$!()NLRs|p8zxBGP=Au`QE!&?hlDCJkLN-h-=I(DXcOPvu6 z(;ydccOj{%P3X4>zhIJT&M`kZ9hrR#BUF%Zl=IzYp{0oqLgFDX#Dp_@MJS%LsFBtdr% zSsXZkkrG)RHE`-3NNkNVV3MPq?GWWoU_9kqiIfGz?`gTUNvha$!uk?HA~eyjB5f@S zzckJf+U#b%Z7=m(X*4(g^L?J%u21BHIclh1=}wp0{J!b@q2rQ!0(pBQv79F{pIc&0;ozdnWTU+gzbk3}mRl$G1)n*ubRXeJkowm$v3lEMb2e zLFFshncd0l0ZcKu73OI;8ghnDCi?d~Cgx}Q11suZS)pl2l|dQPLKHcU#pW5dz*(m>;j^Vlg6-8l^BxhvcyvwzJ<2gF>a@9zfF zWsjnhP(e>Px(P^d29-1L`*j(dEpj;*EgmY!68#ni93EYbg1dSbw7KKk2^E}_25!wP z%02I?fM3@`fK&-gS2--c__&2DRzCt2 z_rSPf|1ilgC{EP6Uu`1}+~E5raFb-VZcw)YbxGhTEn1d>6MZ6`HDpxRHGVI$0uVK8 z1UKP=2sove2wrCZ;g|`Zjd*DKWh-fR;G<(&1MR;E>7fix&DA+v0?4V@(&HL_*Fph_ zJct!p5}6ra(sf7xoqbl_2YlG=m!=*Zuro zaF!|G0nbYYfye2wvTpMqt$h{U;mtS|kOcxI& zL27<5pOcmp%u|}EC8_AN1@iY)g2kougSO%WVZ}fn^I)myp~u~3|9r?OY*bhs17~UR z8@_jYUlcg7CaLJK40=?Dz$VG;XivkIddhU<4w%;0-hE3tFb+2CW36pV)`-xucjKz; z!1aBXJPj{CN5DXv!=G9XFxDc6jG#I1EC=84hrj{Mag&8`oues;<#5>x&omyX5PDm3!G>&h@egM()p3<_-k1<#ryHo?xoXAN`@jutBmZ4uOBgFeg~@T|1#{7G&U1hTC^nF zqlOo#;IHb(WKOjH1qtr&6$GH)uC<+9bpZ6mVp2@9-?hx0G_2c-;yVI%M-OW_z>R#gDIR5Owl=@gs!!SdMomcg**&p zzN6$)3RPfxs0Vfc^x40BVoafRy%BY3%5PX-g7zM3C0$#Z1nf&NK;jewQ1;ES(JS7) z?Ulv3IWaJqbx8cxu>298VxJ%$AT|3l$)>dNkr9vMS)b@rKh=_~{|=k-5%kAt&92q$ zZueVbwtLGYFgoR0<+?MOu_5#$%fA6+!fRhvthVdq=_U*&&^uG>yccK_tqCkBaaa5x z3aEqhs?kT4XMZ^}z{Vfl2Z&-#<*Aq_!M_| zRyn_KhtYwPS$KD!53XTQ;}oGsnX5x7j?ljAd)(sj@ou^hjZH5~k!sGW+F4yOXM`)MIDvE;QcU>l7OC=8k_eLj`ihB zuB12au<$aw&L_Vm7h<=Sf)DFx^1#}8++ zqXfH$dOXy7SvDhEK-{f0T{fr6;My>W)5Ftbz`z&?93mlT2 zFB`30KE&A+g5IP@yCv-op83aorSE`^f6r<;o7s>#DjRFwOwx>uOHRUcg>fu zX@K(6)%U8S$tL>{l$;uioXxqNkczFisPz33rv~BZ*YyV^T+~LVTM?9kNQ)c|j@|BG zicynZhLJNM(=c#@TL-TNS9wbvaJGn0xU-qsL`O^0CHDcGZr0bFtm~2#-BICfHmYGhrakwe%4UYRwew+gzrvP}dXD z71hdN`8`$FqBjQt=pVKud4Z@BZ}OWFb!#?iM8^P+6bcj2`f}g3==8wC(|v&AhQ!7# z{QBfp`-O>kv>o^NpDR8*D?UxxK!DPHem<>NkbYN~nnAnU0eoQ12_}w)Rtsmhokwj~ zlxeOX-|pkR7Cn~Icd7?~YHC&A_QW#u8OKi(>crNs25bv|XRe~KQn@X`m7i^VLoup>@9JKa32e)!^2yGlcOCds zZWTH2`sLlU1mwc+jr4C%zxugI2JWw&#sybn{G&>^X(Isv*io8Z7y zl8ljC_eDb8PM|{KN=75#F&<1SSXr&f{VH{xqwgFm1hxC9J8yjz68jTR@=ZMG12Y&@ z|7e?X00Zm4qXI+L`mf?!_sx3iwb9XAAO;O~SLMlAdRec`Nq31Aigt?|mQvp<`cSXT zVt>Y7^Wm+pbo9iq?$AAHK&E`*>1`S0efbVhwo+Zs1J^j@<0Im(CL8~*t(SZ)S_H=# zF#$0C^dVvTV2JnmV94QzOA^Dd=@G-yJC4N90ep5AN>0qTCID3I|~)@NwPjBcKP$s4GKQ{U?Oh-U_MGV zTnCxe$gs6OcxuxCH}8Jn8Sz2%t#7)o!JQ}rX0TtM z>ah;H*bD8>y%Lq%qwHYcPP45tt8L$?KYvJ_v6aZn=|i~o4^enmW$tu2G1S4O=wj+T%6BXf-VyU{Zsw$ z=s~;Y2>~_qPWOFGiW(2dY3!ML@oW+|7qAj2+28os4XNw0;^5_xe8Fqc&<-ND_wb49 z*FniR9j&tS5W+~d%UDo8%Sm2Wgi;g+`B(2eT~OxXB(rZU$ws?D+m_@WS3VtB^M3TT zXecY;*)x1KY`D#mjs+ip63*gdI`(bz-&*$y&5Qo)Q!vbTh?2p!^fmq=*+Lhs2R}-K zej|UJwE}wnq;jAQ1#ATIclX-$yMnTo{b*Y^ym@{UpW8GRi1CJn_wuz1oa8tykK4P~ zC^-b9A()ugU6rAvZ8zUXWdP9gR(R>lezvY3|~oI zeFbG|JVurOuPlBHB+WSLHd^2FzLLc#a#}6kJZgG%p8(VIIVbMJ3V|CX>8j_bND zIc2Y}3XJ}3^F8ty-8W`(%}A+12r#Oz96cPef_r~4?Wx0sF^)YF8ROqJiy$^S9_+C$ z4nVzzP?onP>`}Ht?EJ;I$HN0j2-{gsHm+fafN3>R@Vp=}^#?D$DV^gan_*e;r|2Df zBV+;?k?3C0qnWZZ7a;2v^0_RtS5%sD39=q-5-0$M2D&-Ae%FhG7#0HpY~7D8;5<~D z!FavL2h8y|L0=e}GAbck4itN(v|Ej`ziZ6K?3XCg9LCY})&2UE4lcQv-afhTZ^Vg6@;DP#-@ z73e|)G`s<5X^8aJ8+BojHJI97|Bc*j6ebRkr!NC!@xZq8-ennpOb_4FPVGS1TIaZ$Aq%fa&d0aDCxCB| z0#UI(pJe%ZRwO(kUJzG*u8r*x`Q=3L!k#r0IIC}ML9m2Mx-RemPJjukZ*Kh)28zH2 zQxNVx7^dSV_zLw`6 zwDc-F3~=7726mO;EN>Tn20R`k2pe>w;(B&u1qyrz_0l1EJOr?v$D8Dy?Eaeq{i)O ze*=u?~eM@!zNeyZm;xZ60b#sjXXfndEOeeG$x^)XI8&?8Cy zv9b)^dWp!@@w?kgsM{O`h9Sx+&K?<7Sa;$Lh(7@^B(K(zMSby+;8`ZE+GBLr zAc&Fc5F-y;%$A)txlj&dPlpOlaFt~bmtCaB+&&7SO>{6Si~E3Qrx+U}r^HHv_Pe7F z=s0U+`}S5DZ^BN847KNd(U3xa=W(cF_o@TJy{4;3cVEqQ8B56Atx8!u!-_N?M> zNU$>ESjV|;7=a7L_)v0b`#-^;fz_8yr>`ZcMVDobW+pin{YNYhz&V4&@Rt6J!Anz% zO1>D#JUF^HP}E%O{!%DfsC@KI(D^YDhq5jI#A%%*(Y+$@r+)GMf_zboeAc(>g7D4d zC=5O*$N6cHp?nsY!+Z z7Fmou5T$3%oyZ&-(}9T7yG!}oYvxk0WlXp~|8M36!QoMub9Ps=w67dJIB`R#T&(Dx zp(Hho8uRmyd<1QwCf;RfX3|UA7*6S=szA<);$H~K<}m8OFYe!PR7eYB%gWz>NWVij zh&d5)5&#w5VfQw^T7aPZ^p9pWWrwIL1ZJHynGuSDEJ{~tgL z05I$52C6Mcx-cAme@Fj%BIQRI@(S#aSnffNwoF|d+d5iFGzV@cSPd`j=9IyFsStbn zKRW1^=}+#d#7)w_mCDH^v2dG*fwVW{2XZc=_smqYb>D%hPh8b!#izqI^H!BJShyKm zFC3iJn3{L~6aQ=Bh;L72{BI!fdI-_D^VZq;|G~Wq*(=I8{upj7?c3(pcf_;et=gCb z(G!KJUcxMO5D33V;E#Sz=PJJ88uV^mdfXgl;p@?oYr!vw3T3Y-?H!l>Z3myz*Xnr> zUyf#W+JwwkJ6QbaKIS*&6++zsy9#@QO%MhYh_Zg^v#hxaLwJ~#g9ppJ3Y&$9e{3~I zb59&hEtwBt7OUC;-|%0VFS9Hjsh}{ie>j2HdFY)^*~iVrDia-?O|+d;+Pqz6dm8P| zkGNubOY#plA6#UCz4Z<5N2e8rQ`}GkGAaO8{gE9_q;lMY=HL1yw*z{UxsS-jq_mff zR_n+Iy!h`dKxMh9mpVrL_z}6^Bkv7xJJmZyC#D8OxjS+#!fh);-K#$Lt^9wy5t6Kx8Gg@Edzp{Ans1j^Gb;~ zNIperjWNTXyK-Td$A7y-0bp74M{Si}rNBl2f7r{k2mguHn+v#Rzk(7m#md%t@9pM00Zg zIx+;b<8P2D=KEW5WYCv516=0TRIoZ1q`CmE-tz&9XBvOofa;HH?d<09pu0I5CX-}B z5Qrw?n8lATp`h=2=|g-y$oh$2`!{ODw`+b`s_c#@s8McK2~x7pL}t=%L+QcG`VY6n zt*+5EZeFFzwR8B9bzy9c$I-URdt;(3QDg4a?uLKGG)?@fY)4H_P$(8~uJ4)}(H?I_1 zmEQwSZc1M}?e8@l)g4qGEr>*Iy%J)(D#|etV*L#%<7N&qAl3&2uw_DbuK4T2WJVol zVm2GpgzjZsjFh=E`CEkCz{Y}t9lN*LWG*q?q)KyvH>?_se)qf937VAt*3H`tk_NuV z0lUIRiPQwIZ*zj=?9G$|V4S%SuinmO1zp`!$NY-!$2@(!OHAgDPN#v`0W0f<0%MWW zDQH^r-{0mLV18;Ll?Xt{K}p+Va=m0s9;-O-^lWKP1N%HX2wu6v1sL$$5u5%!L+dj8 zk!j_-#2>R^+>i02!7>y9disu`*QKy9?izjwM53^?$zw?*)dSz)LB#R53k7S1O2jZ$`mYths$2cMIsIBn4PUFP_2Bk?rNsJIzEp7f)w8zv@A?Lh$m zcE!5`Rl{??r)@4Ugl3VLb1;)3<*!;K9}kfRZ1`AC%oV=9Nq-SUiw{tb+f9%nFrXl= z+M>6=JHnwjy8LBod}m=ulEYM87)X8=LA(&vOZ6a*QdhW1Bo&pR?%T=o9vk?n-BiQw z-O{wQ(NT9Oih`Ms)%Sd&5y$y8NxA{J0giR)NaYtca3kt3Z_MtGA3wjFCMzG0EB;o$ zv$GHrkX02@04-ao;VgzK6qEOmBPxM&_WHWIW4u{5o7R%KIG|Ygi!WQ|fvb1K^hvsw zeNN#stG177#Sm`3rXb$ACkg5}T2G9-cU!j-6p@`w_Vv1*EbDE?Citc9n1e^`2$;31 zk$~Opxt$$?N1!4{=jo|Fwp$UYwc;QWaP=b9oU3|PDQTJwW^nP`=bNN8!Zw2~RE=7H zRfswlxFxf>DKCYpHs^_3Fod}D0x{?hSYGxumBX$S2X(fqTLkL>3_zLnQJ|oT7gN@; z%`C7knZsC&)XnObYpR3CdH|D)|IQrF@yojhOZXN9!)$Ngyfur%j~xfu4pE~L10%g_ zE)k5%~S7;rbR){!KSjI=JP(_V1l5aXVP#PbG! z$8$e>f5+XI!L8%uprD7N^S2cyK0pF zUB~&JicEo@i@BF36mcqmChP+bG>Yt*{#LB$5zMY0PMSJl%0a8$t!3^$j}cHq+U~XV zfnx8E%~rC92}bap-u$N-&&Ew~s?-MRuJ<+yXI(ndw|xfQZQ+{llAQuAgT6Q=gP=$L z?nQ>DVPF+*Y9=eCbGx%Yw(cP&VJ7ukUj)RE5Ple}q-RB(NU#vS_sXJX=P+Wl_+OT$ ziwQGx+-73pU3Ue+YbpX!UC_!uCcx$7V(A=HQBXXtpMW3VW|<+}5Gc?OF|yefg%&r> zDNXpBDLfG~F$*y-cPFlRdrYT-e1q6c;7a9UlHa3d|0=i-1@B3GMpl0jDi5$yUCpX1 z;{wS6@eX8mMox?@+g_+@wxq9eJ%(2swpTAi8w4+QR60CFLNZG$7hi|bdrZ@~#lyh> zizZ4Ok0Le?uZdBU>!39QzOUk}c+z+Ak;6m(0e|O4*7XoZYnFFWDs1#jOiVO1no??N zrg-*NL08I0*{9fbocs*yxls&lxJel{oXPs{eijb-wI|NXR4Jc?+E*b2rdg6(915Zy z$xN_HQ(cSmK5*uc}>^_K#jvg{P1njY(iBMb-4KKcw;cMlDdM<=6}N-)BpnI zzu+d-*zg(Sq~RLBqfQEvl4c1c-gEJ;3f#&d296ZYF+S~k8h?Y+$ukQ0(U5y$- z-|=v1dV0bu1F6?PyG!4J+>*w{YGyF;`iH73GU?%ZUg1Qp;IL!pS!?ZKT8r-Iylzd2hX4LL1n;{6Qi9-B45)r$9mQbOV-%-@U<<_gS3p+hEK_mF8Iz zT==IHF30z7V>2G1vNxN4HT=W!wd>LSjV2WvHG86Ccl%XdXQ_5iyAmK*b?tv=s)sf4Lk0NS7GGy}^olC7 zJ-e#+N&j&y(WsE?E#7N~F>*qxiTn5H_OZ!9C*~Wb3)3c$?7(t>;0jIr39ZoqkIPoaq?Fzmzo)W(x1vepe zTKr;9Hv5=pmhLF}(+xatSjUmw`E$!s(8QCh5AEE9ypV0WGyRyzO=xu<>&TT7LEZm6 zd%Nx=`l%rEw#oTP>oibow5{0 zys+DNXJjGx#1iI_R~Z3HR~%JDh4+VXOENFd*2L;vOFD^$t5VDATEqbDp6}E8xi{p& zt`P*-lv4=U5U7vJ1+J_dveCswmPBCX+V% zvo)6&A5JvCt$q@{>-P&$3yA#)EZzkXqCtV47KCf)i)5H~f2eK<)I-4#>#T4SZ9P@^ zTJ)D0^}W)bR{j|`Rf(x( zGTYZ*ZLk5r77wC{>#W`4GY%7m6@XxzXPb+c3%8>NIPb2?^Oszo{`-rfsabfYs{t5D z_qpI+RpalFueh~Vb7E?il!b+THK1@M>UDmfO*?jRc!OFHQdJ}7We8du+=L^n-E?OQ zGW|4H`ocW)=Fx&{pvOOcbHldb|k&0*a(7LFMEmDw0o5WG&&u^s7|#{~$EZ3m^>tTk6? z**ZOW;#yRP@%Mi?{xvNiOV5K121|9^Iy13)k-3i!Oy_77E=rob@K&z?A4+_nMx7aE zWU3pFe@4o5VaA%odHDFaxEM_4HILTix|E&NBW!FVrnLmFYM&~{9d8l=Qcy`1?#$zo zn>_YEe>$p=Y?tq;EW+1HKMJBe)tHn>4{Mp@<4yNxjQ&k+J23KzA}LuH?XL@_{h~yM zrV1s(In^l~c!&q;)V8!ZR+H)QM8o?#cchNrvnZ{fScGnG@)mt0(DPl9OaPT4TZ;jbv^Czkex|8)re zph|7KbeETx^Jmrvw+rN(J{h4xPLN1%{*A zI%JzX-`t0+@yjVr{Z{Zcod*v-`kia3!nK^S|1R~dHaL+g0NPvSVH1;PC}rz>%3M!t zubfgy-gH0!x_8wdZF0)CG{il&b@`6PBWgb=xY3cNn|PL&^`i&{C~vg(bN; zsX(PUwz0+9SrZ_igAXGwJKl5n8C^K)xm;F0>WiA>MN7||QQ1c6iPk+}cX;btra%|# zg~BgEUGY=5HZ(~;S6f!)!Q8bvDD(P&9pf<+W^XcWqeVCL#~%t_eXENd{nxC8J%AGQD-m z?fcn=6vosuA!nspAtc`0IV#Vl*Z9LupuiuF7$8V(0{7GV?N~JKb~3YbOZ^0Znhw^D zb}I*0ae5cxR+YZJYz=}5h%>MjJ5PhU>#y;_7Tz$H_2$xqb`Kj*ok=+k=#5~g&M&^5#B$e zNK(xZ7WAzV79K+zkmICi=L^L#AMPpkFg$i$7aC7OvcdMtm^-ABA_I>beAZnZI*AsF zB(kn;)E`<&NX7FgL7UFU=bvk8&$tt>)|Hfh1y6SOVHE;9Q%gdZ+;3+-S?st()XHX_ex7k&A2@B z|Ba4HWSw^_)y&A1_!8VYQ-1)?=|#4Dyj-Yy<4VD0`rd~4F=K4`Is6?#%it4e+eo5I zVnv=X`C?kk-U51wPv7y7Q%2+Rm%f{HHRQSg>VeHGmKT2`cPSWXZE~-p%2#YBzbD&j z!2I!ba-29#9O<|toV(IQJZ!Kx9UNqCKM%pX{>%RMzLMR5@iLMzZ&B)pE}Bap^>(G>*_|ocVdhauo}wwF;7!+ z+(nwW(~%{WzM-xH2J~6C{s`EUNwWOlYv$YqbP!ih^R<0 zC@2UxqC`551w@L9f^-zYP?8Xk&=Ex-$`AzyltBn8h?E4777~h10xBJW1V}=Og`p$? zkw9p^6W*J(W_{m!Yu3wO_uRX)_ddUU`ab)dR-oRjOFx0U1ABRG>iaV0{6rAdtT!fd z_H zaCXdM*ZYMLME>sb@Y4DER?vmoDsJA6c^8K4;0rF;ntl#)0bq{k{Rlo;%i!4Yl*U!} z6l>&eo0)46fiv2}>Bxgt^ttTX#fWC}$F_Td%eDdcpWkE)=7v?+k(rD?<{H98=jnal zugU=fIs7RY$yk7z_KpOz9|B}fjH>h5UE?P|o$X_>N2$a|AIM;~vUp$)M-ARPyPs^F zJ%RQz>=*}IZnhh76K`PcEQ`-t-`ItUECQ3J`-&JR-RlbqZgF487*LnuK`$}g#&ymA zy`-D>Xp`YVuK$uxksKT|I`I{|GR0U5T@(7YaxTG$+*PkP9-T!Us?p3(Y@G(h?{O5! zh6p=0##`CdP+p3sev+E@61#);a``)#?3JxUXOE8hko{Pv-H_(sh)6uze|R06lh`bn zk((Sb6wBYG^xf5_xzs+U0mDqVcN4}}gz3)VCXE~Xn9Tt{uUHX`>)Et}FKBB^BNZQ2 zHYEvD6!V9DeG6DMp6;yh(4Luny7!lAamBo`gQwhvGi{yVp2;(sQQ_AMRj1zm0!Z5eU$bZQT2?O7`-I74b&_Bq!JW6d%JX=P?=|^yI2dIu zt_(vCekvBqswF$xMz(44NyvspB>mugDy|qAP$>t0gwE<&NFIFduqB>z+Au-vuf99& zaYs8SVKdpkDi|SJpBSKD9Q-q?EEd>@rQ%6@})?5k-~wPBYNjSM`V+!Z%Oe}wPP zzb$avD}l=NAt#s(htILdF8n$m#nnV^{?^JT&lQ@c6Wrt6(XhmM<=v{Pssqdzl)qTR z5xZjf-L1)UGHm4s8+ifKk73Uz&P3d3n`3XsRAnQ)>Fv7pG2dps2DqFBybn}LZrdraOL?GBJyPUd z;JP}O>HH+Xp)GqW<{YARspi(K(=DHI@u=g#0|hBkJ#%S8-Q2`%yKxwj60~fM?<=#V z>+EGRdo=K#3&M$xcJ3POy8E^2hREnt~; zo6+Kq#F?0~u#fhv+rK=;vvc9`G+Vq`si)>Jpkz?_Jk5$Yo{7CE)ikNrr)=^~R$xrFeHQhMOCEt3p8jQxDzyFBk ztIIjDl5|SQ?RQxnDSwEwH{$V}dVoZoUV5cp8rTFccB8uAyfI-<0b}7}Ie5j8!7x&2 z4FxWaAJer|jw@!h$HLG6vN72NxfL*$tk zogK0`G8LbX5>3DGT>W;Vv6d2|F>q8#3~$DB!yJm9K)$d(%9yex5JVYTFmsf%4LN5O zN79M?(P2UJiUefs6$XwPJ+qu4Yre$ou@6%mx~O;U{&4k-4RWmiYd-z-y}QrW%8 zZw@uQ18Qiu*}Ub?FGI)3(#(jFQ@kXLo@gI!BX}Z3mY;*l9{((ws_3i^9QgJ{G^?oR zQD?H%f$rWAjWOtWIXhKRYm&vQa7{4CLeStOS7$LAxc-)%iIpeQm!(i23O;BaS@kr1 zzL7M%a}|YnMUA4Qz4Z2b`Q0p=SI}ve)mpFPX(u@x_|3g;A*s>5#j`W1|IM2>Hp%Tz zL}4+8v#Mo2LUaEU>FvG)Z~6RzBaeK=TPjYMYuu63DV3Lb0fG^0%O_7^JO;F$hxCv- zmp!*hi<>sX&gvL!$9M?2gDAh5?`#?o&Jyxi)yiguAwFk1hhPn_*NYh?R8csD^It+5 z1$bK69P6HhaipN1rp#k!eW^|Si8Q}nTzz{@x`(<0k3VQN)r#V?EqMbC>dZ--YtcTN z#bb~(=Fnv9EP`B)e-u_1Ss9Zw83P3xyx2G>y5P1&2F?h57tt%P{K8PvEv+;VDz?6q zhgL9+lNl7l5;_)Vj6%fZYL44$Mzxg9HQ4mqL(8gEb0lS*Sa?YZSp4KI%(#+sNZo9C zl2I%za830AznB?rTT0wCW+5Ri zGS`WwdT5^|yOU$Q|4_z;{A{AME;?gtjQUeXjyR}%a!J><&*ExiCLzkwo78_m02Wq z?DJ8J4)_wDWw~az>i086C_Y7$-W6!Y`PQXV#tT z99otl{iwVnA%3+7z;l);6uAw)k29#V?cj#p=ltXZmk$Tmnn^sUB3o7u$e zdIcy5NCykmX+6a1vy4s+G+*py*+qHS->2sw65_(o#K)hVfR7e)k3V0&q0!%yGui#C8I6hpU#D5E#6 z8wuaP)ZR1dL38X5gMbe?%Qla*UVBNi*HKxo7G=cZ4mGWJ!LgzyO=xt@?97I{MzL0n z(19yR$_cuh8^g0nrh3p(dDgYiiE?I6No=aaXr5%|7B>!2*oPM-vNF0aBzCW!)QyvP zEle8#qQeKwSX~p|GT9-rf$js3n$|b;tYB&Eb)QGf9?8J)(+nTcor$32cUvRJJP?15 zOAn#d3leR$Ej&~X4*-|koA_OL*Cdx;iL?+Irc(8=-6UMi)BZ^SD3D$>hk+DV9hHE7 z{V=U6heoKMs~_nEJvCv9v}>3DOxN2fU;knJVcC9y5F*i~TWoU-cl^lA#XM@Eq^}}( zOZXcVD~^jk(81<9w)$CB7N^-4EiyIy3caKjI@xvy>#EJ}uswE|Wh6VopolL_>8kMC6XM(B75QxArhCPo=7yb7gM zWu4HN7_^;>Gu_(Av^@f}-@e*1oC+b(=OTgf1f?@T>cs7?7ml~i$<=rZA03zNml%&? zNXO&UU7wjJ2k_izQ`ADu${%X)AP~ucv!_m6GL4hU6DY__!|;I#tA|J*7e{+(i!xTj zF&aBh?b#Kz68{kBljLwi)sFKO8l@Ja-KGbC*zvuZ%=e$v#z>lY$>yPn?a+YR{L<%CWTT;m+PHOu{$;tLZz~x#*h6g_& zXU5J#w9ea*7J-B;3uaTFBIbpP#rpg0nxNQztI)D)O6l!Et9U`~cj$mO7mLYX0MbGW zXOjRsP$~xT6VLW9wFBrQ>aJUZ7wD#{Sjy~F zGuLnk>~`*U)DA3Mcl48jKKNjOE^N4VU;_e#5(CFPD^W80;(Eky-r!h)a`1rA(*;eR5AGrFX`_JYPBlED%59EWTM=%uwCm&HJ@h+FV$&87$k z1us8UaZ0Xu&Yo-NfT?ut@f|d-vUWCy?V!jE=LWuue(;c zfy|0hvH_<;Y%3E&J>Nmu7oJ?lktOyz^Bj>aQrgKwDIZ+U)noP>jc@UxF z<~KibbFh9N50V7}a1NXquT%GHcSs69b5LVEG@*S`ng#H}SB&I{` z=RLi*e((<-3>?_-6kD9H{IDL**c}e`zh50By3}&t8sp#p9u{X|Qj>s-UnV+#5z6Nt zlih6%0=+H|)~@{Y>fO0!8?Zh6H>}{Su~%~xN(A=0xA=_-4DiU_fArUr)Z@RN zKH5`Rme=!@5ulqn|D-ac{NGQw%8*k#j)Hl(l?jhQauMBS*?XnJ*|LC3TE%K;4dFc2 zo_K7V8aM7*em2YgKz!fi@_Th|+?@iB3;1Xf2npYG)p>3hknih zT%KJnEt#3Ui<5YW&BJQVZ88V0(l|s<2T$`*mGsytz*9|B8M1U%lrgGu^Q|5bQ3gbm zB%ijRL-Vq*rKN7QADgfVM!)&*0;!+e*ubb=*>|$jY%jnYQVHknNkv{$K-HUD zeyp0KgV>KX?ze*pcW~>)$t7TIx>i_y}ea5O8u2M2!o90AUco9s+cT!%08@ zqHC`~5{wf59~Jo1|3>v6*8jlkZ&ZIN@RweHllWh##1pgsQ7duA|2c<$llOno`e&`g tfBt8x|3Lf~rhk+8cO3o;)j81*Xwka1*9r2zkr41Zd)n?4=@+j%{{m2i_jmvR literal 0 HcmV?d00001 diff --git a/admin/web/src/assets/background.svg b/admin/web/src/assets/background.svg new file mode 100644 index 000000000..7375bb59d --- /dev/null +++ b/admin/web/src/assets/background.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin/web/src/assets/banner.jpg b/admin/web/src/assets/banner.jpg new file mode 100644 index 0000000000000000000000000000000000000000..675411ee2ed1dac48d110d2a7ef7920454054f88 GIT binary patch literal 40498 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⪙C3 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 literal 0 HcmV?d00001 diff --git a/admin/web/src/assets/banner2.jpg b/admin/web/src/assets/banner2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..62e08a47059ccd62179b92f9de79dc7b34c52d3d GIT binary patch literal 45773 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

`c)`;wWSDJrsW}Y- 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%0AM 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 go5BBIn+a literal 0 HcmV?d00001 diff --git a/admin/web/src/assets/docs.png b/admin/web/src/assets/docs.png new file mode 100644 index 0000000000000000000000000000000000000000..bb98d6e099fe941de1bd09a8c08a767b219222fa GIT binary patch literal 4701 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 literal 0 HcmV?d00001 diff --git a/admin/web/src/assets/flipped-aurora.png b/admin/web/src/assets/flipped-aurora.png new file mode 100644 index 0000000000000000000000000000000000000000..c94033bd225ffecf7a2876005269b51611d26c11 GIT binary patch literal 72652 zcmeFZcU%)~yDc09rFW1b5EPIO0wP5qHb6i?KzfNt2N9`K69p;KJJOr<5~X*PUPWpE z>Agd!0g~_yKKtGK-S2n)IOqKLp8br!ds0Yda?f0|u63&2ouDKRPV?*HL(@f}1>MDUg1D?Z*W&=qPtd}_Rl zHV_yD!n=z1X94}kf_DX<;OaF(B4QF!-~*LZpew+>uMps0y-Gj;eA);2JBWb#D$Pxi zhu3H|ObKr}(usbE$|T}=^z8?|=HMQu*efSrVv_3&j7-d2x4C(E`Re*8pU zLGh`QvepZ29bG+r12c0AODk&|TW1$nw|DLyo_-(w0|J9S1xLrk#>FRmPE5+m{*sfM zmtRm=R$ftARb5kC*V5Y7-qG3B-7_>iGCGDFpO~CqSX^3OSzTM-KcPtPvr zg$Kg_Yg)kiuZjK3yr_YBT_FGtDdFY3@UFZA20k^x)te&MXdY@1nmW?n68%6#_b4j! z+Ye$6G0i>tS5AW@*E#RbbD=J$_Gf1QdlU2hzna;9PVB$tH3=fa#{*6tJ~ape!fIs& z^ML*ye;{;Jq!kez#qKc1{Bw z!P@k*zCEbK5Ia)Up6ROdV@Z@p0K*kTP*TLvZ|Ai${#A7n+!U1LDCA2yN-ntwZQb>N zfkC8rY!SN}8V@Zn@=Q7-RU2`^nskt>D>wP>|6C@BoO;L}Ek$)`Vfuq@A=FJbwAVa4 zBzF;Z4pA&;qXh?aDkf8k3#;!>mCMuV;*HU1$bUf);@0I>F7{H$@cp5e2yuaXYL*My5zgS!qvFSp;#C%EM#DR zYVV+7BVJ3TVW#&8_E=5~^9H5fQQ|UftI8GbZ@aWUsoC7kcmYa_HVcdm$1-G{a1VUF zPr@t8f4p^%Eu19O+H|p(z6@7)njZP;o3>+|vF*=&GZS&gfF&+idqePjnSSrR=dA_4=d5m=NR6u$EN+Ei_e3A%FeR+E{@RK0f{3-hm~$e z8}3qU(BK!Y2hVy{r&GAoQ!lWAi=xJMm7!$KiYv{m1LLC4HsT!>HkC?{8x3K$JXuXH$1TA;8CU)u z|NR)0VN@6FNqup>NUd`yKa%HxY@a*Mu2#Z)lGd@7@L=%GSyM zKHa3zFo>Ove+Y+mxnyCk_l)V~Mm9znBaa_3huMrFQ4`36?vd624uBXv(%`UsU1 zk&G{q#t?!0HJla9zdB&elNAj5d;E0__yQVB*Kmx3J4aaf^MFqEZ=t<1`&IOGaItYb z09w2A>0h{6K5Sr9Ees@?r!C!#$ z_Ro>j5~tP(Inzz-D2~cUN&Jun(t30-iTO5^gwFZh?a_?p4og|R>l7?AH0EG|b-9Jc zqIUJr{Utup!#~UR8b5v9cae7UR$&eKY-ktG}E5SS^KObZT^MW+w=|HvFEGVT1cR@Sko(|s~BA8cyu)1d# zBXQS9xX24^dYlHoeV3rZ9|FR7@Ovl06`3AiJjT-3CKTavpioR59j`hJhf&?xhvitV>n{FT;7k! z8G2w(`Rf}_zb^Mg@e_2Kko{o;2;vzw6h+8FBh5gY3G0Yy^Dtt8)^O_4gsv7QXzk$= z)x#y-1%2E@Lk>jGQ7(?@-9F4}i%G~;Q6jsa^>#HB`B)*C2LU?v{Tqlgu4FbeR1BAJ zj|lH%RaI4m&`cxqtn$M^&c}m>V(s{;;9qgBh2lPjYa+}=NvCG6b+P!G%yo9;!4PV01aB5AomV$7O4Cyz^$=?TtBT>4n4BJjBQq9u3+gjOo+Gq=`!YAJLZqGE*1WyLsD!ranq@X5h ztP|jxKiu|kxQK4yOzgmKk#K7e#W9bUtaWc-L7lwwfLx~qf8{~?A&RK?Hl(ROW(1+U z|2ZezoTg=CW11u-80(6_L)V|Pdds3ea89m%3`Q$LBNIG|=gDoHW)CN4+08FNJ|xwg zsnh(2-pLX7vJ(;KKCq%AsNGjTCeI8=-LHnU{{XpT6|r^Cn!l1JBGdaI!yK# z+sv5aghSzPT0#FGTLhnP0QB0irTW{ZQN~LTLO*DhDeA6c?5F{aX2?c_Z=30+DWA0V z!XnpTL{=n~jh!Kr5fJwe{!vGP7E)DnuDz!VMV8t<|op-;t*0_Ddl z(y7<)CGGgbq6;1?MrFUz-e*v;WOj2s2C5p)Tb;L~E70!&2I=%5; z65NUN;4>OHgT?^#mnv9q2=g#!a?_IwBT>chOZ^GaQ5QRMLF)K1lmpQ%`|=Xi zeUdgJ#zkMRggYpvF)O{PhkqB+kf)zZ>$*%{{{shJkNfHZG(e8EKUetEYCMsv5@#6K z)83$CkkU)3&OT{;V~XU0^~v^7>9iQ1tIIhpfx*^B($Z;j;ry$J-_&2zXwGIYKs@}d zGPEd;4hV%q*sA}LLO8emuzruk?|$6Ao7^JdOw$S8k0S9N-o>{Em5Ti?$8IrD!G@n* z^P|ej8lLGHFLa~4#jsx*m=>!7RYtphi6^v#cqOns>%Ra!Tb=`-7!nSPZ&spe`Y1Sr z3%0)9aZE^GJHv$QKYH#K37oyIr@)EB)72clJ8jwG!>{|bx~Z)0G0X7@wLp||U}4!; z_6RfRZEq+x@Ma&59nJBM^-G`-j;%2kn>yg=AkrO_)^)1p&cnYs$v@azZe*gZHEC+~ zW;6Rpkpd#vYWV`2c3T32%!Y7@+l%S#?R=96^j6KDu|6xQKKp&YE7S>ZBEa23Hc^T2 zTfP{y08G`y;7+-a7Jb)&1?}`(GeG7u*Xu9GTkv3BT}q}4kW1)E7b2`l5Y_>c+k>PQ zW2Dj?7}g5n?QUxuYfk&TZ<)fwb?IJ!Eb5RO`Vi)Mk+GZ*?9>HlU-w}B;C$exmlPaivt4+3;BAdFM zMEl}3bzb?*| zSDv~%4Q$&{&o4mL&4;yY@nE#IU13ct55(YbHsAtO!5!YjMpSX0s8*k?xF2%AfHINh z0yNC)!NC^Dv2J1|@992u9Sl|-YWtxq2hTIpmsizK_rOS@JLe)kRiS^mg1Mi|Pv1** zx?-fb*{KNL=H*y7Zim?_3>!`AhqSp&>($+_SPVO?SGL7Z-L_YnrGCjHs6^21RHeZ)MR!QG)F z$-qvIadi8LR2RpS91EGYkSRE3XC6oR6{xvoXDQ*y{V>JjWB+HhWzTgn5P*7f3L`Qf|&H^EleXHZrQDizTxRu+I}eK3GcZ8DGu-N z%dj8=-+B}}MK7lx2qSAxnZrvn2mN@N2wZNZP?q?#qP&g;_8dYcecsJiwwch5r6kx{ zJaYLRFM}DRFy*`e(Ydn~j-oolWr!Ey0S4cHHL<(9`S2}wQ$15VH_W)-dgm01bRZM! zqCczBXme*-Hi#{OEPG+_53UKNMEfe0V!$3k7NHii>TWXqRJhf54{>{|Hyd6u z6A`>b3f;f@SOmEAZ%c%nBa^lUCq8uQ^ltJUvL<1?6VG~cHw<9qdA~R6w6`SO-Z>Wk zQYQ^mq7Hb>%R(iuj~E!3Xgv;XLd)mp1<0u1Maoe$#0W^e8|F0Y`xcIh>CLzRd3XSq zzkow=<$WG;R_P7?WW)S1Ys*iqUxHfsdK3w$oRcsR*{;o{&k90*{iTo=ogwe-o>b)r z?ryA4UhWrqVi$jc&g7Wq`xL1cMoC~>4DCX_M-Ew7N(Pyq`AaocI-cDak(~=rZ@pYQ zS{@(dITj$>%mr~7NS-KeXVYTo`+bj}qby@04y}`c2gyw`G3=gft z`CQJhYOx8Z&Jg;D`%;;Tq@R(k^P`!>J$ySm-IYbBtRBV&HKF9pqtmNA?aex+0Q>aF z^Ei079f2!qfgfLhFd2x%2!|XWv2(KvP~l!Ww)v8UteE{?ht}=|0wq2C_w1QbggPbW zE*G7aliiHv7^{NVSR}K75Xa-znOi8rkFIye8ib3CY>=Ni(1v)OC&ubzvO(g9>8%cv zy~-nG(u!@#^20)ERJPBr*ng2LZTO69Knyh*eX$_BBk#eKtLWRm`1*;@PPBpe!RFdd z=&G`~1VpJsXxm{syP>Q$c!4-p#;I*SYUGeToT#Aa;QTw74&%7jvkDJzl^uKbJ9oI$ z>3GO<-Z_?g^r>7eq7cT28-$UfeP0dj{ywKPgvoWfESl%x5|zF?WVU*-Ri3_aZ28<* zdcK6*Q2gZ2X-rHWU; zd}F+(w9mm%lWAjR)6fxr7=!-sPQ|wJVAYiVbladR^##c0MtbI&m+q#IBa{|K0hBKF zxuxlg_E+vFzS0y#WXWky7sxKg!Av(Rmet?d?d-piY?~KUVQKtwADe@N%X$xl^}9nT zgBy*wYlUVlE;Ci7O^g~8hHxWO-)xx+13${=q)k|0= zS-SUULr9Bo6@VdVf+$Pw=iCbhY2uSHp9oLXA4fKzRlUF;y^Uq6d9VF=JqR zFLHy~XGHfNQl$86KkX$7YR3f<0J3XA(VXx^^#B zr7Bq_11s{f4NC_wpYpSUQR*-LUshWAZuIu5N)OqcpMNeXDap36OwuKZ$_kBt^&9XSLvS3twP3BK7R z6@jVN2IqB1KZ!U3m*KFK2PfrDmEuczAMH8trzR?NR35gv)icP(41b^^_H`QmT9+5R zLX^@Big$${-ok)%lkQ_M@L4)ec&Y@{k=dnE8xXAaz6X+c?trnb0{%{`ZsH6v3giBi zg2WB9sxxN0(z`X(rDxvN{pikr{Jx=b|L_9DVu;Ox)QOY2w0_v+H1j($^5LI6EP*~l z3Yd*lgsi>U&SBEO^|Ez+%O^to)WkIIRav(aGCY-e0&@Y%^^j77zyF!p|1ot|uaUC} zT3B}}0Iah85dd^QVa%|xa;)Ck(LPS?lmXrhCZ`9rG}20*3sy#P_;D5j_vRax$R9w=kA>=o5l zS!4S4`SS(H?ZN%IYJ-J1Kk}Z3gqhpWl4`g3>f@masG+>0gEZrE@C%A)UPiticlPj( zCL?s?In|nHUOEE9S{L%DNh+$srd!&3wUk4qvR3A>$K}QgXK0T3l&e}vfu%~^1xWPw zN}YGbAq3K2Bl^72sBs$3Kf82>!5u8x8q_bSQ=nshcuu9=K*jrcWNkPeEE(S>2*IMK zEX<+Du~9EX?GBk;Z<>(g+)MHj|F#!*Q(DYrgI|0wJS#>jr|@ejUB-~c1Cj@(ekPme z_h3W1P?9$bdk&$cTs3>Hed%?v%d<*uGG%|DOmX_WwMOnbg*7|1pOJn5G_Y??<{p=c z@UD8@!TmB!OO>IqZpIOYd23)S*|5lN(;D%*lp}4%MgLViH(4|XS%9V_yI-Wd3X}lW z53fiaXsOTlW>{27wA%A&U10hxaHjHYDSYLMdS7$vNgxybNnB-r<8p(pTTG`6ZB{qk zx3A_d95-Xbi8vzVVc^uI&!Lx)x%pp^sb2y>W@S#Z_Nb@t;?HIH!d2Gi341sp%2np2Zh5r%=g{4M21!f1q`{v2bA#kV zfrgbh@jAFNVpPla01IhW1&l&M2$8*3=`Sd3Z>{7NPH+{$BD40H%|FZJX&Fwm7r&>XT!<;;>j_-haTc}xEJ-RvCq zxSJ&3-sZ=sXG3DFU-8Ya6b{CO#`oX9>viG_ld+bbh)_p8%8h`v@pbhl+o+eP*Z3qy zIGcPaF3ybgCF|l6ZlUJVu1} zp%rKwpW&C|bgR2BKh;~p~pZL;gklpW9mI$)(@V{&V+R^-vjvMQ@JD5YHT{Jr3 znNhvXJAtD7M~!ed4CkCDYsv79exF`>&r=hRupTaN8SKnA&%AxCPF~{YCG@kQdu@s) zVQ-qOmdrdNr~1uY9b`+U%RjGH)qKiMVqn!Cd~YGwrn%Crj1%+*p&d}+$akL(BRI6f z?)|nsej@KdnK2tGeF6G9pD@m5x8M*mTMlhQe7t6_Y^aC=2P`hxPI?o1^KLp4^(uyM zNpKgi@*f!`hwSO+6li0o)ds8p4PFsZa9V2LQqxd(#Ot-UQ*Baoq?n*N;;6#X#Tg*) z`sSflq|F?1w8Fc+yoWY%~%gv!@$7s|#2WK%>lK5!pQ-u_ax99qV=_5D-{}^$M zg1SGHVR)|SYIoQ9u_!S|^gXiDFXmK=1k^Ll6hF(16qJIF#VbS57K4a`ojUM03;i6j zN#7Es%~989(LnjlatO#S|J|(d@BBJD&D}mTdS&57Fi&iK{a0;me9mZ35U*ttzk8a; zlHOvNN_}lh;fH7?*{j~d=g(k62i{N87QdU|H@IU&NMjosLL*)85$+Uh+$6-EZ=N`& zn{jnJGR<&|4-WA!Bs^&zDBJKz%(YSE%i0+AJTx|BI#&Wc{HMIv#FM= zTle!LP}{3^0INFyMC5?x>{MC+vHOlbya|3ttDin^0Am1tfp0GssQQ1Y@}=X;$lU_o zP~*lm{(;gjL%*yMEI38r2R#?@CvuIKFpr{Gc9pb{%Q!};>_I!j-aXpilF=tis*L>3 z!27|O>V%aO9CaeTa`+K{tXAYRnVc!5RZ!RkNPKNTg})zFD-N~y%U?JvjTn7BfRK>| zY+UVbk1jwDM&|27(7q|sUPLK$f4bO@y_CTS<5)&@pyz;yB7mM z|Ew85cjmcoOg)$a)J;t@Zd?-kGAx^8T=$2iH*ZkOOfLOFC!pZ3?we!3$%9FT?zJ zy&R~54gvKMqNuy3{zlwns^T{9u@^$|OqMdKzs8I((ZtaxW~c6~v^nwg&W)P8a}N>4 zFkx70lWsb~o2Ef8Viryb?R%!WA-MWV8T=%MA2$@vzbvb(I@$m!QqOP?QEC`+Rna=% zylkr=GJkC3m6Q36BR5XfJbZuN><^4@e06BIe3pIiY~XhLvS_9Ic_U{P9GM6nOUT@O zMmUyg(ikDM5D9>3Gp-MiwTmx6WIOD8+~IF#pv2G4?cbyTNY}mQ^_f^EVs1mSiw#6h zaE(!BrN)`1nlfpA<_1k^+CZVt;B11W_9dV)Y}AQQq?bhYleh6YI`FB`6|%B2ik3x1 zK4hU%^z`xOJ{QFe?PDaj9glOratYUM|=Tlf=r(`yR-{9E5tm$ z)y)#jq_6fE|CisC?tJ2yN>H~5MVP(Te*)>UM-JJTZ;AX_X+Z?wm+F*d2c+vu`TE~_ zOPs6XhBeX0cIko5ln#k95;qu+ z$RFMQF%q=NV(#+(9i$VtJ98{*lt7*Oik?}T3nljno^CO6;QVYTTw8v$@Mx{BO6)iZ zo6CjvlkIH10D(-(+%0d5%t963USjXfZIJ8*C;~g%c<>!hfzI&(p28>#3IB;ey-@42 zo@+oWKi8j%JYq#E>~&9hiT;_;C$~#{GhMxGmOb`YqCER?l)v#5K1NvreQC9pz6I3P zoD40qJH{8Fb)(UZqh)Z}045Sj3a@>40m^aKEkYi20D#B=6v>MTxY&VdQN#%ecrF4L z4QSH{$z8cO*l!(LEl>c{fH37>abrXj!0qe^8>;tO*;m9oAtID$-9Fmyd$sy>zOr^F zY*f)6T#ThDmzt&)MYBtDX^yAG)9+f2CZ}H~c^#Fsz5+=%?=66!(Rg<1t>8cSIeSK2 z+%vq_j0wF5-L1y?+PpwB&7&wW!H$rl9r&4wEYF}k{bG7o?@J)acrHS0EhWRTjXZM{dlNmR(L@X@8b2>3_sa8a5_flQ>$uOV2RGYz<`%T#-Fou^ z^h%9zZX9B8I&bTqq2KfiIje9r7@{u-=WiFTH(VJ3mAPo;7Q>1VxPpea%O zb6W!vpIm^DnP zFLN(#QTF?u9w#DMqPrVtyJK@~t~nYmj%Y;$C2ZlC;yxO@$>uj-f2~ga6u$aF7O0K$Nc*vI3O!61$bm8aED_=t%)vdvL<5ptD3`i;gc+Q&$|AUoO zKnVZa;$&`pv90kqUw${|9G-Gte$KRSVJP5Hrbkh3OP#AJE$Rz3v0$W{c{6T`*czk= z`sGuP@dRCf;$=hN;D`G@7a+si-QSqTZfayzY@BgnU)QC*u}TC=@w!TrMh2Y@*l=^j zNSb8)>O8Wwe!q$B(W{Y4o@7D|j$)0d3y^@1A^OZ`p!vYJFw6frh_0|Om|g*t)$G*k zG$p4MsP|fW@XcBEbo&1f9UcFm<0b9FF9e`bcr2Tq6S7G-R`gZuQ2kgBpq8Zb4*#H* zo&dEp`xk1t`roK!;=fSKG_Lb!Ja(%1vPqk#ics0<@!!Tm-C2aQjXtF3Di@&6st>p$ zsPCX2f@S4B45Or`rct> zD=Nu0X-wHtO4MI_e8TPrYnXWF^i@iYEED!KScU_h@(U0HLf}~6J(t6o;o&{VXRTi2k)u#1$jl z%Hty(?a|^){gH($MJSV=InWqEFz4_NdY9`!+*83+^F;pPikwk_LH;6yGy04vxt)0uuB~ zXFR!reV~;0L=IKxy5WnoTg3U4UVkIJ2P?94GP@(`-I89`_D~6KC>OXa5$esf00fNZ z;NAr&+=Vpk*5PW;!P`M29vqK3Sio=1>Hd2=c1LI-PGIkyKgKnye}0!?O~XU!@XIWryxnE6M-%g~az+u~C;ezPzOcw>~dD?@=I|^6f!Da=QE5e_5KM>dgWxK zUYVCYk%t82AOmnoVNVa!Q}$3_X=)WHEv|RAq8F{80ttXpA6=Q`DEslnHl|7SlZ;?P zAAC8&!udoRoNyI}B)CHWII5|kepVj0xo@>K$lbIVJdECH*8YV}=j(ra+IZE(@5oqd ztNq`X=zihk5oiNRasGOW&xvdUSR7FB%Ha5;cjxn3jh~bT`)7<+>fK0Q9L(?!#FT9? zb&vAo_fqmjB)k}G5c=0c>37&CTrXt* zp|>DzdlnbhZ-OwaQx*H8r7ob~((Bl(;6kCyE7=f0I z2yy$(cHVVWghqMhO`>aMmE>1uHbox?%?Pl(d3)loHlDnH;5AKy3 z){5KZDW5i&cu1YsxLV8>EF=3^Dsu7?>8^h@6dI4DT-Bsid206~Hf8p1>cnc*(oXB$ z#H`>b5?-5%jFtD-BW!{G;a%HB0&}mi)c|ih1X?5hzCLcyobp~d%?pZib#0uwQ|N zT-*?{PnD_5l~XjO2;Z0T6}j6CBN|a;b5&4$+5XWl#}&WxCGfe#0fK`%zUF+S(m3L` zyH&fb_UYTx$>oHDoSkNrP+H+;H`{=`)!O87%mV2{k1sQwj!c_=0a|OV`4EF&2*ErC zOIYWlMgdiEozU2jemkeXF;+RN4F z(H~OaWfNLc%;@Ze)vZdD@H`~F$mW~M!YsLB!nu74WZezGCe=A6PRiZRsE(%wSemS+ zK(2+?FeQGCguBx1+xy%5GBUpd_w*a!J~vcO<1avaFsmhVXG7@YIw=~xsh4w0)Kj`6 z;IiFq8E+}BbRZ*s@{E!;Ec?ASS+Sd57W2Mp7)T-UK2ZbH=Xs&jo3o4%bB91U&1yRI zSFF2Xn^W|zmj%@wV+pK;-%TUn>d%3uDXl}VW1ge$qX_5a2g^`D{i=F2%>uQm`l`gv zKx%yMMctPx7igRk(lbjx%;l8+V(2pHJZ%JAjAp`3>6*!hTjRGr+alkIev^{2`MkJx4Rm~^>?Qa~Bb4^Ru}nHI#Qr+JD8?h83n1`76u#Re4Rq#ag0u&El}!?$O967f&3Jh57V z@Fi^(+zcSdCT^R;KY%y$hirHaOSn9vrKvQOc^$PJ!*V(l>FVV{uS!Jp3Or2hJ(IaZ zB{Fv)%&Q1*S!aiYDmhzEI92J_&>{MMSB*C1v=CECqq!>4)M6rzeDTcrBMyUdxRTav`kUeV1Jl_< z`YaGW=tby#Es@|;Z+XwppPWbDStUxx6z)pA&AzKiYjkq^yDAsmbN0)ct{=D+g*3^) zo+*P+<0-k|{r8rk*RoQsGSISGIKy*{1&9j_ct0s%@VoZ}H(BAG91M>1~f?!PPOm4%rRa{xN+K1_6N#-)N%WTd6KsPns#pHkjIAzFQ*hKb*H~KM`B> z8kRxzlp}M8e{;2cw49}OIkCd0(4YnWhGrcxscxB8wV5SnhBeId?I?!nU{jJ$7LPgW z)(4MNZD2{|27tm9A(sL00noqGz)WB>VaziaAvAEq!_b3PmC=<1&7nLTL$JaWYV~}u zmQOKv_!lw>x!&AC`7wLM+I8#C*k#ExzsLvd!Ps*DnsBC4Hc8LY(F{PXZb;BR zOOHg|ek@Z~k5Dp+O`n=o-@mi=rC-oTOR+B(0H2$HB&j+2{y&av2HlIVmkciGuZN63 zt?_@g4FRoj$dd0rwZ_w&;irtcfRU#8Fc-ES2d@Eq;RNu*SYY!WogNF|3glTjrOz9{ zG^9N@3NSe2S;T+7JjidUChgskn`f-?e@4@jbZ{Xr*p8uxtyNWwh7RdMJK6 zz_h}cqN5RHt8-#F+^aR+w)}!;E|0O}%=P}XdJD=JUp#~$GNQ(l7Rw*q_^qxiveWeq zD&6tN8Qam?8hXuU_=mk0L)1Hxp1>4w7^D@(VYWYBEGK?$VAtH%2!<-2WS=gEG;sGi zBL&p(Pw3Mvc9c##^9oOMJ+#;32HD0UobctnXqDYFKn$){%&w; zOig}I(vg7-L08w8yeOroX4W~kqQfrF_+t{D4x8}|q1$3AD#6crGq3Ts#LzWRPmxv;@BVCF!(t zVRbuau9@)kOrmw11Ep4)_*lBukm$zD!@j%;$;-xKcKotr-%Rz_AHz;?-y<*K#P;yM zI}@%DyH82M7--gX+W7*6)*ar$EOT}N3KkDh6;rP=9-1=hq9)8(EszDV#C)fpm!rXE zd26BS7vib|nka3E9phh3@f-7*osBK)DG4PtvCE!Uf<_xiw`@P^vjw=%zL zyYOmhmxfV z2(Xo%YuRm;_MVoMR5az80;yv)Zwdf`f3KS~-=;IlQMwy03j5DKKoP)HM{eW{S2D&j z73Xs73Kyo~-<%K31cmPekNJCkwhb=fwh`1)xV)oM%cZcWeJ7%)8H^@vhmbaCInM1e z>Ic1-Rg(s8Z+$QS(hr(@0b+zvq74@%{GqKgHzT@h16}IeGMW9v zW6|@-QFXk?z1Cmv6<38Lyss{#`{Vd(_uhrRZj*@g-s?-eGxIP-U{GWuk5w7~=UdG- z^QD}5DZ|IwF2&Wx801DBINFhra)7sBn|D)b>)ZpfsYMg~z{YNaS5`orb<7QAHy)gn`d1v}o+)=Yq94F5+q?Fzp zAiBMFUnjLHI^2O0wDI?lE-+3D)9HfT}yeq;lAu>H^fRXbS&i;x|WGFNVXWSSuz+7u;-sxGAzb@_eHQC|jjqfd8 zk`@@7`GOvF>DFzzCX%d@M{1&mcej4BxIqK@tX0TEd{emUY}e+GB1oa@iSbl#1P{X2 zq`(?_IS=QMMa5qnlR8Hm1oJk$a{aJu@Wxu$CioHm(GD2JdpF;*7dHzn#OhALuC}L< zV)RPE3E0*cfP$M}fOz3^ifN*+vTs!$$i?}NHSmitmy{r;~Ppk zt*XkxmZ<|jL~2;n42|c%#ZKL>R@IMNCo0f)RJ~PYz49#LS_0j6#jChLN8az6AJ$c> z$5KDC)79XYaCAk5mV*eL9F4gHF!&2#nys4sF#ize<4+?yj<^eaJvTWO4X51s<8QqwKTlF5BXrWn{Z>m_)8hd9rt zlinHUf8`udsD8@O(g;u9Gs&@ssC^wOi5^g^2%aQ3FJPX7AK@x3gZX>ytKDGLYAL+( zkCK!GqXz*Z&wHIt!)PEvjuXv2pBCwgU`G4?R(-$Pqj*0jHE^$!c{$}AA^nQBw$ zGHkyUU;Jq#jHkALLa2DXsHaI7HUy?y}x_vnHB*^x_;;w)TgD2 z(cOTUOpzX&>l9C(H8!lC52cqf49YvPUEL!L!f~L}kUwT9ztPSbVSaA6=?q?xQ01qr z3yIoM0$q7Z5AlPFsFbr)=?eU9FFE-N(|5=}4mo>(SY>8x*2?#J73+Kq8WE)9emf8Ghu z?;z9}NbD6YhSI(=e*Gf-)(rIgHN5DSgWoKXjL4o{OBDLu19j<@S6m~m#B{_CR-FPf z!P0ybbe<_O(}VIbj!$i49XfHI`%WpTzkSrl-l9GAlr^#y?uEK20kEcb16cigWu434 z3uGP`r91SmjKChzRd<;Yvj1d+^WKfT_g(I$YY#Z;vnn%9a+$M=lqOYP493~sMbS<3 z6{5Vd_lr7BuA(5V!t}=O5*^oHk#0Jq@oL?^QA2#w@>9f%OCJAL-W0hW*b(8!#)KZ8 zH_NhnyjAC4VB^xSeD|86RL};bA@D!;a*)^2L=0>UPCM;>zvDXCVnh&TVbLADa_epR zli9Ygh z`t;X7{^ON-&4;+JKDHu%J=Ei!ai0rZfc{9g)qhF19*SG#|6yAIlExp~0_GqYAoDHF z=;h1C*~zmaY)c@T;{x<01JLDv-XEaG=3X=1I~lu7IaG@K2rulHUk%K7_}RvAo}$+4 zJqwnnw6+%@_d~>R8JWDal(vMF!PM-TmR!0}%Zrl0Ku!lQ-0MO;yl7i5yBo<3q7g!3 znAg%+LeF1S!}jyHlpyYlnX-iQx)PqcLJ~5~^?^`{Gmed~`s9t6$EU)hKp8+@8P}N6 zBV=^sJTlUGJNS_^^uAQjm)hK&fiHQ=ce8u1nlrW2#lA@51ZtPt&zCoix!+%amdq=G zn*lFCYqx82HjRZZ-!TJ}^*`@4nA`$3tHX_j zgLyLVurQ|~f`en3EBR>_oqq80G3Gv4YrU#xPHi-UnCs%Y=EHvKFmoNkd&V}}!2Cs5 zeAgWFIK12T{F%VyP8~!WE4C`>=U>*umAU22tZbY%6p=mc`T8u^!?nM%{Ywe?2<44} zA7p$qiR&gsu69=WEiW5CSZEE)K0Frhwqql%uk7!LHCSNF6+1z$Z(=r&o4|{h>vp!1 z1JfhGNrh&wui*|(%R{_x_m%N4IIV0B8YeW<176+Eou|vnW|g;RZNCndMHhd$Gv_On zt4Q8I-J`E>sO0F(Jsr%|3-!N$tQ^i6!c&fpI+ zGm@zM=MX$2@dHM!A;7r}+ywzEXf?V13N_YdIWY0~RJQ}`skr)XDO?dK8_8V4$Z}S4 z6|z00rZ$7JAzPU^D~P|FONDNjPVuHn(wo(tT1ze8?tB5cYe^ryt6}y#v&n#7RxS(l z`qfgBKn4XsL((!ZZ=i5f+P^X^u8iq+O--CxxH#)X@&^+xpfOmtN7)*XF26USPG^UT z4!c_X-O#Z4%P?7}->9x&2gM}1bwQ#X9t6IQXx|O1To+*WL_nP{oG1-Vs&y$c!57!IObvs?ps%*$?OKl`t^#Im?oTDa z4NNz{m9`8}F&anuh6iCoJ+mz#Q86RYAc9CzZ>G5c8q8x97viJL&6jT~NlTTusee#G z?v_dHWi6Q>$^1vAnZ1e;-!p6Fcsk_xuE2OZMr$Nhglj6p)458#o8+X2itK`ZZOSK-Rho5S2{c-8?X(3=S@HjnU=V`f%}lpD>Tw)audj1u|( z*n7{YrnN|CF3lk_(+(FxD{D=Tz(5BPU%Pe9o1@3Bz9Pr1#ulr&b6Ac3{B|fid zS!1|;+*8zI&>{8H^NW@7fWjh{_a!^SX}`ZEo+)0f)t{&(ANno;A-(Uy7D?^Nr6Bfl`C0UyU^0PW6ZZf( zw_{m_z{!}3K_UZHLkU6z0n3b3Zl@d^i2N=&0|%y@C}_n>9I{%0e@b+I8p9j_sBTBa z!z1&}U?kPtei>k6^{daY=rgM~np(7dOwicy&cQsb=p1Np>|v^${dQjP3%5h9SAzdH zuC_GZtqldTic|ity)>sYXU-l%`QOLz5Ut*z-qUzRm zNuQ(T>EISs^U+tbS;4!?cYI?|Z_~jX)BR*X)8O@ca#c>7ejBxr9Iv+4<13q|9yt1@ zIY5(vOU7oxVthwi?58H{O%I_~!{i+;*fyE84}9&~$H3$dh@MK6Ve_sReCRY1VEE8F zr<}3&_K!-lbzV>Oc9nR#z9g)|c;M!g3bHm%8 zDoCx(k(TPkeKdEqG~Lto-ckKTEW(e@Z8y=WO!E)JHy_@g zam|^q(#~(7|4ojf{<^FaRJ;6d1uG@a1JdZoF;Ti~+79Hk|gR!*Zm#`7x7 zdZa$tS6sqn35X8Mt*xs@+;Uic-^mvZd}qPm1F-K4zj=eYiuAt|l9U=!GAWjM%j`%N zt1`M@Ia_L_Q%9u=voY6LFAH(K6wwvFcl7HuvS3Nos_8`JBn}p=tc_=GsRRg@q&Kxg z;(d=F^9D%?Kf z8DH4>P95>%3}&msR4@A2IQY4U?`!`+=C3{a;Qx{K4y}^4`TK&&_*YtjOFmKIt2X7% z+>YJ0lf8$52;Z#*6IOF<(RAX-_MRmo<+svo`%hrXWXJUndb}5}5G^WoG)>6mt4(Bh z?A2DIq{~GQ;foKr5DDZaJ{BkzFDw{Ik;y4|LGfn@%L&Q;HeeJvevG)&{w)#re_ti| zwzo3`S#l9a=iGA~&0g#~sSG;!r3%Rmfw<1avMSmo=)0E#26-0#(7<_ z(P7uac9E0S?&10N9yNkF##=y1l;|KwsdhNpvfiF65-i&URPsfSCWKg^1Lbhs++cPv z<>}Fob&eJ-AP~+QV;F!O`zPf8<)0|{7t!Oj{ZAL~yZ;T*<14s%(#<}Lfv`-~vJ`^I zZi34}WWN>kwmE_i$tsw7Aepg+Kcw2Pn~S68L^A$j;iaBupG~f??>7d@;BijZUx9Te zG)-6Gv;ef_N0I?K#$DN7dVq7GqiSgDZTz7*I*()vM=uye0K`99z}*`0H|0_?nEW2_ zMy0SHuIPQz(`nDorxA!xQd0__*9RhhPYG0c%+Pg;3k+$g0|~v>`A0|4ugQzXJ=na` zpG}k*^)Yim@YcwwJC&feA&A(-OC=xVf~np>+Q-z+y$oxE9pNUK0)BzhP5Tnn9$ zVqyF)v_g|Fy8kKR4bH^#I~MJ0fSI$5LKcu2m>E9|6>K%D6T@kAS?|zzoJ@`s-5jh8 z-);KKO(fcek_;T0p$}w{nYiyRhsIu|!Zy0EoW4J^H*INjeY1P-&?)bHFf!)bx4GFx z!2yKnRVyi(mx)uRJ9?J(B|6G-{SB=xoh*97-ri5Ye^-o$&_H`K*#6KsBQE*CF<;0c zK@yi8t%W8MUq_weq{K_wgSX}s;q5u_Zaicj-GYASuFNAitezfQ=i#UFMr!iJWDivs zQuoR4ggM8JFB4yA6rrmo@+W`gIF2VQA-XCLQK%@0!K}N>t;B6@nnLu)?HvnMyQ(N< z6D$(6E`bGcI;AL9kQQh^M3>jK7x(ktMAEbhvFE+Ib72^V41uf7BNj(_XA-cw-$W)&9O*FC+{N( zaQ80aCqs}b4^d(c{lkQ{e#G@2>i7tc=QF=iuZ{{jQnWhxMzaCJK{TRexXlnC9F`KR z?ANPb$ll}^8u$kw{2~8u4G8}`nBCcq1)r2*hz{hw+%+)f6AbyQJSA}QJkmeW-7?kg zl9i6iQg;n)8U8sO?Z;G}RVLN)Y!#bdE1QHp&;-g(gt9!A;jB;4K@&>!7vHaa3l#$7 z)Xq7u*L|0mcejnW7`IJ^kHGllS@|%kED^5BtC>d(gJ>vPeq$E6qQpLQpdh z%LR52at_P@1uan zHO^3i1Z)>Rv@*yScG3tqj_~{E-YAXUNUg+4Rb>MkAZY}Ef1kOXzT$*{ouKc{)Ct5& zC6Y57O6Iphtp7&uCA(XLr`8Z5R88b5?V(@lW+lH|WzU|R=B-k$nNvrI>E#%(>FIxE z6IO{I&Peam3tCDj6-_=_?y-3iP?$1-x2(V|%?W#~HMzuBgBT_Z=}_}F?;_}~>G08G z%|@oA0y`x2Hv~9OL?#Kt%lI*8Yk<_1tZ0#*p5~0@tgBg!HqVTtC^3X)z0J+njeG~a zU+By9DfcjY3n+I*ofm`(DgG@Sppf!{X!jPsisH1qiQ?5_aocl|s(CdlwE8|Q^KA0n ztX={`hSO~!ZpYyRVm`EU8p*O?PA9y{#}g!#@2B{CJGrwW0_`@W`)zC@53{f{o2{O& z?E9&g9DedH($^svJ9&QGY2I-FZ^!KKwB>(Wo4z(II0{$)KIP~aJuoFZJd~@&ISBya z*Rx)%2fp&Bux?%jF@b&Ds9%7>A|0jKVOMVv)-;{YJd=VbMPn8Z(#P=#LLL5~<&9j_ z9DiC93X~u;{Rq7}3MjaB_(5v`o8ftTjgGO^=ZUT+)288;)`3dz1BE;*^y5ZbqIqdb zNz+gDK#AckOdq}`lvgr;Q;MmM%=YEC!R!6Z(iWd(r&io93?wo z>A)9!ppX4GA0HB0nSkxKX)l+^$lEvl^nKi$QxD!%kiZv+$N#QRA*fy8q5(&Y?HTHI zS~s#DvEg`UAHM-g-;&UT()MLo=4Xh$Xz=y*#Wn^}ru5*Nh23AvKEdI_Hi_ueLey0% zvU^wMcLHC*C&Nu0xITp~V-a0|I!(UNuS!SsX=dgpG{C=4q-R6VYJhh7Bm*~`#=O^j zS6BD+U{OGLeMwS|Wj#{Z1vC0%%+Xj;@2iGf$egN#L=kdcTE1nKgK zfm>2vNe%9+)Jx;3915vw;0>8W1;8D`c>R(~H889?4MO7yZ*c&BpHP*9_^-g1Tf zd9?hJca!PnHjp5etDWaq>yd8QTmC6YMNN361K{Rf-aY-~GM9qv5jW`$nPwnR_PMr7 zU2`5WOD8%sEuVER%)nLdHlV$D$XtVnFx#Dn4(uZ@IHd$~;8j1$eg5vS^P$)|htx3@ z&Ks&PX$S=Ns0SW2)ok)y&+4u)ChJk4DT@a%eEN+}$Au)<2H_Wg(R=wY<14JRW(vpL z)J{pmP;RC+SkbgpN!=mu8j<~#5iKhucUR4_E`1U7%xp%Qydn*^_oRmVaL z{7(M07<9CwkJ@Yuy6~1IFyk)M??>ZmE6i zp;UYs6v7q;q<(72Po~jy^**Nc8h8O(PlzVl?umX+qYyg~ zM1hn9qEGIVm5AU|W=?F&x+CN6^l>(7zvaIM4oHx6ViZpeq6CR$Bn=?34`urUjaKA& zb6sIj=J0_Q+mH0e9I%%!Gd4yCZGKC{^oc1HKScEWp-~^**;T&zD_ruUsawKQ#tw3{ zWS^~dds4J9{fzW;6zv1DNwI)leT18}%go;3v-b-KPwD<3Ba{sO4^2x35B$ln267&K zS;x=X>S-BdYQoH4^YW~p%Wdo5sY_Nt3W*}ubk9njc__O7A}bVk`_@t(j{`7M%`8qX z1iGf;&q~O^OW{ALhDQfybkEXcv8;(sy}-Q%pe;?WHq9xSS*bfbiC^r#c+p6ks`B8r z>aY|qOf%2bU)casTiBTk0UKElJfYX-p>3#@J=yunw5GvH?Al+nUPRz2IBh?QZsp9~7*dP;N}m3Z7iE65D^|JC(cja$bAvYj z7{Nuh8nul82(CVTgW^vYk`t7}!NQsu1bFV;zW}9-=Kt20XeUaQikSuxhcgR1@T(|+ zAMc`-4CvxwP>&j4;hBl~N-4yo2D`}h#`zL7p&V=TLK3+SoEN&q5knn5^rJI8k~a%Oqlk6i5Q8jNBkrnHj8KO@TamXD4E&%Z&0rW=v{AVEzEiffrGU%r+geZ0 zom0O@yJi5=p2Vr98jSO6^*n#5F|ySk<)P9e#bZmbn`po_BahWZ^&@#@O@FOy0abx@ zX!lo4cv5{W`hEcKylXhmC8B#)W!*gFvZ;et@LOpxMik*C1CoiRb_F@jBcf(1Q=;IO zcKxU;&EU|zti*ta=A4J^q*F<+{exg%&p0*KYa)H3+WcD5ui*o$ybB#~Eqfe5s=ra! z=)+NwUrww(Nj%XH1u}q7&q-b>ct=?v1tjgP?~-Esb}c1ZthOO^Tbk$z#NrOAMQEp& zzqh3nqTFX`lJjbtm#47Dm+vSwQ205ry!K(o;d!mjsfb8Zp`2IufHYK@*EGEf^g{-( z_wrvnJ!9GLA?DO-I-D0ume1=B$cH_+5p*u>W`NcT&2bezovOnCYzaXMCjj(R3$L1p z-U|}C7H(69xN}xZ@bwA7NXjYV2^_5^Q9hIMeWNu1>qKlsp-$b9IxaPDY4g`wZ1!m2 z1Vn~j=kuv`J+c%9ma^pkQCu4;U(i(^G)0Z-K{QId_LKNo#Ehwyf)SICqzEKj{k8Rl z*7W;N4}i}TPK`XzM61ExJA#sb3C~+$eiaPm( zG66H`n>|*mNYOWS8EnG0<%N|h0b=Xq1)*ZVHPVI*L!a}&dt)g!Vng3eCm&bxI8U{K zYp$*+xmdAy%NB94N=ZNY*4hQJsj%~OY4P_Xwx<$}UP$E0)L zT0Jxyu~M>b2A-l?ov1l2ZFMJiV3>=z0445=es#++Vo(>OVRhFWlyzlv905SVGqJ6& z0oOv$-7qgJp;~kvKcH`P2Fr^KV8db_D)dcGQ}+mWFE!MQ)-1U9pP&GW6NQr~3MCzE z-yl7AHi1%95bg>$4B!zj*6c~{@ZGTztU5?5U|gX=(yCydFO>V{6wWk*h7Wm0wBtv5 z*I%_ikca@D;0!_l@kBP6djK`{?VKwdg1E|7_1gBC0VaV_g@q@tfHD2`iFIJv6EQE9 zRI;BFjuZt9JTN!XS?2SaXVi7S6tPkQ<^XAKuJs1Jb#bv8?s>B zqN)mtG?Orh!`U~@(EHryA5D~~U>sSqWjCNU?ZK9}Ek3hAS=JtAEhW^cTC+wWqra_T z`4%O>>yZ1v-MQNXs2r1xzeFshtH&q|mDO>Hycvr3yFe3QO+>cwT*b*n%ZDqQ6-}&q zU8bxSgPseP0u!&l(CPFhR3Omi!iflJ7E=wuq0ZIo6o~5~Kg8uVPWRpZKoVf1poa0w z@04w^+u^mHc!n6y$!Md!1?h{p>PFQ3Jd8VzrsebR*e7>tqg?(pc zBOm#(cuT@l>2N>5@(4?%u2x|@-G!4)%!WVL$TWWOV5vq9mHy76O;k&uqfq%ZYm?YbC)0^$Uu@kN-c8{!et^N)3Vg(jU?k8$gVUGGUzu_NJ)P!a5%LTE zx5ag!@pu8CJ{osI<6U{_i}GBoih{y~E*qG|(~t z$P2$~Gio16=4Cv7FpVrV*NKn#xh}EZ3o3{1doKaE{MF~{f8nZQZ1q3!^$N_WDoUiC zv-|*1YCb`9jMr4vt0{}EKLJYZRqy1drvRl!?8Y8!hRh?wMdc!9eR6Tdp@*6u`Fkl{ zfIo0PT$3c6Kpjy)`U}nV1isqVkP9d^KA`Tq@v_jDH@+oVFurA<-_3lgKcM~~C{ifrI9&!H8g$P-2bQX9 zLSG7uMf%e692NVroU>L4|7Gw(Rah8y*L-rv#%3I=Mvg2~Y-JZW7Ycw`N4QqY#wGHL zrvQpT`Gm072E9FcMT~8UYxc%Yj^X)&FD5gl|7h{g*aim%zrlp0FDC*cd-oqBI~FF; zxo)>#b3Gf{LPGr9&?YHT`zE}bbYwj@eV;YpZ8wb5pY8I;^ZaN*>v2k-pki=Fp}l^hCVuPPhEwe!KT zi$l41ral8*XZ0xao59~d?ZFA*ta6P>c)(lC`T?lIYR4ey4sdCt zpU|9j7}C@4dLZJ@$+@Y2G^0JZu~b)Im-MWvDuhn!=DU}ych+?HKj$oE`MTz=jwD!E zOm+O2{CR;b-sI|>;geTdvIi*}QzV6PvS6;HN*Iep}o#yl`Ov7Mr$v zyxO7v@;_vy^S@ouAsH!Iz(v&oW2t}_Z4Q4aC90fQ;(Ys1pCE;alLAZyW<*GZzbET( zP}^wtdOKrSQZ~8(%$va)k`tZ10)<9_`H+Pbmjv}{2P)Fo3q9%BP3h7cQeMP8P|OF7 ze*N|ni2@QyA>*~}*riW!$lTjdOq(2|l`#Dkm3y+Qr}Ijv>*=2vxK+>8R-%5n7JGa) z>T(h#0S75?=r4TVk$16>;hyNh#W2;Zbls(xB(_%2u&*?JW1Fua0qBy+V1ERb%mmsI z9B?S>{;lB^Vo49aeDT+}%%nyUGc&U~Qnd_4b;PNlF8|8aYaw)C0bz`Qu_(SvC0pL(_rx#yClCf-F%U+e(!P70tLZrMXkA)}E`; z{9#_@;@g*7;3>;nX3c%Owd`UH4hAORP?bM4v*Lxokj*_FvIc{q-s6Z28zq7>C9Nos z%_J`hZpI>SDOMLtbCO3zPL8XUeZ@S6yPZ_frtLp`Tw*so;cay-R@?x+&A`79J0_{X zaXSFO4yFIkkp1zWA^R_a0(pi4X!Y$1z)?nGSj_qCE0(uPqRuVT@#HYYTua=$G|s02 zq^Kkvd>1V^6OBw6jp1bw)j}8rYRVnAdsXi*MzT=?mKQv5ma7LCv$OcR@7M!@;0SOG81s)QrD|K_k^64?X9n{#51Wp=I6RAyu#(7#Q>?D5x+oY zz3J`c4dw94a*+MJ`Kid&{htRJ+Bqjn3RTO{!w9)+Y9S+4>PO><1n>Bf&h=LfPYH;u zU2gSjw{wtgS#9^;Xy3V-nUffl;p*z{>wYOZy1Ysrdh4d--G7x)up!D z*4~yV7Ri>7^na9C1)2+_0w9+*b(6|rra2E`|H;}O<&s7DZ4;&=F2|M010;dlbKf?c z=dV}ZR{R>@Pe_tYAX)<=_V_gnRJhB@Zl97QcB3kQ=?{%+2V!HGBxeSfs)%i~y!DUq zFYx#H@87${*sO2>9H`;wSb7zxin6u9O408C3H&?Yr;W4E7l(jK+ZnKV5curaqdoRN zX(jr9`dxwC(#^Xh>1Mp@2!z{*XId1&Zqjl(&W6vg{GmbwMm>?9NsYP=uNK`?VFj85 zLAwip;_DSphG)cezxz4RKHwo*(E5i4s=4ssR;$cNiqX!b`Q*cX^SihH9==06g$$=T zKI#N4lI~NxQZ-x0Ia{Zh>Zju3fYB*Xyxk|IsBTA9 z;7|xq|Atg$zNDmiswK(gZjqc`qYG3=~N=adRysmL#0NAxx4>KezbwQm1u) z#~p>)GweOfT$({$`IQSfs&S8j-)h5d&oD17oYOT=Uxe(g6AtV_mzl%pi?Dc1jF@#= zD^zNU&Z}8Gg_HgX@EFjuhuz&lvWxAhut4Y8L!ZH(e8Dy1-5nJB6R*D}_dgN( zJIyi#@LOp<{WpvGIF@hx=k=d!@Sid8pE2;CG4O9W2JF`r%bpX^jus+Ra`NP&Neef% zn(tzutX@=_Ud%$D<0tc+QW+s6&?<OM#nwG=YT70Z{0sR%Nm7nw(c)QquF^_U4*(`?sJP&IP?EXhAjkG ztHaypW+q+QEr!L)D~a}j{B=oI5zZB7K7RZ#OLNg-X1d`cmEXeb4FMBoDHc%@&(!h= ztS}b2+=iRBPkAWSs=w&!>ZeHQO?&1OEZ&y8@}$;mRZMlyYX}O+bLd?9UjmI8E*@Rt zFfiW+gwxMCH|bBwea6ACXYC z@CzOa`0caq$YRJWpa(3swvDRU`@mb$e`qE|SA}tw{-z=?zh+VQ*p_*l;3f&J4iCEXYZXx^g49_p! zek?8WZPogEH@~xZ`cYAu%1wHww>_D8AsM&4S4JjO1|XJ$;F;~(5|Df&Be^g zscr`+T)UAN^M^5u>FkSFxh_8uWzgjna#b6WXLVapnQet@SMZs7XSD|b0`Bo z)jez1ro|}QKsBvR5;8ANPjt{<54+#SW zlL3P=-wLOkj01RBl4R0IyS&ch4p(+mFuW)0!vNk{0e#-Jd{T5fw05|i@JZsPey@xd ze$g$7cBzw{ox8fA(bl-WFEi(Dn9S#m;^fJ#0fmjp^C@?SS};kX7UKcj|&f8@m;glGiqR@o~!E$Z~Gu&3|$iO^^z(rAS$EhfLU z&p3do?%-<`^|Y^3)Q*EJs~MCs4&+1%{*r+*ekb3>^L&23+l)(Tl`s9)5lLHiTlFI_ zXddEcE(cD^rC%CS4~tg4NNm>No2OjUDAfFRsa$^6sju-R!TMw6kHn#gl^jmW?@*q5*`GyMRj!ylIVBPUZfccm8a}i3$v*oUfJW+Uw8v zN>k)!iDIq7#lpO6!TrJJ3O#7n6wEkwwTq!w#`{^x2lxw>Tpma5T5oQ3*3U)jy!R{2 zGB$ee5zEdnj^D9G=iq zM0WI+muZ{Zup0MdI7amo^z2Mk5h@TCGH6XHvN?5h$7uyufBmzygUFtcrG$Mi>tP43 zz|BZ6tUyH8$yD33TVhAGBok$g8{Mq1N3K;)Xc27#1?uCTg@}2c`8eL$NF(DgL>fh| z!I+eFo>`4b!FlkuL*HvIw~T}d{!;ZqJY4##jy;yaKN&KwtpAFnK5`AgUaaG0*JhCC z=iX#@AaZZA?#eZgoK1c15lwtBv!hXaR%Wh8_cBY4XeStEol0=>o4z=u_FFlb#CVGf zTWW8EO~W?oQ1J`EwZv37M9i5lpI5gMdA2Y&Ef-J1j{W$po`hZV=kQFCxA#HUYVi!H z%iZQpmQ5CfYMw|+T#!^br?NV{yIjs8{P*|K6c|{d6F{_aV|lvE!K>ntz<7*P%900ze@wKf)u?%7Yb4SC!L3kJ$&U1oho%M6dp zp2Edc*Pobh#ex$Fi!0k4u`Rnt(AYIFPFIv$e1Ag%#kl76t8LO$^z(i7YpQEj9l&Pn z9M`gJR5E?6EV|(g@wR~Phdz<6R|nk@1wd7C!R+dl?6q$boos-hILV}3$kZ5q6b>*yrN?k&Dt8s~Wy_aypeQ(AC*?XVK^$%N9Y`sGuf znhYgnZx4F;$+{+Jg|LQTV_6YRQY^O+O)72AC(pPiOj+7D{3vd)8;5l$XN=kLZ2 z-slVag9rWfU87}Uc^B@?Iy+DK?&UYyjBN})y!yah-ro_ZDE-#%pRT|Vg_z_KRJ{LY zseOMUv=fb+j^oLo0|BBF7$vg~6bh^OgkY@rxc3~zw(h_u>ASdhn7nu56L+&CD?xdw z;USqV{n_)b2Gc9=JY7>c5DTLU`EHharr8enDi+1q{<=kHBe9Fb#1(0_=IyIp0cSt3 z9sVeGH6w0+doJ|L0?v)EyI22_sf97NaWp|ws0|7)pGOu#m->!sQ1n6dK-58eC-uR8 zq-Yyliew5XVa{~^+E=k#Z1NQo^-86;axDl*A7lL$GWHyQ7*1hfa11C#?w=L%h^hkD+D_D{gyZ6W`u)1ZvCr z=2K>#8USt^4b;d^cwoX?9fx+iu`qYCfxFFd&+dT6vSswb9}S{49HPN z7whVK#rtBA?EwAeyT)w5CpgeLTaN20=r7D}&ADZ9{>HthjJ7~8HXLwyrv#Q~^@a44n zDgzOVj#US?RLx^}K$f87Srd~y104Q>2PM&bc4cucqZc16zva#2;urVj zsnjY;u#LpoxO_*xA25NYnZAVecQGpKeX__2rDj#`I&Br#CQIRxxJk#TXs})2M;E8M z^T}3#>UXRE)n3W6wfhSm3+ld}<_Lkb;Wy0MI7YS_^puU>fJ=0q^ue>pd?vu*A0#Vx4{6oM$1I*FZKJ@VhLq z`bo-f6l4(wc(0rNO;jI&>0Cl@f&B|axPtrNFt+;nBIxaW3(bFc)WpwBM^;cw74Y?U zQ*3v2TZRC@>Sa1P05Tqc2?}m5pOQ2pnm4Z&*&oNF6~J#3N#K^xB04Y)Acv4Plx-*b zxowpP!{~E+56sog>8iGu$!;kFo_HDGr{!oFuonBX>NL?d!za%T>oXpkAmY|NpwT{m zXb7+I5^tW#BXNRlP_G9y{z*F*kfU7==Z-!>ndgv`>vJ0UFDp^kfxL(C@>{*Z(q=_H z!t2#-lYkRQmPG+7-};PN5tg}3k9SL%O+&o#REWKq1zQ8UO#wYZvxwy>*Z_I;XGG05 zDkFX?v34{Vu5Mj0UU?#ZM0b+mz#?!*zKkZI;8ndy;_}1Ctjszt>Crqrf*f)BQMCX` z`-J5$-qOG(UcuVIHcs#I6+7$KG;3PLev??*5Km#gsQW{l#!_MpaLSA`gc zM&4)XXTExCrj6hgNm|u+l;unCcS3(17|=EQo%WW2;E;uJU~`~)^2O+BpBu+y&I|6! zUa1U?)GCP0tl@UWBEpo#XNjekwACvGT_?piIX4QZd!03x{Yy^PS}5uNs_=iXe@Rfc zvY{f;R$_}WNW^Kjl7eLXnR{@3BkRbi8oNlLLY<&UQQB5Z zQB}53VQ91IQn4+c^Di;woe$Il-jn4y4-K=wo+*$nFk-p3_QdPKbSBCJ7@eE3wzDdx zmv|25G$ZEIeR?8gFD2W=>e6pE|AyW|xIO@f&As@r`Pt^wADT?jm6exvz7e_K2fz9> zWXOO`cvy%9LV1GcjmpeOr9egsr0_ZNGnI=vnt}VFd@r55ifH!z{9<%A?8kIY=LwC+ zv)w$jD5pVB*N7uR#*4vmGB^J7-fJ5^hvzlqWz51QH){odsqXcgssgF+rYm2?U@)oe zMw}{**20QI0*kDYn3Wy&@$CIYRKBq3p?0n+Fd$JB0P0gt9+5@{@ zZv)!_JFzSS2reH$*TPn6;bh4ccXKJ#Xo`=}Qxl|N-7R0-`3I z+ObrI_FVfzQ;ojOQ$&)PRP)9cG*#~2_(6IS{~FQ2^>}_?50*K6I8KrP5m8hop3>ut z2fm6UL6|W1$?q~nhrL|%UOq^@QBSXXO_mH8oVCJ%ihDSIKpRCJm?^6a!>2XxI4TAd zUO|cd!U{C)2^lDG9WrHL4aPEReYtEvabVj5DOdblhey<_NcKun^SWg($qKlJ9zc)zAI>S%pyBz; zFPqR=)ppJ{L{}Uy?)Q->>2U-4V}=#A8R&;IE&l05N^Kp!myRazyyQxRpjDk~vj1MRVq1W;kWvuGJt;Zr)c;_}CVfZU&3-l?Yf=~A+ z>5fm$(+F#g&fKD7!XO#wGQe?20R={}QX}Ks$Ubu6^x{uVT9B2-?;SxT)Js*274|UW_A! zr22g>32O@UD0K%;&V*PDIlarJh`@j-!k|pUuzBzqqI)})rw-R1=`n`9>B=kAwI+Xk zV+9csrmOCv2kR+cd;D(rq069YZq*~|n@_w(;>=Kp#>Yb~;G?~>XjUhlOG9^jJ<@8# zuEsTg&xpfQ2a{aeYB6s?Tx0~!@`!H$xj0l0uI7wpTGD?e`n&1zBMItu7ev>ooKjLA zUOp3j;H_5-uJGvAZaa9&0b0C_WV*n>N?{8w&@7Y(AC?oeu6vKEJKZd^M*d)u~rtqd~XM$qtUT zAWFcITYHFbdFo|*fp&{Bhamfqny~jXk!-Z1bgM8I=p0!q@n(u-G6y|=puHgjp#$xp zIPvzRI|aqucsK|&*j`)i%c#1^2ySZNac5$;#-F|r^MvX9(~N68l#!8DL|@jjh9qeEj7pktqj@N1#-+4hp4WM)|DS%TWDa^>eZ6Ntld*uaDsO7<5*3?A2^i&b)+ktp-&QbNmU|}Xjj#oQp zCAM_rT?Q=z9S;uaqIV_?aV2eLGr<+)3`hdyvBVs0I|Kx0Lq*8=uX;)_Z1uK9xC=`r zZ9Nkd0G!6&i-JJ9BQtIn%MeNx^fCI?I(?<~)`WQwlJhlxRbs$OHf+vQMYI21(4|1j z6X~KW=&_7e?^JM$()Kwvfo6+R-}-u7mE>$F=!rN(G20+2D%!`NA*u-?nZwHQV|a2? zylU5MW^rO#@Pd8PiWP5A;r99G$br48lsLks&+?|{MJ6&0o>!YmNu58c#n>h*pp-Fg z<>iATX3KUIvdogr6C}s3y39R$(bN9Cp=q=;a0QUUp!}C#RIrzp@?z{gKL} z1%bU+@D^|F4nW5bZ85~X4Wco%Z#L2g99(@BBFu5oV-Bz1~Sh{ZeVh)a?R}V&;Z!q zSy1;pl~3?{MlML{4AtI26C! zDPkkt`Yby`;+9*$ytAs4{SqtCWnd?=y)lgVW(dF63XFSGR5daV;p){p`uf*^>^%es zz)2x)FJ)@n*DfIte`p@Wt-4vEASQ?~fX>qFa_VH=&)DE8IZVIpT+o0hyQ8p7yQv%) zgwip9K?8sG1dh%F$#nLQz()qKe76Rp9TB{RFJkXo#wT1h8=;krDna4TFaw|q@jU5n zGg*fyKMRUd)Lxwy9eCzTh;v0Sy{)OTo&vFx>F@3#-^xxhyaO`dW9D9W$qU1uN?)q^t#E+706_2xl<-L%3rOH>bA8COYMi zEAJ<4QocD|rnU-)Zb>s%mM`jQ5)vQAj7jh*pSk$EslBx3+TO*-X95IaaXXXh%67tf z7RFB(Y&ec433n-9khwKgM5XzLyUPJ-aPYUshu((la`94YKQ(mIADn?)v!BFL&*!|& zyBTO)#9H{ZA;o9E%JWxlDi8Fco5iCi$;>>I2&!tSnW9U$V|Y2N$GJ^C+|kyCBk zyRDywclpv^>(X{1s%T>grYk_`+zat@4r7kBkpS_N?D()Ldc$1Wn(_vAy(^^ub?bR< zO}#CZR)2;~wmf2j9{xoJ(>^DB1tpXOOs3*23o?w)7$0mrd_6xT7vR(pppMYQCG(`i z5(aX#^85i)@m5A&^A7kj7Owut(q7-dNLziS5}`u1UlnYW2%pEMG;M-f%DSSzwS{&Y z7~Y=rX6srwayc|-&}3cl#9=lJ@w8M%WH>ba6e`U*HZpVO#rZjjulIiYxrR%s=E|@h z$OiJZ-i8u1?DgM5t0^l-;GJQ3>~QJDW)kGYg{pdJUMyyOYu@Wf8c;P?FvHiaE+l}wPKHLvkigWa5(h?w*ibuH<)Tm;4|0&7r$Jo zG8jM%gCB_*Og|BhBTlvCI0D`L)!hMT zgr)`4h0fPo%Nr=QXdS=+M>WFe7yyclL854xM68^6qJL+gJ&)DlOHWg(NuZ|xU&0*L z!#8-Vt3`n#K7-Tek`)qK*w(_MUVnK!Mgv`?1X{suz<6)CBQ513P`Qs(3ii+!7e?f2 zsaW8!aX9mUGdKAKDck)E*v6+$dz^$vRgn^JCd67CPt>MvLEF8UYwdUL!ZPTFpevdi ziIXTAB7iR|;NYECjz?f75p9wHqnSSdqq9433iNcOhpwd9-G~iV9C9-sSEh!G1rp5I zpI^~(V2CiXtqra*ceHB-juz3{wc4GKE7ybTZC&-Yl7{<2F?Y^~@*1 zDT}c5mqx0^w70XriVCBkOBshs(p%L9XCR!(PnMx>=G~o0B5f;*4Nn`Pe5t4v3p=|G zReYSop!L)>`7UMrGPzWObn_gtA2vQS-<#5wLH{tNU(*_^Bdq5sdLPUi{JyL;-Aj&v zHc6SMh3fOA*Kd0x@B8oP8QgHy>+cKwGR;>HE-)}~-s}59;gOL&ioLo}4ac0&ug@Y) zICN?kB^Rl#B5TMPjDod1@^x!d$o6Arfd&s9(ARQ3^}w@%U94?DbIOX@?O-aWE2B>( zv6!6m`IE6zROg+(j#2cBgb?$EI0K+ZPV85>2+n!?9S~H{UClT-SwM3jUG+OcrE`1i zq~8Vd6n-YU9)j0y_2!Yld#>iy7mw z2VU(j@~Eb&V1H3Ap^k~{1&J>AgU7*NR;X783d!Iv4!E&Z0^`jq1iU!wWe=X^8o~Hi z5CN1oB@8ZcC1ZBeyJZu!?I}83VZ=+kZVs%)Prj!b?lq{&xBr|+l;YfUoBIdUUo5)% zLF!h$i_>4ku>90|hAwv#>#DFY3t)Rt++CEi5(F&!B2<6GV5jM-`6_CIZhuXBs0H*1 zwt(o~osD%QR?S1g>TOA)#I@0OFl~k458QIN?TrAjpYgpeEH*9p<*U!)3#*@n(%>-@ z!;9bw+K?Fp(!foOSrA!lLP^hVY{{u0Ywnda=~PIuM6vd=f{rhMQk&-pCYo zAL9NKd$VF5_v*92yb;eHU%5kZ$_By7Dx(oS0d0b@-{81E5Y5?w*RZs>WpYbE?;!gP zahGi-H*!oiU5b8Z|LDjQS!@0ZqFBSeAj&k`zfq-p z>~z2uQf5S?y1Io13#ZYNbkmdudg=wdnP|rQ)tp4O{|Rinu>;4j-xg7^Yw&K3QkImhc(%?$xPGJ|%}y^B1mBM@9?h?fxK z!wl5;(Jr+j#NC&VCOTfOlMz7A+%}(5`Q9}&KvAs<9Z3+bOMxP4>tDS7F~H}c8iony z3cLh!hBnWXNIMfNz9A3F&jUdXaMpuMfezSqm+Y>SXc$7xmThM zajrteQTyu56Wqiljllr4V&;!ssESwOZ?`p|^Ef8hmh+bCIiImxil1&V_UCg2si+4Q z`AkA|U_WaKNkUzg4k@~lgy$a+rsN-YlTIH&M~SX6`39%V=mb5_3)Ckj7i>ch%a(lB z&tLGVHU3a=TOgyG%5@Y*u%~>h=ngQ&%wbx^^!Dk@+RXx_O{!gI)P)ruxNs7OV@`Gi zK7DZM{VUZDWap3b>;~n8+bjMY6g8r6k+~0$qsBD~c*f`F^#+T?fxP?xtdZ+b>fy9% zrzDXh5nug$=X;92wo{QyiuS$=ioSQ<+5vOPVybB*zjm?U@iHFR=KEPO%ojmbu*t1phoViL8TVtlk12Kce5XAQD^MDBjMI;{N zuYqvE=wM}4?e%mC_iW>qqKe4U1Zn#p#=#2&Af|1~{vo=5U%P?VpgewLtIICvxs62; zke7QJx}MHHcU{M*O^dYE>Ai?T4pcZDO7EsCxAf`dg~m45?XRFVA!?M=Kt4oM1+gNx z*Y}n6pe$DAvzp+7#qzt1~S5DsY(o6wt9Qz)NfIJro>q+kRVk$zz1N(Ex3tt|Q}RX3d-SmS3k$ zP6PSA0Y_wv_AwYK+q?EJAo4HS@Bhbpr`iDd!%rX)72IaTlUIKn_G6LYNGw!@x~nod71n>;Sfth>lvsJmTol zz4~tKXlsq~87$-s(dW#jy{xALJStp3dRum8JafBO| zf!Xop*tnUnoiAsyN|L6Lb^nP@WN;zsh9igt?Eq#VqV=IMoY8@8bV{$>7tC(1@j@Q$ zChuE0_05U%P)9zS8{GicBpaFUH<8s>!bF7RCk&DuN=Ns5GUBQUnwd z8z3S^K?u?!AR-`5dP{CWKxq+>A}t~yQX{<+=}iee^xkU{NC;`3i~D)sQ@$VHIX@i3 zkuiWPdtZC6x#pT{uBrM=O8fkL+IB;VEr3DFed;V6X|kc%1t?It;DqqvunTr(j7M~S ziU5QtP4s34nj%+tVS`4b?Tzbz-6;xy2>2oN0WqTmk92ckMCa2mouFLTWk&c44N(A? zDeO0o|2H)UQA65K?<-hd{mX`laA8V@*Yzs^j$Dxc{;&w3JM{Q`vyeWp-39<6T&h#0 zUfdTam@of3-Jw0+}{Cq zI3``1)o@@NWw)sgVMMGT|D74OKTnwlidgq@kRy#i)$*G(CS!dWJs!c*J&pd$W^=5P z^)Ve7`^VA|a3NxTw?ly6V~Zq|&c?4=RvwIV3?L-ZfplC;(^qB}W6F&G0OT~l-#+6H zJOuMnI@WiN<1gC@6EtlP0;D663*D@9fY@Ok#rllu6T1APsHkwmDZ9S*%f+Ch_L4C@ zeCJNaLcj7c2IHJNnB-aqTnN(1Y@`UG+iI0^A0cqq}mU6#lN z*x+lJ6eeBW`t6n3KXr{4^5+?-t%Sint8gwrk0^pTO8_U)qUb!!Hy7^Ni6Muf(hmmx zNq(25!&$N2KOH5yE)TS-h&?EBTzy4Gdg7W&Www=nuXUfUuFl$YuILL~u4J{cgldPu zmnWznpG9aMZrp27Q_?40`pY(b$&d=0@N5Eni}kYt#0W3Jf&)u*qlp4%6|Vji)3ups zGr?QW*!W-?hbCg7-tbU2u;a=3>*<#ge-&h|)G#JTX?X6%k}ksaOv5=pU}nHVz&0JA zsye0mvHy}K{r=%pS25Ww=s1d{E0z?faqnQ4uFB4qZ^Cn_fZ@ih(DZcT3c|lFpLGu4 z@P}yWrF40B;&zh>i@D8;`2+uEv_DCgjrq%V>d^n%mivx#3>_pPAIhpf;7$fzFI8AT zh*+l7MK}jy%-i0LSbXs=_o#>P1N3NQDVAgf_cEVo1oZZkgPrHDiR(&4yZ-q4G8Smv z|BZ@ZsY5@&c^P-+%HA}3dRdn*Lsp3Y#IPK`vQ#i(f}W2a+iDnCZmQJ>EPjJ~PIsuy z3vvn^@5R=uD^xp7FYcxO5DAi9fsT|m!%VTQT+_VAAU3_qdnYF&chY`f_nNwphb%!e zF2{jJMVAi)O$hb@(9;G9Uy{O^=j~{6TxTEuc<##AblF|Anf4|p@tN(QS;C+9(~h?aEAs>`-!f?bSlc2I|YK-1iCv{*%uD z|MEh5!$(ApucZpz0!95(3$Y|`QJVL_<{QRc!-gSh6 zqYP>=oE40-UW}hJ_+M1ylg*@Fm;L|zQeXhaJ8CU4r3rrALwZo9wrXkcm4Tec;3r=^ z_b%(4=m&EflD&H)y)B+5!guQ5uV>6&r zKqK?3h9S=s`|@LvhueA^8hLtLMU&{Wn%v+ac&a5Q@m2XeYV1gJx;#zgb&!fju+i_nm7j-J& zy$o>D<-JBFflnuq%bvOpE zvDY9Y61r`Pc5Mm8kuuaE=06mrYF2r7duJnNwLspbU^aI!p{`G#(O*qHeKm22Xc(!y z7+2Ye&H*%k3@45jmoOTC)P>t&hGoyCB9Yx!+Ph_tzg`-HLOfM=kQ=u!dj5nxQ^H2S zGplpxQOrr^$ntHVp|4^19yOVE7n`nBz}sEQIr4f_@Rpx5_6UWMf1WrZV-i*ui^afO zfZ#&H0j-MvaY~Zy0joKByZOAu{i4Y&X~NqQKW3UKGFLJX&aXgQ_ps?L;k$k$$mwF@ zc6*^qi+q)8wD zluqN^L?b`FDHH}????heqDrc2p0o{^wof9`z?%(rpPoA;CxqpWI?|NeMDIPSiYE2n zVJNmWCK5UtDN@*{46Zq*Qu`ElAHz1Y!b!#d!PZmVB}JV-!pO41YG_R|Fj4tB+!E3B zm+g!OQ^|~Kfpq_(yvG_Eh)01R6@KjXm&;ORR$y#ePL{5lT%LbXzXo_-Vo6@~(Ir=H ztBvahP5P@Uo@>}Y2%Vup0MjZWz60++TY&n%H@Qhaa>Md~(sPzw9>M*VT~Pq8@`r74 zf7z^e|HG8D?}ZIxzfuiZ_mhy&A#|i0h!G|0$NE4&OA%PsCIaCgCm+f@{E!yfV95lc z>%xgS;C6s|p=n3(gDTWL0G@TMVe=RqlEv|a#A%OD+Y^?^Ivg!t}&Q=6T z%8?$<8r@#gRoP#qLjzui&7o1&WqlHgD`U5Bj3FlC+{8iYw{ND-0MkV0s9e(dUtaYV zC2pb4$S^;%t}yPW3vZu!t~V=xG!5q#2%^30UOjho(Ve+^ zBU0p_N!cw0;4R<|kSBCEf1<*hj6rr~AGTvAE4wwq3St2_ws#xX*x1-)H>%phfBFPy zK03|o;3-uwHl(FV-gc6!SPKX{>AED-a7m!BFy?EO-*KEYBZyBWD5h&|H>HIA#i)Bikg9 zXOQ{aQqdR@$vFIZIW#Y?-^!s8%ZNa1dXBmPUPykeo- zIvwar3Tah1nrvejyXcb=LYRCBS;~=7$qdKVh zEbSo#z=DFqm>(0pThs0XMWj0bLf2`8yTX7n-l2O$9TPd%=@G>cG3@FYyM5UB%Md|p zHnPe-Myz&O5k84p2%P4SwRI!EQixW0-~nZK9BXY3iSE{~PW!lPk_22(j1tr>ZJlNK zOT-9LcsEU{nU|qVU}cL;Fr_6@Pt#d`Oyp2Pt(4Wu0zi7W==g!GByrb;mvNp=5C)c3m$@zt9{> z`?H~jvB8ARUlq?x8xE0EAj}5(R}N2>_0kZ09eK9O*@isCo-r@I9E>Y)_06FQ0B#r4E<9#_<>^=jM0mORoK18;TNoo2&XV z&ZSQT9V+P}8eoTKJZ?uM&U8S2DsO>}>*IJWIy485X98iZ-mwwlgbSM@xHMum zH*Es{In5^`-*vLBow;xnIS%ooxy%4!w=E4G!k%tl`C06xz${eM#>& zzI`Dk&NAR<5yIi$i=ily8s={(3wLN8~D@2<)>XcKa8>fS6r@p!?#p- zywUk!B!7JOwXPh_h`qQyreqCV@-+?gCAb%lWwoNuKxPG~G2Sx~qjyKBC1+k7B_#fo zRb;BR)`1K@$S%)GXv@kuiF|libFERE;~2C`oli45=f{d)d_sNZpG5~D+ZOMH*wZ)u z2(ugR{u_8-{~?_D^Hb#-YEvGRzRFx)RJXkG{FS0W2*t7?MP(0E@Y$IET>$*M54~{= z5?SW;rwH>p@p9HeTT4J+CH-OJXL@eo7|SX5ghAZm)bg$1)7DY zXdan?hs+ZbTal-zZ=LRLbzWWiGUl)Q$o)d}35k6H4{2v5_^V08pPInm+82q)xyXPQ zHZ74l3s1uLZ&6IGv0@t1TYJGkv|YmV2@wKr|1LIp)RlUN%G_ssW2+2qBf` z_JYNouVYN&(z(xKK-q{ifYB8M3y)Djlw<-lLb=v9_%Tte-0p2hFlXhJo4zArv+=9~ z$KF}zYb~!l@)oOuADhQ6#K+cuW8xj<%c0$daUazjqTx56hn_N(aWgQj9~*+>kcJx$ zfTHChME`)W03TwOAQ!5pkRw-d`zk(V7LH?(H^A59@7hHUDjs}Pr->9|h0kL4&(30- zP%Wj~`+~y&)(5vTLm>-1K)HlQD+Rn1Brren7pF&TSa-k*0P&FmIe<3dSb@3eeNh-0 z9t$xz7(Usz%QOKZz)skKj^$qmFN7On086s01B65H^Z%0=`OX^Ts+^yW&|%&vholu~ z*L6YaWNC09H;=ItW}IQfP;USd4{^@FY;%cAh!9pMmgK?$5L4wgQ$D@kt1f~9@CmVp z{tvbU5KsPZY>D9i$(BHfuRZ$K=Y(CDJeq;ra1F!+lJuZ#nl9jk-PLOKAmA9$*QV#S z)%O??vH%%^K6N^%79Er>1!ZR#5pW-~%d2bjZdDqGj;eO8OF)H>!spc9x=r&nV=mP@ zQl5vaH)v3GC5>!P`BB9oile`uu{e^M%j&6CiTHRej||zOHQYU7Y6Wk?%}D!NRkcxy zw<-FC77=4M*n{H^I7JQdG27Dhw0E-M0pPp-O*F%+3KfpvW@aO9*kcJ%X6*p}JLH3K zp)#V)_UdY@1&_)Ke7f*7`kAU`rwFX?9Bjtq(WEUo6MjNl#%)O5QXfolNdR=4TVa3^ zN$329RclAx)~)z&=s#Co7)krkCiqW|;Ek+kq^Qr%bNt~mro)}*n>fhE7wX<4Yt|fr zIw?C`nxz<{kxWGE)1`RyUh5fpm}@<-_T?5#BFhn2=!uk z26;y#@IwaldXCWN&)FU2PT6&3C6&vA6slr~Uy@*rr>{*9MYnd;(c6;>(qKFx*lo%Z zbM*hRy?2%-EdNt>qD|qF z`|}kgT0h@0@13!{3z_s(Av^KyC|6;Wnr0E%!Liv;|2q=nTMOILz{Z1guZsN|#*kAD zJf)|A7yPYp_`Wz-V*@)Ota*}G;k1u*bA{=RG@g#2;R_ilwd!jd{-wN7ceApm|IWlq zUD}#9_J11r;4-<#0tb|Ty~$>34-i2tVOGqy@-q|Wq(z4SwtQ8NVJI7vi^>puvkdwJa&&-jw&jS# zK#BbAJSUXmTnYpp;4-uXFg(QR4V5g1>yr+wv&5dts|8)=S1T1KPfUwk`7Beye7r_W z4`(5}eIgXQq;c=_yBBTk)(Q}~$WB)^s+#phWD2%}(M^4B^W*ijh=%+($MdnKQzJgH zJyUZh*oD-2XYX*VXd#keUDIL+k?&D-tpHVOM9y7fQKaS1vbr(wQ_pjG&(L>?Cz0cq z{P9C7yQ%KUhCTK(#bvA^RnM`9#*W+L9s66{zo4?|3LKD96}w3l57s8*AW!=TJR-XQ z86RhX87Gk=gJJ6~a7!2f67!FE&v~F~c_me(65#Ufi z4>w?#!PxnocI4J7n(hX_jBxUN1Mc+F@L?bwjfKQQj{VL`S1e`iVwKjBBRZ}wA}1Nu1JL;h6stVY3Vwst z+#nX){mXRunqN|aM<$@$3K8NJYuWvqV238Ik(_O_V2k}v$ohXC+?80tKl@&i_{R)=W|&0 zz-u|P3^=60w1tD*H*5v)nPJX~Fle&O>c+;xmw1F+Ct=7|JKM$YDo$6^m@b7XSivNE zD2!*`AM`YH)HM6Wf8%`I!{SbNv2Rk>+0`b0wpAa|m{}I9mtB+DQF;?`tz%JOewCzd zIhNz^lIP^9d9qTi@zt6^JLCKz_zw?=U zD<3pR!%=P3!1-jIuOj3$M=8obIGg8?LUz&}-&Xn4>>Gvt`T|Q3nq%G6%cK5fYnu|F z%Fd2f>=!a?3p;y8oUGs67eH<6#zil}hAXxZA^}#ssuY*D#ESQ>F*o8xQeZzj)iK#7 zH$C2t80H#Zi-+|LEK!Z-MdK!SG`)@Og5{pA+2q{xNF7q?&Tf#llOL;3lpzg?6;hB{t=6J20;mBXH)Aa=?Fd#cXclhE8wQ;6OS)-ZKyr@eF>Hq#N7e!Awx+sw>{ zKg7(V*&WcQ8Sp$GB@_5=nxebV&s8~{qy@3xSwhVh6lWsw+i_7Rn@`UZ4SzK3_EG<+ z5C5`0SCI;uLk;Wc)jHBmTnch@H@C!}TgRU^>I*e`w{`r&;Ly&2CXslYikbEIc}TOS zg}r}u9)7*kQMKEwviI_9fKZPQJOu9%OknqQzWSZ_JHhKOoAiz;x^~9RwcRS?V@??$ z=a~rrHofR<0o9!X2nS+|0mwa4%gy>c0mH>GXUUkaNauUN4d_j z4Y&fJ&PR}(HC!t-<}X`Qp$ohZWW#Dc!jb^Xfh!ZRH2L*a+CGvZV8slZ8{Dleha^L& zut7e6@%buuJbtyJFCvyf53_%hX}t7mTa8n(pW1_mqm0Q8P1@W?v#h;;lCciX|!`?pO~ zu1)_-1NAXf{<0woS=`KO&?(^J`HXF)DaGBX{$~?z5Wve!;b?(`)WE|fsflx%7d=&L zqz0gg-BEX;5&=7%x0J?KvW`Zk z3e710WfSzlP#zHNf%^PM)_EUqn35D--}Hi2Rq|DIZE8N7ChYYEGNyLD9qWKS^4zC; zhEnJ&nWwoIHb6J%x2S%}vvZlF=%jy%=3N>mH6TCoy1-iTFFTj9-t?#ddAOa==<1Nt zX6b2A&ihQc$42y;;vvt+lk&2K{^|Ze!}#>CJ#RgZh`O@{D?h8*%*Q(ti*suK;rRt* zVJ37~AE35@g=si`GxLdSO8!%n``w~w#o1~4ZN<&ti8DSIW`Y|JW3PLC;- zYNkH{H7EK*)ww|oZx@X>s*tDuGzroB=O)Kp?FVbs92zz&)Zl+y9p2~LdPmM-x+VuI zwE_({^EU!0`fW)cjiW>wrTKFdjUDmXs6N^bbj(0qg33298Of`iYBEpH7_3}5 zFWXaT;fH1hEF?ZA3qHw;IV+rhlFwG{0zK%mz#2_F1 zi3NrK3UkFAIsd)jnd(dVf|gyL3Z%@BfCpa!>6^s7ntoZ-gvKn`U!-(%SZaP4lg5keK`+eO#xMn1%z3v>C(SE zjwY1iO||eQLfc@;FU4f*d(XGE7FjN}#HR+W)f&AMpf}Y6KS2@dBcJ)>PPA!N?E1wZ zecGo5P2i8vxjSHUp~awAz1fiX7Cy>ifcJUgdAs2gQK-E((eNK8= z=FMH%(XAX}MN;+J;6VBnj(xTMgMrLi^@B$5nkfp+dde!^7u+H)5C?$f?zLlHLaLj4 zwjxj3^BGsFLeKs38X%v0o5@U?JcAGp33T&xbP9Bmk6$>Ud^S@{&bb<dN|-d5fq$p@?!t?LiEY>fY9bvH1&wQH-V*aqAl;4QeFW0Yf)B8)ci^^4BBnB znGxy>SBgnTwj7>Bl96nAfPPL>}-J zhI+ros}~QhI%mGdT)5W)yI!n&&fnQU+n)!1y|wOgiL{8e(Bo%Wn?=MxvIlj$1b1-Oby(Lw+yoeO*dix1GgPeiwZ*aL1_i*-NtCTiz{o^*3`z6l%oJ^o>f zxBj91x5|($=@DDtNEZ`g4w$a=T;j!20wlii z=0si>q%F{3wXwb7MsD#^U%wWlVB2N0%cmleP37f$)w9j$8p_rSOI!Vw&7zhR(^`3d zwANEgJ?Un2R0{(ua(bG6^gj3M6I}vkUK4kg;Z+;uzkk#33d=L7n=tA2gq{PG1>(sJ zokz+nDw3qrg)FywX9(O$0-l0SRy-pHJPKv2;H!>4HFjIBzH zekI<>hU6^K2<1r9@6yOakoLFtZlTRs%@7`Mvvmlkr*LX{?#+q>5pvWxKM%_GEe1t% zn2IN-AxwQZ+VIFz47YDSYrmC{FD*{kMfr8VX@1#BZTo|AZHT8u*!nvD7;XGv(i1ms z-cou`MkGIz?cacoUo!WN57fC%`^{eqgVTKj;FE(3!9pZcwd>a^G29!!9;c3$)zi{o?6Wo;lw4v}|Vu|7` zZ9ox$aRh)t%&s{g7Q3|imn{l_>eo4&i8?`bs`CmP&2y(T%qyah=W|}t!^>bi==gV{ z)fb|}NQGUpGr2=)f>#EdRE9vd2B|WwP2Xw)1pwyc*uS8j4KWHLStvj@1nn5fb(Wwy z*-`A9H|Xd@*4Te_AEjQytTYIt7~-?oU#RAtRb*2dlBl!+A9=B?NBn2E3Q(!{N@9Mj zFXBebkuCXcr`Fx={*~uG#m_O!0BR+V1r%y^b=Fc<4{A7I)DF-|3F{G?fq{s6KzAeS zEY)5^ckYl}%pXs_&u3b|zLEhm{%jdZ=sOcLFe^AWYZ!F)0&%*50Xf|mn z{_*|9p4+aPp8~Mk{Fv{vawxjLVie~`y4sYXW692r7aStK?31IKpr6oFf4)AmpMMJw zO#sEg8GyiDf_{VCggCsbq`V_K08EcHlJt<1v;x%L2LG}-w6KjVhr4H;Sfd zvw!&`KWus_ZeIu({01ODU>ws>9B|TV{%h_%CLQT~5fEgBLBZ)4DuBipC4BGD4t}!< zIoD_3&D=g?#?1)U^dR(9w+S;Gcdi5CqUUgi9M#GXR>10hi30Qk0L}s+Dv%UQKVbuz z0;&Ql?*Kq@Q(O;;PaUE+APVE>fSGf^pH4ui$E_y;(MJTSjQG%}#m5*%?EW`%7C@xG zMRr~oz~y|9$L>BN6S_Pw)mUexgJ*jT-}VU_K`scc&t?no(a~YW4UeYx7Wc(T;2U1R zl%oSh3Gg0Y2Z=Exqc?ql^>K%-#3Q^BU2A6Z%kS%>h`_?D=kIDaD74tV=}!iVmH)oV zWD#IJ|9zENPXYR{fsKCq;J{&onHY9pMPsnFlJ9_@?9d@&UShtqelq@Bv-$9V78H*s`218_7Tb z9|0$D;WL?etc3?)R|=p^|AR)8*0**6?c6N2gY=il1H2&nsmu+Gx0MZYfmzWbj4FLt zd1n8IaCbany~yucXQRlD@#Sx6Q%xoHO;xxf3}Zrji^3UdW;08~_mvB$d8@srxg`;=FGURJFjdC@KM){TW~bXr_xejqBnZJ9SG)vBcH z#$I(QJ?q;(Vh{6e2b>4YJ#$fojEiwSZkhtV?0heNM1{n$xz1yu*LarV<9Ut|1>~2b z`Hwm98sD(3z;^wdt5;0MFaTx-Z1QjC$TX4u-wKM)Iu5jU$|?9X71+g%^ANpcrcxCC zs-78Z@<4>KzkH&0X&3viHp3Knr>N@m%SKMnC*}Rh_tP}B|>nl5IskGf21dS+IO@1 z*2RTf7bnh>WwF(q;oz)yMnd;PxN2qcB4d5nV8ied{$CLbHNkUxk!6&3_wDHH{kF}Y zn%Or$?{=kXbE`?#=3-Vv=+~PU1XN9-wy%q}za4meGN4y}4fhl~eDk{QjoDtWy^bSBwA|w7}9{;~l?LI-6}5?ddFn!@fD(9L~4< zZckCfsSLF|z0?*`{ptw#m#1{E5Yq(E7dp$#RTDLG)eRP&?bl2(hDjDEWSGxe5r|N@ ziu%N6|2X{59%r=gTt%o}%E$9+-wd`k0O#sXX+o~-AMh>9X}FJ9^K4t#Ev-E264B$m z2SoWsl7hEtpOrBP2G0(d;^rpT5HtOy$bku)jQG*5IU*OAwo!6@g~e27z+}w58I`OC zaqvZI=Z}_dNF+vgpqU(@_x^Lm)72_=#LX@CdWXDKQ`>}y@20P5Zw?FV3GhxGZ@(XP zP6pM%Sq@9GZm9kc|Ey3ncJo{zA4&JpH8o^jZ`6&gJ(s%lLdN((a1k61$fUmw(AX*P zK!ytiVsWqL^#(_Eq@>kDaCA&Oj;UImq)+(%uZ{IC{g@z7Oi2&;Ij@ObfyZK+_ww_R8PM;8K|3SHgwIk4#X;Y_71hIbU z`3waHdI{B4HGP%VAfr|#FOALn%ciog)Q^1y|G+Uj-FE7#V+vYU509ODia4>)_KCH0 zZ^S9_HO|%>)91&Cx2ivW@>xt|bmkORmjwW2pf-?3sMR5gwsg=U-BRQqTv_$}w~0si zpowRJ8WpeBlWuz~bI*3Ifqw@Em+%YV`rhkYkQhQ0V8{6Y4jrbaLu(FHD^Sc%rn(h*?TJ48sxH1=T z*UCQt;)9=;_y@S;_l_AyOG{*B*17Pn13ZTms!vd#odvmA5uyw7Lw&)Hdr_&YJh` zQ!O`9D|iw_kR5#OfXV;@(ScGW5D=6LR#Thj-ynMtbb#GZL7--fs%XX#_pCl&=i0mvjBs# z8WfmPI>En+_9OqY0ib&KM>_U7SbjjebXMQ-fov7re(%Nzd0H4yEjZj&yyi&!&>j0w zFN0Q)sB8+6n3s&vT@U|ss;ha>IvZO_7t8Pk;1w9J*o2(9s0=Yf_P8Q) zqkM(T=tO1xum;=>UKzmM?RL-e=9D)#Wv;~mS^N&&grOWayVrVxoMLk0^^}YP(xK~G z@dT0bp`rGJ+Xkzx&Oz}IJ60cYjUnIHpg>C(w%BnOQ)Y&FGnr%+ z7n4GCx7{8YB0hgI-PifbTrOr(f4Ywh&x727*y?MkD7+IB@^m$wurRCIm36;-gw*Oc z6wpdPPW_@#$34f*rBR*uXs~s{G5r%xKmaJO&wTn(!6?xTYgEk2eLiZX|Zp~>%(vokyA~LrH^(;wdkA(&9(H`Qr#2Z zSyTs^ZX=E})KliLEon+kZBkA**!jn5qd&fW&znWLI&z0a)QoZ+yMfN9#+jA^Qy0rt zJw;(pKHYP2I==s<9{TNFtO4uE3lvo2-H74J=89o~l=Jl}nJ zzDiu)PO#|~V&bFRYo4Fy>j$*>{wlAFx}EJ-2Wt1xK(1tDsMebM2eEaP6gWlMCzJNY zlJkCK@Z8OdWMgML2NZj7cAIt`pvrIa&j@;cjX$^b1OU_6-D*%2(Xhe^ws?S*S5PCLkgY?5IOc62$?PM*#>qs`#{Ft)W?to< zhdLJMe4#8N?AxRFe`A|L9rOlnpmt#nPvK|X=`X*4JE zI3^hB&651ZW)+BNCBeT2lf1Wa5c;@Y8Sd}^Lz z^$Gev30uj~N!60gSkj%X%q|qrqgVjTM{pk+Q&@7sr7) z^^TiW{3^82mp}CB@gDn3G-)4wrFGvM?|QlbRu6?I!guyTAQ459>=tgMeU6(U3FktE zD7=#Y;q%a?_)NQGAxPeN`gU#RU$zP(LXw)A${M^B-U6o@W6yi6*VdwCb%%-B4(bq@ z{GX(3aEin@&ejf)@77-N{SH%Wv~=CeBuxekvAn<>Av=@oc-%X|DCJ3()*&G0C~k32ngy9*3i8{iV(m2K|nJn zlDsMU`32Vr8zb<7q6o{=V_xv#fR{x_YBrxwKAML$G7k|^_~M7Od!9>qku^`cRA+E~ z2Q~(TyQnN8B1bAekZ4NN`}jaXvL`9F_Qea^_YsWzQ^*%1FqT6Eh#my+mo#Rr%-lxA zk$*Bn;+?F{p=;&w9gG~M+0=2tsN4@BniAudyU5>_bj}W8U83L1^Ze&uvSbygLbQZB z*;({P4bZiajZJ8t!+bci#wFAxXB|Ew`ab5<)GO$3q>{xb)h_o36W__u}|k?`SdTV`vq(Fza{CcJ!0h`~Xj4KyT{OiI!Jq$|A)kaLVlH%x(6R2$`6Wi2VKbd1a8UW+&!HBIQU~R4J&H<4Erckj9OX zv$GKZF&)3buZrc#sSdwltR1c?r&D(%^<~#ck1Y9j9C-GzXrmtY`=Z4w0s2h`YRF%2 zVu1-}=rlV))iZWJ@uu|>v7_$Voq;s0(%QqzMM!f(TTZ@4>~o)i=L1~1MtW|C?(ZGS zl!inhBp+XoD@#ixk1!41y1~M$0SgS3w<_Gxma=t`i{QSKZ1mQnZs?&?zMo%g;d1H& zV=#!8+#d`oLZ+nmUm@gkYB|gG5p$%bRbRU&F2GP)iQPcli=H)~=L^rZFtprFp=J(ESFb`!_;$VFhdmr$zA2CTvsp_& z>q_G8zhjBB)FfajfwfdKdrzCp04V-ESM|}7)>DNg8Ln&-M?XrP=5W|$o_$!`UpOT@ z=|?y6cA9b;`JjlpEv_ZNkR@)Ad`zjdkx2Q+uOI3c3i++=S#b9&dWL7d&kk!DSab{5 zpH%rCVK>;1NkuvfRtDVaEjSm|@8}4Ij6RS&deG&#gFy@{cc&Q~3&)!^3<#Qee|ddL zZmRvawgAn#u5u3d3%EI&R&#R$bZRU0ufOEUwC{T$&@fRvW{6Ixm@UxeKlY`s)q1)6 zcx}C~|E)J@Gj`v5JQ2T!nrG*Ib)P#VS|E??%qDUW$ruA>5=Q(dl{kCi`wO1$)xqkl zEAqAg@NG}xq?<^AO2Djd`d|0T1UPGK|q2(J7ipY90? zb4>HmDs0*WPv1HL$T3uIO&(kG!Lc%pg>FB3@=1cEp_4Rao z78;1aAkYo-H+45=xvNQYhBUkmJajP9MpkPQX@SFEdF&pwECw6b*#{8Rlh@~-J|7!S z^v)a4GWOe~B&>|rb>HEU-w0^2LVX99&Bb{+w{(^j#pBqRRZEiu`t6^FJGzA@h^($V z7o-7(h`XmjN;U{lPU9vz&YaF6+IqAMvMx=SeNQO)Ft}P=^_Kp&-BB7M?;FDm&X9%> z%7lr1Isi7o;HicmNpH|Hq5M3?19d5TFZcXBHf?sCC1x2orQed4A-CE(6?{D`>v00i zr0q)steaHrIj=*ej!8=Bx!AF1aE9+cm_CPn_lL|dr`_=p7wqh7i;wb?FQYbh%Rdgi z>$vd6j6;(Mzq?{7I<%W9IH+Nt?=XJuidjtJ2FiA6Svb^<@Fe;WA~{^ttL zxA0A$=A3ccH#%We{H!3gC^^b`OKz{9Z)PN}aw!#Fjxv9kj=AG@82 zKPwlu8;LJ)G<}rZ`lB2L8&saJ^-g)Y79Cj9HG$v*pcW6rt{e5i_~+;}Lmyuk5%4*B z?Q+hO_Na-Yo$Y*^FZr%jy=jUT=LefKJ%nmgyjOgE%dYbg;$J_Bn)uc~B)<#M4F7}_ zA}fprt=)p45ZtlGhfK|?YWDnfRmmFj_PBRKOFWjTqJ62$ZFaqrz(Dkh#>{xeONF6uT3O<)tVim)Gd% zeNZ0V?=RcVDsyGiF##BIs?<1rQdwOEcYdvJ{8I43uPo)gQ=>MIv*^2`9YyYzRK2-P zha*RkA{y{l#~VqM6F8+VdhM#(@eXxm05Vp`aXf{0x#Gs{xEl@8tCC_aTi-&Dt3{oH@my}1m&|* zhO(C!qs0C5TB%QaY?5r|u`1Jw{;k>H>~dfo*vf(rcreHNYf~R{)kS|vjT`|WGrFq* zO_UBJ%X?Z|Kw4=Pq1K%cD>L^c5T$Npf^{Dj#n5BMZ?mKwyv75VA)LpHer?_T)Q=(Z#e=j~>fhue)pkR6j2dQQL_FjUaZ9D=MN0+N~(9K_*Hs z{LbOBNmk24S7djQSPopnmOciRRTic<@TbQ|ydY{#d~qIgi>01g;k z6yG^&t=8)&^Mj2$J1`yAG79JHi|tyyS-w!P2A$OPiZpbd+!{b6gH)nAcEm0w`$i7Q z=)JzPqq?-v58KSt8{Egtpf>EXeT@B)=K^50<#_Uk@NV{$PF*SB2kja6>#a)*Qa zpzIAP+20y&DgANtZT0@p22F+sk@Q{$g5e3XC0&;7Tw+F4ENl>H7TbviY9{~bEXSx5 z$`TOQsW}*Gxujl%%c)<ZQ2Cw3uWu?Q^gQ*zu{ z7vNHYw$$deAWdwK=GSx!pr=_lnoEc`(#mJM^~RkZlRF;a0<@T)L5`zM1)?&1BKf9o zjV;$pYGL?4_A8@vbGA|Wrsp$#v?l3Yn}ua0zwH{Gygs8{I;^-fv|q1tYfO^!Rcf#} zy_oGJi_f~wJF}N!J+52+0vL97Qg#+NY3{#zU&GHRV}(Y(^S4kiqW#h6*A>TW0d3CH z8g9BB+-}`Zaor1P-;}I9I{_dMIqC^%n(n*4V4?Aryi>&MBO4o8AS->$z9A> ztPLuhhkC1>UyN&L>!_dI$1XX!&5-bYA81Zej?0iiZ5T7i$(@c3cei2I3+ghZ&aGfy zjEKKzpG*A}{Hbqc)&%qB{H5ab_tWLCJ|~j8b^12@(1#KUTmA{wRv$C< zRrwixL!M^j1FKurZ&a5SfJ;T&`_JcZ-YJ-WmaOY!b;|u+WbybXsC&9&7N*7F?lrH- zxLxd846XG_mjfvRRfyH@8jDV|$saLaHBHNc8GFZQoNbiZ(iMMkPxkaokXFDcBA#MG zv}@+-MxUWx?b@~=#MoRob0uN@v1P5qogEFEvWV0}JBK3Cy)v?69X=8K&E&L4;9}%C zOB@6J%Gb9?C!}Z9|K?i1nXWT-#utJCkC?0tr&@L|zn&9I>1hu^~J!vJmo-=}rAS zP!d~TfJZ8|&uWOt%C<`NO8s>D0puh# ze-_pXIXtbKL(Q3Gl^=>Ey1Tr=-vF>(Ur^49tB$Me9*ckOjg(_>h)mqm!gF!@W0d|i z`ZNI|FefI~I&63*CRrm@til{4f`N(gt5ioC!6u#|3c*Ex@L*?$i2e9?V?#<{^0_Zut9S-;a}Xm?!Xn=hg&K^w(9CyJF>Ow zw!%BFryYIK{>{FdBCpOT@M&B2Uy-V|h%c*+ulC&Zr|8&Q1`<&-s34ZgYjHiIWNl-h zgJ2ocVEXV}A1-61!&Oz>HSohxzf*jqBy)>^Q?KW)k=#-}^_@}QTdY>CS8uA!W%JkA z{Ss0LMaUuAk>}*=poOck4(WW=F2z&RBjPIct@%9N3Q|{|%?4E(WF5W(%<>&57Z!y> z0T-b0q%HZYbI&vWm22Jsy~|yQR)3lq&({|!X!4S0Ma#~9O0<91=eLluR1HpfD!+^YPGbe{di5mr=cRy zaD!|y^*xR6O!$+x&gS$p*jeb9Th#I{CZ%|Ly2IdZ(C^zvV)X;;i9L{y$TA?^nkVQq zYx3Tig`eu_(<_(^4e!yz)JnV~?u0ZxIdfejJt~>bX>p@xCfJi79sEgBG7(%01!X@s zG7@;gw#&D<;ESW2@{Y{Zf0(DQ)&06M(`#+Bd`z!YBXV3e-A&gj4;q_-AejLU-&)h! z)1LTlFgpT=;Gs%pmzoq0lIc}VmTLo2qHXSrZPj3NU)C92wPPQDwdXz6@~xV17Qc>> zdo1H@m;kgB!rzXH0)w|to)VZP5TAyRDy$QMwl-udST5y%w0G}MNoP?2z;mV@%`!7H z#ALCNk}S7$ee95{Ij&|nLS>2KYg1A~jWkmc)Y`VpcWFs!sErw^m>G-M16(t)ErInB z_*i%4AwE$s4Fs|M?*Fhqet)?Cz?sk7JNKM(&+Q4`!B*FDp9Ombt$fVLyc<@tbV$w@9QR(No=Q@e}Q)X<&XB? z9kenVj*>+EF}7TZXQWp9I?1+iVCDVIaYu#;ld8lIlF{JA#I8HTc|Dp+4Lq{aet*$q z9aAk|voqj%Xq~mXM~>}=TgA?g(R#X6#gPWTWJDN>&UtwMQup1XS97dNB!j%&nyhui zTS83_ez7!-RPF}Dh&~E|M=25Zxwew&hZm4>pAW=6|6DT5hQ%ZFk!R>)uIPm{LEVXz zCin8Qa@-{`UpTT18;w>am)6{GTX&V;dX&kjfV$T_*d+6`Q2w+4wvyl^^j4z(&u(qEDC&(TNIitc@W799S z(4$_g&-jMr_9$yHuh4jlOLoRXTnRx%4+QM{ zwUMwnHJhGR0X-2~MwXk}7^*zOF^(VRhPpo$PnT6MXmOFPVGKn$D*H`=D+6bZ3BEVa z2%*28$$KhxD)q)DPcChp^KpCylmX(eRoh;^S(_5~1I_p(1*w-5CdD9)j1AZEzO*Mv zRdw6kX{UI~4a?tRf+%Zgo%T69TUI?fyI9-lOmAGCukM}9U5*Qc4eWw- zUB}7lM#EDq&aliiAtYPM(g1t0ZGFplyZ$cAJI1qSLsY*y7OI6bW0ry9q&fEG%s<7D=Ru?<+W=X3udd^yIyrK{i=o>dC+GjSwTAn;}3ecTxoPktnt4T%FaPI<3g`hRW z`*gf6f^f)8ANQKL++I^MD ztdT8kC&oa;dv`_`!L=e9^mkF zy#?l<;BnOrIuH%LLQPN101`8#@Z z8)UvC`F>&&g3HpCGW`+E82_wfo9+ZBM1{FsFZ}tLV;W6J7i>qqMsHNoV2X>bmz$ST zb4N)Mf~ar)QYNw43scQr(bDDy7GhbncPqL&wV1!Ws5rr#MR(ZMJ1O1j*UtM0J5yGK z(lKt}o2^pZ!s`|)Qqm}1Ar(U&NA)WHiUdCSEp)zq=k}u9VcHZ5^t9#m`Q!}2<<-)E#qH|aED)fU?3^6?mEx;REPmeS1=rggQu+=1gIA#d%l`+JL zhR61EfnDSSJ0ajJmEdT%j literal 0 HcmV?d00001 diff --git a/admin/web/src/assets/github.png b/admin/web/src/assets/github.png new file mode 100644 index 0000000000000000000000000000000000000000..d1d200ed1ad07d3cd7e17ffb7fc8cd23e8fe97d1 GIT binary patch literal 7793 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 literal 0 HcmV?d00001 diff --git a/admin/web/src/assets/icons/ai-gva.svg b/admin/web/src/assets/icons/ai-gva.svg new file mode 100644 index 000000000..fcbea9308 --- /dev/null +++ b/admin/web/src/assets/icons/ai-gva.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin/web/src/assets/icons/customer-gva.svg b/admin/web/src/assets/icons/customer-gva.svg new file mode 100644 index 000000000..1e7220196 --- /dev/null +++ b/admin/web/src/assets/icons/customer-gva.svg @@ -0,0 +1 @@ + diff --git a/admin/web/src/assets/kefu.png b/admin/web/src/assets/kefu.png new file mode 100644 index 0000000000000000000000000000000000000000..47cab15b6b39ed4b413db06f247712fb58945306 GIT binary patch literal 6770 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/web/src/assets/login_left.svg b/admin/web/src/assets/login_left.svg new file mode 100644 index 000000000..9c48b0b15 --- /dev/null +++ b/admin/web/src/assets/login_left.svg @@ -0,0 +1,123 @@ + + + 搭建网站 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/web/src/assets/login_right_banner.jpg b/admin/web/src/assets/login_right_banner.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0a597c155aeba0cd6124a73203815693210f2296 GIT binary patch literal 719028 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