From f6622a4e9891a14301e31673847388f8ebf2de2c Mon Sep 17 00:00:00 2001 From: Echo <1711788888@qq.com> Date: Wed, 9 Apr 2025 12:17:33 +0800 Subject: [PATCH] :sparkles: init project --- .gitignore | 4 +- Dockerfile | 31 + README.md | 54 +- api/v1/article/article.go | 90 + api/v1/article/enter.go | 7 + api/v1/bot/bot.go | 193 + api/v1/bot/enter.go | 7 + api/v1/category/category.go | 188 + api/v1/category/enter.go | 7 + api/v1/enter.go | 19 + api/v1/example/enter.go | 15 + api/v1/example/exa_attachment_category.go | 82 + api/v1/example/exa_breakpoint_continue.go | 150 + api/v1/example/exa_customer.go | 176 + api/v1/example/exa_file_upload_download.go | 135 + api/v1/system/auto_code_history.go | 115 + api/v1/system/auto_code_package.go | 100 + api/v1/system/auto_code_plugin.go | 119 + api/v1/system/auto_code_template.go | 121 + api/v1/system/enter.go | 47 + api/v1/system/sys_api.go | 323 + api/v1/system/sys_authority.go | 202 + api/v1/system/sys_authority_btn.go | 80 + api/v1/system/sys_auto_code.go | 155 + api/v1/system/sys_captcha.go | 70 + api/v1/system/sys_casbin.go | 69 + api/v1/system/sys_dictionary.go | 129 + api/v1/system/sys_dictionary_detail.go | 148 + api/v1/system/sys_export_template.go | 428 + api/v1/system/sys_initdb.go | 59 + api/v1/system/sys_jwt_blacklist.go | 33 + api/v1/system/sys_menu.go | 265 + api/v1/system/sys_operation_record.go | 149 + api/v1/system/sys_params.go | 171 + api/v1/system/sys_system.go | 88 + api/v1/system/sys_user.go | 483 + config.docker.yaml | 220 + config.yaml | 241 + config/auto_code.go | 22 + config/captcha.go | 9 + config/config.go | 37 + config/cors.go | 14 + config/db_list.go | 52 + config/disk.go | 9 + config/email.go | 11 + config/excel.go | 5 + config/gorm_mssql.go | 10 + config/gorm_mysql.go | 9 + config/gorm_oracle.go | 10 + config/gorm_pgsql.go | 17 + config/gorm_sqlite.go | 13 + config/jwt.go | 8 + config/mongo.go | 41 + config/oss_aliyun.go | 10 + config/oss_aws.go | 13 + config/oss_cloudflare.go | 10 + config/oss_huawei.go | 9 + config/oss_local.go | 6 + config/oss_minio.go | 11 + config/oss_qiniu.go | 11 + config/oss_tencent.go | 10 + config/redis.go | 10 + config/system.go | 14 + config/zap.go | 71 + core/internal/constant.go | 9 + core/internal/cutter.go | 121 + core/internal/zap_core.go | 68 + core/server.go | 41 + core/server_other.go | 19 + core/server_win.go | 21 + core/viper.go | 71 + core/zap.go | 32 + docs/docs.go | 9311 +++++++++++++++++ docs/swagger.json | 9286 ++++++++++++++++ docs/swagger.yaml | 5677 ++++++++++ global/global.go | 66 + global/model.go | 14 + go.mod | 177 + go.sum | 824 ++ initialize/db_list.go | 36 + initialize/ensure_tables.go | 113 + initialize/gorm.go | 78 + initialize/gorm_biz.go | 21 + initialize/gorm_mssql.go | 60 + initialize/gorm_mysql.go | 55 + initialize/gorm_oracle.go | 52 + initialize/gorm_pgsql.go | 50 + initialize/gorm_sqlite.go | 42 + initialize/internal/gorm.go | 46 + initialize/internal/gorm_logger_writer.go | 41 + initialize/internal/mongo.go | 29 + initialize/mongo.go | 155 + initialize/other.go | 32 + initialize/plugin.go | 15 + initialize/plugin_biz_v1.go | 34 + initialize/plugin_biz_v2.go | 16 + initialize/redis.go | 59 + initialize/register_init.go | 10 + initialize/router.go | 111 + initialize/router_biz.go | 28 + initialize/timer.go | 37 + initialize/validator.go | 22 + log/2025-03-28/error.log | 2 + log/2025-03-28/info.log | 25 + log/2025-04-03/error.log | 1 + log/2025-04-03/info.log | 4 + log/2025-04-09/error.log | 6 + log/2025-04-09/info.log | 46 + main.go | 45 + middleware/casbin_rbac.go | 36 + middleware/cors.go | 73 + middleware/email.go | 60 + middleware/error.go | 61 + middleware/jwt.go | 80 + middleware/limit_ip.go | 92 + middleware/loadtls.go | 27 + middleware/logger.go | 89 + middleware/operation.go | 133 + model/article/article.go | 19 + model/article/request/article.go | 11 + model/bot/bot.go | 18 + model/bot/request/bot.go | 16 + model/category/category.go | 20 + model/category/request/category.go | 14 + model/common/basetypes.go | 43 + model/common/clearDB.go | 7 + model/common/request/common.go | 48 + model/common/response/common.go | 8 + model/common/response/response.go | 63 + model/example/exa_attachment_category.go | 16 + model/example/exa_breakpoint_continue.go | 24 + model/example/exa_customer.go | 15 + model/example/exa_file_upload_download.go | 18 + .../request/exa_file_upload_and_downloads.go | 10 + .../response/exa_breakpoint_continue.go | 11 + model/example/response/exa_customer.go | 7 + .../response/exa_file_upload_download.go | 7 + model/system/Class.go | 12 + model/system/article.go | 17 + model/system/request/class.go | 7 + model/system/request/jwt.go | 21 + model/system/request/sys_api.go | 14 + model/system/request/sys_authority_btn.go | 7 + model/system/request/sys_auto_code.go | 286 + model/system/request/sys_auto_code_package.go | 31 + model/system/request/sys_auto_history.go | 57 + model/system/request/sys_casbin.go | 27 + model/system/request/sys_dictionary_detail.go | 11 + model/system/request/sys_export_template.go | 14 + model/system/request/sys_init.go | 124 + model/system/request/sys_menu.go | 27 + model/system/request/sys_operation_record.go | 11 + model/system/request/sys_params.go | 14 + model/system/request/sys_user.go | 65 + model/system/response/sys_api.go | 18 + model/system/response/sys_authority.go | 12 + model/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 + model/system/response/sys_menu.go | 15 + model/system/response/sys_system.go | 7 + model/system/response/sys_user.go | 15 + model/system/sys_api.go | 28 + model/system/sys_authority.go | 23 + model/system/sys_authority_btn.go | 8 + model/system/sys_authority_menu.go | 19 + model/system/sys_auto_code_history.go | 68 + model/system/sys_auto_code_package.go | 18 + model/system/sys_base_menu.go | 42 + model/system/sys_dictionary.go | 20 + model/system/sys_dictionary_detail.go | 21 + model/system/sys_export_template.go | 44 + model/system/sys_jwt_blacklist.go | 10 + model/system/sys_menu_btn.go | 10 + model/system/sys_operation_record.go | 24 + model/system/sys_params.go | 20 + model/system/sys_system.go | 10 + model/system/sys_user.go | 62 + model/system/sys_user_authority.go | 11 + plugin/announcement/api/enter.go | 10 + plugin/announcement/api/info.go | 183 + plugin/announcement/config/config.go | 4 + 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 + plugin/announcement/model/info.go | 20 + plugin/announcement/model/request/info.go | 12 + plugin/announcement/plugin.go | 26 + plugin/announcement/plugin/plugin.go | 5 + plugin/announcement/router/enter.go | 10 + plugin/announcement/router/info.go | 31 + plugin/announcement/service/enter.go | 5 + plugin/announcement/service/info.go | 78 + plugin/email/README.MD | 75 + plugin/email/api/enter.go | 7 + plugin/email/api/sys_email.go | 53 + plugin/email/config/email.go | 11 + plugin/email/global/gloabl.go | 5 + plugin/email/main.go | 28 + plugin/email/model/response/email.go | 7 + plugin/email/router/enter.go | 7 + plugin/email/router/sys_email.go | 19 + plugin/email/service/enter.go | 7 + plugin/email/service/sys_email.go | 32 + plugin/email/utils/email.go | 82 + plugin/plugin-tool/utils/check.go | 50 + resource/function/api.go.tpl | 44 + resource/function/api.js.tpl | 32 + resource/function/server.go.tpl | 25 + resource/package/readme.txt.tpl | 7 + resource/package/server/api/api.go.tpl | 260 + resource/package/server/api/enter.go.tpl | 4 + resource/package/server/model/model.go.tpl | 111 + .../server/model/request/request.go.tpl | 58 + resource/package/server/router/enter.go.tpl | 4 + resource/package/server/router/router.go.tpl | 42 + resource/package/server/service/enter.go.tpl | 4 + .../package/server/service/service.go.tpl | 251 + resource/package/web/api/api.js.tpl | 130 + resource/package/web/view/form.vue.tpl | 464 + resource/package/web/view/table.vue.tpl | 1363 +++ resource/plugin/server/api/api.go.template | 255 + resource/plugin/server/api/enter.go.template | 6 + .../plugin/server/config/config.go.template | 4 + resource/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 | 112 + .../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 | 259 + resource/plugin/web/api/api.js.template | 127 + resource/plugin/web/form/form.vue.template | 464 + resource/plugin/web/view/view.vue.template | 1361 +++ router/article/article.go | 28 + router/article/enter.go | 7 + router/bot/bot.go | 28 + router/bot/enter.go | 7 + router/category/category.go | 28 + router/category/enter.go | 7 + router/enter.go | 19 + router/example/enter.go | 17 + router/example/exa_attachment_category.go | 16 + router/example/exa_customer.go | 22 + .../example/exa_file_upload_and_download.go | 22 + router/system/enter.go | 44 + router/system/sys_api.go | 33 + router/system/sys_authority.go | 23 + router/system/sys_authority_btn.go | 19 + router/system/sys_auto_code.go | 40 + router/system/sys_auto_code_history.go | 17 + router/system/sys_base.go | 16 + router/system/sys_casbin.go | 19 + router/system/sys_dictionary.go | 22 + router/system/sys_dictionary_detail.go | 22 + router/system/sys_export_template.go | 34 + router/system/sys_initdb.go | 15 + router/system/sys_jwt.go | 14 + router/system/sys_menu.go | 27 + router/system/sys_operation_record.go | 19 + router/system/sys_params.go | 25 + router/system/sys_system.go | 22 + router/system/sys_user.go | 28 + service/article/article.go | 41 + service/article/enter.go | 3 + service/bot/bot.go | 74 + service/bot/enter.go | 3 + service/category/category.go | 85 + service/category/enter.go | 3 + service/enter.go | 19 + service/example/enter.go | 7 + service/example/exa_attachment_category.go | 66 + service/example/exa_breakpoint_continue.go | 71 + service/example/exa_customer.go | 87 + service/example/exa_file_upload_download.go | 123 + service/system/auto_code_history.go | 217 + service/system/auto_code_package.go | 680 ++ service/system/auto_code_package_test.go | 105 + service/system/auto_code_plugin.go | 249 + service/system/auto_code_template.go | 452 + service/system/auto_code_template_test.go | 84 + service/system/enter.go | 24 + service/system/jwt_black_list.go | 84 + service/system/sys_api.go | 326 + service/system/sys_authority.go | 330 + 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 + service/system/sys_base_menu.go | 146 + service/system/sys_casbin.go | 221 + service/system/sys_dictionary.go | 112 + service/system/sys_dictionary_detail.go | 118 + service/system/sys_export_template.go | 424 + service/system/sys_initdb.go | 189 + service/system/sys_initdb_mssql.go | 92 + service/system/sys_initdb_mysql.go | 97 + service/system/sys_initdb_pgsql.go | 101 + service/system/sys_initdb_sqlite.go | 88 + service/system/sys_menu.go | 289 + service/system/sys_operation_record.go | 88 + service/system/sys_params.go | 82 + service/system/sys_system.go | 62 + service/system/sys_user.go | 317 + source/example/file_upload_download.go | 65 + source/system/api.go | 206 + source/system/api_ignore.go | 77 + source/system/authorities_menus.go | 84 + source/system/authority.go | 88 + source/system/casbin.go | 292 + source/system/dictionary.go | 71 + source/system/dictionary_detail.go | 121 + source/system/excel_template.go | 75 + source/system/menu.go | 102 + source/system/user.go | 106 + task/clearTable.go | 51 + ...d5ea26224c629c055734360_20250409105756.png | Bin 0 -> 243308 bytes utils/ast/ast.go | 231 + utils/ast/ast_auto_enter.go | 47 + utils/ast/ast_enter.go | 181 + utils/ast/ast_gorm.go | 166 + utils/ast/ast_init_test.go | 11 + utils/ast/ast_rollback.go | 173 + utils/ast/ast_router.go | 135 + utils/ast/ast_test.go | 32 + utils/ast/ast_type.go | 53 + utils/ast/import.go | 94 + utils/ast/interfaces.go | 17 + utils/ast/interfaces_base.go | 76 + utils/ast/package_enter.go | 85 + 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 + utils/ast/package_initialize_router_test.go | 158 + utils/ast/package_module_enter.go | 180 + utils/ast/package_module_enter_test.go | 185 + utils/ast/plugin_enter.go | 167 + utils/ast/plugin_enter_test.go | 200 + utils/ast/plugin_gen.go | 189 + 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 + utils/ast/plugin_initialize_router_test.go | 155 + utils/ast/plugin_initialize_v2.go | 52 + utils/ast/plugin_initialize_v2_test.go | 100 + utils/breakpoint_continue.go | 112 + utils/captcha/redis.go | 60 + utils/claims.go | 148 + utils/directory.go | 124 + utils/fmt_plus.go | 108 + utils/hash.go | 31 + utils/human_duration.go | 29 + utils/human_duration_test.go | 49 + utils/json.go | 34 + utils/json_test.go | 53 + utils/jwt.go | 87 + utils/plugin/plugin.go | 18 + utils/plugin/v2/plugin.go | 11 + utils/reload.go | 18 + utils/request/http.go | 62 + utils/server.go | 126 + utils/timer/timed_task.go | 229 + utils/timer/timed_task_test.go | 72 + utils/upload/aliyun_oss.go | 75 + utils/upload/aws_s3.go | 97 + utils/upload/cloudflare_r2.go | 85 + utils/upload/local.go | 109 + utils/upload/minio_oss.go | 99 + utils/upload/obs.go | 69 + utils/upload/qiniu.go | 96 + utils/upload/tencent_cos.go | 61 + utils/upload/upload.go | 46 + utils/validator.go | 294 + utils/validator_test.go | 37 + utils/verify.go | 19 + utils/zip.go | 53 + 392 files changed, 55744 insertions(+), 3 deletions(-) create mode 100644 Dockerfile create mode 100644 api/v1/article/article.go create mode 100644 api/v1/article/enter.go create mode 100644 api/v1/bot/bot.go create mode 100644 api/v1/bot/enter.go create mode 100644 api/v1/category/category.go create mode 100644 api/v1/category/enter.go create mode 100644 api/v1/enter.go create mode 100644 api/v1/example/enter.go create mode 100644 api/v1/example/exa_attachment_category.go create mode 100644 api/v1/example/exa_breakpoint_continue.go create mode 100644 api/v1/example/exa_customer.go create mode 100644 api/v1/example/exa_file_upload_download.go create mode 100644 api/v1/system/auto_code_history.go create mode 100644 api/v1/system/auto_code_package.go create mode 100644 api/v1/system/auto_code_plugin.go create mode 100644 api/v1/system/auto_code_template.go create mode 100644 api/v1/system/enter.go create mode 100644 api/v1/system/sys_api.go create mode 100644 api/v1/system/sys_authority.go create mode 100644 api/v1/system/sys_authority_btn.go create mode 100644 api/v1/system/sys_auto_code.go create mode 100644 api/v1/system/sys_captcha.go create mode 100644 api/v1/system/sys_casbin.go create mode 100644 api/v1/system/sys_dictionary.go create mode 100644 api/v1/system/sys_dictionary_detail.go create mode 100644 api/v1/system/sys_export_template.go create mode 100644 api/v1/system/sys_initdb.go create mode 100644 api/v1/system/sys_jwt_blacklist.go create mode 100644 api/v1/system/sys_menu.go create mode 100644 api/v1/system/sys_operation_record.go create mode 100644 api/v1/system/sys_params.go create mode 100644 api/v1/system/sys_system.go create mode 100644 api/v1/system/sys_user.go create mode 100644 config.docker.yaml create mode 100644 config.yaml create mode 100644 config/auto_code.go create mode 100644 config/captcha.go create mode 100644 config/config.go create mode 100644 config/cors.go create mode 100644 config/db_list.go create mode 100644 config/disk.go create mode 100644 config/email.go create mode 100644 config/excel.go create mode 100644 config/gorm_mssql.go create mode 100644 config/gorm_mysql.go create mode 100644 config/gorm_oracle.go create mode 100644 config/gorm_pgsql.go create mode 100644 config/gorm_sqlite.go create mode 100644 config/jwt.go create mode 100644 config/mongo.go create mode 100644 config/oss_aliyun.go create mode 100644 config/oss_aws.go create mode 100644 config/oss_cloudflare.go create mode 100644 config/oss_huawei.go create mode 100644 config/oss_local.go create mode 100644 config/oss_minio.go create mode 100644 config/oss_qiniu.go create mode 100644 config/oss_tencent.go create mode 100644 config/redis.go create mode 100644 config/system.go create mode 100644 config/zap.go create mode 100644 core/internal/constant.go create mode 100644 core/internal/cutter.go create mode 100644 core/internal/zap_core.go create mode 100644 core/server.go create mode 100644 core/server_other.go create mode 100644 core/server_win.go create mode 100644 core/viper.go create mode 100644 core/zap.go create mode 100644 docs/docs.go create mode 100644 docs/swagger.json create mode 100644 docs/swagger.yaml create mode 100644 global/global.go create mode 100644 global/model.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 initialize/db_list.go create mode 100644 initialize/ensure_tables.go create mode 100644 initialize/gorm.go create mode 100644 initialize/gorm_biz.go create mode 100644 initialize/gorm_mssql.go create mode 100644 initialize/gorm_mysql.go create mode 100644 initialize/gorm_oracle.go create mode 100644 initialize/gorm_pgsql.go create mode 100644 initialize/gorm_sqlite.go create mode 100644 initialize/internal/gorm.go create mode 100644 initialize/internal/gorm_logger_writer.go create mode 100644 initialize/internal/mongo.go create mode 100644 initialize/mongo.go create mode 100644 initialize/other.go create mode 100644 initialize/plugin.go create mode 100644 initialize/plugin_biz_v1.go create mode 100644 initialize/plugin_biz_v2.go create mode 100644 initialize/redis.go create mode 100644 initialize/register_init.go create mode 100644 initialize/router.go create mode 100644 initialize/router_biz.go create mode 100644 initialize/timer.go create mode 100644 initialize/validator.go create mode 100644 log/2025-03-28/error.log create mode 100644 log/2025-03-28/info.log create mode 100644 log/2025-04-03/error.log create mode 100644 log/2025-04-03/info.log create mode 100644 log/2025-04-09/error.log create mode 100644 log/2025-04-09/info.log create mode 100644 main.go create mode 100644 middleware/casbin_rbac.go create mode 100644 middleware/cors.go create mode 100644 middleware/email.go create mode 100644 middleware/error.go create mode 100644 middleware/jwt.go create mode 100644 middleware/limit_ip.go create mode 100644 middleware/loadtls.go create mode 100644 middleware/logger.go create mode 100644 middleware/operation.go create mode 100644 model/article/article.go create mode 100644 model/article/request/article.go create mode 100644 model/bot/bot.go create mode 100644 model/bot/request/bot.go create mode 100644 model/category/category.go create mode 100644 model/category/request/category.go create mode 100644 model/common/basetypes.go create mode 100644 model/common/clearDB.go create mode 100644 model/common/request/common.go create mode 100644 model/common/response/common.go create mode 100644 model/common/response/response.go create mode 100644 model/example/exa_attachment_category.go create mode 100644 model/example/exa_breakpoint_continue.go create mode 100644 model/example/exa_customer.go create mode 100644 model/example/exa_file_upload_download.go create mode 100644 model/example/request/exa_file_upload_and_downloads.go create mode 100644 model/example/response/exa_breakpoint_continue.go create mode 100644 model/example/response/exa_customer.go create mode 100644 model/example/response/exa_file_upload_download.go create mode 100644 model/system/Class.go create mode 100644 model/system/article.go create mode 100644 model/system/request/class.go create mode 100644 model/system/request/jwt.go create mode 100644 model/system/request/sys_api.go create mode 100644 model/system/request/sys_authority_btn.go create mode 100644 model/system/request/sys_auto_code.go create mode 100644 model/system/request/sys_auto_code_package.go create mode 100644 model/system/request/sys_auto_history.go create mode 100644 model/system/request/sys_casbin.go create mode 100644 model/system/request/sys_dictionary_detail.go create mode 100644 model/system/request/sys_export_template.go create mode 100644 model/system/request/sys_init.go create mode 100644 model/system/request/sys_menu.go create mode 100644 model/system/request/sys_operation_record.go create mode 100644 model/system/request/sys_params.go create mode 100644 model/system/request/sys_user.go create mode 100644 model/system/response/sys_api.go create mode 100644 model/system/response/sys_authority.go create mode 100644 model/system/response/sys_authority_btn.go create mode 100644 model/system/response/sys_auto_code.go create mode 100644 model/system/response/sys_captcha.go create mode 100644 model/system/response/sys_casbin.go create mode 100644 model/system/response/sys_menu.go create mode 100644 model/system/response/sys_system.go create mode 100644 model/system/response/sys_user.go create mode 100644 model/system/sys_api.go create mode 100644 model/system/sys_authority.go create mode 100644 model/system/sys_authority_btn.go create mode 100644 model/system/sys_authority_menu.go create mode 100644 model/system/sys_auto_code_history.go create mode 100644 model/system/sys_auto_code_package.go create mode 100644 model/system/sys_base_menu.go create mode 100644 model/system/sys_dictionary.go create mode 100644 model/system/sys_dictionary_detail.go create mode 100644 model/system/sys_export_template.go create mode 100644 model/system/sys_jwt_blacklist.go create mode 100644 model/system/sys_menu_btn.go create mode 100644 model/system/sys_operation_record.go create mode 100644 model/system/sys_params.go create mode 100644 model/system/sys_system.go create mode 100644 model/system/sys_user.go create mode 100644 model/system/sys_user_authority.go create mode 100644 plugin/announcement/api/enter.go create mode 100644 plugin/announcement/api/info.go create mode 100644 plugin/announcement/config/config.go create mode 100644 plugin/announcement/gen/gen.go create mode 100644 plugin/announcement/initialize/api.go create mode 100644 plugin/announcement/initialize/gorm.go create mode 100644 plugin/announcement/initialize/menu.go create mode 100644 plugin/announcement/initialize/router.go create mode 100644 plugin/announcement/initialize/viper.go create mode 100644 plugin/announcement/model/info.go create mode 100644 plugin/announcement/model/request/info.go create mode 100644 plugin/announcement/plugin.go create mode 100644 plugin/announcement/plugin/plugin.go create mode 100644 plugin/announcement/router/enter.go create mode 100644 plugin/announcement/router/info.go create mode 100644 plugin/announcement/service/enter.go create mode 100644 plugin/announcement/service/info.go create mode 100644 plugin/email/README.MD create mode 100644 plugin/email/api/enter.go create mode 100644 plugin/email/api/sys_email.go create mode 100644 plugin/email/config/email.go create mode 100644 plugin/email/global/gloabl.go create mode 100644 plugin/email/main.go create mode 100644 plugin/email/model/response/email.go create mode 100644 plugin/email/router/enter.go create mode 100644 plugin/email/router/sys_email.go create mode 100644 plugin/email/service/enter.go create mode 100644 plugin/email/service/sys_email.go create mode 100644 plugin/email/utils/email.go create mode 100644 plugin/plugin-tool/utils/check.go create mode 100644 resource/function/api.go.tpl create mode 100644 resource/function/api.js.tpl create mode 100644 resource/function/server.go.tpl create mode 100644 resource/package/readme.txt.tpl create mode 100644 resource/package/server/api/api.go.tpl create mode 100644 resource/package/server/api/enter.go.tpl create mode 100644 resource/package/server/model/model.go.tpl create mode 100644 resource/package/server/model/request/request.go.tpl create mode 100644 resource/package/server/router/enter.go.tpl create mode 100644 resource/package/server/router/router.go.tpl create mode 100644 resource/package/server/service/enter.go.tpl create mode 100644 resource/package/server/service/service.go.tpl create mode 100644 resource/package/web/api/api.js.tpl create mode 100644 resource/package/web/view/form.vue.tpl create mode 100644 resource/package/web/view/table.vue.tpl create mode 100644 resource/plugin/server/api/api.go.template create mode 100644 resource/plugin/server/api/enter.go.template create mode 100644 resource/plugin/server/config/config.go.template create mode 100644 resource/plugin/server/gen/gen.go.template create mode 100644 resource/plugin/server/initialize/api.go.template create mode 100644 resource/plugin/server/initialize/gorm.go.template create mode 100644 resource/plugin/server/initialize/menu.go.template create mode 100644 resource/plugin/server/initialize/router.go.template create mode 100644 resource/plugin/server/initialize/viper.go.template create mode 100644 resource/plugin/server/model/model.go.template create mode 100644 resource/plugin/server/model/request/request.go.template create mode 100644 resource/plugin/server/plugin.go.template create mode 100644 resource/plugin/server/plugin/plugin.go.template create mode 100644 resource/plugin/server/router/enter.go.template create mode 100644 resource/plugin/server/router/router.go.template create mode 100644 resource/plugin/server/service/enter.go.template create mode 100644 resource/plugin/server/service/service.go.template create mode 100644 resource/plugin/web/api/api.js.template create mode 100644 resource/plugin/web/form/form.vue.template create mode 100644 resource/plugin/web/view/view.vue.template create mode 100644 router/article/article.go create mode 100644 router/article/enter.go create mode 100644 router/bot/bot.go create mode 100644 router/bot/enter.go create mode 100644 router/category/category.go create mode 100644 router/category/enter.go create mode 100644 router/enter.go create mode 100644 router/example/enter.go create mode 100644 router/example/exa_attachment_category.go create mode 100644 router/example/exa_customer.go create mode 100644 router/example/exa_file_upload_and_download.go create mode 100644 router/system/enter.go create mode 100644 router/system/sys_api.go create mode 100644 router/system/sys_authority.go create mode 100644 router/system/sys_authority_btn.go create mode 100644 router/system/sys_auto_code.go create mode 100644 router/system/sys_auto_code_history.go create mode 100644 router/system/sys_base.go create mode 100644 router/system/sys_casbin.go create mode 100644 router/system/sys_dictionary.go create mode 100644 router/system/sys_dictionary_detail.go create mode 100644 router/system/sys_export_template.go create mode 100644 router/system/sys_initdb.go create mode 100644 router/system/sys_jwt.go create mode 100644 router/system/sys_menu.go create mode 100644 router/system/sys_operation_record.go create mode 100644 router/system/sys_params.go create mode 100644 router/system/sys_system.go create mode 100644 router/system/sys_user.go create mode 100644 service/article/article.go create mode 100644 service/article/enter.go create mode 100644 service/bot/bot.go create mode 100644 service/bot/enter.go create mode 100644 service/category/category.go create mode 100644 service/category/enter.go create mode 100644 service/enter.go create mode 100644 service/example/enter.go create mode 100644 service/example/exa_attachment_category.go create mode 100644 service/example/exa_breakpoint_continue.go create mode 100644 service/example/exa_customer.go create mode 100644 service/example/exa_file_upload_download.go create mode 100644 service/system/auto_code_history.go create mode 100644 service/system/auto_code_package.go create mode 100644 service/system/auto_code_package_test.go create mode 100644 service/system/auto_code_plugin.go create mode 100644 service/system/auto_code_template.go create mode 100644 service/system/auto_code_template_test.go create mode 100644 service/system/enter.go create mode 100644 service/system/jwt_black_list.go create mode 100644 service/system/sys_api.go create mode 100644 service/system/sys_authority.go create mode 100644 service/system/sys_authority_btn.go create mode 100644 service/system/sys_auto_code_interface.go create mode 100644 service/system/sys_auto_code_mssql.go create mode 100644 service/system/sys_auto_code_mysql.go create mode 100644 service/system/sys_auto_code_oracle.go create mode 100644 service/system/sys_auto_code_pgsql.go create mode 100644 service/system/sys_auto_code_sqlite.go create mode 100644 service/system/sys_base_menu.go create mode 100644 service/system/sys_casbin.go create mode 100644 service/system/sys_dictionary.go create mode 100644 service/system/sys_dictionary_detail.go create mode 100644 service/system/sys_export_template.go create mode 100644 service/system/sys_initdb.go create mode 100644 service/system/sys_initdb_mssql.go create mode 100644 service/system/sys_initdb_mysql.go create mode 100644 service/system/sys_initdb_pgsql.go create mode 100644 service/system/sys_initdb_sqlite.go create mode 100644 service/system/sys_menu.go create mode 100644 service/system/sys_operation_record.go create mode 100644 service/system/sys_params.go create mode 100644 service/system/sys_system.go create mode 100644 service/system/sys_user.go create mode 100644 source/example/file_upload_download.go create mode 100644 source/system/api.go create mode 100644 source/system/api_ignore.go create mode 100644 source/system/authorities_menus.go create mode 100644 source/system/authority.go create mode 100644 source/system/casbin.go create mode 100644 source/system/dictionary.go create mode 100644 source/system/dictionary_detail.go create mode 100644 source/system/excel_template.go create mode 100644 source/system/menu.go create mode 100644 source/system/user.go create mode 100644 task/clearTable.go create mode 100644 uploads/file/9c97e3210d5ea26224c629c055734360_20250409105756.png create mode 100644 utils/ast/ast.go create mode 100644 utils/ast/ast_auto_enter.go create mode 100644 utils/ast/ast_enter.go create mode 100644 utils/ast/ast_gorm.go create mode 100644 utils/ast/ast_init_test.go create mode 100644 utils/ast/ast_rollback.go create mode 100644 utils/ast/ast_router.go create mode 100644 utils/ast/ast_test.go create mode 100644 utils/ast/ast_type.go create mode 100644 utils/ast/import.go create mode 100644 utils/ast/interfaces.go create mode 100644 utils/ast/interfaces_base.go create mode 100644 utils/ast/package_enter.go create mode 100644 utils/ast/package_enter_test.go create mode 100644 utils/ast/package_initialize_gorm.go create mode 100644 utils/ast/package_initialize_gorm_test.go create mode 100644 utils/ast/package_initialize_router.go create mode 100644 utils/ast/package_initialize_router_test.go create mode 100644 utils/ast/package_module_enter.go create mode 100644 utils/ast/package_module_enter_test.go create mode 100644 utils/ast/plugin_enter.go create mode 100644 utils/ast/plugin_enter_test.go create mode 100644 utils/ast/plugin_gen.go create mode 100644 utils/ast/plugin_gen_test.go create mode 100644 utils/ast/plugin_initialize_gorm.go create mode 100644 utils/ast/plugin_initialize_gorm_test.go create mode 100644 utils/ast/plugin_initialize_router.go create mode 100644 utils/ast/plugin_initialize_router_test.go create mode 100644 utils/ast/plugin_initialize_v2.go create mode 100644 utils/ast/plugin_initialize_v2_test.go create mode 100644 utils/breakpoint_continue.go create mode 100644 utils/captcha/redis.go create mode 100644 utils/claims.go create mode 100644 utils/directory.go create mode 100644 utils/fmt_plus.go create mode 100644 utils/hash.go create mode 100644 utils/human_duration.go create mode 100644 utils/human_duration_test.go create mode 100644 utils/json.go create mode 100644 utils/json_test.go create mode 100644 utils/jwt.go create mode 100644 utils/plugin/plugin.go create mode 100644 utils/plugin/v2/plugin.go create mode 100644 utils/reload.go create mode 100644 utils/request/http.go create mode 100644 utils/server.go create mode 100644 utils/timer/timed_task.go create mode 100644 utils/timer/timed_task_test.go create mode 100644 utils/upload/aliyun_oss.go create mode 100644 utils/upload/aws_s3.go create mode 100644 utils/upload/cloudflare_r2.go create mode 100644 utils/upload/local.go create mode 100644 utils/upload/minio_oss.go create mode 100644 utils/upload/obs.go create mode 100644 utils/upload/qiniu.go create mode 100644 utils/upload/tencent_cos.go create mode 100644 utils/upload/upload.go create mode 100644 utils/validator.go create mode 100644 utils/validator_test.go create mode 100644 utils/verify.go create mode 100644 utils/zip.go diff --git a/.gitignore b/.gitignore index adf8f72..2205864 100644 --- a/.gitignore +++ b/.gitignore @@ -16,8 +16,8 @@ *.out # Dependency directories (remove the comment below to include it) -# vendor/ - + vendor/ +.idea # Go workspace file go.work diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..815e2bb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +FROM golang:alpine as builder + +WORKDIR /go/src/git.echol.cn/loser/lckt +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 apk update && apk add --no-cache tzdata openntpd \ + && ln -sf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +WORKDIR /go/src/git.echol.cn/loser/lckt + +COPY --from=0 /go/src/git.echol.cn/loser/lckt/server ./ +COPY --from=0 /go/src/git.echol.cn/loser/lckt/resource ./resource/ +COPY --from=0 /go/src/git.echol.cn/loser/lckt/config.docker.yaml ./ + +# 挂载目录:如果使用了sqlite数据库,容器命令示例:docker run -d -v /宿主机路径/gva.db:/go/src/git.echol.cn/loser/lckt/gva.db -p 8888:8888 --name gva-server-v1 gva-server:1.0 +# VOLUME ["/go/src/git.echol.cn/loser/lckt"] + +EXPOSE 8888 +ENTRYPOINT ./server -c config.docker.yaml diff --git a/README.md b/README.md index cb0ecf1..9a34870 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,54 @@ -# lckt-server +## 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/api/v1/article/article.go b/api/v1/article/article.go new file mode 100644 index 0000000..529f851 --- /dev/null +++ b/api/v1/article/article.go @@ -0,0 +1,90 @@ +package article + +import ( + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/article" + "git.echol.cn/loser/lckt/model/article/request" + r "git.echol.cn/loser/lckt/model/common/response" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type ArticleApi struct{} + +func (ArticleApi) Create(ctx *gin.Context) { + var p article.Article + if err := ctx.ShouldBind(&p); err != nil { + r.FailWithMessage(err.Error(), ctx) + global.GVA_LOG.Error("参数有误!", zap.Error(err)) + return + } + err := articleService.CreateArticle(p) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + r.FailWithMessage("创建失败:"+err.Error(), ctx) + return + } + + r.OkWithMessage("创建成功", ctx) +} + +func (ArticleApi) Delete(ctx *gin.Context) { + var ids request.DeleteIds + if err := ctx.ShouldBind(&ids); err != nil { + r.FailWithMessage(err.Error(), ctx) + global.GVA_LOG.Error("参数有误!", zap.Error(err)) + return + } + + err := articleService.DeleteArticle(ids) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + r.FailWithMessage("删除失败:"+err.Error(), ctx) + return + } + r.OkWithMessage("删除成功", ctx) +} + +func (ArticleApi) Update(ctx *gin.Context) { + var p article.Article + if err := ctx.ShouldBind(&p); err != nil { + r.FailWithMessage(err.Error(), ctx) + global.GVA_LOG.Error("参数有误!", zap.Error(err)) + return + } + err := articleService.UpdateArticle(p) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + r.FailWithMessage("更新失败:"+err.Error(), ctx) + return + } + r.OkWithMessage("更新成功", ctx) +} + +func (ArticleApi) List(ctx *gin.Context) { + var p request.GetList + if err := ctx.ShouldBind(&p); err != nil { + r.FailWithMessage(err.Error(), ctx) + global.GVA_LOG.Error("参数有误!", zap.Error(err)) + return + } + list, total, err := articleService.GetArticleList(p) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + r.FailWithMessage("查询失败:"+err.Error(), ctx) + return + } + r.OkWithDetailed(gin.H{"list": list, "total": total}, "查询成功", ctx) +} + +// ById 根据ID获取文章 +func (ArticleApi) ById(ctx *gin.Context) { + id := ctx.Param("id") + article, err := articleService.GetArticle(id) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + r.FailWithMessage("查询失败:"+err.Error(), ctx) + return + } + r.OkWithData(article, ctx) +} diff --git a/api/v1/article/enter.go b/api/v1/article/enter.go new file mode 100644 index 0000000..a6a076f --- /dev/null +++ b/api/v1/article/enter.go @@ -0,0 +1,7 @@ +package article + +import "git.echol.cn/loser/lckt/service" + +type ApiGroup struct{ ArticleApi } + +var articleService = service.ServiceGroupApp.ArticleGroup.ArticleService diff --git a/api/v1/bot/bot.go b/api/v1/bot/bot.go new file mode 100644 index 0000000..e3bce02 --- /dev/null +++ b/api/v1/bot/bot.go @@ -0,0 +1,193 @@ +package bot + +import ( + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/bot" + botReq "git.echol.cn/loser/lckt/model/bot/request" + "git.echol.cn/loser/lckt/model/common/response" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type BotApi struct{} + +// CreateBot 创建机器人 +// @Tags Bot +// @Summary 创建机器人 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body bot.Bot true "创建机器人" +// @Success 200 {object} response.Response{msg=string} "创建成功" +// @Router /bt/createBot [post] +func (btApi *BotApi) CreateBot(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + var bt bot.Bot + err := c.ShouldBindJSON(&bt) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = btService.CreateBot(ctx, &bt) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败:"+err.Error(), c) + return + } + response.OkWithMessage("创建成功", c) +} + +// DeleteBot 删除机器人 +// @Tags Bot +// @Summary 删除机器人 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body bot.Bot true "删除机器人" +// @Success 200 {object} response.Response{msg=string} "删除成功" +// @Router /bt/deleteBot [delete] +func (btApi *BotApi) DeleteBot(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + ID := c.Query("ID") + err := btService.DeleteBot(ctx, ID) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败:"+err.Error(), c) + return + } + response.OkWithMessage("删除成功", c) +} + +// DeleteBotByIds 批量删除机器人 +// @Tags Bot +// @Summary 批量删除机器人 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "批量删除成功" +// @Router /bt/deleteBotByIds [delete] +func (btApi *BotApi) DeleteBotByIds(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + IDs := c.QueryArray("IDs[]") + err := btService.DeleteBotByIds(ctx, IDs) + if err != nil { + global.GVA_LOG.Error("批量删除失败!", zap.Error(err)) + response.FailWithMessage("批量删除失败:"+err.Error(), c) + return + } + response.OkWithMessage("批量删除成功", c) +} + +// UpdateBot 更新机器人 +// @Tags Bot +// @Summary 更新机器人 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body bot.Bot true "更新机器人" +// @Success 200 {object} response.Response{msg=string} "更新成功" +// @Router /bt/updateBot [put] +func (btApi *BotApi) UpdateBot(c *gin.Context) { + // 从ctx获取标准context进行业务行为 + ctx := c.Request.Context() + + var bt bot.Bot + err := c.ShouldBindJSON(&bt) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = btService.UpdateBot(ctx, bt) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败:"+err.Error(), c) + return + } + response.OkWithMessage("更新成功", c) +} + +// FindBot 用id查询机器人 +// @Tags Bot +// @Summary 用id查询机器人 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param ID query uint true "用id查询机器人" +// @Success 200 {object} response.Response{data=bot.Bot,msg=string} "查询成功" +// @Router /bt/findBot [get] +func (btApi *BotApi) FindBot(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + ID := c.Query("ID") + rebt, err := btService.GetBot(ctx, ID) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败:"+err.Error(), c) + return + } + response.OkWithData(rebt, c) +} + +// GetBotList 分页获取机器人列表 +// @Tags Bot +// @Summary 分页获取机器人列表 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data query botReq.BotSearch true "分页获取机器人列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /bt/getBotList [get] +func (btApi *BotApi) GetBotList(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + var pageInfo botReq.BotSearch + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := btService.GetBotInfoList(ctx, 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) +} + +// FindKey 不需要鉴权的机器人接口 +// @Tags Bot +// @Summary 不需要鉴权的机器人接口 +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=object,msg=string} "获取成功" +// @Router /bt/getBotPublic [get] +func (btApi *BotApi) FindKey(c *gin.Context) { + var req botReq.FindKey + if err := c.ShouldBind(&req); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + bt, err := btService.GetBotPublic(req) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败:"+err.Error(), c) + return + } + + response.OkWithDetailed(bt.Content, "获取成功", c) +} diff --git a/api/v1/bot/enter.go b/api/v1/bot/enter.go new file mode 100644 index 0000000..75e1c6b --- /dev/null +++ b/api/v1/bot/enter.go @@ -0,0 +1,7 @@ +package bot + +import "git.echol.cn/loser/lckt/service" + +type ApiGroup struct{ BotApi } + +var btService = service.ServiceGroupApp.BotServiceGroup.BotService diff --git a/api/v1/category/category.go b/api/v1/category/category.go new file mode 100644 index 0000000..22a04b4 --- /dev/null +++ b/api/v1/category/category.go @@ -0,0 +1,188 @@ +package category + +import ( + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/category" + categoryReq "git.echol.cn/loser/lckt/model/category/request" + "git.echol.cn/loser/lckt/model/common/response" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type CategoryApi struct{} + +// CreateCategory 创建类别 +// @Tags Category +// @Summary 创建类别 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body category.Category true "创建类别" +// @Success 200 {object} response.Response{msg=string} "创建成功" +// @Router /cat/createCategory [post] +func (catApi *CategoryApi) CreateCategory(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + var cat category.Category + err := c.ShouldBindJSON(&cat) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = catService.CreateCategory(ctx, &cat) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败:"+err.Error(), c) + return + } + response.OkWithMessage("创建成功", c) +} + +// DeleteCategory 删除类别 +// @Tags Category +// @Summary 删除类别 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body category.Category true "删除类别" +// @Success 200 {object} response.Response{msg=string} "删除成功" +// @Router /cat/deleteCategory [delete] +func (catApi *CategoryApi) DeleteCategory(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + ID := c.Query("ID") + err := catService.DeleteCategory(ctx, ID) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败:"+err.Error(), c) + return + } + response.OkWithMessage("删除成功", c) +} + +// DeleteCategoryByIds 批量删除类别 +// @Tags Category +// @Summary 批量删除类别 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "批量删除成功" +// @Router /cat/deleteCategoryByIds [delete] +func (catApi *CategoryApi) DeleteCategoryByIds(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + IDs := c.QueryArray("IDs[]") + err := catService.DeleteCategoryByIds(ctx, IDs) + if err != nil { + global.GVA_LOG.Error("批量删除失败!", zap.Error(err)) + response.FailWithMessage("批量删除失败:"+err.Error(), c) + return + } + response.OkWithMessage("批量删除成功", c) +} + +// UpdateCategory 更新类别 +// @Tags Category +// @Summary 更新类别 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body category.Category true "更新类别" +// @Success 200 {object} response.Response{msg=string} "更新成功" +// @Router /cat/updateCategory [put] +func (catApi *CategoryApi) UpdateCategory(c *gin.Context) { + // 从ctx获取标准context进行业务行为 + ctx := c.Request.Context() + + var cat category.Category + err := c.ShouldBindJSON(&cat) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = catService.UpdateCategory(ctx, cat) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败:"+err.Error(), c) + return + } + response.OkWithMessage("更新成功", c) +} + +// FindCategory 用id查询类别 +// @Tags Category +// @Summary 用id查询类别 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param ID query uint true "用id查询类别" +// @Success 200 {object} response.Response{data=category.Category,msg=string} "查询成功" +// @Router /cat/findCategory [get] +func (catApi *CategoryApi) FindCategory(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + ID := c.Query("ID") + recat, err := catService.GetCategory(ctx, ID) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败:"+err.Error(), c) + return + } + response.OkWithData(recat, c) +} + +// GetCategoryList 分页获取类别列表 +// @Tags Category +// @Summary 分页获取类别列表 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data query categoryReq.CategorySearch true "分页获取类别列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /cat/getCategoryList [get] +func (catApi *CategoryApi) GetCategoryList(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + var pageInfo categoryReq.CategorySearch + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := catService.GetCategoryInfoList(ctx, 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) +} + +// GetCategoryPublic 不需要鉴权的类别接口 +// @Tags Category +// @Summary 不需要鉴权的类别接口 +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=object,msg=string} "获取成功" +// @Router /cat/getCategoryPublic [get] +func (catApi *CategoryApi) GetCategoryPublic(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + // 此接口不需要鉴权 + // 示例为返回了一个固定的消息接口,一般本接口用于C端服务,需要自己实现业务逻辑 + catService.GetCategoryPublic(ctx) + response.OkWithDetailed(gin.H{ + "info": "不需要鉴权的类别接口信息", + }, "获取成功", c) +} diff --git a/api/v1/category/enter.go b/api/v1/category/enter.go new file mode 100644 index 0000000..b65d864 --- /dev/null +++ b/api/v1/category/enter.go @@ -0,0 +1,7 @@ +package category + +import "git.echol.cn/loser/lckt/service" + +type ApiGroup struct{ CategoryApi } + +var catService = service.ServiceGroupApp.CategoryServiceGroup.CategoryService diff --git a/api/v1/enter.go b/api/v1/enter.go new file mode 100644 index 0000000..d7bcab0 --- /dev/null +++ b/api/v1/enter.go @@ -0,0 +1,19 @@ +package v1 + +import ( + "git.echol.cn/loser/lckt/api/v1/article" + "git.echol.cn/loser/lckt/api/v1/bot" + "git.echol.cn/loser/lckt/api/v1/category" + "git.echol.cn/loser/lckt/api/v1/example" + "git.echol.cn/loser/lckt/api/v1/system" +) + +var ApiGroupApp = new(ApiGroup) + +type ApiGroup struct { + SystemApiGroup system.ApiGroup + ExampleApiGroup example.ApiGroup + CategoryApiGroup category.ApiGroup + BotApiGroup bot.ApiGroup + ArticleApiGroup article.ApiGroup +} diff --git a/api/v1/example/enter.go b/api/v1/example/enter.go new file mode 100644 index 0000000..0dc99cc --- /dev/null +++ b/api/v1/example/enter.go @@ -0,0 +1,15 @@ +package example + +import "git.echol.cn/loser/lckt/service" + +type ApiGroup struct { + CustomerApi + FileUploadAndDownloadApi + AttachmentCategoryApi +} + +var ( + customerService = service.ServiceGroupApp.ExampleServiceGroup.CustomerService + fileUploadAndDownloadService = service.ServiceGroupApp.ExampleServiceGroup.FileUploadAndDownloadService + attachmentCategoryService = service.ServiceGroupApp.ExampleServiceGroup.AttachmentCategoryService +) diff --git a/api/v1/example/exa_attachment_category.go b/api/v1/example/exa_attachment_category.go new file mode 100644 index 0000000..b19c423 --- /dev/null +++ b/api/v1/example/exa_attachment_category.go @@ -0,0 +1,82 @@ +package example + +import ( + "git.echol.cn/loser/lckt/global" + common "git.echol.cn/loser/lckt/model/common/request" + "git.echol.cn/loser/lckt/model/common/response" + "git.echol.cn/loser/lckt/model/example" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type AttachmentCategoryApi struct{} + +// GetCategoryList +// @Tags GetCategoryList +// @Summary 媒体库分类列表 +// @Security AttachmentCategory +// @Produce application/json +// @Success 200 {object} response.Response{data=example.ExaAttachmentCategory,msg=string} "媒体库分类列表" +// @Router /attachmentCategory/getCategoryList [get] +func (a *AttachmentCategoryApi) GetCategoryList(c *gin.Context) { + res, err := attachmentCategoryService.GetCategoryList() + if err != nil { + global.GVA_LOG.Error("获取分类列表失败!", zap.Error(err)) + response.FailWithMessage("获取分类列表失败", c) + return + } + response.OkWithData(res, c) +} + +// AddCategory +// @Tags AddCategory +// @Summary 添加媒体库分类 +// @Security AttachmentCategory +// @accept application/json +// @Produce application/json +// @Param data body example.ExaAttachmentCategory true "媒体库分类数据"// @Success 200 {object} response.Response{msg=string} "添加媒体库分类" +// @Router /attachmentCategory/addCategory [post] +func (a *AttachmentCategoryApi) AddCategory(c *gin.Context) { + var req example.ExaAttachmentCategory + if err := c.ShouldBindJSON(&req); err != nil { + global.GVA_LOG.Error("参数错误!", zap.Error(err)) + response.FailWithMessage("参数错误", c) + return + } + + if err := attachmentCategoryService.AddCategory(&req); err != nil { + global.GVA_LOG.Error("创建/更新失败!", zap.Error(err)) + response.FailWithMessage("创建/更新失败:"+err.Error(), c) + return + } + response.OkWithMessage("创建/更新成功", c) +} + +// DeleteCategory +// @Tags DeleteCategory +// @Summary 删除分类 +// @Security AttachmentCategory +// @accept application/json +// @Produce application/json +// @Param data body common.GetById true "分类id" +// @Success 200 {object} response.Response{msg=string} "删除分类" +// @Router /attachmentCategory/deleteCategory [post] +func (a *AttachmentCategoryApi) DeleteCategory(c *gin.Context) { + var req common.GetById + if err := c.ShouldBindJSON(&req); err != nil { + response.FailWithMessage("参数错误", c) + return + } + + if req.ID == 0 { + response.FailWithMessage("参数错误", c) + return + } + + if err := attachmentCategoryService.DeleteCategory(&req.ID); err != nil { + response.FailWithMessage("删除失败", c) + return + } + + response.OkWithMessage("删除成功", c) +} diff --git a/api/v1/example/exa_breakpoint_continue.go b/api/v1/example/exa_breakpoint_continue.go new file mode 100644 index 0000000..acd4517 --- /dev/null +++ b/api/v1/example/exa_breakpoint_continue.go @@ -0,0 +1,150 @@ +package example + +import ( + "fmt" + "io" + "mime/multipart" + "strconv" + + "git.echol.cn/loser/lckt/model/example" + + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/common/response" + exampleRes "git.echol.cn/loser/lckt/model/example/response" + "git.echol.cn/loser/lckt/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/api/v1/example/exa_customer.go b/api/v1/example/exa_customer.go new file mode 100644 index 0000000..5d0de5e --- /dev/null +++ b/api/v1/example/exa_customer.go @@ -0,0 +1,176 @@ +package example + +import ( + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/common/request" + "git.echol.cn/loser/lckt/model/common/response" + "git.echol.cn/loser/lckt/model/example" + exampleRes "git.echol.cn/loser/lckt/model/example/response" + "git.echol.cn/loser/lckt/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/api/v1/example/exa_file_upload_download.go b/api/v1/example/exa_file_upload_download.go new file mode 100644 index 0000000..dbced61 --- /dev/null +++ b/api/v1/example/exa_file_upload_download.go @@ -0,0 +1,135 @@ +package example + +import ( + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/common/response" + "git.echol.cn/loser/lckt/model/example" + "git.echol.cn/loser/lckt/model/example/request" + exampleRes "git.echol.cn/loser/lckt/model/example/response" + "github.com/gin-gonic/gin" + "go.uber.org/zap" + "strconv" +) + +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") + classId, _ := strconv.Atoi(c.DefaultPostForm("classId", "0")) + if err != nil { + global.GVA_LOG.Error("接收文件失败!", zap.Error(err)) + response.FailWithMessage("接收文件失败", c) + return + } + file, err = fileUploadAndDownloadService.UploadFile(header, noSave, classId) // 文件上传后拿到文件路径 + 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.ExaAttachmentCategorySearch true "页码, 每页大小, 分类id" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页文件列表,返回包括列表,总数,页码,每页数量" +// @Router /fileUploadAndDownload/getFileList [post] +func (b *FileUploadAndDownloadApi) GetFileList(c *gin.Context) { + var pageInfo request.ExaAttachmentCategorySearch + 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/api/v1/system/auto_code_history.go b/api/v1/system/auto_code_history.go new file mode 100644 index 0000000..b40faae --- /dev/null +++ b/api/v1/system/auto_code_history.go @@ -0,0 +1,115 @@ +package system + +import ( + "git.echol.cn/loser/lckt/global" + common "git.echol.cn/loser/lckt/model/common/request" + "git.echol.cn/loser/lckt/model/common/response" + request "git.echol.cn/loser/lckt/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/api/v1/system/auto_code_package.go b/api/v1/system/auto_code_package.go new file mode 100644 index 0000000..fe04144 --- /dev/null +++ b/api/v1/system/auto_code_package.go @@ -0,0 +1,100 @@ +package system + +import ( + "git.echol.cn/loser/lckt/global" + common "git.echol.cn/loser/lckt/model/common/request" + "git.echol.cn/loser/lckt/model/common/response" + "git.echol.cn/loser/lckt/model/system/request" + "git.echol.cn/loser/lckt/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/api/v1/system/auto_code_plugin.go b/api/v1/system/auto_code_plugin.go new file mode 100644 index 0000000..062580f --- /dev/null +++ b/api/v1/system/auto_code_plugin.go @@ -0,0 +1,119 @@ +package system + +import ( + "fmt" + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/common/response" + "git.echol.cn/loser/lckt/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) +} + +// InitMenu +// @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) +} + +// InitAPI +// @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/api/v1/system/auto_code_template.go b/api/v1/system/auto_code_template.go new file mode 100644 index 0000000..3c0292b --- /dev/null +++ b/api/v1/system/auto_code_template.go @@ -0,0 +1,121 @@ +package system + +import ( + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/common/response" + "git.echol.cn/loser/lckt/model/system/request" + "git.echol.cn/loser/lckt/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(err.Error(), zap.Error(err)) + response.FailWithMessage("预览失败:"+err.Error(), 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) + } +} + +// AddFunc +// @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/api/v1/system/enter.go b/api/v1/system/enter.go new file mode 100644 index 0000000..0906a2c --- /dev/null +++ b/api/v1/system/enter.go @@ -0,0 +1,47 @@ +package system + +import "git.echol.cn/loser/lckt/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 + 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/api/v1/system/sys_api.go b/api/v1/system/sys_api.go new file mode 100644 index 0000000..0da0c9c --- /dev/null +++ b/api/v1/system/sys_api.go @@ -0,0 +1,323 @@ +package system + +import ( + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/common/request" + "git.echol.cn/loser/lckt/model/common/response" + "git.echol.cn/loser/lckt/model/system" + systemReq "git.echol.cn/loser/lckt/model/system/request" + systemRes "git.echol.cn/loser/lckt/model/system/response" + "git.echol.cn/loser/lckt/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/api/v1/system/sys_authority.go b/api/v1/system/sys_authority.go new file mode 100644 index 0000000..e843902 --- /dev/null +++ b/api/v1/system/sys_authority.go @@ -0,0 +1,202 @@ +package system + +import ( + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/common/response" + "git.echol.cn/loser/lckt/model/system" + systemRes "git.echol.cn/loser/lckt/model/system/response" + "git.echol.cn/loser/lckt/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/api/v1/system/sys_authority_btn.go b/api/v1/system/sys_authority_btn.go new file mode 100644 index 0000000..0c5d618 --- /dev/null +++ b/api/v1/system/sys_authority_btn.go @@ -0,0 +1,80 @@ +package system + +import ( + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/common/response" + "git.echol.cn/loser/lckt/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/api/v1/system/sys_auto_code.go b/api/v1/system/sys_auto_code.go new file mode 100644 index 0000000..1f9d1e4 --- /dev/null +++ b/api/v1/system/sys_auto_code.go @@ -0,0 +1,155 @@ +package system + +import ( + "fmt" + "git.echol.cn/loser/lckt/model/common" + "github.com/goccy/go-json" + "io" + "strings" + + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/common/response" + "git.echol.cn/loser/lckt/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/api/v1/system/sys_captcha.go b/api/v1/system/sys_captcha.go new file mode 100644 index 0000000..79d3f35 --- /dev/null +++ b/api/v1/system/sys_captcha.go @@ -0,0 +1,70 @@ +package system + +import ( + "time" + + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/common/response" + systemRes "git.echol.cn/loser/lckt/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/api/v1/system/sys_casbin.go b/api/v1/system/sys_casbin.go new file mode 100644 index 0000000..b45e982 --- /dev/null +++ b/api/v1/system/sys_casbin.go @@ -0,0 +1,69 @@ +package system + +import ( + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/common/response" + "git.echol.cn/loser/lckt/model/system/request" + systemRes "git.echol.cn/loser/lckt/model/system/response" + "git.echol.cn/loser/lckt/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/api/v1/system/sys_dictionary.go b/api/v1/system/sys_dictionary.go new file mode 100644 index 0000000..8ef7006 --- /dev/null +++ b/api/v1/system/sys_dictionary.go @@ -0,0 +1,129 @@ +package system + +import ( + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/common/response" + "git.echol.cn/loser/lckt/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/api/v1/system/sys_dictionary_detail.go b/api/v1/system/sys_dictionary_detail.go new file mode 100644 index 0000000..53cad9b --- /dev/null +++ b/api/v1/system/sys_dictionary_detail.go @@ -0,0 +1,148 @@ +package system + +import ( + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/common/response" + "git.echol.cn/loser/lckt/model/system" + "git.echol.cn/loser/lckt/model/system/request" + "git.echol.cn/loser/lckt/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/api/v1/system/sys_export_template.go b/api/v1/system/sys_export_template.go new file mode 100644 index 0000000..00ef8e4 --- /dev/null +++ b/api/v1/system/sys_export_template.go @@ -0,0 +1,428 @@ +package system + +import ( + "fmt" + "net/http" + "net/url" + "sync" + "time" + + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/common/request" + "git.echol.cn/loser/lckt/model/common/response" + "git.echol.cn/loser/lckt/model/system" + systemReq "git.echol.cn/loser/lckt/model/system/request" + "git.echol.cn/loser/lckt/service" + "git.echol.cn/loser/lckt/utils" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +// 用于token一次性存储 +var ( + exportTokenCache = make(map[string]interface{}) + exportTokenExpiration = make(map[string]time.Time) + tokenMutex sync.RWMutex +) + +// 五分钟检测窗口过期 +func cleanupExpiredTokens() { + for { + time.Sleep(5 * time.Minute) + tokenMutex.Lock() + now := time.Now() + for token, expiry := range exportTokenExpiration { + if now.After(expiry) { + delete(exportTokenCache, token) + delete(exportTokenExpiration, token) + } + } + tokenMutex.Unlock() + } +} + +func init() { + go cleanupExpiredTokens() +} + +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 导出表格token +// @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") + if templateID == "" { + response.FailWithMessage("模板ID不能为空", c) + return + } + + queryParams := c.Request.URL.Query() + + //创造一次性token + token := utils.RandomString(32) // 随机32位 + + // 记录本次请求参数 + exportParams := map[string]interface{}{ + "templateID": templateID, + "queryParams": queryParams, + } + + // 参数保留记录完成鉴权 + tokenMutex.Lock() + exportTokenCache[token] = exportParams + exportTokenExpiration[token] = time.Now().Add(30 * time.Minute) + tokenMutex.Unlock() + + // 生成一次性链接 + exportUrl := fmt.Sprintf("/sysExportTemplate/exportExcelByToken?token=%s", token) + response.OkWithData(exportUrl, c) +} + +// ExportExcelByToken 导出表格 +// @Tags ExportExcelByToken +// @Summary 导出表格 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Router /sysExportTemplate/exportExcelByToken [get] +func (sysExportTemplateApi *SysExportTemplateApi) ExportExcelByToken(c *gin.Context) { + token := c.Query("token") + if token == "" { + response.FailWithMessage("导出token不能为空", c) + return + } + + // 获取token并且从缓存中剔除 + tokenMutex.RLock() + exportParamsRaw, exists := exportTokenCache[token] + expiry, _ := exportTokenExpiration[token] + tokenMutex.RUnlock() + + if !exists || time.Now().After(expiry) { + global.GVA_LOG.Error("导出token无效或已过期!") + response.FailWithMessage("导出token无效或已过期", c) + return + } + + // 从token获取参数 + exportParams, ok := exportParamsRaw.(map[string]interface{}) + if !ok { + global.GVA_LOG.Error("解析导出参数失败!") + response.FailWithMessage("解析导出参数失败", c) + return + } + + // 获取导出参数 + templateID := exportParams["templateID"].(string) + queryParams := exportParams["queryParams"].(url.Values) + + // 清理一次性token + tokenMutex.Lock() + delete(exportTokenCache, token) + delete(exportTokenExpiration, token) + tokenMutex.Unlock() + + // 导出 + 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/exportTemplate [get] +func (sysExportTemplateApi *SysExportTemplateApi) ExportTemplate(c *gin.Context) { + templateID := c.Query("templateID") + if templateID == "" { + response.FailWithMessage("模板ID不能为空", c) + return + } + + // 创造一次性token + token := utils.RandomString(32) // 随机32位 + + // 记录本次请求参数 + exportParams := map[string]interface{}{ + "templateID": templateID, + "isTemplate": true, + } + + // 参数保留记录完成鉴权 + tokenMutex.Lock() + exportTokenCache[token] = exportParams + exportTokenExpiration[token] = time.Now().Add(30 * time.Minute) + tokenMutex.Unlock() + + // 生成一次性链接 + exportUrl := fmt.Sprintf("/sysExportTemplate/exportTemplateByToken?token=%s", token) + response.OkWithData(exportUrl, c) +} + +// ExportTemplateByToken 通过token导出表格模板 +// @Tags ExportTemplateByToken +// @Summary 通过token导出表格模板 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Router /sysExportTemplate/exportTemplateByToken [get] +func (sysExportTemplateApi *SysExportTemplateApi) ExportTemplateByToken(c *gin.Context) { + token := c.Query("token") + if token == "" { + response.FailWithMessage("导出token不能为空", c) + return + } + + // 获取token并且从缓存中剔除 + tokenMutex.RLock() + exportParamsRaw, exists := exportTokenCache[token] + expiry, _ := exportTokenExpiration[token] + tokenMutex.RUnlock() + + if !exists || time.Now().After(expiry) { + global.GVA_LOG.Error("导出token无效或已过期!") + response.FailWithMessage("导出token无效或已过期", c) + return + } + + // 从token获取参数 + exportParams, ok := exportParamsRaw.(map[string]interface{}) + if !ok { + global.GVA_LOG.Error("解析导出参数失败!") + response.FailWithMessage("解析导出参数失败", c) + return + } + + // 检查是否为模板导出 + isTemplate, _ := exportParams["isTemplate"].(bool) + if !isTemplate { + global.GVA_LOG.Error("token类型错误!") + response.FailWithMessage("token类型错误", c) + return + } + + // 获取导出参数 + templateID := exportParams["templateID"].(string) + + // 清理一次性token + tokenMutex.Lock() + delete(exportTokenCache, token) + delete(exportTokenExpiration, token) + tokenMutex.Unlock() + + // 导出模板 + 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/api/v1/system/sys_initdb.go b/api/v1/system/sys_initdb.go new file mode 100644 index 0000000..65055ab --- /dev/null +++ b/api/v1/system/sys_initdb.go @@ -0,0 +1,59 @@ +package system + +import ( + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/common/response" + "git.echol.cn/loser/lckt/model/system/request" + "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 + } + 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/api/v1/system/sys_jwt_blacklist.go b/api/v1/system/sys_jwt_blacklist.go new file mode 100644 index 0000000..607dedd --- /dev/null +++ b/api/v1/system/sys_jwt_blacklist.go @@ -0,0 +1,33 @@ +package system + +import ( + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/common/response" + "git.echol.cn/loser/lckt/model/system" + "git.echol.cn/loser/lckt/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/api/v1/system/sys_menu.go b/api/v1/system/sys_menu.go new file mode 100644 index 0000000..e08c4d8 --- /dev/null +++ b/api/v1/system/sys_menu.go @@ -0,0 +1,265 @@ +package system + +import ( + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/common/request" + "git.echol.cn/loser/lckt/model/common/response" + "git.echol.cn/loser/lckt/model/system" + systemReq "git.echol.cn/loser/lckt/model/system/request" + systemRes "git.echol.cn/loser/lckt/model/system/response" + "git.echol.cn/loser/lckt/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/api/v1/system/sys_operation_record.go b/api/v1/system/sys_operation_record.go new file mode 100644 index 0000000..f90abab --- /dev/null +++ b/api/v1/system/sys_operation_record.go @@ -0,0 +1,149 @@ +package system + +import ( + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/common/request" + "git.echol.cn/loser/lckt/model/common/response" + "git.echol.cn/loser/lckt/model/system" + systemReq "git.echol.cn/loser/lckt/model/system/request" + "git.echol.cn/loser/lckt/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/api/v1/system/sys_params.go b/api/v1/system/sys_params.go new file mode 100644 index 0000000..23ea801 --- /dev/null +++ b/api/v1/system/sys_params.go @@ -0,0 +1,171 @@ +package system + +import ( + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/common/response" + "git.echol.cn/loser/lckt/model/system" + systemReq "git.echol.cn/loser/lckt/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/api/v1/system/sys_system.go b/api/v1/system/sys_system.go new file mode 100644 index 0000000..afb8e63 --- /dev/null +++ b/api/v1/system/sys_system.go @@ -0,0 +1,88 @@ +package system + +import ( + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/common/response" + "git.echol.cn/loser/lckt/model/system" + systemRes "git.echol.cn/loser/lckt/model/system/response" + "git.echol.cn/loser/lckt/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/api/v1/system/sys_user.go b/api/v1/system/sys_user.go new file mode 100644 index 0000000..a61b0d0 --- /dev/null +++ b/api/v1/system/sys_user.go @@ -0,0 +1,483 @@ +package system + +import ( + "strconv" + "time" + + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/common" + "git.echol.cn/loser/lckt/model/common/request" + "git.echol.cn/loser/lckt/model/common/response" + "git.echol.cn/loser/lckt/model/system" + systemReq "git.echol.cn/loser/lckt/model/system/request" + systemRes "git.echol.cn/loser/lckt/model/system/response" + "git.echol.cn/loser/lckt/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 + } + var authorities []system.SysAuthority + for _, v := range r.AuthorityIds { + authorities = append(authorities, system.SysAuthority{ + AuthorityId: v, + }) + } + 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) + 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) + 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) + claims.AuthorityId = sua.AuthorityId + token, err := utils.NewJWT().CreateToken(*claims) + if err != nil { + global.GVA_LOG.Error("修改失败!", zap.Error(err)) + response.FailWithMessage(err.Error(), c) + return + } + 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, + }) + 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) { + var user system.SysUser + err := c.ShouldBindJSON(&user) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = userService.ResetPassword(user.ID) + if err != nil { + global.GVA_LOG.Error("重置失败!", zap.Error(err)) + response.FailWithMessage("重置失败"+err.Error(), c) + return + } + response.OkWithMessage("重置成功", c) +} diff --git a/config.docker.yaml b/config.docker.yaml new file mode 100644 index 0000000..c1af7e0 --- /dev/null +++ b/config.docker.yaml @@ -0,0 +1,220 @@ +# git.echol.cn/loser/lckt Global Configuration + +# jwt configuration +jwt: + signing-key: qmPlus + expires-time: 7d + buffer-time: 1d + issuer: qmPlus +# zap logger configuration +zap: + level: info + format: console + prefix: "[git.echol.cn/loser/lckt]" + director: log + show-line: true + encode-level: LowercaseColorLevelEncoder + stacktrace-key: stacktrace + log-in-console: true + +# redis configuration +redis: + db: 0 + addr: 177.7.0.14:6379 + password: "" + +# mongo configuration +mongo: + coll: '' + options: '' + database: '' + username: '' + password: '' + min-pool-size: 0 + max-pool-size: 100 + socket-timeout-ms: 0 + connect-timeout-ms: 0 + is-zap: false + hosts: + - host: '' + port: '' + +# email configuration +email: + to: xxx@qq.com + port: 465 + from: xxx@163.com + host: smtp.163.com + is-ssl: true + secret: xxx + nickname: test + +# system configuration +system: + env: public # 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-redis: false # 使用redis + use-mongo: false # 使用mongo + use-multipoint: false + # IP限制次数 一个小时15000次 + iplimit-count: 15000 + # IP限制一个小时 + iplimit-time: 3600 + +# captcha configuration +captcha: + key-long: 6 + img-width: 240 + img-height: 80 + open-captcha: 0 # 0代表一直开启,大于0代表限制次数 + open-captcha-timeout: 3600 # open-captcha大于0时才生效 + +# mysql connect configuration +# 未初始化之前请勿手动修改数据库信息!!!如果一定要手动初始化请看(https://gin-vue-admin.com/docs/first_master) +mysql: + path: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + max-idle-conns: 10 + max-open-conns: 100 + log-mode: "" + log-zap: false + +# pgsql connect configuration +# 未初始化之前请勿手动修改数据库信息!!!如果一定要手动初始化请看(https://gin-vue-admin.com/docs/first_master) +pgsql: + path: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + max-idle-conns: 10 + max-open-conns: 100 + log-mode: "" + log-zap: false + +db-list: + - disable: true # 是否禁用 + type: "" # 数据库的类型,目前支持mysql、pgsql + alias-name: "" # 数据库的名称,注意: alias-name 需要在db-list中唯一 + path: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + max-idle-conns: 10 + max-open-conns: 100 + log-mode: "" + log-zap: false + + +# local configuration +local: + path: uploads/file + store-path: uploads/file + +# autocode configuration +autocode: + transfer-restart: true + # root 自动适配项目根目录 + # 请不要手动配置,他会在项目加载的时候识别出根路径 + root: "" + server: /server + server-plug: /plugin/%s + server-api: /api/v1/%s + server-initialize: /initialize + server-model: /model/%s + server-request: /model/%s/request/ + server-router: /router/%s + server-service: /service/%s + web: /web/src + web-api: /api + 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: your-secret-id + secret-key: your-secret-key + base-url: https://gin.vue.admin + path-prefix: git.echol.cn/loser/lckt + +# aws s3 configuration (minio compatible) +aws-s3: + bucket: xxxxx-10005608 + region: ap-shanghai + endpoint: "" + s3-force-path-style: false + disable-ssl: false + secret-id: your-secret-id + secret-key: your-secret-key + base-url: https://gin.vue.admin + path-prefix: git.echol.cn/loser/lckt + +# huawei obs configuration +hua-wei-obs: + path: you-path + bucket: you-bucket + endpoint: you-endpoint + access-key: you-access-key + secret-key: you-secret-key + +# 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: sys_operation_records + compareField: created_at + interval: 2160h + - tableName: jwt_blacklists + compareField: created_at + interval: 168h + +# 跨域配置 +# 需要配合 server/initialize/router.go -> `Router.Use(middleware.CorsByRules())` 使用 +cors: + mode: whitelist # 放行模式: allow-all, 放行全部; whitelist, 白名单模式, 来自白名单内域名的请求添加 cors 头; strict-whitelist 严格白名单模式, 白名单外的请求一律拒绝 + whitelist: + - allow-origin: example1.com + allow-headers: content-type + allow-methods: GET, POST + expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type + allow-credentials: true # 布尔值 + - allow-origin: example2.com + allow-headers: content-type + allow-methods: GET, POST + expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type + allow-credentials: true # 布尔值 \ No newline at end of file diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..d8bf972 --- /dev/null +++ b/config.yaml @@ -0,0 +1,241 @@ +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: C:\Users\Administrator\Desktop\lckt + server: server + module: git.echol.cn/loser/lckt + ai-path: "https://ai.gin-vue-admin.com/{FUNC}/loser7659/c178e970-6a59-497d-96ed-86fee6b3285a" +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: git.echol.cn/loser/lckt + s3-force-path-style: false + disable-ssl: false +captcha: + key-long: 4 + 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/ +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: f5a4f443-779c-4734-a2f0-c033dc1f12a2 + expires-time: 7d + buffer-time: 1d + issuer: qmPlus +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 +mongo: + coll: "" + options: "" + database: "" + username: "" + password: "" + auth-source: "" + min-pool-size: 0 + max-pool-size: 100 + socket-timeout-ms: 0 + connect-timeout-ms: 0 + is-zap: false + hosts: + - host: "" + port: "" +mssql: + prefix: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + path: "" + engine: "" + log-mode: "" + max-idle-conns: 10 + max-open-conns: 100 + singular: false + log-zap: false +mysql: + prefix: "" + port: "3307" + config: charset=utf8mb4&parseTime=True&loc=Local + db-name: lckt + username: lckt + password: loser765911 + path: 47.120.70.120 + engine: "" + log-mode: error + max-idle-conns: 10 + max-open-conns: 100 + singular: false + log-zap: false +oracle: + prefix: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + path: "" + engine: "" + log-mode: "" + max-idle-conns: 10 + max-open-conns: 100 + singular: false + log-zap: false +pgsql: + prefix: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + path: "" + engine: "" + log-mode: "" + 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: 127.0.0.1:6379 + password: "" + db: 0 + useCluster: false + clusterAddrs: + - 172.21.0.3:7000 + - 172.21.0.4:7001 + - 172.21.0.2:7002 +redis-list: + - name: cache + addr: 127.0.0.1:6379 + password: "" + db: 0 + useCluster: false + clusterAddrs: + - 172.21.0.3:7000 + - 172.21.0.4:7001 + - 172.21.0.2:7002 +sqlite: + prefix: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + path: "" + engine: "" + log-mode: "" + max-idle-conns: 10 + max-open-conns: 100 + singular: false + log-zap: false +system: + db-type: mysql + oss-type: local + router-prefix: "" + addr: 8888 + iplimit-count: 15000 + iplimit-time: 3600 + use-multipoint: false + use-redis: false + use-mongo: false + use-strict-auth: false +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: git.echol.cn/loser/lckt +zap: + level: info + prefix: '[git.echol.cn/loser/lckt]' + format: console + director: log + encode-level: LowercaseColorLevelEncoder + stacktrace-key: stacktrace + show-line: true + log-in-console: true + retention-day: -1 diff --git a/config/auto_code.go b/config/auto_code.go new file mode 100644 index 0000000..ade79a0 --- /dev/null +++ b/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/config/captcha.go b/config/captcha.go new file mode 100644 index 0000000..074a9bf --- /dev/null +++ b/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/config/config.go b/config/config.go new file mode 100644 index 0000000..15da9c7 --- /dev/null +++ b/config/config.go @@ -0,0 +1,37 @@ +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"` + 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"` +} diff --git a/config/cors.go b/config/cors.go new file mode 100644 index 0000000..7fba993 --- /dev/null +++ b/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/config/db_list.go b/config/db_list.go new file mode 100644 index 0000000..39767f5 --- /dev/null +++ b/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/config/disk.go b/config/disk.go new file mode 100644 index 0000000..59a6332 --- /dev/null +++ b/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/config/email.go b/config/email.go new file mode 100644 index 0000000..0984616 --- /dev/null +++ b/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/config/excel.go b/config/excel.go new file mode 100644 index 0000000..13caab7 --- /dev/null +++ b/config/excel.go @@ -0,0 +1,5 @@ +package config + +type Excel struct { + Dir string `mapstructure:"dir" json:"dir" yaml:"dir"` +} diff --git a/config/gorm_mssql.go b/config/gorm_mssql.go new file mode 100644 index 0000000..d187119 --- /dev/null +++ b/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/config/gorm_mysql.go b/config/gorm_mysql.go new file mode 100644 index 0000000..77e0245 --- /dev/null +++ b/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/config/gorm_oracle.go b/config/gorm_oracle.go new file mode 100644 index 0000000..1bbeb46 --- /dev/null +++ b/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/config/gorm_pgsql.go b/config/gorm_pgsql.go new file mode 100644 index 0000000..29fe03f --- /dev/null +++ b/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/config/gorm_sqlite.go b/config/gorm_sqlite.go new file mode 100644 index 0000000..46f2e19 --- /dev/null +++ b/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/config/jwt.go b/config/jwt.go new file mode 100644 index 0000000..c95d30d --- /dev/null +++ b/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/config/mongo.go b/config/mongo.go new file mode 100644 index 0000000..2034a3f --- /dev/null +++ b/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/config/oss_aliyun.go b/config/oss_aliyun.go new file mode 100644 index 0000000..934bd78 --- /dev/null +++ b/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/config/oss_aws.go b/config/oss_aws.go new file mode 100644 index 0000000..7ec6acc --- /dev/null +++ b/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/config/oss_cloudflare.go b/config/oss_cloudflare.go new file mode 100644 index 0000000..ab7a393 --- /dev/null +++ b/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/config/oss_huawei.go b/config/oss_huawei.go new file mode 100644 index 0000000..45dfbcd --- /dev/null +++ b/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/config/oss_local.go b/config/oss_local.go new file mode 100644 index 0000000..7038d4a --- /dev/null +++ b/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/config/oss_minio.go b/config/oss_minio.go new file mode 100644 index 0000000..a0faac7 --- /dev/null +++ b/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/config/oss_qiniu.go b/config/oss_qiniu.go new file mode 100644 index 0000000..298fe2d --- /dev/null +++ b/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/config/oss_tencent.go b/config/oss_tencent.go new file mode 100644 index 0000000..39a29d1 --- /dev/null +++ b/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/config/redis.go b/config/redis.go new file mode 100644 index 0000000..94b5bf6 --- /dev/null +++ b/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/config/system.go b/config/system.go new file mode 100644 index 0000000..b47dbf6 --- /dev/null +++ b/config/system.go @@ -0,0 +1,14 @@ +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"` // 使用树形角色分配模式 +} diff --git a/config/zap.go b/config/zap.go new file mode 100644 index 0000000..0e8ae2b --- /dev/null +++ b/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/core/internal/constant.go b/core/internal/constant.go new file mode 100644 index 0000000..b22362c --- /dev/null +++ b/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/core/internal/cutter.go b/core/internal/cutter.go new file mode 100644 index 0000000..e053af6 --- /dev/null +++ b/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/core/internal/zap_core.go b/core/internal/zap_core.go new file mode 100644 index 0000000..cfdaf4b --- /dev/null +++ b/core/internal/zap_core.go @@ -0,0 +1,68 @@ +package internal + +import ( + "git.echol.cn/loser/lckt/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/core/server.go b/core/server.go new file mode 100644 index 0000000..d693a3b --- /dev/null +++ b/core/server.go @@ -0,0 +1,41 @@ +package core + +import ( + "fmt" + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/initialize" + "git.echol.cn/loser/lckt/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() + } + + 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() + + 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/core/server_other.go b/core/server_other.go new file mode 100644 index 0000000..83645fc --- /dev/null +++ b/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/core/server_win.go b/core/server_win.go new file mode 100644 index 0000000..20cf44b --- /dev/null +++ b/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/core/viper.go b/core/viper.go new file mode 100644 index 0000000..d8ef508 --- /dev/null +++ b/core/viper.go @@ -0,0 +1,71 @@ +package core + +import ( + "flag" + "fmt" + "git.echol.cn/loser/lckt/core/internal" + "github.com/gin-gonic/gin" + "os" + "path/filepath" + + "github.com/fsnotify/fsnotify" + "github.com/spf13/viper" + + "git.echol.cn/loser/lckt/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/core/zap.go b/core/zap.go new file mode 100644 index 0000000..82b7356 --- /dev/null +++ b/core/zap.go @@ -0,0 +1,32 @@ +package core + +import ( + "fmt" + "git.echol.cn/loser/lckt/core/internal" + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/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/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..9edb572 --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,9311 @@ +// 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": { + "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/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" + } + } + } + ] + } + } + } + } + }, + "/attachmentCategory/addCategory": { + "post": { + "security": [ + { + "AttachmentCategory": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AddCategory" + ], + "summary": "添加媒体库分类", + "parameters": [ + { + "description": "媒体库分类数据", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/example.ExaAttachmentCategory" + } + } + ], + "responses": {} + } + }, + "/attachmentCategory/deleteCategory": { + "post": { + "security": [ + { + "AttachmentCategory": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "DeleteCategory" + ], + "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" + } + } + } + ] + } + } + } + } + }, + "/attachmentCategory/getCategoryList": { + "get": { + "security": [ + { + "AttachmentCategory": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "GetCategoryList" + ], + "summary": "媒体库分类列表", + "responses": { + "200": { + "description": "媒体库分类列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/example.ExaAttachmentCategory" + }, + "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": { + "put": { + "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/getDB": { + "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/initAPI": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCodePlugin" + ], + "summary": "打包插件", + "responses": { + "200": { + "description": "打包插件成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/initMenu": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCodePlugin" + ], + "summary": "打包插件", + "responses": { + "200": { + "description": "打包插件成功", + "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": { + "post": { + "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": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaFileUploadAndDownload" + ], + "summary": "查找文件", + "parameters": [ + { + "type": "file", + "description": "Find the file, 查找文件", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "查找文件,返回包括文件详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.FileResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + }, + "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": "页码, 每页大小, 分类id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.ExaAttachmentCategorySearch" + } + } + ], + "responses": { + "200": { + "description": "分页文件列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/fileUploadAndDownload/importURL": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaFileUploadAndDownload" + ], + "summary": "导入URL", + "parameters": [ + { + "description": "对象", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/example.ExaFileUploadAndDownload" + } + } + ], + "responses": { + "200": { + "description": "导入URL", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "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/ExportTemplate": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysExportTemplate" + ], + "summary": "导出表格模板", + "responses": {} + } + }, + "/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" + } + } + } + ] + } + } + } + } + }, + "/sysParams/createSysParams": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysParams" + ], + "summary": "创建参数", + "parameters": [ + { + "description": "创建参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysParams" + } + } + ], + "responses": { + "200": { + "description": "创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysParams/deleteSysParams": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysParams" + ], + "summary": "删除参数", + "parameters": [ + { + "description": "删除参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysParams" + } + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysParams/deleteSysParamsByIds": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysParams" + ], + "summary": "批量删除参数", + "responses": { + "200": { + "description": "批量删除成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysParams/findSysParams": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysParams" + ], + "summary": "用id查询参数", + "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": "key", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "参数名称", + "name": "name", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + }, + { + "type": "string", + "description": "参数值", + "name": "value", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "查询成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/system.SysParams" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysParams/getSysParam": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysParams" + ], + "summary": "根据key获取参数value", + "parameters": [ + { + "type": "string", + "description": "key", + "name": "key", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/system.SysParams" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysParams/getSysParamsList": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysParams" + ], + "summary": "分页获取参数列表", + "parameters": [ + { + "type": "string", + "name": "endCreatedAt", + "in": "query" + }, + { + "type": "string", + "name": "key", + "in": "query" + }, + { + "type": "string", + "description": "关键字", + "name": "keyword", + "in": "query" + }, + { + "type": "string", + "name": "name", + "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" + } + } + } + ] + } + } + } + } + }, + "/sysParams/updateSysParams": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysParams" + ], + "summary": "更新参数", + "parameters": [ + { + "description": "更新参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysParams" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "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/SetSelfSetting": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "设置用户配置", + "parameters": [ + { + "description": "用户配置数据", + "name": "data", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": true + } + } + ], + "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.GetUserList" + } + } + ], + "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": { + "common.JSONMap": { + "type": "object", + "additionalProperties": true + }, + "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.Minio": { + "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" + }, + "use-ssl": { + "type": "boolean" + } + } + }, + "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" + }, + "name": { + "description": "代表当前实例的名字", + "type": "string" + }, + "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" + } + ] + }, + "minio": { + "$ref": "#/definitions/config.Minio" + }, + "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" + }, + "redis-list": { + "type": "array", + "items": { + "$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" + }, + "use-strict-auth": { + "description": "使用树形角色分配模式", + "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.ExaAttachmentCategory": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "children": { + "type": "array", + "items": { + "$ref": "#/definitions/example.ExaAttachmentCategory" + } + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "name": { + "type": "string" + }, + "pid": { + "type": "integer" + }, + "updatedAt": { + "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" + }, + "classId": { + "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", + "properties": { + "abbreviation": { + "description": "Struct简称", + "type": "string", + "example": "Struct简称" + }, + "autoCreateApiToSql": { + "description": "是否自动创建api", + "type": "boolean", + "example": false + }, + "autoCreateBtnAuth": { + "description": "是否自动创建按钮权限", + "type": "boolean", + "example": false + }, + "autoCreateMenuToSql": { + "description": "是否自动创建menu", + "type": "boolean", + "example": false + }, + "autoCreateResource": { + "description": "是否自动创建资源标识", + "type": "boolean", + "example": false + }, + "autoMigrate": { + "description": "是否自动迁移表结构", + "type": "boolean", + "example": false + }, + "businessDB": { + "description": "业务数据库", + "type": "string", + "example": "业务数据库" + }, + "description": { + "description": "Struct中文名称", + "type": "string", + "example": "Struct中文名称" + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/definitions/request.AutoCodeField" + } + }, + "generateServer": { + "description": "是否生成server", + "type": "boolean", + "example": true + }, + "generateWeb": { + "description": "是否生成web", + "type": "boolean", + "example": true + }, + "gvaModel": { + "description": "是否使用gva默认Model", + "type": "boolean", + "example": false + }, + "humpPackageName": { + "description": "go文件名称", + "type": "string", + "example": "go文件名称" + }, + "isAdd": { + "description": "是否新增", + "type": "boolean", + "example": false + }, + "isTree": { + "description": "是否树形结构", + "type": "boolean", + "example": false + }, + "onlyTemplate": { + "description": "是否只生成模板", + "type": "boolean", + "example": false + }, + "package": { + "type": "string" + }, + "packageName": { + "description": "文件名称", + "type": "string", + "example": "文件名称" + }, + "primaryField": { + "$ref": "#/definitions/request.AutoCodeField" + }, + "structName": { + "description": "Struct名称", + "type": "string", + "example": "Struct名称" + }, + "tableName": { + "description": "表名", + "type": "string", + "example": "表名" + }, + "treeJson": { + "description": "展示的树json字段", + "type": "string", + "example": "展示的树json字段" + } + } + }, + "request.AutoCodeField": { + "type": "object", + "properties": { + "checkDataSource": { + "description": "是否检查数据源", + "type": "boolean" + }, + "clearable": { + "description": "是否可清空", + "type": "boolean" + }, + "columnName": { + "description": "数据库字段", + "type": "string" + }, + "comment": { + "description": "数据库字段描述", + "type": "string" + }, + "dataSource": { + "description": "数据源", + "allOf": [ + { + "$ref": "#/definitions/request.DataSource" + } + ] + }, + "dataTypeLong": { + "description": "数据库字段长度", + "type": "string" + }, + "defaultValue": { + "description": "是否必填", + "type": "string" + }, + "desc": { + "description": "是否前端详情", + "type": "boolean" + }, + "dictType": { + "description": "字典", + "type": "string" + }, + "errorText": { + "description": "校验失败文字", + "type": "string" + }, + "excel": { + "description": "是否导入/导出", + "type": "boolean" + }, + "fieldDesc": { + "description": "中文名", + "type": "string" + }, + "fieldIndexType": { + "description": "索引类型", + "type": "string" + }, + "fieldJson": { + "description": "FieldJson", + "type": "string" + }, + "fieldName": { + "description": "Field名", + "type": "string" + }, + "fieldSearchHide": { + "description": "是否隐藏查询条件", + "type": "boolean" + }, + "fieldSearchType": { + "description": "搜索条件", + "type": "string" + }, + "fieldType": { + "description": "Field数据类型", + "type": "string" + }, + "form": { + "description": "Front bool ` + "`" + `json:\"front\"` + "`" + ` // 是否前端可见", + "type": "boolean" + }, + "primaryKey": { + "description": "是否主键", + "type": "boolean" + }, + "require": { + "description": "是否必填", + "type": "boolean" + }, + "sort": { + "description": "是否增加排序", + "type": "boolean" + }, + "table": { + "description": "是否前端表格列", + "type": "boolean" + } + } + }, + "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.DataSource": { + "type": "object", + "properties": { + "association": { + "description": "关联关系 1 一对一 2 一对多", + "type": "integer" + }, + "dbName": { + "type": "string" + }, + "hasDeletedAt": { + "type": "boolean" + }, + "label": { + "type": "string" + }, + "table": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "request.Empty": { + "type": "object" + }, + "request.ExaAttachmentCategorySearch": { + "type": "object", + "properties": { + "classId": { + "type": "integer" + }, + "keyword": { + "description": "关键字", + "type": "string" + }, + "page": { + "description": "页码", + "type": "integer" + }, + "pageSize": { + "description": "每页大小", + "type": "integer" + } + } + }, + "request.GetAuthorityId": { + "type": "object", + "properties": { + "authorityId": { + "description": "角色ID", + "type": "integer" + } + } + }, + "request.GetById": { + "type": "object", + "properties": { + "id": { + "description": "主键ID", + "type": "integer" + } + } + }, + "request.GetUserList": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "keyword": { + "description": "关键字", + "type": "string" + }, + "nickName": { + "type": "string" + }, + "page": { + "description": "页码", + "type": "integer" + }, + "pageSize": { + "description": "每页大小", + "type": "integer" + }, + "phone": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "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" + }, + "template": { + "description": "postgresql指定template", + "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.SysParams": { + "type": "object", + "required": [ + "key", + "name", + "value" + ], + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "desc": { + "description": "参数说明", + "type": "string" + }, + "key": { + "description": "参数键", + "type": "string" + }, + "name": { + "description": "参数名称", + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + }, + "value": { + "description": "参数值", + "type": "string" + } + } + }, + "system.SysUser": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "authorities": { + "description": "多用户角色", + "type": "array", + "items": { + "$ref": "#/definitions/system.SysAuthority" + } + }, + "authority": { + "description": "用户角色", + "allOf": [ + { + "$ref": "#/definitions/system.SysAuthority" + } + ] + }, + "authorityId": { + "description": "用户角色ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "email": { + "description": "用户邮箱", + "type": "string" + }, + "enable": { + "description": "用户是否被冻结 1正常 2冻结", + "type": "integer" + }, + "headerImg": { + "description": "用户头像", + "type": "string" + }, + "nickName": { + "description": "用户昵称", + "type": "string" + }, + "originSetting": { + "description": "配置", + "allOf": [ + { + "$ref": "#/definitions/common.JSONMap" + } + ] + }, + "phone": { + "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" + } + }, + "tags": [ + { + "name": "Base" + }, + { + "description": "用户", + "name": "SysUser" + } + ] +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "v2.8.0", + Host: "", + BasePath: "", + Schemes: []string{}, + Title: "Gin-Vue-Admin Swagger API接口文档", + Description: "使用gin+vue进行极速开发的全栈开发基础平台", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 0000000..9eb3da4 --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,9286 @@ +{ + "swagger": "2.0", + "info": { + "description": "使用gin+vue进行极速开发的全栈开发基础平台", + "title": "Gin-Vue-Admin Swagger API接口文档", + "contact": {}, + "version": "v2.7.9-beta" + }, + "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": { + "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/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" + } + } + } + ] + } + } + } + } + }, + "/attachmentCategory/addCategory": { + "post": { + "security": [ + { + "AttachmentCategory": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AddCategory" + ], + "summary": "添加媒体库分类", + "parameters": [ + { + "description": "媒体库分类数据", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/example.ExaAttachmentCategory" + } + } + ], + "responses": {} + } + }, + "/attachmentCategory/deleteCategory": { + "post": { + "security": [ + { + "AttachmentCategory": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "DeleteCategory" + ], + "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" + } + } + } + ] + } + } + } + } + }, + "/attachmentCategory/getCategoryList": { + "get": { + "security": [ + { + "AttachmentCategory": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "GetCategoryList" + ], + "summary": "媒体库分类列表", + "responses": { + "200": { + "description": "媒体库分类列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/example.ExaAttachmentCategory" + }, + "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": { + "put": { + "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/getDB": { + "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/initAPI": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCodePlugin" + ], + "summary": "打包插件", + "responses": { + "200": { + "description": "打包插件成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/initMenu": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCodePlugin" + ], + "summary": "打包插件", + "responses": { + "200": { + "description": "打包插件成功", + "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": { + "post": { + "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": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaFileUploadAndDownload" + ], + "summary": "查找文件", + "parameters": [ + { + "type": "file", + "description": "Find the file, 查找文件", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "查找文件,返回包括文件详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.FileResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + }, + "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": "页码, 每页大小, 分类id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.ExaAttachmentCategorySearch" + } + } + ], + "responses": { + "200": { + "description": "分页文件列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/fileUploadAndDownload/importURL": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaFileUploadAndDownload" + ], + "summary": "导入URL", + "parameters": [ + { + "description": "对象", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/example.ExaFileUploadAndDownload" + } + } + ], + "responses": { + "200": { + "description": "导入URL", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "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/ExportTemplate": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysExportTemplate" + ], + "summary": "导出表格模板", + "responses": {} + } + }, + "/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" + } + } + } + ] + } + } + } + } + }, + "/sysParams/createSysParams": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysParams" + ], + "summary": "创建参数", + "parameters": [ + { + "description": "创建参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysParams" + } + } + ], + "responses": { + "200": { + "description": "创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysParams/deleteSysParams": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysParams" + ], + "summary": "删除参数", + "parameters": [ + { + "description": "删除参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysParams" + } + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysParams/deleteSysParamsByIds": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysParams" + ], + "summary": "批量删除参数", + "responses": { + "200": { + "description": "批量删除成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysParams/findSysParams": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysParams" + ], + "summary": "用id查询参数", + "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": "key", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "参数名称", + "name": "name", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + }, + { + "type": "string", + "description": "参数值", + "name": "value", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "查询成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/system.SysParams" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysParams/getSysParam": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysParams" + ], + "summary": "根据key获取参数value", + "parameters": [ + { + "type": "string", + "description": "key", + "name": "key", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/system.SysParams" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysParams/getSysParamsList": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysParams" + ], + "summary": "分页获取参数列表", + "parameters": [ + { + "type": "string", + "name": "endCreatedAt", + "in": "query" + }, + { + "type": "string", + "name": "key", + "in": "query" + }, + { + "type": "string", + "description": "关键字", + "name": "keyword", + "in": "query" + }, + { + "type": "string", + "name": "name", + "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" + } + } + } + ] + } + } + } + } + }, + "/sysParams/updateSysParams": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysParams" + ], + "summary": "更新参数", + "parameters": [ + { + "description": "更新参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysParams" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "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/SetSelfSetting": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "设置用户配置", + "parameters": [ + { + "description": "用户配置数据", + "name": "data", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": true + } + } + ], + "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.GetUserList" + } + } + ], + "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": { + "common.JSONMap": { + "type": "object", + "additionalProperties": true + }, + "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.Minio": { + "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" + }, + "use-ssl": { + "type": "boolean" + } + } + }, + "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" + }, + "name": { + "description": "代表当前实例的名字", + "type": "string" + }, + "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" + } + ] + }, + "minio": { + "$ref": "#/definitions/config.Minio" + }, + "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" + }, + "redis-list": { + "type": "array", + "items": { + "$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" + }, + "use-strict-auth": { + "description": "使用树形角色分配模式", + "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.ExaAttachmentCategory": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "children": { + "type": "array", + "items": { + "$ref": "#/definitions/example.ExaAttachmentCategory" + } + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "name": { + "type": "string" + }, + "pid": { + "type": "integer" + }, + "updatedAt": { + "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" + }, + "classId": { + "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", + "properties": { + "abbreviation": { + "description": "Struct简称", + "type": "string", + "example": "Struct简称" + }, + "autoCreateApiToSql": { + "description": "是否自动创建api", + "type": "boolean", + "example": false + }, + "autoCreateBtnAuth": { + "description": "是否自动创建按钮权限", + "type": "boolean", + "example": false + }, + "autoCreateMenuToSql": { + "description": "是否自动创建menu", + "type": "boolean", + "example": false + }, + "autoCreateResource": { + "description": "是否自动创建资源标识", + "type": "boolean", + "example": false + }, + "autoMigrate": { + "description": "是否自动迁移表结构", + "type": "boolean", + "example": false + }, + "businessDB": { + "description": "业务数据库", + "type": "string", + "example": "业务数据库" + }, + "description": { + "description": "Struct中文名称", + "type": "string", + "example": "Struct中文名称" + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/definitions/request.AutoCodeField" + } + }, + "generateServer": { + "description": "是否生成server", + "type": "boolean", + "example": true + }, + "generateWeb": { + "description": "是否生成web", + "type": "boolean", + "example": true + }, + "gvaModel": { + "description": "是否使用gva默认Model", + "type": "boolean", + "example": false + }, + "humpPackageName": { + "description": "go文件名称", + "type": "string", + "example": "go文件名称" + }, + "isAdd": { + "description": "是否新增", + "type": "boolean", + "example": false + }, + "isTree": { + "description": "是否树形结构", + "type": "boolean", + "example": false + }, + "onlyTemplate": { + "description": "是否只生成模板", + "type": "boolean", + "example": false + }, + "package": { + "type": "string" + }, + "packageName": { + "description": "文件名称", + "type": "string", + "example": "文件名称" + }, + "primaryField": { + "$ref": "#/definitions/request.AutoCodeField" + }, + "structName": { + "description": "Struct名称", + "type": "string", + "example": "Struct名称" + }, + "tableName": { + "description": "表名", + "type": "string", + "example": "表名" + }, + "treeJson": { + "description": "展示的树json字段", + "type": "string", + "example": "展示的树json字段" + } + } + }, + "request.AutoCodeField": { + "type": "object", + "properties": { + "checkDataSource": { + "description": "是否检查数据源", + "type": "boolean" + }, + "clearable": { + "description": "是否可清空", + "type": "boolean" + }, + "columnName": { + "description": "数据库字段", + "type": "string" + }, + "comment": { + "description": "数据库字段描述", + "type": "string" + }, + "dataSource": { + "description": "数据源", + "allOf": [ + { + "$ref": "#/definitions/request.DataSource" + } + ] + }, + "dataTypeLong": { + "description": "数据库字段长度", + "type": "string" + }, + "defaultValue": { + "description": "是否必填", + "type": "string" + }, + "desc": { + "description": "是否前端详情", + "type": "boolean" + }, + "dictType": { + "description": "字典", + "type": "string" + }, + "errorText": { + "description": "校验失败文字", + "type": "string" + }, + "excel": { + "description": "是否导入/导出", + "type": "boolean" + }, + "fieldDesc": { + "description": "中文名", + "type": "string" + }, + "fieldIndexType": { + "description": "索引类型", + "type": "string" + }, + "fieldJson": { + "description": "FieldJson", + "type": "string" + }, + "fieldName": { + "description": "Field名", + "type": "string" + }, + "fieldSearchHide": { + "description": "是否隐藏查询条件", + "type": "boolean" + }, + "fieldSearchType": { + "description": "搜索条件", + "type": "string" + }, + "fieldType": { + "description": "Field数据类型", + "type": "string" + }, + "form": { + "description": "Front bool `json:\"front\"` // 是否前端可见", + "type": "boolean" + }, + "primaryKey": { + "description": "是否主键", + "type": "boolean" + }, + "require": { + "description": "是否必填", + "type": "boolean" + }, + "sort": { + "description": "是否增加排序", + "type": "boolean" + }, + "table": { + "description": "是否前端表格列", + "type": "boolean" + } + } + }, + "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.DataSource": { + "type": "object", + "properties": { + "association": { + "description": "关联关系 1 一对一 2 一对多", + "type": "integer" + }, + "dbName": { + "type": "string" + }, + "hasDeletedAt": { + "type": "boolean" + }, + "label": { + "type": "string" + }, + "table": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "request.Empty": { + "type": "object" + }, + "request.ExaAttachmentCategorySearch": { + "type": "object", + "properties": { + "classId": { + "type": "integer" + }, + "keyword": { + "description": "关键字", + "type": "string" + }, + "page": { + "description": "页码", + "type": "integer" + }, + "pageSize": { + "description": "每页大小", + "type": "integer" + } + } + }, + "request.GetAuthorityId": { + "type": "object", + "properties": { + "authorityId": { + "description": "角色ID", + "type": "integer" + } + } + }, + "request.GetById": { + "type": "object", + "properties": { + "id": { + "description": "主键ID", + "type": "integer" + } + } + }, + "request.GetUserList": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "keyword": { + "description": "关键字", + "type": "string" + }, + "nickName": { + "type": "string" + }, + "page": { + "description": "页码", + "type": "integer" + }, + "pageSize": { + "description": "每页大小", + "type": "integer" + }, + "phone": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "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" + }, + "template": { + "description": "postgresql指定template", + "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.SysParams": { + "type": "object", + "required": [ + "key", + "name", + "value" + ], + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "desc": { + "description": "参数说明", + "type": "string" + }, + "key": { + "description": "参数键", + "type": "string" + }, + "name": { + "description": "参数名称", + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + }, + "value": { + "description": "参数值", + "type": "string" + } + } + }, + "system.SysUser": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "authorities": { + "description": "多用户角色", + "type": "array", + "items": { + "$ref": "#/definitions/system.SysAuthority" + } + }, + "authority": { + "description": "用户角色", + "allOf": [ + { + "$ref": "#/definitions/system.SysAuthority" + } + ] + }, + "authorityId": { + "description": "用户角色ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "email": { + "description": "用户邮箱", + "type": "string" + }, + "enable": { + "description": "用户是否被冻结 1正常 2冻结", + "type": "integer" + }, + "headerImg": { + "description": "用户头像", + "type": "string" + }, + "nickName": { + "description": "用户昵称", + "type": "string" + }, + "originSetting": { + "description": "配置", + "allOf": [ + { + "$ref": "#/definitions/common.JSONMap" + } + ] + }, + "phone": { + "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" + } + }, + "tags": [ + { + "name": "Base" + }, + { + "description": "用户", + "name": "SysUser" + } + ] +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 0000000..1a1f8cc --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,5677 @@ +definitions: + common.JSONMap: + additionalProperties: true + type: object + 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.Minio: + 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 + use-ssl: + type: boolean + 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 + name: + description: 代表当前实例的名字 + type: string + 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 + minio: + $ref: '#/definitions/config.Minio' + 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' + redis-list: + items: + $ref: '#/definitions/config.Redis' + type: array + 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 + use-strict-auth: + description: 使用树形角色分配模式 + 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.ExaAttachmentCategory: + properties: + ID: + description: 主键ID + type: integer + children: + items: + $ref: '#/definitions/example.ExaAttachmentCategory' + type: array + createdAt: + description: 创建时间 + type: string + name: + type: string + pid: + type: integer + updatedAt: + 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 + classId: + 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: + properties: + abbreviation: + description: Struct简称 + example: Struct简称 + type: string + autoCreateApiToSql: + description: 是否自动创建api + example: false + type: boolean + autoCreateBtnAuth: + description: 是否自动创建按钮权限 + example: false + type: boolean + autoCreateMenuToSql: + description: 是否自动创建menu + example: false + type: boolean + autoCreateResource: + description: 是否自动创建资源标识 + example: false + type: boolean + autoMigrate: + description: 是否自动迁移表结构 + example: false + type: boolean + businessDB: + description: 业务数据库 + example: 业务数据库 + type: string + description: + description: Struct中文名称 + example: Struct中文名称 + type: string + fields: + items: + $ref: '#/definitions/request.AutoCodeField' + type: array + generateServer: + description: 是否生成server + example: true + type: boolean + generateWeb: + description: 是否生成web + example: true + type: boolean + gvaModel: + description: 是否使用gva默认Model + example: false + type: boolean + humpPackageName: + description: go文件名称 + example: go文件名称 + type: string + isAdd: + description: 是否新增 + example: false + type: boolean + isTree: + description: 是否树形结构 + example: false + type: boolean + onlyTemplate: + description: 是否只生成模板 + example: false + type: boolean + package: + type: string + packageName: + description: 文件名称 + example: 文件名称 + type: string + primaryField: + $ref: '#/definitions/request.AutoCodeField' + structName: + description: Struct名称 + example: Struct名称 + type: string + tableName: + description: 表名 + example: 表名 + type: string + treeJson: + description: 展示的树json字段 + example: 展示的树json字段 + type: string + type: object + request.AutoCodeField: + properties: + checkDataSource: + description: 是否检查数据源 + type: boolean + clearable: + description: 是否可清空 + type: boolean + columnName: + description: 数据库字段 + type: string + comment: + description: 数据库字段描述 + type: string + dataSource: + allOf: + - $ref: '#/definitions/request.DataSource' + description: 数据源 + dataTypeLong: + description: 数据库字段长度 + type: string + defaultValue: + description: 是否必填 + type: string + desc: + description: 是否前端详情 + type: boolean + dictType: + description: 字典 + type: string + errorText: + description: 校验失败文字 + type: string + excel: + description: 是否导入/导出 + type: boolean + fieldDesc: + description: 中文名 + type: string + fieldIndexType: + description: 索引类型 + type: string + fieldJson: + description: FieldJson + type: string + fieldName: + description: Field名 + type: string + fieldSearchHide: + description: 是否隐藏查询条件 + type: boolean + fieldSearchType: + description: 搜索条件 + type: string + fieldType: + description: Field数据类型 + type: string + form: + description: Front bool `json:"front"` // 是否前端可见 + type: boolean + primaryKey: + description: 是否主键 + type: boolean + require: + description: 是否必填 + type: boolean + sort: + description: 是否增加排序 + type: boolean + table: + description: 是否前端表格列 + type: boolean + 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.DataSource: + properties: + association: + description: 关联关系 1 一对一 2 一对多 + type: integer + dbName: + type: string + hasDeletedAt: + type: boolean + label: + type: string + table: + type: string + value: + type: string + type: object + request.Empty: + type: object + request.ExaAttachmentCategorySearch: + properties: + classId: + type: integer + keyword: + description: 关键字 + type: string + page: + description: 页码 + type: integer + pageSize: + description: 每页大小 + type: integer + type: object + request.GetAuthorityId: + properties: + authorityId: + description: 角色ID + type: integer + type: object + request.GetById: + properties: + id: + description: 主键ID + type: integer + type: object + request.GetUserList: + properties: + email: + type: string + keyword: + description: 关键字 + type: string + nickName: + type: string + page: + description: 页码 + type: integer + pageSize: + description: 每页大小 + type: integer + phone: + type: string + username: + type: string + 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 + template: + description: postgresql指定template + 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.SysParams: + properties: + ID: + description: 主键ID + type: integer + createdAt: + description: 创建时间 + type: string + desc: + description: 参数说明 + type: string + key: + description: 参数键 + type: string + name: + description: 参数名称 + type: string + updatedAt: + description: 更新时间 + type: string + value: + description: 参数值 + type: string + required: + - key + - name + - value + type: object + system.SysUser: + properties: + ID: + description: 主键ID + type: integer + authorities: + description: 多用户角色 + items: + $ref: '#/definitions/system.SysAuthority' + type: array + authority: + allOf: + - $ref: '#/definitions/system.SysAuthority' + description: 用户角色 + authorityId: + description: 用户角色ID + type: integer + createdAt: + description: 创建时间 + type: string + email: + description: 用户邮箱 + type: string + enable: + description: 用户是否被冻结 1正常 2冻结 + type: integer + headerImg: + description: 用户头像 + type: string + nickName: + description: 用户昵称 + type: string + originSetting: + allOf: + - $ref: '#/definitions/common.JSONMap' + description: 配置 + phone: + 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.9-beta +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: + 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/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 + /attachmentCategory/addCategory: + post: + consumes: + - application/json + parameters: + - description: 媒体库分类数据 + in: body + name: data + required: true + schema: + $ref: '#/definitions/example.ExaAttachmentCategory' + produces: + - application/json + responses: {} + security: + - AttachmentCategory: [] + summary: 添加媒体库分类 + tags: + - AddCategory + /attachmentCategory/deleteCategory: + 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: + - AttachmentCategory: [] + summary: 删除分类 + tags: + - DeleteCategory + /attachmentCategory/getCategoryList: + get: + produces: + - application/json + responses: + "200": + description: 媒体库分类列表 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/example.ExaAttachmentCategory' + msg: + type: string + type: object + security: + - AttachmentCategory: [] + summary: 媒体库分类列表 + tags: + - GetCategoryList + /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: + put: + 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/getDB: + 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/initAPI: + post: + 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: + - AutoCodePlugin + /autoCode/initMenu: + post: + 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: + - AutoCodePlugin + /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: + post: + 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: + get: + consumes: + - multipart/form-data + parameters: + - description: Find the file, 查找文件 + 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.FileResponse' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 查找文件 + tags: + - ExaFileUploadAndDownload + 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: 页码, 每页大小, 分类id + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.ExaAttachmentCategorySearch' + 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/importURL: + post: + parameters: + - description: 对象 + in: body + name: data + required: true + schema: + $ref: '#/definitions/example.ExaFileUploadAndDownload' + produces: + - application/json + responses: + "200": + description: 导入URL + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 导入URL + 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/ExportTemplate: + get: + consumes: + - application/json + produces: + - application/json + responses: {} + security: + - ApiKeyAuth: [] + summary: 导出表格模板 + tags: + - SysExportTemplate + /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 + /sysParams/createSysParams: + post: + consumes: + - application/json + parameters: + - description: 创建参数 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysParams' + produces: + - application/json + responses: + "200": + description: 创建成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 创建参数 + tags: + - SysParams + /sysParams/deleteSysParams: + delete: + consumes: + - application/json + parameters: + - description: 删除参数 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysParams' + produces: + - application/json + responses: + "200": + description: 删除成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 删除参数 + tags: + - SysParams + /sysParams/deleteSysParamsByIds: + 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: + - SysParams + /sysParams/findSysParams: + 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: key + required: true + type: string + - description: 参数名称 + in: query + name: name + required: true + type: string + - description: 更新时间 + in: query + name: updatedAt + type: string + - description: 参数值 + in: query + name: value + required: true + type: string + produces: + - application/json + responses: + "200": + description: 查询成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/system.SysParams' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 用id查询参数 + tags: + - SysParams + /sysParams/getSysParam: + get: + consumes: + - application/json + parameters: + - description: key + in: query + name: key + required: true + type: string + produces: + - application/json + responses: + "200": + description: 获取成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/system.SysParams' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 根据key获取参数value + tags: + - SysParams + /sysParams/getSysParamsList: + get: + consumes: + - application/json + parameters: + - in: query + name: endCreatedAt + type: string + - in: query + name: key + type: string + - description: 关键字 + in: query + name: keyword + type: string + - in: query + name: name + 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: + - SysParams + /sysParams/updateSysParams: + put: + consumes: + - application/json + parameters: + - description: 更新参数 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysParams' + produces: + - application/json + responses: + "200": + description: 更新成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 更新参数 + tags: + - SysParams + /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/SetSelfSetting: + put: + consumes: + - application/json + parameters: + - description: 用户配置数据 + in: body + name: data + required: true + schema: + additionalProperties: true + type: object + 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.GetUserList' + 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" +tags: +- name: Base +- description: 用户 + name: SysUser diff --git a/global/global.go b/global/global.go new file mode 100644 index 0000000..c829da1 --- /dev/null +++ b/global/global.go @@ -0,0 +1,66 @@ +package global + +import ( + "fmt" + "sync" + + "github.com/gin-gonic/gin" + "github.com/qiniu/qmgo" + + "git.echol.cn/loser/lckt/utils/timer" + "github.com/songzhibin97/gkit/cache/local_cache" + + "golang.org/x/sync/singleflight" + + "go.uber.org/zap" + + "git.echol.cn/loser/lckt/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_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/global/model.go b/global/model.go new file mode 100644 index 0000000..9772eb3 --- /dev/null +++ b/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/go.mod b/go.mod new file mode 100644 index 0000000..87118bc --- /dev/null +++ b/go.mod @@ -0,0 +1,177 @@ +module git.echol.cn/loser/lckt + +go 1.22.2 + +require ( + github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible + github.com/aws/aws-sdk-go v1.55.6 + github.com/casbin/casbin/v2 v2.103.0 + github.com/casbin/gorm-adapter/v3 v3.32.0 + github.com/fsnotify/fsnotify v1.8.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-sql-driver/mysql v1.8.1 + github.com/goccy/go-json v0.10.4 + github.com/golang-jwt/jwt/v5 v5.2.1 + 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.9 + github.com/minio/minio-go/v7 v7.0.84 + github.com/mojocn/base64Captcha v1.3.8 + github.com/otiai10/copy v1.14.1 + github.com/pkg/errors v0.9.1 + github.com/qiniu/go-sdk/v7 v7.25.2 + github.com/qiniu/qmgo v1.1.9 + github.com/redis/go-redis/v9 v9.7.0 + 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.10.0 + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.0 + github.com/swaggo/swag v1.16.4 + github.com/tencentyun/cos-go-sdk-v5 v0.7.60 + github.com/unrolled/secure v1.17.0 + github.com/xuri/excelize/v2 v2.9.0 + go.mongodb.org/mongo-driver v1.17.2 + go.uber.org/automaxprocs v1.6.0 + go.uber.org/zap v1.27.0 + golang.org/x/crypto v0.32.0 + golang.org/x/sync v0.10.0 + golang.org/x/text v0.21.0 + gorm.io/datatypes v1.2.5 + gorm.io/driver/mysql v1.5.7 + gorm.io/driver/postgres v1.5.11 + gorm.io/driver/sqlserver v1.5.4 + 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/STARRY-S/zip v0.1.0 // 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.8.0 // indirect + github.com/bodgit/plumbing v1.3.0 // indirect + github.com/bodgit/sevenzip v1.6.0 // indirect + github.com/bodgit/windows v1.0.1 // indirect + github.com/bytedance/sonic v1.12.7 // indirect + github.com/bytedance/sonic/loader v0.2.3 // indirect + github.com/casbin/govaluate v1.3.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.5 // 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.2-0.20230904184137-39efe44ab707 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gammazero/toposort v0.1.1 // indirect + github.com/gin-contrib/sse v1.0.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.24.0 // 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.2 // 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.9 // 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.9 // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/microsoft/go-mssqldb v1.8.0 // 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.1 // indirect + github.com/otiai10/mint v1.6.3 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pierrec/lz4/v4 v4.1.22 // 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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/richardlehane/mscfb v1.0.4 // indirect + github.com/richardlehane/msoleps v1.0.4 // indirect + github.com/rs/xid v1.6.0 // indirect + github.com/sagikazarmark/locafero v0.7.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/sorairolake/lzip-go v0.3.5 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.12.0 // indirect + github.com/spf13/cast v1.7.1 // 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-20241211021726-c4e992084aa6 // indirect + github.com/xuri/nfp v0.0.0-20250111060730-82a408b9aa71 // 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.13.0 // indirect + golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect + golang.org/x/image v0.23.0 // indirect + golang.org/x/mod v0.22.0 // indirect + golang.org/x/net v0.34.0 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/time v0.9.0 // indirect + golang.org/x/tools v0.29.0 // indirect + google.golang.org/protobuf v1.36.3 // 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 + modernc.org/fileutil v1.3.0 // indirect + modernc.org/libc v1.61.9 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.8.2 // indirect + modernc.org/sqlite v1.34.5 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d2ad132 --- /dev/null +++ b/go.sum @@ -0,0 +1,824 @@ +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.7.0/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/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 h1:U2rTu3Ef+7w9FHKIAXM6ZyqF3UOWJZ12zIm8zECAFfg= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= +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/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 h1:jBQA3cKT4L2rWMpgE7Yt3Hwh2aUj8KXjIGLxjHeYNNo= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg= +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 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.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/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/STARRY-S/zip v0.1.0 h1:eUER3jKmHKXjv+iy3BekLa+QnNSo1Lqz4eTzYBcGDqo= +github.com/STARRY-S/zip v0.1.0/go.mod h1:qj/mTZkvb3AvfGQ2e775/3AODRvB4peSw8KNMvrM8/I= +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.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk= +github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bmatcuk/doublestar/v4 v4.8.0 h1:DSXtrypQddoug1459viM9X9D3dp1Z7993fw36I2kNcQ= +github.com/bmatcuk/doublestar/v4 v4.8.0/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.6.0 h1:a4R0Wu6/P1o1pP/3VV++aEOcyeBxeO/xE2Y9NSTrr6A= +github.com/bodgit/sevenzip v1.6.0/go.mod h1:zOBh9nJUof7tcrlqJFv1koWRrhz3LbDbUNngkuZxLMc= +github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= +github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= +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.7 h1:CQU8pxOy9HToxhndH0Kx/S1qU/CuS9GnKYrGioDcU1Q= +github.com/bytedance/sonic v1.12.7/go.mod h1:tnbal4mxOMju17EGfknm2XyYcpyCnIROYOEYuemj13I= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0= +github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/casbin/casbin/v2 v2.103.0 h1:dHElatNXNrr8XcseUov0ZSiWjauwmZZE6YMV3eU1yic= +github.com/casbin/casbin/v2 v2.103.0/go.mod h1:Ee33aqGrmES+GNL17L0h9X28wXuo829wnNUnS0edAco= +github.com/casbin/gorm-adapter/v3 v3.32.0 h1:Au+IOILBIE9clox5BJhI2nA3p9t7Ep1ePlupdGbGfus= +github.com/casbin/gorm-adapter/v3 v3.32.0/go.mod h1:Zre/H8p17mpv5U3EaWgPoxLILLdXO3gHW5aoQQpUDZI= +github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc= +github.com/casbin/govaluate v1.3.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.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= +github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +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.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4= +github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= +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/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= +git.echol.cn/loser/lckt v0.0.0-20250327142420-7e7ec5229064 h1:FleStwTSVqGqSBhkRbQTGtSbl+6HM/3yKQY70hT3x5Y= +git.echol.cn/loser/lckt v0.0.0-20250327142420-7e7ec5229064/go.mod h1:oMHjLxfdG3L3OxroCNaK6SkelydV0U4eGk8eWTNAopM= +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.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +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.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gammazero/toposort v0.1.1 h1:OivGxsWxF3U3+U80VoLJ+f50HcPU1MIqE1JlKzoJ2Eg= +github.com/gammazero/toposort v0.1.1/go.mod h1:H2cozTnNpMw0hg2VHAYsAxmkHXBYroNangj2NTBQDvw= +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 v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= +github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= +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.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg= +github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= +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.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= +github.com/goccy/go-json v0.10.4/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/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.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/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.5.5/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.5.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/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.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= +github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +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/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.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +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.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= +github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +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.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.9 h1:EZgAsW6DsuawxDgTtIdjCUBa2TQ6AOe9pnCidofSRtE= +github.com/mholt/archiver/v4 v4.0.0-alpha.9/go.mod h1:5D3uct315OMkMRXKwEuMB+wQi/2m5NQngKDmApqwVlo= +github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= +github.com/microsoft/go-mssqldb v1.8.0 h1:7cyZ/AT7ycDsEoWPIXibd+aVKFtteUNhDGf3aobP+tw= +github.com/microsoft/go-mssqldb v1.8.0/go.mod h1:6znkekS3T2vp0waiMhen4GPU1BiAsrP+iXHcE7a7rFo= +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.84 h1:D1HVmAF8JF8Bpi6IU4V9vIEj+8pc+xU88EWMs2yed0E= +github.com/minio/minio-go/v7 v7.0.84/go.mod h1:57YXpvc5l3rjPdhqNrDsvVlY0qPI6UTk1bflAe+9doY= +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.8 h1:rrN9BhCwXKS8ht1e21kvR3iTaMgf4qPC9sRoV52bqEg= +github.com/mojocn/base64Captcha v1.3.8/go.mod h1:QFZy927L8HVP3+VV5z2b1EAEiv1KxVJKZbAucVgLUy4= +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/nwaples/rardecode/v2 v2.0.1 h1:3MN6/R+Y4c7e+21U3yhWuUcf72sYmcmr6jtiuAVSH1A= +github.com/nwaples/rardecode/v2 v2.0.1/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY= +github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8= +github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I= +github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs= +github.com/otiai10/mint v1.6.3/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.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= +github.com/pierrec/lz4/v4 v4.1.22/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.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/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdkk= +github.com/qiniu/go-sdk/v7 v7.25.2 h1:URwgZpxySdiwu2yQpHk93X4LXWHyFRp1x3Vmlk/YWvo= +github.com/qiniu/go-sdk/v7 v7.25.2/go.mod h1:dmKtJ2ahhPWFVi9o1D5GemmWoh/ctuB9peqTowyTO8o= +github.com/qiniu/qmgo v1.1.9 h1:3G3h9RLyjIUW9YSAQEPP2WqqNnboZ2Z/zO3mugjVb3E= +github.com/qiniu/qmgo v1.1.9/go.mod h1:aba4tNSlMWrwUhe7RdILfwBRIgvBujt1y10X+T1YZSI= +github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs= +github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= +github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= +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.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= +github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= +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/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/sorairolake/lzip-go v0.3.5 h1:ms5Xri9o1JBIWvOFAorYtUNik6HI3HgBTkISiqu0Cwg= +github.com/sorairolake/lzip-go v0.3.5/go.mod h1:N0KYq5iWrMXI0ZEXKXaS9hCyOjZUQdBDEIbXfoUwbdk= +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.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= +github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.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.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A= +github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg= +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.60 h1:/e/tmvRmfKexr/QQIBzWhOkZWsmY3EK72NrI6G/Tv0o= +github.com/tencentyun/cos-go-sdk-v5 v0.7.60/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/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.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +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.17.0 h1:Io7ifFgo99Bnh0J7+Q+qcMzWM6kaDPCA5FroFZEdbWU= +github.com/unrolled/secure v1.17.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.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +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-20241211021726-c4e992084aa6 h1:8m6DWBG+dlFNbx5ynvrE7NgI+Y7OlZVMVTpayoW+rCc= +github.com/xuri/efp v0.0.0-20241211021726-c4e992084aa6/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-20250111060730-82a408b9aa71 h1:hOh7aVDrvGJRxzXrQbDY8E+02oaI//5cHL+97oYpEPw= +github.com/xuri/nfp v0.0.0-20250111060730-82a408b9aa71/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-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.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4= +go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793SqyhzM= +go.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +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.13.0 h1:KCkqVVV1kGg0X87TFysjCJ8MxtZEIU4Ja/yXGeoECdA= +golang.org/x/arch v0.13.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.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +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-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= +golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= +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.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= +golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= +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.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.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-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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.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.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.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-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-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-20190916202348-b4ddaad3f8a3/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-20200212091648-12a6c2dcc1e4/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-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.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +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.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +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.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +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.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.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.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= +golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= +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 v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= +google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +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-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/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +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.8/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.5 h1:9UogU3jkydFVW1bIVVeoYsTpLRgwDVW3rHfJG6/Ek9I= +gorm.io/datatypes v1.2.5/go.mod h1:I5FUdlKpLb5PMqeMQhm30CQ6jXP8Rj89xkTeCSAaAD4= +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.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= +gorm.io/driver/postgres v1.5.11/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.4 h1:xA+Y1KDNspv79q43bPyjDMUgHoYHLhXYmdFcYPobg8g= +gorm.io/driver/sqlserver v1.5.4/go.mod h1:+frZ/qYmuna11zHPlh5oc2O6ZA/lS88Keb0XSH1Zh/g= +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= +modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0= +modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.23.13 h1:PFiaemQwE/jdwi8XEHyEV+qYWoIuikLP3T4rvDeJb00= +modernc.org/ccgo/v4 v4.23.13/go.mod h1:vdN4h2WR5aEoNondUx26K7G8X+nuBscYnAEWSRmN2/0= +modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8= +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.6.1 h1:+Qf6xdG8l7B27TQ8D8lw/iFMUj1RXRBOuMUWziJOsk8= +modernc.org/gc/v2 v2.6.1/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/libc v1.61.9 h1:PLSBXVkifXGELtJ5BOnBUyAHr7lsatNwFU/RRo4kfJM= +modernc.org/libc v1.61.9/go.mod h1:61xrnzk/aR8gr5bR7Uj/lLFLuXu2/zMpIjcry63Eumk= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI= +modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g= +modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +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/initialize/db_list.go b/initialize/db_list.go new file mode 100644 index 0000000..707bad6 --- /dev/null +++ b/initialize/db_list.go @@ -0,0 +1,36 @@ +package initialize + +import ( + "git.echol.cn/loser/lckt/config" + "git.echol.cn/loser/lckt/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/initialize/ensure_tables.go b/initialize/ensure_tables.go new file mode 100644 index 0000000..cebe70a --- /dev/null +++ b/initialize/ensure_tables.go @@ -0,0 +1,113 @@ +package initialize + +import ( + "context" + "git.echol.cn/loser/lckt/model/example" + sysModel "git.echol.cn/loser/lckt/model/system" + "git.echol.cn/loser/lckt/plugin/announcement/model" + "git.echol.cn/loser/lckt/service/system" + adapter "github.com/casbin/gorm-adapter/v3" + "gorm.io/gorm" +) + +const initOrderEnsureTables = system.InitOrderExternal - 1 + +type ensureTables struct{} + +// auto run +func init() { + system.RegisterInit(initOrderEnsureTables, &ensureTables{}) +} + +func (e *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{}, + example.ExaAttachmentCategory{}, + + model.Info{}, + } + 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{}, + example.ExaAttachmentCategory{}, + + model.Info{}, + } + yes := true + for _, t := range tables { + yes = yes && db.Migrator().HasTable(t) + } + return yes +} diff --git a/initialize/gorm.go b/initialize/gorm.go new file mode 100644 index 0000000..6739ffa --- /dev/null +++ b/initialize/gorm.go @@ -0,0 +1,78 @@ +package initialize + +import ( + "os" + + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/example" + "git.echol.cn/loser/lckt/model/system" + + "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{}, + example.ExaAttachmentCategory{}, + ) + 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/initialize/gorm_biz.go b/initialize/gorm_biz.go new file mode 100644 index 0000000..37d7e84 --- /dev/null +++ b/initialize/gorm_biz.go @@ -0,0 +1,21 @@ +package initialize + +import ( + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/article" + "git.echol.cn/loser/lckt/model/bot" + "git.echol.cn/loser/lckt/model/category" +) + +func bizModel() error { + db := global.GVA_DB + err := db.AutoMigrate( + category.Category{}, + bot.Bot{}, + article.Article{}, + ) + if err != nil { + return err + } + return nil +} diff --git a/initialize/gorm_mssql.go b/initialize/gorm_mssql.go new file mode 100644 index 0000000..f262eb6 --- /dev/null +++ b/initialize/gorm_mssql.go @@ -0,0 +1,60 @@ +package initialize + +/* + * @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 + */ + +import ( + "git.echol.cn/loser/lckt/config" + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/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/initialize/gorm_mysql.go b/initialize/gorm_mysql.go new file mode 100644 index 0000000..f9028aa --- /dev/null +++ b/initialize/gorm_mysql.go @@ -0,0 +1,55 @@ +package initialize + +import ( + "git.echol.cn/loser/lckt/config" + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/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/initialize/gorm_oracle.go b/initialize/gorm_oracle.go new file mode 100644 index 0000000..9b5ed1e --- /dev/null +++ b/initialize/gorm_oracle.go @@ -0,0 +1,52 @@ +package initialize + +import ( + //"github.com/dzwvip/oracle" + "git.echol.cn/loser/lckt/config" + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/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/initialize/gorm_pgsql.go b/initialize/gorm_pgsql.go new file mode 100644 index 0000000..2f97917 --- /dev/null +++ b/initialize/gorm_pgsql.go @@ -0,0 +1,50 @@ +package initialize + +import ( + "git.echol.cn/loser/lckt/config" + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/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/initialize/gorm_sqlite.go b/initialize/gorm_sqlite.go new file mode 100644 index 0000000..4150039 --- /dev/null +++ b/initialize/gorm_sqlite.go @@ -0,0 +1,42 @@ +package initialize + +import ( + "git.echol.cn/loser/lckt/config" + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/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/initialize/internal/gorm.go b/initialize/internal/gorm.go new file mode 100644 index 0000000..ef1ce29 --- /dev/null +++ b/initialize/internal/gorm.go @@ -0,0 +1,46 @@ +package internal + +import ( + "git.echol.cn/loser/lckt/config" + "git.echol.cn/loser/lckt/global" + "gorm.io/gorm" + "gorm.io/gorm/logger" + "gorm.io/gorm/schema" + "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), logger.Config{ + SlowThreshold: 200 * time.Millisecond, + LogLevel: general.LogLevel(), + Colorful: true, + }), + NamingStrategy: schema.NamingStrategy{ + TablePrefix: prefix, + SingularTable: singular, + }, + DisableForeignKeyConstraintWhenMigrating: true, + } +} diff --git a/initialize/internal/gorm_logger_writer.go b/initialize/internal/gorm_logger_writer.go new file mode 100644 index 0000000..4828764 --- /dev/null +++ b/initialize/internal/gorm_logger_writer.go @@ -0,0 +1,41 @@ +package internal + +import ( + "fmt" + "git.echol.cn/loser/lckt/config" + "git.echol.cn/loser/lckt/global" + "gorm.io/gorm/logger" +) + +type Writer struct { + config config.GeneralDB + writer logger.Writer +} + +func NewWriter(config config.GeneralDB) *Writer { + return &Writer{config: config} +} + +// Printf 格式化打印日志 +func (c *Writer) Printf(message string, data ...any) { + + // 当有日志时候均需要输出到控制台 + fmt.Printf(message, data...) + + // 当开启了zap的情况,会打印到日志记录 + if c.config.LogZap { + switch c.config.LogLevel() { + case logger.Silent: + global.GVA_LOG.Debug(fmt.Sprintf(message, data...)) + case logger.Error: + global.GVA_LOG.Error(fmt.Sprintf(message, data...)) + case logger.Warn: + global.GVA_LOG.Warn(fmt.Sprintf(message, data...)) + case logger.Info: + global.GVA_LOG.Info(fmt.Sprintf(message, data...)) + default: + global.GVA_LOG.Info(fmt.Sprintf(message, data...)) + } + return + } +} diff --git a/initialize/internal/mongo.go b/initialize/internal/mongo.go new file mode 100644 index 0000000..c4992d7 --- /dev/null +++ b/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/initialize/mongo.go b/initialize/mongo.go new file mode 100644 index 0000000..3157f69 --- /dev/null +++ b/initialize/mongo.go @@ -0,0 +1,155 @@ +package initialize + +import ( + "context" + "fmt" + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/initialize/internal" + "git.echol.cn/loser/lckt/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() + config := &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, + } + if global.GVA_CONFIG.Mongo.Username != "" && global.GVA_CONFIG.Mongo.Password != "" { + config.Auth = &qmgo.Credential{ + Username: global.GVA_CONFIG.Mongo.Username, + Password: global.GVA_CONFIG.Mongo.Password, + AuthSource: global.GVA_CONFIG.Mongo.AuthSource, + } + } + client, err := qmgo.Open(ctx, config, 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/initialize/other.go b/initialize/other.go new file mode 100644 index 0000000..42755e0 --- /dev/null +++ b/initialize/other.go @@ -0,0 +1,32 @@ +package initialize + +import ( + "bufio" + "github.com/songzhibin97/gkit/cache/local_cache" + "os" + "strings" + + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/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/initialize/plugin.go b/initialize/plugin.go new file mode 100644 index 0000000..9f3ab8f --- /dev/null +++ b/initialize/plugin.go @@ -0,0 +1,15 @@ +package initialize + +import ( + "git.echol.cn/loser/lckt/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/initialize/plugin_biz_v1.go b/initialize/plugin_biz_v1.go new file mode 100644 index 0000000..d6a3916 --- /dev/null +++ b/initialize/plugin_biz_v1.go @@ -0,0 +1,34 @@ +package initialize + +import ( + "fmt" + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/plugin/email" + "git.echol.cn/loser/lckt/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/initialize/plugin_biz_v2.go b/initialize/plugin_biz_v2.go new file mode 100644 index 0000000..fc5c931 --- /dev/null +++ b/initialize/plugin_biz_v2.go @@ -0,0 +1,16 @@ +package initialize + +import ( + "git.echol.cn/loser/lckt/plugin/announcement" + "git.echol.cn/loser/lckt/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/initialize/redis.go b/initialize/redis.go new file mode 100644 index 0000000..4e7129b --- /dev/null +++ b/initialize/redis.go @@ -0,0 +1,59 @@ +package initialize + +import ( + "context" + + "git.echol.cn/loser/lckt/config" + "git.echol.cn/loser/lckt/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 +} + +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/initialize/register_init.go b/initialize/register_init.go new file mode 100644 index 0000000..8dbc19a --- /dev/null +++ b/initialize/register_init.go @@ -0,0 +1,10 @@ +package initialize + +import ( + _ "git.echol.cn/loser/lckt/source/example" + _ "git.echol.cn/loser/lckt/source/system" +) + +func init() { + // do nothing,only import source package so that inits can be registered +} diff --git a/initialize/router.go b/initialize/router.go new file mode 100644 index 0000000..d0d3ee7 --- /dev/null +++ b/initialize/router.go @@ -0,0 +1,111 @@ +package initialize + +import ( + "net/http" + "os" + + "git.echol.cn/loser/lckt/docs" + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/middleware" + "git.echol.cn/loser/lckt/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, PublicGroup) // 导出模板 + systemRouter.InitSysParamsRouter(PrivateGroup, PublicGroup) // 参数管理 + exampleRouter.InitCustomerRouter(PrivateGroup) // 客户路由 + exampleRouter.InitFileUploadAndDownloadRouter(PrivateGroup) // 文件上传下载功能路由 + exampleRouter.InitAttachmentCategoryRouterRouter(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/initialize/router_biz.go b/initialize/router_biz.go new file mode 100644 index 0000000..45a4340 --- /dev/null +++ b/initialize/router_biz.go @@ -0,0 +1,28 @@ +package initialize + +import ( + "git.echol.cn/loser/lckt/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) + { + categoryRouter := router.RouterGroupApp.Category + categoryRouter.InitCategoryRouter(privateGroup, publicGroup) + } // 占位方法,保证文件可以正确加载,避免go空变量检测报错,请勿删除。 + { + botRouter := router.RouterGroupApp.Bot + botRouter.InitBotRouter(privateGroup, publicGroup) + } + { + articleRouter := router.RouterGroupApp.Article + articleRouter.InitBotRouter(privateGroup, publicGroup) + } +} diff --git a/initialize/timer.go b/initialize/timer.go new file mode 100644 index 0000000..d405f77 --- /dev/null +++ b/initialize/timer.go @@ -0,0 +1,37 @@ +package initialize + +import ( + "fmt" + "git.echol.cn/loser/lckt/task" + + "github.com/robfig/cron/v3" + + "git.echol.cn/loser/lckt/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/initialize/validator.go b/initialize/validator.go new file mode 100644 index 0000000..7469159 --- /dev/null +++ b/initialize/validator.go @@ -0,0 +1,22 @@ +package initialize + +import "git.echol.cn/loser/lckt/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/log/2025-03-28/error.log b/log/2025-03-28/error.log new file mode 100644 index 0000000..da885ea --- /dev/null +++ b/log/2025-03-28/error.log @@ -0,0 +1,2 @@ +[git.echol.cn/loser/lckt]2025-03-28 22:07:32.143 error C:/Users/Administrator/Desktop/lckt/server/api/v1/category/category.go:161 获取失败! {"error": "Error 1146 (42S02): Table 'lckt.categories' doesn't exist"} +[git.echol.cn/loser/lckt]2025-03-28 22:07:37.462 error C:/Users/Administrator/Desktop/lckt/server/api/v1/category/category.go:161 获取失败! {"error": "Error 1146 (42S02): Table 'lckt.categories' doesn't exist"} diff --git a/log/2025-03-28/info.log b/log/2025-03-28/info.log new file mode 100644 index 0000000..9287e31 --- /dev/null +++ b/log/2025-03-28/info.log @@ -0,0 +1,25 @@ +[git.echol.cn/loser/lckt]2025-03-28 21:37:57.372 info C:/Users/Administrator/Desktop/lckt/server/initialize/router.go:60 register swagger handler +[git.echol.cn/loser/lckt]2025-03-28 21:37:57.377 info C:/Users/Administrator/Desktop/lckt/server/initialize/plugin.go:10 项目暂未初始化,无法安装插件,初始化后重启项目即可完成插件安装 +[git.echol.cn/loser/lckt]2025-03-28 21:37:57.378 info C:/Users/Administrator/Desktop/lckt/server/initialize/router.go:109 router register success +[git.echol.cn/loser/lckt]2025-03-28 21:37:57.378 info C:/Users/Administrator/Desktop/lckt/server/core/server.go:38 server run success on {"address": ":8888"} +[git.echol.cn/loser/lckt]2025-03-28 21:44:26.187 info C:/Users/Administrator/Desktop/lckt/server/api/v1/system/sys_initdb.go:57 前往初始化数据库 +[git.echol.cn/loser/lckt]2025-03-28 21:46:08.101 info C:/Users/Administrator/Desktop/lckt/server/initialize/gorm.go:77 register table success +[git.echol.cn/loser/lckt]2025-03-28 21:46:08.134 info C:/Users/Administrator/Desktop/lckt/server/initialize/router.go:60 register swagger handler +[git.echol.cn/loser/lckt]2025-03-28 21:46:08.997 info C:/Users/Administrator/Desktop/lckt/server/initialize/router.go:109 router register success +[git.echol.cn/loser/lckt]2025-03-28 21:46:08.998 info C:/Users/Administrator/Desktop/lckt/server/core/server.go:38 server run success on {"address": ":8888"} +[git.echol.cn/loser/lckt]2025-03-28 21:49:38.325 info C:/Users/Administrator/Desktop/lckt/server/initialize/gorm.go:77 register table success +[git.echol.cn/loser/lckt]2025-03-28 21:49:38.359 info C:/Users/Administrator/Desktop/lckt/server/initialize/router.go:60 register swagger handler +[git.echol.cn/loser/lckt]2025-03-28 21:49:39.202 info C:/Users/Administrator/Desktop/lckt/server/initialize/router.go:109 router register success +[git.echol.cn/loser/lckt]2025-03-28 21:49:39.202 info C:/Users/Administrator/Desktop/lckt/server/core/server.go:38 server run success on {"address": ":8888"} +[git.echol.cn/loser/lckt]2025-03-28 21:56:23.897 info C:/Users/Administrator/Desktop/lckt/server/initialize/gorm.go:77 register table success +[git.echol.cn/loser/lckt]2025-03-28 21:56:23.935 info C:/Users/Administrator/Desktop/lckt/server/initialize/router.go:60 register swagger handler +[git.echol.cn/loser/lckt]2025-03-28 21:56:24.865 info C:/Users/Administrator/Desktop/lckt/server/initialize/router.go:109 router register success +[git.echol.cn/loser/lckt]2025-03-28 21:56:24.865 info C:/Users/Administrator/Desktop/lckt/server/core/server.go:38 server run success on {"address": ":8888"} +[git.echol.cn/loser/lckt]2025-03-28 22:03:04.372 info C:/Users/Administrator/Desktop/lckt/server/initialize/gorm.go:77 register table success +[git.echol.cn/loser/lckt]2025-03-28 22:03:04.409 info C:/Users/Administrator/Desktop/lckt/server/initialize/router.go:60 register swagger handler +[git.echol.cn/loser/lckt]2025-03-28 22:03:05.325 info C:/Users/Administrator/Desktop/lckt/server/initialize/router.go:109 router register success +[git.echol.cn/loser/lckt]2025-03-28 22:03:05.326 info C:/Users/Administrator/Desktop/lckt/server/core/server.go:38 server run success on {"address": ":8888"} +[git.echol.cn/loser/lckt]2025-03-28 22:08:03.543 info C:/Users/Administrator/Desktop/lckt/server/initialize/gorm.go:77 register table success +[git.echol.cn/loser/lckt]2025-03-28 22:08:03.580 info C:/Users/Administrator/Desktop/lckt/server/initialize/router.go:60 register swagger handler +[git.echol.cn/loser/lckt]2025-03-28 22:08:04.493 info C:/Users/Administrator/Desktop/lckt/server/initialize/router.go:109 router register success +[git.echol.cn/loser/lckt]2025-03-28 22:08:04.493 info C:/Users/Administrator/Desktop/lckt/server/core/server.go:38 server run success on {"address": ":8888"} diff --git a/log/2025-04-03/error.log b/log/2025-04-03/error.log new file mode 100644 index 0000000..904053e --- /dev/null +++ b/log/2025-04-03/error.log @@ -0,0 +1 @@ +[git.echol.cn/loser/lckt]2025-04-03 17:04:21.965 error C:/Users/Administrator/Desktop/lckt/server/api/v1/system/sys_user.go:56 登陆失败! 用户名不存在或者密码错误! {"error": "密码错误"} diff --git a/log/2025-04-03/info.log b/log/2025-04-03/info.log new file mode 100644 index 0000000..0a3f931 --- /dev/null +++ b/log/2025-04-03/info.log @@ -0,0 +1,4 @@ +[git.echol.cn/loser/lckt]2025-04-03 16:44:04.749 info C:/Users/Administrator/Desktop/lckt/server/initialize/gorm.go:77 register table success +[git.echol.cn/loser/lckt]2025-04-03 16:44:04.784 info C:/Users/Administrator/Desktop/lckt/server/initialize/router.go:60 register swagger handler +[git.echol.cn/loser/lckt]2025-04-03 16:44:05.634 info C:/Users/Administrator/Desktop/lckt/server/initialize/router.go:109 router register success +[git.echol.cn/loser/lckt]2025-04-03 16:44:05.634 info C:/Users/Administrator/Desktop/lckt/server/core/server.go:38 server run success on {"address": ":8888"} diff --git a/log/2025-04-09/error.log b/log/2025-04-09/error.log new file mode 100644 index 0000000..09cefe7 --- /dev/null +++ b/log/2025-04-09/error.log @@ -0,0 +1,6 @@ +[git.echol.cn/loser/lckt]2025-04-09 11:56:23.490 error C:/Users/Administrator/Desktop/lckt/server/api/v1/article/article.go:18 参数有误! {"error": "invalid character '}' looking for beginning of object key string"} +[git.echol.cn/loser/lckt]2025-04-09 11:57:17.901 error C:/Users/Administrator/Desktop/lckt/server/api/v1/article/article.go:23 创建失败! {"error": "Error 1146 (42S02): Table 'lckt.article' doesn't exist"} +[git.echol.cn/loser/lckt]2025-04-09 11:58:58.317 error C:/Users/Administrator/Desktop/lckt/server/api/v1/article/article.go:52 参数有误! {"error": "invalid character '}' looking for beginning of object key string"} +[git.echol.cn/loser/lckt]2025-04-09 11:59:41.494 error C:/Users/Administrator/Desktop/lckt/server/api/v1/article/article.go:35 参数有误! {"error": "json: cannot unmarshal object into Go value of type []string"} +[git.echol.cn/loser/lckt]2025-04-09 12:01:05.763 error C:/Users/Administrator/Desktop/lckt/server/api/v1/article/article.go:35 参数有误! {"error": "json: cannot unmarshal number into Go struct field DeleteIds.ids of type string"} +[git.echol.cn/loser/lckt]2025-04-09 12:02:35.671 error C:/Users/Administrator/Desktop/lckt/server/api/v1/article/article.go:41 删除失败! {"error": "sql: converting argument $2 type: unsupported type request.DeleteIds, a struct"} diff --git a/log/2025-04-09/info.log b/log/2025-04-09/info.log new file mode 100644 index 0000000..41aa22b --- /dev/null +++ b/log/2025-04-09/info.log @@ -0,0 +1,46 @@ +[git.echol.cn/loser/lckt]2025-04-09 10:52:11.988 info C:/Users/Administrator/Desktop/lckt/server/initialize/gorm.go:77 register table success +[git.echol.cn/loser/lckt]2025-04-09 10:52:12.021 info C:/Users/Administrator/Desktop/lckt/server/initialize/router.go:60 register swagger handler +[git.echol.cn/loser/lckt]2025-04-09 10:52:12.856 info C:/Users/Administrator/Desktop/lckt/server/initialize/router.go:109 router register success +[git.echol.cn/loser/lckt]2025-04-09 10:52:12.856 info C:/Users/Administrator/Desktop/lckt/server/core/server.go:38 server run success on {"address": ":8888"} +[git.echol.cn/loser/lckt]2025-04-09 10:57:32.057 info C:/Users/Administrator/Desktop/lckt/server/initialize/gorm.go:77 register table success +[git.echol.cn/loser/lckt]2025-04-09 10:57:32.091 info C:/Users/Administrator/Desktop/lckt/server/initialize/router.go:60 register swagger handler +[git.echol.cn/loser/lckt]2025-04-09 10:57:32.892 info C:/Users/Administrator/Desktop/lckt/server/initialize/router.go:109 router register success +[git.echol.cn/loser/lckt]2025-04-09 10:57:32.892 info C:/Users/Administrator/Desktop/lckt/server/core/server.go:38 server run success on {"address": ":8888"} +[git.echol.cn/loser/lckt]2025-04-09 11:08:25.203 info C:/Users/Administrator/Desktop/lckt/server/initialize/gorm.go:77 register table success +[git.echol.cn/loser/lckt]2025-04-09 11:08:25.242 info C:/Users/Administrator/Desktop/lckt/server/initialize/router.go:60 register swagger handler +[git.echol.cn/loser/lckt]2025-04-09 11:08:26.222 info C:/Users/Administrator/Desktop/lckt/server/initialize/router.go:109 router register success +[git.echol.cn/loser/lckt]2025-04-09 11:08:26.223 info C:/Users/Administrator/Desktop/lckt/server/core/server.go:38 server run success on {"address": ":8888"} +[git.echol.cn/loser/lckt]2025-04-09 11:13:20.423 info C:/Users/Administrator/Desktop/lckt/server/initialize/gorm.go:77 register table success +[git.echol.cn/loser/lckt]2025-04-09 11:13:20.461 info C:/Users/Administrator/Desktop/lckt/server/initialize/router.go:60 register swagger handler +[git.echol.cn/loser/lckt]2025-04-09 11:13:21.392 info C:/Users/Administrator/Desktop/lckt/server/initialize/router.go:109 router register success +[git.echol.cn/loser/lckt]2025-04-09 11:13:21.392 info C:/Users/Administrator/Desktop/lckt/server/core/server.go:38 server run success on {"address": ":8888"} +[git.echol.cn/loser/lckt]2025-04-09 11:47:30.968 info C:/Users/Administrator/Desktop/lckt/server/initialize/gorm.go:77 register table success +[git.echol.cn/loser/lckt]2025-04-09 11:47:31.002 info C:/Users/Administrator/Desktop/lckt/server/initialize/router.go:60 register swagger handler +[git.echol.cn/loser/lckt]2025-04-09 11:48:28.705 info C:/Users/Administrator/Desktop/lckt/server/initialize/gorm.go:77 register table success +[git.echol.cn/loser/lckt]2025-04-09 11:48:28.740 info C:/Users/Administrator/Desktop/lckt/server/initialize/router.go:60 register swagger handler +[git.echol.cn/loser/lckt]2025-04-09 11:48:29.585 info C:/Users/Administrator/Desktop/lckt/server/initialize/router.go:109 router register success +[git.echol.cn/loser/lckt]2025-04-09 11:48:29.585 info C:/Users/Administrator/Desktop/lckt/server/core/server.go:38 server run success on {"address": ":8888"} +[git.echol.cn/loser/lckt]2025-04-09 11:52:13.750 info C:/Users/Administrator/Desktop/lckt/server/initialize/gorm.go:77 register table success +[git.echol.cn/loser/lckt]2025-04-09 11:52:13.786 info C:/Users/Administrator/Desktop/lckt/server/initialize/router.go:60 register swagger handler +[git.echol.cn/loser/lckt]2025-04-09 11:52:14.677 info C:/Users/Administrator/Desktop/lckt/server/initialize/router.go:109 router register success +[git.echol.cn/loser/lckt]2025-04-09 11:52:14.678 info C:/Users/Administrator/Desktop/lckt/server/core/server.go:38 server run success on {"address": ":8888"} +[git.echol.cn/loser/lckt]2025-04-09 11:58:20.324 info C:/Users/Administrator/Desktop/lckt/server/initialize/gorm.go:77 register table success +[git.echol.cn/loser/lckt]2025-04-09 11:58:20.363 info C:/Users/Administrator/Desktop/lckt/server/initialize/router.go:60 register swagger handler +[git.echol.cn/loser/lckt]2025-04-09 11:58:21.305 info C:/Users/Administrator/Desktop/lckt/server/initialize/router.go:109 router register success +[git.echol.cn/loser/lckt]2025-04-09 11:58:21.305 info C:/Users/Administrator/Desktop/lckt/server/core/server.go:38 server run success on {"address": ":8888"} +[git.echol.cn/loser/lckt]2025-04-09 12:01:03.057 info C:/Users/Administrator/Desktop/lckt/server/initialize/gorm.go:77 register table success +[git.echol.cn/loser/lckt]2025-04-09 12:01:03.091 info C:/Users/Administrator/Desktop/lckt/server/initialize/router.go:60 register swagger handler +[git.echol.cn/loser/lckt]2025-04-09 12:01:03.936 info C:/Users/Administrator/Desktop/lckt/server/initialize/router.go:109 router register success +[git.echol.cn/loser/lckt]2025-04-09 12:01:03.937 info C:/Users/Administrator/Desktop/lckt/server/core/server.go:38 server run success on {"address": ":8888"} +[git.echol.cn/loser/lckt]2025-04-09 12:02:28.681 info C:/Users/Administrator/Desktop/lckt/server/initialize/gorm.go:77 register table success +[git.echol.cn/loser/lckt]2025-04-09 12:02:28.718 info C:/Users/Administrator/Desktop/lckt/server/initialize/router.go:60 register swagger handler +[git.echol.cn/loser/lckt]2025-04-09 12:02:29.660 info C:/Users/Administrator/Desktop/lckt/server/initialize/router.go:109 router register success +[git.echol.cn/loser/lckt]2025-04-09 12:02:29.660 info C:/Users/Administrator/Desktop/lckt/server/core/server.go:38 server run success on {"address": ":8888"} +[git.echol.cn/loser/lckt]2025-04-09 12:03:22.537 info C:/Users/Administrator/Desktop/lckt/server/initialize/gorm.go:77 register table success +[git.echol.cn/loser/lckt]2025-04-09 12:03:22.573 info C:/Users/Administrator/Desktop/lckt/server/initialize/router.go:60 register swagger handler +[git.echol.cn/loser/lckt]2025-04-09 12:03:23.455 info C:/Users/Administrator/Desktop/lckt/server/initialize/router.go:109 router register success +[git.echol.cn/loser/lckt]2025-04-09 12:03:23.455 info C:/Users/Administrator/Desktop/lckt/server/core/server.go:38 server run success on {"address": ":8888"} +[git.echol.cn/loser/lckt]2025-04-09 12:14:32.734 info C:/Users/Administrator/GolandProjects/zb/lckt-server/initialize/gorm.go:77 register table success +[git.echol.cn/loser/lckt]2025-04-09 12:14:32.768 info C:/Users/Administrator/GolandProjects/zb/lckt-server/initialize/router.go:60 register swagger handler +[git.echol.cn/loser/lckt]2025-04-09 12:14:33.632 info C:/Users/Administrator/GolandProjects/zb/lckt-server/initialize/router.go:109 router register success +[git.echol.cn/loser/lckt]2025-04-09 12:14:33.632 info C:/Users/Administrator/GolandProjects/zb/lckt-server/core/server.go:38 server run success on {"address": ":8888"} diff --git a/main.go b/main.go new file mode 100644 index 0000000..627f763 --- /dev/null +++ b/main.go @@ -0,0 +1,45 @@ +package main + +import ( + "git.echol.cn/loser/lckt/core" + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/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 + +// 这部分 @Tag 设置用于排序, 需要排序的接口请按照下面的格式添加 +// swag init 对 @Tag 只会从入口文件解析, 默认 main.go +// 也可通过 --generalInfo flag 指定其他文件 +// @Tag.Name Base +// @Tag.Name SysUser +// @Tag.Description 用户 + +// @title Gin-Vue-Admin Swagger API接口文档 +// @version v2.8.0 +// @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/middleware/casbin_rbac.go b/middleware/casbin_rbac.go new file mode 100644 index 0000000..b9d6ab8 --- /dev/null +++ b/middleware/casbin_rbac.go @@ -0,0 +1,36 @@ +package middleware + +import ( + "strconv" + "strings" + + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/common/response" + "git.echol.cn/loser/lckt/service" + "git.echol.cn/loser/lckt/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/middleware/cors.go b/middleware/cors.go new file mode 100644 index 0000000..41553b6 --- /dev/null +++ b/middleware/cors.go @@ -0,0 +1,73 @@ +package middleware + +import ( + "git.echol.cn/loser/lckt/config" + "git.echol.cn/loser/lckt/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/middleware/email.go b/middleware/email.go new file mode 100644 index 0000000..2d587ea --- /dev/null +++ b/middleware/email.go @@ -0,0 +1,60 @@ +package middleware + +import ( + "bytes" + "io" + "strconv" + "time" + + "git.echol.cn/loser/lckt/plugin/email/utils" + utils2 "git.echol.cn/loser/lckt/utils" + + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/system" + "git.echol.cn/loser/lckt/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/middleware/error.go b/middleware/error.go new file mode 100644 index 0000000..5cf451c --- /dev/null +++ b/middleware/error.go @@ -0,0 +1,61 @@ +package middleware + +import ( + "net" + "net/http" + "net/http/httputil" + "os" + "runtime/debug" + "strings" + + "git.echol.cn/loser/lckt/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/middleware/jwt.go b/middleware/jwt.go new file mode 100644 index 0000000..09a3aa2 --- /dev/null +++ b/middleware/jwt.go @@ -0,0 +1,80 @@ +package middleware + +import ( + "errors" + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/utils" + "github.com/golang-jwt/jwt/v5" + "strconv" + "time" + + "git.echol.cn/loser/lckt/model/common/response" + "git.echol.cn/loser/lckt/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() + //} + c.Set("claims", claims) + if claims.ExpiresAt.Unix()-time.Now().Unix() < claims.BufferTime { + dr, _ := utils.ParseDuration(global.GVA_CONFIG.JWT.ExpiresTime) + claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(dr)) + newToken, _ := j.CreateTokenByOldToken(token, *claims) + newClaims, _ := j.ParseToken(newToken) + c.Header("new-token", newToken) + c.Header("new-expires-at", strconv.FormatInt(newClaims.ExpiresAt.Unix(), 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/middleware/limit_ip.go b/middleware/limit_ip.go new file mode 100644 index 0000000..0f3d205 --- /dev/null +++ b/middleware/limit_ip.go @@ -0,0 +1,92 @@ +package middleware + +import ( + "context" + "errors" + "net/http" + "time" + + "go.uber.org/zap" + + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/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.Error()}) + 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/middleware/loadtls.go b/middleware/loadtls.go new file mode 100644 index 0000000..a17cf65 --- /dev/null +++ b/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/middleware/logger.go b/middleware/logger.go new file mode 100644 index 0000000..fabc334 --- /dev/null +++ b/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/middleware/operation.go b/middleware/operation.go new file mode 100644 index 0000000..da1017f --- /dev/null +++ b/middleware/operation.go @@ -0,0 +1,133 @@ +package middleware + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "net/url" + "strconv" + "strings" + "sync" + "time" + + "git.echol.cn/loser/lckt/utils" + + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/system" + "git.echol.cn/loser/lckt/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/model/article/article.go b/model/article/article.go new file mode 100644 index 0000000..ee0dcf3 --- /dev/null +++ b/model/article/article.go @@ -0,0 +1,19 @@ +package article + +import ( + "git.echol.cn/loser/lckt/global" +) + +type Article struct { + global.GVA_MODEL + Title string `json:"title" gorm:"comment:文章标题"` + Desc string `json:"desc" gorm:"comment:文章描述"` + Content string `json:"content" gorm:"comment:文章内容"` + CoverImg string `json:"coverImg" gorm:"comment:文章封面图"` + Price int64 `json:"price" gorm:"comment:文章价格(单位为分)"` +} + +// TableName 文章表 +func (Article) TableName() string { + return "article" +} diff --git a/model/article/request/article.go b/model/article/request/article.go new file mode 100644 index 0000000..4ba84b8 --- /dev/null +++ b/model/article/request/article.go @@ -0,0 +1,11 @@ +package request + +import "git.echol.cn/loser/lckt/model/common/request" + +type GetList struct { + request.PageInfo +} + +type DeleteIds struct { + Ids []int `json:"ids" form:"ids" binding:"required"` +} diff --git a/model/bot/bot.go b/model/bot/bot.go new file mode 100644 index 0000000..d9e9f15 --- /dev/null +++ b/model/bot/bot.go @@ -0,0 +1,18 @@ +// 自动生成模板Bot +package bot + +import ( + "git.echol.cn/loser/lckt/global" +) + +// 机器人 结构体 Bot +type Bot struct { + global.GVA_MODEL + Keyword string `json:"keyword" form:"keyword" gorm:"column:keyword;comment:关键词;" binding:"required"` //关键词 + Content *string `json:"content" form:"content" gorm:"column:content;comment:内容;type:text;" binding:"required"` //内容 +} + +// TableName 机器人 Bot自定义表名 bots +func (Bot) TableName() string { + return "bots" +} diff --git a/model/bot/request/bot.go b/model/bot/request/bot.go new file mode 100644 index 0000000..ddd1aad --- /dev/null +++ b/model/bot/request/bot.go @@ -0,0 +1,16 @@ +package request + +import ( + "git.echol.cn/loser/lckt/model/common/request" + "time" +) + +type BotSearch struct { + StartCreatedAt *time.Time `json:"startCreatedAt" form:"startCreatedAt"` + EndCreatedAt *time.Time `json:"endCreatedAt" form:"endCreatedAt"` + request.PageInfo +} + +type FindKey struct { + KeyWord string `json:"keyWord" form:"keyWord"` +} diff --git a/model/category/category.go b/model/category/category.go new file mode 100644 index 0000000..25720a9 --- /dev/null +++ b/model/category/category.go @@ -0,0 +1,20 @@ +// 自动生成模板Category +package category + +import ( + "git.echol.cn/loser/lckt/global" +) + +// 类别 结构体 Category +type Category struct { + global.GVA_MODEL + Name *string `json:"name" form:"name" gorm:"column:name;comment:类别名称;" binding:"required"` //名称 + Order *int `json:"order" form:"order" gorm:"column:order;comment:排序字段;"` //排序 + Active *bool `json:"active" form:"active" gorm:"column:active;comment:是否激活;"` //状态 + ParentId *int `json:"parentId" form:"parentId" gorm:"column:parent_id;comment:父类别ID;"` //父ID +} + +// TableName 类别 Category自定义表名 categories +func (Category) TableName() string { + return "categories" +} diff --git a/model/category/request/category.go b/model/category/request/category.go new file mode 100644 index 0000000..7c164f7 --- /dev/null +++ b/model/category/request/category.go @@ -0,0 +1,14 @@ +package request + +import ( + "git.echol.cn/loser/lckt/model/common/request" + "time" +) + +type CategorySearch struct { + StartCreatedAt *time.Time `json:"startCreatedAt" form:"startCreatedAt"` + EndCreatedAt *time.Time `json:"endCreatedAt" form:"endCreatedAt"` + request.PageInfo + Sort string `json:"sort" form:"sort"` + Order string `json:"order" form:"order"` +} diff --git a/model/common/basetypes.go b/model/common/basetypes.go new file mode 100644 index 0000000..1a133d5 --- /dev/null +++ b/model/common/basetypes.go @@ -0,0 +1,43 @@ +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 +} + +type TreeNode[T any] interface { + GetChildren() []T + SetChildren(children T) + GetID() int + GetParentID() int +} diff --git a/model/common/clearDB.go b/model/common/clearDB.go new file mode 100644 index 0000000..e7fc757 --- /dev/null +++ b/model/common/clearDB.go @@ -0,0 +1,7 @@ +package common + +type ClearDB struct { + TableName string + CompareField string + Interval string +} diff --git a/model/common/request/common.go b/model/common/request/common.go new file mode 100644 index 0000000..c729f3d --- /dev/null +++ b/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/model/common/response/common.go b/model/common/response/common.go new file mode 100644 index 0000000..7461096 --- /dev/null +++ b/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/model/common/response/response.go b/model/common/response/response.go new file mode 100644 index 0000000..a429b12 --- /dev/null +++ b/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/model/example/exa_attachment_category.go b/model/example/exa_attachment_category.go new file mode 100644 index 0000000..c073c18 --- /dev/null +++ b/model/example/exa_attachment_category.go @@ -0,0 +1,16 @@ +package example + +import ( + "git.echol.cn/loser/lckt/global" +) + +type ExaAttachmentCategory struct { + global.GVA_MODEL + Name string `json:"name" form:"name" gorm:"default:null;type:varchar(255);column:name;comment:分类名称;"` + Pid uint `json:"pid" form:"pid" gorm:"default:0;type:int;column:pid;comment:父节点ID;"` + Children []*ExaAttachmentCategory `json:"children" gorm:"-"` +} + +func (ExaAttachmentCategory) TableName() string { + return "exa_attachment_category" +} diff --git a/model/example/exa_breakpoint_continue.go b/model/example/exa_breakpoint_continue.go new file mode 100644 index 0000000..8533ab8 --- /dev/null +++ b/model/example/exa_breakpoint_continue.go @@ -0,0 +1,24 @@ +package example + +import ( + "git.echol.cn/loser/lckt/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/model/example/exa_customer.go b/model/example/exa_customer.go new file mode 100644 index 0000000..a32a1ab --- /dev/null +++ b/model/example/exa_customer.go @@ -0,0 +1,15 @@ +package example + +import ( + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/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/model/example/exa_file_upload_download.go b/model/example/exa_file_upload_download.go new file mode 100644 index 0000000..c3cff1f --- /dev/null +++ b/model/example/exa_file_upload_download.go @@ -0,0 +1,18 @@ +package example + +import ( + "git.echol.cn/loser/lckt/global" +) + +type ExaFileUploadAndDownload struct { + global.GVA_MODEL + Name string `json:"name" form:"name" gorm:"column:name;comment:文件名"` // 文件名 + ClassId int `json:"classId" form:"classId" gorm:"default:0;type:int;column:class_id;comment:分类id;"` // 分类id + Url string `json:"url" form:"url" gorm:"column:url;comment:文件地址"` // 文件地址 + Tag string `json:"tag" form:"tag" gorm:"column:tag;comment:文件标签"` // 文件标签 + Key string `json:"key" form:"key" gorm:"column:key;comment:编号"` // 编号 +} + +func (ExaFileUploadAndDownload) TableName() string { + return "exa_file_upload_and_downloads" +} diff --git a/model/example/request/exa_file_upload_and_downloads.go b/model/example/request/exa_file_upload_and_downloads.go new file mode 100644 index 0000000..eb1a6ba --- /dev/null +++ b/model/example/request/exa_file_upload_and_downloads.go @@ -0,0 +1,10 @@ +package request + +import ( + "git.echol.cn/loser/lckt/model/common/request" +) + +type ExaAttachmentCategorySearch struct { + ClassId int `json:"classId" form:"classId"` + request.PageInfo +} diff --git a/model/example/response/exa_breakpoint_continue.go b/model/example/response/exa_breakpoint_continue.go new file mode 100644 index 0000000..e1345f8 --- /dev/null +++ b/model/example/response/exa_breakpoint_continue.go @@ -0,0 +1,11 @@ +package response + +import "git.echol.cn/loser/lckt/model/example" + +type FilePathResponse struct { + FilePath string `json:"filePath"` +} + +type FileResponse struct { + File example.ExaFile `json:"file"` +} diff --git a/model/example/response/exa_customer.go b/model/example/response/exa_customer.go new file mode 100644 index 0000000..b66e2fc --- /dev/null +++ b/model/example/response/exa_customer.go @@ -0,0 +1,7 @@ +package response + +import "git.echol.cn/loser/lckt/model/example" + +type ExaCustomerResponse struct { + Customer example.ExaCustomer `json:"customer"` +} diff --git a/model/example/response/exa_file_upload_download.go b/model/example/response/exa_file_upload_download.go new file mode 100644 index 0000000..cd19b8f --- /dev/null +++ b/model/example/response/exa_file_upload_download.go @@ -0,0 +1,7 @@ +package response + +import "git.echol.cn/loser/lckt/model/example" + +type ExaFileResponse struct { + File example.ExaFileUploadAndDownload `json:"file"` +} diff --git a/model/system/Class.go b/model/system/Class.go new file mode 100644 index 0000000..5417f9f --- /dev/null +++ b/model/system/Class.go @@ -0,0 +1,12 @@ +package system + +import "git.echol.cn/loser/lckt/global" + +type Class struct { + global.GVA_MODEL + Name string `json:"name" gorm:"comment:分类名"` +} + +func (c *Class) TableName() string { + return "class" +} diff --git a/model/system/article.go b/model/system/article.go new file mode 100644 index 0000000..fb420b9 --- /dev/null +++ b/model/system/article.go @@ -0,0 +1,17 @@ +package system + +import "git.echol.cn/loser/lckt/global" + +type Article struct { + global.GVA_MODEL + Title string `json:"title" gorm:"comment:标题"` + Content string `json:"content" gorm:"comment:内容"` + Desc string `json:"desc" gorm:"comment:简述"` + Price float64 `json:"price" gorm:"comment:价格"` + ClassId int `json:"classId" gorm:"comment:分类ID"` + CoverImg string `json:"coverImg" gorm:"comment:封面图片"` +} + +func (a *Article) TableName() string { + return "article" +} diff --git a/model/system/request/class.go b/model/system/request/class.go new file mode 100644 index 0000000..21000de --- /dev/null +++ b/model/system/request/class.go @@ -0,0 +1,7 @@ +package request + +import common "git.echol.cn/loser/lckt/model/common/request" + +type GetClassList struct { + common.PageInfo +} diff --git a/model/system/request/jwt.go b/model/system/request/jwt.go new file mode 100644 index 0000000..1e1615d --- /dev/null +++ b/model/system/request/jwt.go @@ -0,0 +1,21 @@ +package request + +import ( + jwt "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" +) + +// CustomClaims structure +type CustomClaims struct { + BaseClaims + BufferTime int64 + jwt.RegisteredClaims +} + +type BaseClaims struct { + UUID uuid.UUID + ID uint + Username string + NickName string + AuthorityId uint +} diff --git a/model/system/request/sys_api.go b/model/system/request/sys_api.go new file mode 100644 index 0000000..a129e7f --- /dev/null +++ b/model/system/request/sys_api.go @@ -0,0 +1,14 @@ +package request + +import ( + "git.echol.cn/loser/lckt/model/common/request" + "git.echol.cn/loser/lckt/model/system" +) + +// api分页条件查询及排序结构体 +type SearchApiParams struct { + system.SysApi + request.PageInfo + OrderKey string `json:"orderKey"` // 排序 + Desc bool `json:"desc"` // 排序方式:升序false(默认)|降序true +} diff --git a/model/system/request/sys_authority_btn.go b/model/system/request/sys_authority_btn.go new file mode 100644 index 0000000..98493ff --- /dev/null +++ b/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/model/system/request/sys_auto_code.go b/model/system/request/sys_auto_code.go new file mode 100644 index 0000000..d398a9c --- /dev/null +++ b/model/system/request/sys_auto_code.go @@ -0,0 +1,286 @@ +package request + +import ( + "encoding/json" + "fmt" + "git.echol.cn/loser/lckt/global" + model "git.echol.cn/loser/lckt/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"` // 是否只生成模板 + IsTree bool `json:"isTree" example:"false"` // 是否树形结构 + TreeJson string `json:"treeJson" example:"展示的树json字段"` // 展示的树json字段 + IsAdd bool `json:"isAdd" example:"false"` // 是否新增 + Fields []*AutoCodeField `json:"fields"` + GenerateWeb bool `json:"generateWeb" example:"true"` // 是否生成web + GenerateServer bool `json:"generateServer" example:"true"` // 是否生成server + 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/model/system/request/sys_auto_code_package.go b/model/system/request/sys_auto_code_package.go new file mode 100644 index 0000000..28276e6 --- /dev/null +++ b/model/system/request/sys_auto_code_package.go @@ -0,0 +1,31 @@ +package request + +import ( + "git.echol.cn/loser/lckt/global" + model "git.echol.cn/loser/lckt/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/model/system/request/sys_auto_history.go b/model/system/request/sys_auto_history.go new file mode 100644 index 0000000..22479be --- /dev/null +++ b/model/system/request/sys_auto_history.go @@ -0,0 +1,57 @@ +package request + +import ( + common "git.echol.cn/loser/lckt/model/common/request" + model "git.echol.cn/loser/lckt/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, + Abbreviation: 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/model/system/request/sys_casbin.go b/model/system/request/sys_casbin.go new file mode 100644 index 0000000..3ca4212 --- /dev/null +++ b/model/system/request/sys_casbin.go @@ -0,0 +1,27 @@ +package request + +// CasbinInfo Casbin info structure +type CasbinInfo struct { + Path string `json:"path"` // 路径 + Method string `json:"method"` // 方法 +} + +// CasbinInReceive 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/model/system/request/sys_dictionary_detail.go b/model/system/request/sys_dictionary_detail.go new file mode 100644 index 0000000..746c528 --- /dev/null +++ b/model/system/request/sys_dictionary_detail.go @@ -0,0 +1,11 @@ +package request + +import ( + "git.echol.cn/loser/lckt/model/common/request" + "git.echol.cn/loser/lckt/model/system" +) + +type SysDictionaryDetailSearch struct { + system.SysDictionaryDetail + request.PageInfo +} diff --git a/model/system/request/sys_export_template.go b/model/system/request/sys_export_template.go new file mode 100644 index 0000000..0afd6ba --- /dev/null +++ b/model/system/request/sys_export_template.go @@ -0,0 +1,14 @@ +package request + +import ( + "git.echol.cn/loser/lckt/model/common/request" + "git.echol.cn/loser/lckt/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/model/system/request/sys_init.go b/model/system/request/sys_init.go new file mode 100644 index 0000000..dd32633 --- /dev/null +++ b/model/system/request/sys_init.go @@ -0,0 +1,124 @@ +package request + +import ( + "fmt" + "git.echol.cn/loser/lckt/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数据库文件路径 + Template string `json:"template"` // postgresql指定template +} + +// 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/model/system/request/sys_menu.go b/model/system/request/sys_menu.go new file mode 100644 index 0000000..991838b --- /dev/null +++ b/model/system/request/sys_menu.go @@ -0,0 +1,27 @@ +package request + +import ( + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/system" +) + +// AddMenuAuthorityInfo 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/model/system/request/sys_operation_record.go b/model/system/request/sys_operation_record.go new file mode 100644 index 0000000..53a5073 --- /dev/null +++ b/model/system/request/sys_operation_record.go @@ -0,0 +1,11 @@ +package request + +import ( + "git.echol.cn/loser/lckt/model/common/request" + "git.echol.cn/loser/lckt/model/system" +) + +type SysOperationRecordSearch struct { + system.SysOperationRecord + request.PageInfo +} diff --git a/model/system/request/sys_params.go b/model/system/request/sys_params.go new file mode 100644 index 0000000..45c2d48 --- /dev/null +++ b/model/system/request/sys_params.go @@ -0,0 +1,14 @@ +package request + +import ( + "git.echol.cn/loser/lckt/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/model/system/request/sys_user.go b/model/system/request/sys_user.go new file mode 100644 index 0000000..70c47ce --- /dev/null +++ b/model/system/request/sys_user.go @@ -0,0 +1,65 @@ +package request + +import ( + common "git.echol.cn/loser/lckt/model/common/request" + "git.echol.cn/loser/lckt/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:"电子邮箱"` +} + +// Login User login structure +type Login struct { + Username string `json:"username"` // 用户名 + Password string `json:"password"` // 密码 + Captcha string `json:"captcha"` // 验证码 + CaptchaId string `json:"captchaId"` // 验证码ID +} + +// ChangePasswordReq Modify password structure +type ChangePasswordReq struct { + ID uint `json:"-"` // 从 JWT 中提取 user id,避免越权 + Password string `json:"password"` // 密码 + NewPassword string `json:"newPassword"` // 新密码 +} + +// SetUserAuth Modify user's auth structure +type SetUserAuth struct { + AuthorityId uint `json:"authorityId"` // 角色ID +} + +// SetUserAuthorities 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:冻结用户"` //冻结用户 + 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"` +} diff --git a/model/system/response/sys_api.go b/model/system/response/sys_api.go new file mode 100644 index 0000000..d32b7dd --- /dev/null +++ b/model/system/response/sys_api.go @@ -0,0 +1,18 @@ +package response + +import ( + "git.echol.cn/loser/lckt/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/model/system/response/sys_authority.go b/model/system/response/sys_authority.go new file mode 100644 index 0000000..d49f699 --- /dev/null +++ b/model/system/response/sys_authority.go @@ -0,0 +1,12 @@ +package response + +import "git.echol.cn/loser/lckt/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/model/system/response/sys_authority_btn.go b/model/system/response/sys_authority_btn.go new file mode 100644 index 0000000..2f772cf --- /dev/null +++ b/model/system/response/sys_authority_btn.go @@ -0,0 +1,5 @@ +package response + +type SysAuthorityBtnRes struct { + Selected []uint `json:"selected"` +} diff --git a/model/system/response/sys_auto_code.go b/model/system/response/sys_auto_code.go new file mode 100644 index 0000000..9e99bde --- /dev/null +++ b/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/model/system/response/sys_captcha.go b/model/system/response/sys_captcha.go new file mode 100644 index 0000000..0c3995a --- /dev/null +++ b/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/model/system/response/sys_casbin.go b/model/system/response/sys_casbin.go new file mode 100644 index 0000000..004fcd5 --- /dev/null +++ b/model/system/response/sys_casbin.go @@ -0,0 +1,9 @@ +package response + +import ( + "git.echol.cn/loser/lckt/model/system/request" +) + +type PolicyPathResponse struct { + Paths []request.CasbinInfo `json:"paths"` +} diff --git a/model/system/response/sys_menu.go b/model/system/response/sys_menu.go new file mode 100644 index 0000000..59d955d --- /dev/null +++ b/model/system/response/sys_menu.go @@ -0,0 +1,15 @@ +package response + +import "git.echol.cn/loser/lckt/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/model/system/response/sys_system.go b/model/system/response/sys_system.go new file mode 100644 index 0000000..c746f8d --- /dev/null +++ b/model/system/response/sys_system.go @@ -0,0 +1,7 @@ +package response + +import "git.echol.cn/loser/lckt/config" + +type SysConfigResponse struct { + Config config.Server `json:"config"` +} diff --git a/model/system/response/sys_user.go b/model/system/response/sys_user.go new file mode 100644 index 0000000..94325c0 --- /dev/null +++ b/model/system/response/sys_user.go @@ -0,0 +1,15 @@ +package response + +import ( + "git.echol.cn/loser/lckt/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/model/system/sys_api.go b/model/system/sys_api.go new file mode 100644 index 0000000..c9031fb --- /dev/null +++ b/model/system/sys_api.go @@ -0,0 +1,28 @@ +package system + +import ( + "git.echol.cn/loser/lckt/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/model/system/sys_authority.go b/model/system/sys_authority.go new file mode 100644 index 0000000..01c5efa --- /dev/null +++ b/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/model/system/sys_authority_btn.go b/model/system/sys_authority_btn.go new file mode 100644 index 0000000..e005984 --- /dev/null +++ b/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/model/system/sys_authority_menu.go b/model/system/sys_authority_menu.go new file mode 100644 index 0000000..4467a7e --- /dev/null +++ b/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/model/system/sys_auto_code_history.go b/model/system/sys_auto_code_history.go new file mode 100644 index 0000000..e871346 --- /dev/null +++ b/model/system/sys_auto_code_history.go @@ -0,0 +1,68 @@ +package system + +import ( + "git.echol.cn/loser/lckt/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:结构体名称"` + Abbreviation string `json:"abbreviation" gorm:"column:abbreviation;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/model/system/sys_auto_code_package.go b/model/system/sys_auto_code_package.go new file mode 100644 index 0000000..312ab52 --- /dev/null +++ b/model/system/sys_auto_code_package.go @@ -0,0 +1,18 @@ +package system + +import ( + "git.echol.cn/loser/lckt/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/model/system/sys_base_menu.go b/model/system/sys_base_menu.go new file mode 100644 index 0000000..b0bf8dc --- /dev/null +++ b/model/system/sys_base_menu.go @@ -0,0 +1,42 @@ +package system + +import ( + "git.echol.cn/loser/lckt/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/model/system/sys_dictionary.go b/model/system/sys_dictionary.go new file mode 100644 index 0000000..30c73d2 --- /dev/null +++ b/model/system/sys_dictionary.go @@ -0,0 +1,20 @@ +// 自动生成模板SysDictionary +package system + +import ( + "git.echol.cn/loser/lckt/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/model/system/sys_dictionary_detail.go b/model/system/sys_dictionary_detail.go new file mode 100644 index 0000000..0b5a88d --- /dev/null +++ b/model/system/sys_dictionary_detail.go @@ -0,0 +1,21 @@ +// 自动生成模板SysDictionaryDetail +package system + +import ( + "git.echol.cn/loser/lckt/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/model/system/sys_export_template.go b/model/system/sys_export_template.go new file mode 100644 index 0000000..6b9e017 --- /dev/null +++ b/model/system/sys_export_template.go @@ -0,0 +1,44 @@ +// 自动生成模板SysExportTemplate +package system + +import ( + "git.echol.cn/loser/lckt/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/model/system/sys_jwt_blacklist.go b/model/system/sys_jwt_blacklist.go new file mode 100644 index 0000000..94bc30f --- /dev/null +++ b/model/system/sys_jwt_blacklist.go @@ -0,0 +1,10 @@ +package system + +import ( + "git.echol.cn/loser/lckt/global" +) + +type JwtBlacklist struct { + global.GVA_MODEL + Jwt string `gorm:"type:text;comment:jwt"` +} diff --git a/model/system/sys_menu_btn.go b/model/system/sys_menu_btn.go new file mode 100644 index 0000000..bd544a1 --- /dev/null +++ b/model/system/sys_menu_btn.go @@ -0,0 +1,10 @@ +package system + +import "git.echol.cn/loser/lckt/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/model/system/sys_operation_record.go b/model/system/sys_operation_record.go new file mode 100644 index 0000000..9c5bda8 --- /dev/null +++ b/model/system/sys_operation_record.go @@ -0,0 +1,24 @@ +// 自动生成模板SysOperationRecord +package system + +import ( + "time" + + "git.echol.cn/loser/lckt/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/model/system/sys_params.go b/model/system/sys_params.go new file mode 100644 index 0000000..4395ac8 --- /dev/null +++ b/model/system/sys_params.go @@ -0,0 +1,20 @@ +// 自动生成模板SysParams +package system + +import ( + "git.echol.cn/loser/lckt/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/model/system/sys_system.go b/model/system/sys_system.go new file mode 100644 index 0000000..c66706d --- /dev/null +++ b/model/system/sys_system.go @@ -0,0 +1,10 @@ +package system + +import ( + "git.echol.cn/loser/lckt/config" +) + +// 配置文件结构体 +type System struct { + Config config.Server `json:"config"` +} diff --git a/model/system/sys_user.go b/model/system/sys_user.go new file mode 100644 index 0000000..19977be --- /dev/null +++ b/model/system/sys_user.go @@ -0,0 +1,62 @@ +package system + +import ( + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/common" + "github.com/google/uuid" +) + +type Login interface { + GetUsername() string + GetNickname() string + GetUUID() uuid.UUID + GetUserId() uint + GetAuthorityId() uint + GetUserInfo() any +} + +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 +} diff --git a/model/system/sys_user_authority.go b/model/system/sys_user_authority.go new file mode 100644 index 0000000..1aa83cb --- /dev/null +++ b/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/plugin/announcement/api/enter.go b/plugin/announcement/api/enter.go new file mode 100644 index 0000000..289a6cc --- /dev/null +++ b/plugin/announcement/api/enter.go @@ -0,0 +1,10 @@ +package api + +import "git.echol.cn/loser/lckt/plugin/announcement/service" + +var ( + Api = new(api) + serviceInfo = service.Service.Info +) + +type api struct{ Info info } diff --git a/plugin/announcement/api/info.go b/plugin/announcement/api/info.go new file mode 100644 index 0000000..c3a0297 --- /dev/null +++ b/plugin/announcement/api/info.go @@ -0,0 +1,183 @@ +package api + +import ( + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/common/response" + "git.echol.cn/loser/lckt/plugin/announcement/model" + "git.echol.cn/loser/lckt/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/plugin/announcement/config/config.go b/plugin/announcement/config/config.go new file mode 100644 index 0000000..809bc99 --- /dev/null +++ b/plugin/announcement/config/config.go @@ -0,0 +1,4 @@ +package config + +type Config struct { +} diff --git a/plugin/announcement/gen/gen.go b/plugin/announcement/gen/gen.go new file mode 100644 index 0000000..115c008 --- /dev/null +++ b/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 + "git.echol.cn/loser/lckt/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/plugin/announcement/initialize/api.go b/plugin/announcement/initialize/api.go new file mode 100644 index 0000000..217345d --- /dev/null +++ b/plugin/announcement/initialize/api.go @@ -0,0 +1,49 @@ +package initialize + +import ( + "context" + model "git.echol.cn/loser/lckt/model/system" + "git.echol.cn/loser/lckt/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/plugin/announcement/initialize/gorm.go b/plugin/announcement/initialize/gorm.go new file mode 100644 index 0000000..ec76a3a --- /dev/null +++ b/plugin/announcement/initialize/gorm.go @@ -0,0 +1,20 @@ +package initialize + +import ( + "context" + "fmt" + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/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/plugin/announcement/initialize/menu.go b/plugin/announcement/initialize/menu.go new file mode 100644 index 0000000..ac8efed --- /dev/null +++ b/plugin/announcement/initialize/menu.go @@ -0,0 +1,22 @@ +package initialize + +import ( + "context" + model "git.echol.cn/loser/lckt/model/system" + "git.echol.cn/loser/lckt/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/plugin/announcement/initialize/router.go b/plugin/announcement/initialize/router.go new file mode 100644 index 0000000..9db476f --- /dev/null +++ b/plugin/announcement/initialize/router.go @@ -0,0 +1,15 @@ +package initialize + +import ( + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/middleware" + "git.echol.cn/loser/lckt/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/plugin/announcement/initialize/viper.go b/plugin/announcement/initialize/viper.go new file mode 100644 index 0000000..70e0bc5 --- /dev/null +++ b/plugin/announcement/initialize/viper.go @@ -0,0 +1,17 @@ +package initialize + +import ( + "fmt" + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/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/plugin/announcement/model/info.go b/plugin/announcement/model/info.go new file mode 100644 index 0000000..c3db1c1 --- /dev/null +++ b/plugin/announcement/model/info.go @@ -0,0 +1,20 @@ +package model + +import ( + "git.echol.cn/loser/lckt/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/plugin/announcement/model/request/info.go b/plugin/announcement/model/request/info.go new file mode 100644 index 0000000..f57f946 --- /dev/null +++ b/plugin/announcement/model/request/info.go @@ -0,0 +1,12 @@ +package request + +import ( + "git.echol.cn/loser/lckt/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/plugin/announcement/plugin.go b/plugin/announcement/plugin.go new file mode 100644 index 0000000..a9bce64 --- /dev/null +++ b/plugin/announcement/plugin.go @@ -0,0 +1,26 @@ +package announcement + +import ( + "context" + "git.echol.cn/loser/lckt/plugin/announcement/initialize" + interfaces "git.echol.cn/loser/lckt/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/plugin/announcement/plugin/plugin.go b/plugin/announcement/plugin/plugin.go new file mode 100644 index 0000000..673a975 --- /dev/null +++ b/plugin/announcement/plugin/plugin.go @@ -0,0 +1,5 @@ +package plugin + +import "git.echol.cn/loser/lckt/plugin/announcement/config" + +var Config config.Config diff --git a/plugin/announcement/router/enter.go b/plugin/announcement/router/enter.go new file mode 100644 index 0000000..8b6dc6d --- /dev/null +++ b/plugin/announcement/router/enter.go @@ -0,0 +1,10 @@ +package router + +import "git.echol.cn/loser/lckt/plugin/announcement/api" + +var ( + Router = new(router) + apiInfo = api.Api.Info +) + +type router struct{ Info info } diff --git a/plugin/announcement/router/info.go b/plugin/announcement/router/info.go new file mode 100644 index 0000000..66e7f22 --- /dev/null +++ b/plugin/announcement/router/info.go @@ -0,0 +1,31 @@ +package router + +import ( + "git.echol.cn/loser/lckt/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/plugin/announcement/service/enter.go b/plugin/announcement/service/enter.go new file mode 100644 index 0000000..988fbcd --- /dev/null +++ b/plugin/announcement/service/enter.go @@ -0,0 +1,5 @@ +package service + +var Service = new(service) + +type service struct{ Info info } diff --git a/plugin/announcement/service/info.go b/plugin/announcement/service/info.go new file mode 100644 index 0000000..8942371 --- /dev/null +++ b/plugin/announcement/service/info.go @@ -0,0 +1,78 @@ +package service + +import ( + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/plugin/announcement/model" + "git.echol.cn/loser/lckt/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/plugin/email/README.MD b/plugin/email/README.MD new file mode 100644 index 0000000..1720283 --- /dev/null +++ b/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/plugin/email/api/enter.go b/plugin/email/api/enter.go new file mode 100644 index 0000000..353404d --- /dev/null +++ b/plugin/email/api/enter.go @@ -0,0 +1,7 @@ +package api + +type ApiGroup struct { + EmailApi +} + +var ApiGroupApp = new(ApiGroup) diff --git a/plugin/email/api/sys_email.go b/plugin/email/api/sys_email.go new file mode 100644 index 0000000..a6780d1 --- /dev/null +++ b/plugin/email/api/sys_email.go @@ -0,0 +1,53 @@ +package api + +import ( + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/common/response" + email_response "git.echol.cn/loser/lckt/plugin/email/model/response" + "git.echol.cn/loser/lckt/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/plugin/email/config/email.go b/plugin/email/config/email.go new file mode 100644 index 0000000..c535348 --- /dev/null +++ b/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/plugin/email/global/gloabl.go b/plugin/email/global/gloabl.go new file mode 100644 index 0000000..f1c2ae7 --- /dev/null +++ b/plugin/email/global/gloabl.go @@ -0,0 +1,5 @@ +package global + +import "git.echol.cn/loser/lckt/plugin/email/config" + +var GlobalConfig = new(config.Email) diff --git a/plugin/email/main.go b/plugin/email/main.go new file mode 100644 index 0000000..ae5bae5 --- /dev/null +++ b/plugin/email/main.go @@ -0,0 +1,28 @@ +package email + +import ( + "git.echol.cn/loser/lckt/plugin/email/global" + "git.echol.cn/loser/lckt/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/plugin/email/model/response/email.go b/plugin/email/model/response/email.go new file mode 100644 index 0000000..ed25475 --- /dev/null +++ b/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/plugin/email/router/enter.go b/plugin/email/router/enter.go new file mode 100644 index 0000000..e081a54 --- /dev/null +++ b/plugin/email/router/enter.go @@ -0,0 +1,7 @@ +package router + +type RouterGroup struct { + EmailRouter +} + +var RouterGroupApp = new(RouterGroup) diff --git a/plugin/email/router/sys_email.go b/plugin/email/router/sys_email.go new file mode 100644 index 0000000..b66b3e2 --- /dev/null +++ b/plugin/email/router/sys_email.go @@ -0,0 +1,19 @@ +package router + +import ( + "git.echol.cn/loser/lckt/middleware" + "git.echol.cn/loser/lckt/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/plugin/email/service/enter.go b/plugin/email/service/enter.go new file mode 100644 index 0000000..e96e267 --- /dev/null +++ b/plugin/email/service/enter.go @@ -0,0 +1,7 @@ +package service + +type ServiceGroup struct { + EmailService +} + +var ServiceGroupApp = new(ServiceGroup) diff --git a/plugin/email/service/sys_email.go b/plugin/email/service/sys_email.go new file mode 100644 index 0000000..b303e74 --- /dev/null +++ b/plugin/email/service/sys_email.go @@ -0,0 +1,32 @@ +package service + +import ( + "git.echol.cn/loser/lckt/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/plugin/email/utils/email.go b/plugin/email/utils/email.go new file mode 100644 index 0000000..5877648 --- /dev/null +++ b/plugin/email/utils/email.go @@ -0,0 +1,82 @@ +package utils + +import ( + "crypto/tls" + "fmt" + "net/smtp" + "strings" + + "git.echol.cn/loser/lckt/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/plugin/plugin-tool/utils/check.go b/plugin/plugin-tool/utils/check.go new file mode 100644 index 0000000..bb623b7 --- /dev/null +++ b/plugin/plugin-tool/utils/check.go @@ -0,0 +1,50 @@ +package utils + +import ( + "fmt" + + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/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/resource/function/api.go.tpl b/resource/function/api.go.tpl new file mode 100644 index 0000000..35a1cd5 --- /dev/null +++ b/resource/function/api.go.tpl @@ -0,0 +1,44 @@ +{{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) { + // 创建业务用Context + ctx := c.Request.Context() + // 请添加自己的业务逻辑 + err := service{{ .StructName }}.{{.FuncName}}(ctx) + 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) { + // 创建业务用Context + ctx := c.Request.Context() + // 请添加自己的业务逻辑 + err := {{.Abbreviation}}Service.{{.FuncName}}(ctx) + if err != nil { + global.GVA_LOG.Error("失败!", zap.Error(err)) + response.FailWithMessage("失败", c) + return + } + response.OkWithData("返回数据",c) +} +{{end}} diff --git a/resource/function/api.js.tpl b/resource/function/api.js.tpl new file mode 100644 index 0000000..a07b102 --- /dev/null +++ b/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 -}} diff --git a/resource/function/server.go.tpl b/resource/function/server.go.tpl new file mode 100644 index 0000000..7327604 --- /dev/null +++ b/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}}(ctx context.Context) (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}}(ctx context.Context) (err error) { + // 请在这里实现自己的业务逻辑 + db := {{$db}}.Model(&{{.Package}}.{{.StructName}}{}) + return db.Error +} +{{end}} diff --git a/resource/package/readme.txt.tpl b/resource/package/readme.txt.tpl new file mode 100644 index 0000000..9bd5418 --- /dev/null +++ b/resource/package/readme.txt.tpl @@ -0,0 +1,7 @@ +代码解压后把fe的api文件内容粘贴进前端api文件夹下并修改为自己想要的名字即可 + +后端代码解压后同理,放到自己想要的 mvc对应路径 并且到 initRouter中注册自动生成的路由 到registerTable中注册自动生成的model + +项目github:"https://github.com/piexlmax/git.echol.cn/loser/lckt" + +希望大家给个star多多鼓励 diff --git a/resource/package/server/api/api.go.tpl b/resource/package/server/api/api.go.tpl new file mode 100644 index 0000000..528487f --- /dev/null +++ b/resource/package/server/api/api.go.tpl @@ -0,0 +1,260 @@ +package {{.Package}} + +import ( + {{if not .OnlyTemplate}} + "{{.Module}}/global" + "{{.Module}}/model/common/response" + "{{.Module}}/model/{{.Package}}" + {{- if not .IsTree}} + {{.Package}}Req "{{.Module}}/model/{{.Package}}/request" + {{- end }} + "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) { + // 创建业务用Context + ctx := c.Request.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}}(ctx,&{{.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) { + // 创建业务用Context + ctx := c.Request.Context() + + {{.PrimaryField.FieldJson}} := c.Query("{{.PrimaryField.FieldJson}}") + {{- if .AutoCreateResource }} + userID := utils.GetUserID(c) + {{- end }} + err := {{.Abbreviation}}Service.Delete{{.StructName}}(ctx,{{.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) { + // 创建业务用Context + ctx := c.Request.Context() + + {{.PrimaryField.FieldJson}}s := c.QueryArray("{{.PrimaryField.FieldJson}}s[]") + {{- if .AutoCreateResource }} + userID := utils.GetUserID(c) + {{- end }} + err := {{.Abbreviation}}Service.Delete{{.StructName}}ByIds(ctx,{{.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) { + // 从ctx获取标准context进行业务行为 + ctx := c.Request.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}}(ctx,{{.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 {{.PrimaryField.FieldJson}} query {{.PrimaryField.FieldType}} 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) { + // 创建业务用Context + ctx := c.Request.Context() + + {{.PrimaryField.FieldJson}} := c.Query("{{.PrimaryField.FieldJson}}") + re{{.Abbreviation}}, err := {{.Abbreviation}}Service.Get{{.StructName}}(ctx,{{.PrimaryField.FieldJson}}) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败:" + err.Error(), c) + return + } + response.OkWithData(re{{.Abbreviation}}, c) +} + +{{- if .IsTree }} +// Get{{.StructName}}List 分页获取{{.Description}}列表,Tree模式下不接受参数 +// @Tags {{.StructName}} +// @Summary 分页获取{{.Description}}列表 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @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) { + // 创建业务用Context + ctx := c.Request.Context() + + list, err := {{.Abbreviation}}Service.Get{{.StructName}}InfoList(ctx) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败:" + err.Error(), c) + return + } + response.OkWithDetailed(list, "获取成功", c) +} +{{- else }} +// 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) { + // 创建业务用Context + ctx := c.Request.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(ctx,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) +} +{{- end }} + +{{- 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) { + // 创建业务用Context + ctx := c.Request.Context() + + // 此接口为获取数据源定义的数据 + dataSource, err := {{.Abbreviation}}Service.Get{{.StructName}}DataSource(ctx) + 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 +// @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) { + // 创建业务用Context + ctx := c.Request.Context() + + // 此接口不需要鉴权 + // 示例为返回了一个固定的消息接口,一般本接口用于C端服务,需要自己实现业务逻辑 + {{.Abbreviation}}Service.Get{{.StructName}}Public(ctx) + response.OkWithDetailed(gin.H{ + "info": "不需要鉴权的{{.Description}}接口信息", + }, "获取成功", c) +} diff --git a/resource/package/server/api/enter.go.tpl b/resource/package/server/api/enter.go.tpl new file mode 100644 index 0000000..778b314 --- /dev/null +++ b/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/resource/package/server/model/model.go.tpl b/resource/package/server/model/model.go.tpl new file mode 100644 index 0000000..3114b9c --- /dev/null +++ b/resource/package/server/model/model.go.tpl @@ -0,0 +1,111 @@ +{{- 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 }} + {{- if .IsTree }} + Children []*{{.StructName}} `json:"children" gorm:"-"` //子节点 + ParentID int `json:"parentID" gorm:"column:parent_id;comment:父节点"` + {{- end }} +{{- end }} +} + +{{ if .TableName }} +// TableName {{.Description}} {{.StructName}}自定义表名 {{.TableName}} +func ({{.StructName}}) TableName() string { + return "{{.TableName}}" +} +{{ end }} + +{{if .IsTree }} +// GetChildren 实现TreeNode接口 +func (s *{{.StructName}}) GetChildren() []*{{.StructName}} { + return s.Children +} + +// SetChildren 实现TreeNode接口 +func (s *{{.StructName}}) SetChildren(children *{{.StructName}}) { + s.Children = append(s.Children, children) +} + +// GetID 实现TreeNode接口 +func (s *{{.StructName}}) GetID() int { + return int({{if not .GvaModel}}*{{- end }}s.{{.PrimaryField.FieldName}}) +} + +// GetParentID 实现TreeNode接口 +func (s *{{.StructName}}) GetParentID() int { + return s.ParentID +} +{{ end }} + +{{ end }} diff --git a/resource/package/server/model/request/request.go.tpl b/resource/package/server/model/request/request.go.tpl new file mode 100644 index 0000000..ee5816d --- /dev/null +++ b/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/resource/package/server/router/enter.go.tpl b/resource/package/server/router/enter.go.tpl new file mode 100644 index 0000000..178aecf --- /dev/null +++ b/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/resource/package/server/router/router.go.tpl b/resource/package/server/router/router.go.tpl new file mode 100644 index 0000000..cac47ab --- /dev/null +++ b/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/resource/package/server/service/enter.go.tpl b/resource/package/server/service/enter.go.tpl new file mode 100644 index 0000000..adf1db0 --- /dev/null +++ b/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/resource/package/server/service/service.go.tpl b/resource/package/server/service/service.go.tpl new file mode 100644 index 0000000..d0cbb4f --- /dev/null +++ b/resource/package/server/service/service.go.tpl @@ -0,0 +1,251 @@ +{{- $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 }} + "context" + "{{.Module}}/global" + "{{.Module}}/model/{{.Package}}" + {{- if not .IsTree}} + {{.Package}}Req "{{.Module}}/model/{{.Package}}/request" + {{- else }} + "{{.Module}}/utils" + "errors" + {{- end }} + {{- 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}}(ctx context.Context, {{.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}}(ctx context.Context, {{.PrimaryField.FieldJson}} string{{- if .AutoCreateResource -}},userID uint{{- end -}}) (err error) { + {{- if .IsTree }} + var count int64 + err = {{$db}}.Find(&{{.Package}}.{{.StructName}}{},"parent_id = ?",{{.PrimaryField.FieldJson}}).Count(&count).Error + if count > 0 { + return errors.New("此节点存在子节点不允许删除") + } + if err != nil { + return err + } + {{- end }} + + {{- 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(ctx context.Context, {{.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}}(ctx context.Context, {{.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}}(ctx context.Context, {{.PrimaryField.FieldJson}} string) ({{.Abbreviation}} {{.Package}}.{{.StructName}}, err error) { + err = {{$db}}.Where("{{.PrimaryField.ColumnName}} = ?", {{.PrimaryField.FieldJson}}).First(&{{.Abbreviation}}).Error + return +} + + +{{- if .IsTree }} +// Get{{.StructName}}InfoList 分页获取{{.Description}}记录,Tree模式下不添加分页和搜索 +// Author [yourname](https://github.com/yourname) +func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}InfoList(ctx context.Context) (list []*{{.Package}}.{{.StructName}},err error) { + // 创建db + db := {{$db}}.Model(&{{.Package}}.{{.StructName}}{}) + var {{.Abbreviation}}s []*{{.Package}}.{{.StructName}} + + err = db.Find(&{{.Abbreviation}}s).Error + + return utils.BuildTree({{.Abbreviation}}s), err +} +{{- else }} +// Get{{.StructName}}InfoList 分页获取{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}InfoList(ctx context.Context, 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 +} + +{{- end }} + +{{- if .HasDataSource }} +func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}DataSource(ctx context.Context) (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(ctx context.Context) { + // 此方法为获取数据源定义的数据 + // 请自行实现 +} +{{- end }} diff --git a/resource/package/web/api/api.js.tpl b/resource/package/web/api/api.js.tpl new file mode 100644 index 0000000..a41ef6f --- /dev/null +++ b/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/resource/package/web/view/form.vue.tpl b/resource/package/web/view/form.vue.tpl new file mode 100644 index 0000000..e45b375 --- /dev/null +++ b/resource/package/web/view/form.vue.tpl @@ -0,0 +1,464 @@ +{{- 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 .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/resource/package/web/view/table.vue.tpl b/resource/package/web/view/table.vue.tpl new file mode 100644 index 0000000..c6d5f55 --- /dev/null +++ b/resource/package/web/view/table.vue.tpl @@ -0,0 +1,1363 @@ +{{- $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}} + + {{- 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" }} + {{- if .DictType}} + + + + {{- else }} + + {{- end }} + {{- 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 .CheckDataSource }} + + {{- else 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 "richtext" }} + + {{- 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 .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/resource/plugin/server/api/api.go.template b/resource/plugin/server/api/api.go.template new file mode 100644 index 0000000..e69ae82 --- /dev/null +++ b/resource/plugin/server/api/api.go.template @@ -0,0 +1,255 @@ +package api + +import ( +{{if not .OnlyTemplate}} + "{{.Module}}/global" + "{{.Module}}/model/common/response" + "{{.Module}}/plugin/{{.Package}}/model" + {{- if not .IsTree}} + "{{.Module}}/plugin/{{.Package}}/model/request" + {{- end }} + "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) { + // 创建业务用Context + ctx := c.Request.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}}(ctx,&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) { + // 创建业务用Context + ctx := c.Request.Context() + + {{.PrimaryField.FieldJson}} := c.Query("{{.PrimaryField.FieldJson}}") +{{- if .AutoCreateResource }} + userID := utils.GetUserID(c) +{{- end }} + err := service{{ .StructName }}.Delete{{.StructName}}(ctx,{{.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) { + // 创建业务用Context + ctx := c.Request.Context() + + {{.PrimaryField.FieldJson}}s := c.QueryArray("{{.PrimaryField.FieldJson}}s[]") +{{- if .AutoCreateResource }} + userID := utils.GetUserID(c) +{{- end }} + err := service{{ .StructName }}.Delete{{.StructName}}ByIds(ctx,{{.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) { + // 创建业务用Context + ctx := c.Request.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}}(ctx,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 {{.PrimaryField.FieldJson}} query {{.PrimaryField.FieldType}} 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) { + // 创建业务用Context + ctx := c.Request.Context() + + {{.PrimaryField.FieldJson}} := c.Query("{{.PrimaryField.FieldJson}}") + re{{.Abbreviation}}, err := service{{ .StructName }}.Get{{.StructName}}(ctx,{{.PrimaryField.FieldJson}}) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败:" + err.Error(), c) + return + } + response.OkWithData(re{{.Abbreviation}}, c) +} + +{{- if .IsTree }} +// Get{{.StructName}}List 分页获取{{.Description}}列表 +// @Tags {{.StructName}} +// @Summary 分页获取{{.Description}}列表 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @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) { + // 创建业务用Context + ctx := c.Request.Context() + + list, err := service{{ .StructName }}.Get{{.StructName}}InfoList(ctx) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败:" + err.Error(), c) + return + } + response.OkWithDetailed(list, "获取成功", c) +} +{{- else }} +// 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) { + // 创建业务用Context + ctx := c.Request.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(ctx,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) +} +{{- end }} + +{{- 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) { + // 创建业务用Context + ctx := c.Request.Context() + + // 此接口为获取数据源定义的数据 + dataSource, err := service{{ .StructName }}.Get{{.StructName}}DataSource(ctx) + 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 +// @Success 200 {object} response.Response{data=object,msg=string} "获取成功" +// @Router /{{.Abbreviation}}/get{{.StructName}}Public [get] +func (a *{{.Abbreviation}}) Get{{.StructName}}Public(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + // 此接口不需要鉴权 示例为返回了一个固定的消息接口,一般本接口用于C端服务,需要自己实现业务逻辑 + service{{ .StructName }}.Get{{.StructName}}Public(ctx) + response.OkWithDetailed(gin.H{"info": "不需要鉴权的{{.Description}}接口信息"}, "获取成功", c) +} diff --git a/resource/plugin/server/api/enter.go.template b/resource/plugin/server/api/enter.go.template new file mode 100644 index 0000000..989fb35 --- /dev/null +++ b/resource/plugin/server/api/enter.go.template @@ -0,0 +1,6 @@ +package api + +var Api = new(api) + +type api struct { +} diff --git a/resource/plugin/server/config/config.go.template b/resource/plugin/server/config/config.go.template new file mode 100644 index 0000000..809bc99 --- /dev/null +++ b/resource/plugin/server/config/config.go.template @@ -0,0 +1,4 @@ +package config + +type Config struct { +} diff --git a/resource/plugin/server/gen/gen.go.template b/resource/plugin/server/gen/gen.go.template new file mode 100644 index 0000000..5639d4a --- /dev/null +++ b/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/resource/plugin/server/initialize/api.go.template b/resource/plugin/server/initialize/api.go.template new file mode 100644 index 0000000..dfbea23 --- /dev/null +++ b/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/resource/plugin/server/initialize/gorm.go.template b/resource/plugin/server/initialize/gorm.go.template new file mode 100644 index 0000000..52c8183 --- /dev/null +++ b/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/resource/plugin/server/initialize/menu.go.template b/resource/plugin/server/initialize/menu.go.template new file mode 100644 index 0000000..8774f35 --- /dev/null +++ b/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/resource/plugin/server/initialize/router.go.template b/resource/plugin/server/initialize/router.go.template new file mode 100644 index 0000000..fbf03a3 --- /dev/null +++ b/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/resource/plugin/server/initialize/viper.go.template b/resource/plugin/server/initialize/viper.go.template new file mode 100644 index 0000000..e759ad6 --- /dev/null +++ b/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/resource/plugin/server/model/model.go.template b/resource/plugin/server/model/model.go.template new file mode 100644 index 0000000..0483771 --- /dev/null +++ b/resource/plugin/server/model/model.go.template @@ -0,0 +1,112 @@ +{{- 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 }} + {{- if .IsTree }} + Children []*{{.StructName}} `json:"children" gorm:"-"` //子节点 + ParentID int `json:"parentID" gorm:"column:parent_id;comment:父节点"` + {{- end }} + {{- end }} +} + +{{ if .TableName }} +// TableName {{.Description}} {{.StructName}}自定义表名 {{.TableName}} +func ({{.StructName}}) TableName() string { + return "{{.TableName}}" +} +{{ end }} + + +{{if .IsTree }} +// GetChildren 实现TreeNode接口 +func (s *{{.StructName}}) GetChildren() []*{{.StructName}} { + return s.Children +} + +// SetChildren 实现TreeNode接口 +func (s *{{.StructName}}) SetChildren(children *{{.StructName}}) { + s.Children = append(s.Children, children) +} + +// GetID 实现TreeNode接口 +func (s *{{.StructName}}) GetID() int { + return int({{if not .GvaModel}}*{{- end }}s.{{.PrimaryField.FieldName}}) +} + +// GetParentID 实现TreeNode接口 +func (s *{{.StructName}}) GetParentID() int { + return s.ParentID +} +{{ end }} + + +{{ end }} diff --git a/resource/plugin/server/model/request/request.go.template b/resource/plugin/server/model/request/request.go.template new file mode 100644 index 0000000..2100a64 --- /dev/null +++ b/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/resource/plugin/server/plugin.go.template b/resource/plugin/server/plugin.go.template new file mode 100644 index 0000000..255b7af --- /dev/null +++ b/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/resource/plugin/server/plugin/plugin.go.template b/resource/plugin/server/plugin/plugin.go.template new file mode 100644 index 0000000..7e25e07 --- /dev/null +++ b/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/resource/plugin/server/router/enter.go.template b/resource/plugin/server/router/enter.go.template new file mode 100644 index 0000000..78517b3 --- /dev/null +++ b/resource/plugin/server/router/enter.go.template @@ -0,0 +1,6 @@ +package router + +var Router = new(router) + +type router struct { +} diff --git a/resource/plugin/server/router/router.go.template b/resource/plugin/server/router/router.go.template new file mode 100644 index 0000000..34bf4d8 --- /dev/null +++ b/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/resource/plugin/server/service/enter.go.template b/resource/plugin/server/service/enter.go.template new file mode 100644 index 0000000..034facb --- /dev/null +++ b/resource/plugin/server/service/enter.go.template @@ -0,0 +1,7 @@ +package service + +var Service = new(service) + +type service struct { +} + diff --git a/resource/plugin/server/service/service.go.template b/resource/plugin/server/service/service.go.template new file mode 100644 index 0000000..6d11da1 --- /dev/null +++ b/resource/plugin/server/service/service.go.template @@ -0,0 +1,259 @@ +{{- $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 }} + "context" + "{{.Module}}/global" + "{{.Module}}/plugin/{{.Package}}/model" + {{- if not .IsTree }} + "{{.Module}}/plugin/{{.Package}}/model/request" + {{- else }} + "errors" + {{- end }} + {{- if .AutoCreateResource }} + "gorm.io/gorm" + {{- end}} +{{- if .IsTree }} + "{{.Module}}/utils" +{{- 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}}(ctx context.Context, {{.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}}(ctx context.Context, {{.PrimaryField.FieldJson}} string{{- if .AutoCreateResource -}},userID uint{{- end -}}) (err error) { + + {{- if .IsTree }} + var count int64 + err = {{$db}}.Find(&model.{{.StructName}}{},"parent_id = ?",{{.PrimaryField.FieldJson}}).Count(&count).Error + if count > 0 { + return errors.New("此节点存在子节点不允许删除") + } + if err != nil { + return err + } + {{- end }} + + {{- 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(ctx context.Context, {{.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}}(ctx context.Context, {{.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}}(ctx context.Context, {{.PrimaryField.FieldJson}} string) ({{.Abbreviation}} model.{{.StructName}}, err error) { + err = {{$db}}.Where("{{.PrimaryField.ColumnName}} = ?", {{.PrimaryField.FieldJson}}).First(&{{.Abbreviation}}).Error + return +} + + +{{- if .IsTree }} +// Get{{.StructName}}InfoList 分页获取{{.Description}}记录,Tree模式下不添加分页和搜索 +// Author [yourname](https://github.com/yourname) +func (s *{{.Abbreviation}}) Get{{.StructName}}InfoList(ctx context.Context) (list []*model.{{.StructName}},err error) { + // 创建db + db := {{$db}}.Model(&model.{{.StructName}}{}) + var {{.Abbreviation}}s []*model.{{.StructName}} + + err = db.Find(&{{.Abbreviation}}s).Error + + return utils.BuildTree({{.Abbreviation}}s), err +} +{{- else }} +// Get{{.StructName}}InfoList 分页获取{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func (s *{{.Abbreviation}}) Get{{.StructName}}InfoList(ctx context.Context, 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 +} +{{- end }} +{{- if .HasDataSource }} +func (s *{{.Abbreviation}})Get{{.StructName}}DataSource(ctx context.Context) (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(ctx context.Context) { + +} +{{- end }} diff --git a/resource/plugin/web/api/api.js.template b/resource/plugin/web/api/api.js.template new file mode 100644 index 0000000..0462fde --- /dev/null +++ b/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', + }) +} diff --git a/resource/plugin/web/form/form.vue.template b/resource/plugin/web/form/form.vue.template new file mode 100644 index 0000000..e45b375 --- /dev/null +++ b/resource/plugin/web/form/form.vue.template @@ -0,0 +1,464 @@ +{{- 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 .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/resource/plugin/web/view/view.vue.template b/resource/plugin/web/view/view.vue.template new file mode 100644 index 0000000..aa13244 --- /dev/null +++ b/resource/plugin/web/view/view.vue.template @@ -0,0 +1,1361 @@ +{{- $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}} + + {{- 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" }} + {{- if .DictType}} + + + + {{- else }} + + {{- end }} + {{- 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 .CheckDataSource }} + + {{- else 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 "richtext" }} + + {{- 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 .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/router/article/article.go b/router/article/article.go new file mode 100644 index 0000000..f77ab64 --- /dev/null +++ b/router/article/article.go @@ -0,0 +1,28 @@ +package article + +import ( + "git.echol.cn/loser/lckt/middleware" + "github.com/gin-gonic/gin" +) + +type ArticleRouter struct{} + +// InitBotRouter 初始化 文章 路由信息 +func (s *ArticleRouter) InitBotRouter(Router *gin.RouterGroup, PublicRouter *gin.RouterGroup) { + articleRouter := Router.Group("article").Use(middleware.OperationRecord()) + articleRouterWithoutRecord := Router.Group("article") + articleRouterWithoutAuth := PublicRouter.Group("article") + { + articleRouter.POST("", artApi.Create) // 新建文章 + articleRouter.DELETE("", artApi.Delete) // 批量删除文章 + articleRouter.PUT("", artApi.Update) // 更新文章 + + } + { + articleRouterWithoutRecord.GET(":id", artApi.ById) // 根据ID获取文章 + articleRouter.GET("list", artApi.List) // 获取文章列表 + } + { + articleRouterWithoutAuth.GET("", artApi.ById) // 文章开放接口 + } +} diff --git a/router/article/enter.go b/router/article/enter.go new file mode 100644 index 0000000..fc08f1c --- /dev/null +++ b/router/article/enter.go @@ -0,0 +1,7 @@ +package article + +import api "git.echol.cn/loser/lckt/api/v1" + +type RouterGroup struct{ ArticleRouter } + +var artApi = api.ApiGroupApp.ArticleApiGroup.ArticleApi diff --git a/router/bot/bot.go b/router/bot/bot.go new file mode 100644 index 0000000..af0c60b --- /dev/null +++ b/router/bot/bot.go @@ -0,0 +1,28 @@ +package bot + +import ( + "git.echol.cn/loser/lckt/middleware" + "github.com/gin-gonic/gin" +) + +type BotRouter struct{} + +// InitBotRouter 初始化 机器人 路由信息 +func (s *BotRouter) InitBotRouter(Router *gin.RouterGroup, PublicRouter *gin.RouterGroup) { + btRouter := Router.Group("bt").Use(middleware.OperationRecord()) + btRouterWithoutRecord := Router.Group("bt") + btRouterWithoutAuth := PublicRouter.Group("bt") + { + btRouter.POST("createBot", btApi.CreateBot) // 新建机器人 + btRouter.DELETE("deleteBot", btApi.DeleteBot) // 删除机器人 + btRouter.DELETE("deleteBotByIds", btApi.DeleteBotByIds) // 批量删除机器人 + btRouter.PUT("updateBot", btApi.UpdateBot) // 更新机器人 + } + { + btRouterWithoutRecord.GET("findBot", btApi.FindBot) // 根据ID获取机器人 + btRouterWithoutRecord.GET("getBotList", btApi.GetBotList) // 获取机器人列表 + } + { + btRouterWithoutAuth.POST("findKey", btApi.FindKey) // 机器人开放接口 + } +} diff --git a/router/bot/enter.go b/router/bot/enter.go new file mode 100644 index 0000000..5d98976 --- /dev/null +++ b/router/bot/enter.go @@ -0,0 +1,7 @@ +package bot + +import api "git.echol.cn/loser/lckt/api/v1" + +type RouterGroup struct{ BotRouter } + +var btApi = api.ApiGroupApp.BotApiGroup.BotApi diff --git a/router/category/category.go b/router/category/category.go new file mode 100644 index 0000000..57a1ad1 --- /dev/null +++ b/router/category/category.go @@ -0,0 +1,28 @@ +package category + +import ( + "git.echol.cn/loser/lckt/middleware" + "github.com/gin-gonic/gin" +) + +type CategoryRouter struct{} + +// InitCategoryRouter 初始化 类别 路由信息 +func (s *CategoryRouter) InitCategoryRouter(Router *gin.RouterGroup, PublicRouter *gin.RouterGroup) { + catRouter := Router.Group("cat").Use(middleware.OperationRecord()) + catRouterWithoutRecord := Router.Group("cat") + catRouterWithoutAuth := PublicRouter.Group("cat") + { + catRouter.POST("createCategory", catApi.CreateCategory) // 新建类别 + catRouter.DELETE("deleteCategory", catApi.DeleteCategory) // 删除类别 + catRouter.DELETE("deleteCategoryByIds", catApi.DeleteCategoryByIds) // 批量删除类别 + catRouter.PUT("updateCategory", catApi.UpdateCategory) // 更新类别 + } + { + catRouterWithoutRecord.GET("findCategory", catApi.FindCategory) // 根据ID获取类别 + catRouterWithoutRecord.GET("getCategoryList", catApi.GetCategoryList) // 获取类别列表 + } + { + catRouterWithoutAuth.GET("getCategoryPublic", catApi.GetCategoryPublic) // 类别开放接口 + } +} diff --git a/router/category/enter.go b/router/category/enter.go new file mode 100644 index 0000000..9571d9a --- /dev/null +++ b/router/category/enter.go @@ -0,0 +1,7 @@ +package category + +import api "git.echol.cn/loser/lckt/api/v1" + +type RouterGroup struct{ CategoryRouter } + +var catApi = api.ApiGroupApp.CategoryApiGroup.CategoryApi diff --git a/router/enter.go b/router/enter.go new file mode 100644 index 0000000..1870450 --- /dev/null +++ b/router/enter.go @@ -0,0 +1,19 @@ +package router + +import ( + "git.echol.cn/loser/lckt/router/article" + "git.echol.cn/loser/lckt/router/bot" + "git.echol.cn/loser/lckt/router/category" + "git.echol.cn/loser/lckt/router/example" + "git.echol.cn/loser/lckt/router/system" +) + +var RouterGroupApp = new(RouterGroup) + +type RouterGroup struct { + System system.RouterGroup + Example example.RouterGroup + Category category.RouterGroup + Bot bot.RouterGroup + Article article.RouterGroup +} diff --git a/router/example/enter.go b/router/example/enter.go new file mode 100644 index 0000000..bd2aa65 --- /dev/null +++ b/router/example/enter.go @@ -0,0 +1,17 @@ +package example + +import ( + api "git.echol.cn/loser/lckt/api/v1" +) + +type RouterGroup struct { + CustomerRouter + FileUploadAndDownloadRouter + AttachmentCategoryRouter +} + +var ( + exaCustomerApi = api.ApiGroupApp.ExampleApiGroup.CustomerApi + exaFileUploadAndDownloadApi = api.ApiGroupApp.ExampleApiGroup.FileUploadAndDownloadApi + attachmentCategoryApi = api.ApiGroupApp.ExampleApiGroup.AttachmentCategoryApi +) diff --git a/router/example/exa_attachment_category.go b/router/example/exa_attachment_category.go new file mode 100644 index 0000000..4900292 --- /dev/null +++ b/router/example/exa_attachment_category.go @@ -0,0 +1,16 @@ +package example + +import ( + "github.com/gin-gonic/gin" +) + +type AttachmentCategoryRouter struct{} + +func (r *AttachmentCategoryRouter) InitAttachmentCategoryRouterRouter(Router *gin.RouterGroup) { + router := Router.Group("attachmentCategory") + { + router.GET("getCategoryList", attachmentCategoryApi.GetCategoryList) // 分类列表 + router.POST("addCategory", attachmentCategoryApi.AddCategory) // 添加/编辑分类 + router.POST("deleteCategory", attachmentCategoryApi.DeleteCategory) // 删除分类 + } +} diff --git a/router/example/exa_customer.go b/router/example/exa_customer.go new file mode 100644 index 0000000..c9c3416 --- /dev/null +++ b/router/example/exa_customer.go @@ -0,0 +1,22 @@ +package example + +import ( + "git.echol.cn/loser/lckt/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/router/example/exa_file_upload_and_download.go b/router/example/exa_file_upload_and_download.go new file mode 100644 index 0000000..84f6ecd --- /dev/null +++ b/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/router/system/enter.go b/router/system/enter.go new file mode 100644 index 0000000..c652d10 --- /dev/null +++ b/router/system/enter.go @@ -0,0 +1,44 @@ +package system + +import api "git.echol.cn/loser/lckt/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/router/system/sys_api.go b/router/system/sys_api.go new file mode 100644 index 0000000..59bf5fd --- /dev/null +++ b/router/system/sys_api.go @@ -0,0 +1,33 @@ +package system + +import ( + "git.echol.cn/loser/lckt/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/router/system/sys_authority.go b/router/system/sys_authority.go new file mode 100644 index 0000000..871f18b --- /dev/null +++ b/router/system/sys_authority.go @@ -0,0 +1,23 @@ +package system + +import ( + "git.echol.cn/loser/lckt/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/router/system/sys_authority_btn.go b/router/system/sys_authority_btn.go new file mode 100644 index 0000000..370db85 --- /dev/null +++ b/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/router/system/sys_auto_code.go b/router/system/sys_auto_code.go new file mode 100644 index 0000000..e25e1ce --- /dev/null +++ b/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/router/system/sys_auto_code_history.go b/router/system/sys_auto_code_history.go new file mode 100644 index 0000000..42a2bef --- /dev/null +++ b/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/router/system/sys_base.go b/router/system/sys_base.go new file mode 100644 index 0000000..7d959bb --- /dev/null +++ b/router/system/sys_base.go @@ -0,0 +1,16 @@ +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) + } + return baseRouter +} diff --git a/router/system/sys_casbin.go b/router/system/sys_casbin.go new file mode 100644 index 0000000..b81a9fc --- /dev/null +++ b/router/system/sys_casbin.go @@ -0,0 +1,19 @@ +package system + +import ( + "git.echol.cn/loser/lckt/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/router/system/sys_dictionary.go b/router/system/sys_dictionary.go new file mode 100644 index 0000000..7eb40c0 --- /dev/null +++ b/router/system/sys_dictionary.go @@ -0,0 +1,22 @@ +package system + +import ( + "git.echol.cn/loser/lckt/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/router/system/sys_dictionary_detail.go b/router/system/sys_dictionary_detail.go new file mode 100644 index 0000000..ac615ef --- /dev/null +++ b/router/system/sys_dictionary_detail.go @@ -0,0 +1,22 @@ +package system + +import ( + "git.echol.cn/loser/lckt/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/router/system/sys_export_template.go b/router/system/sys_export_template.go new file mode 100644 index 0000000..a40f085 --- /dev/null +++ b/router/system/sys_export_template.go @@ -0,0 +1,34 @@ +package system + +import ( + "git.echol.cn/loser/lckt/middleware" + "github.com/gin-gonic/gin" +) + +type SysExportTemplateRouter struct { +} + +// InitSysExportTemplateRouter 初始化 导出模板 路由信息 +func (s *SysExportTemplateRouter) InitSysExportTemplateRouter(Router *gin.RouterGroup, pubRouter *gin.RouterGroup) { + sysExportTemplateRouter := Router.Group("sysExportTemplate").Use(middleware.OperationRecord()) + sysExportTemplateRouterWithoutRecord := Router.Group("sysExportTemplate") + sysExportTemplateRouterWithoutAuth := pubRouter.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) // 导入excel模板数据 + } + { + sysExportTemplateRouterWithoutRecord.GET("findSysExportTemplate", exportTemplateApi.FindSysExportTemplate) // 根据ID获取导出模板 + sysExportTemplateRouterWithoutRecord.GET("getSysExportTemplateList", exportTemplateApi.GetSysExportTemplateList) // 获取导出模板列表 + sysExportTemplateRouterWithoutRecord.GET("exportExcel", exportTemplateApi.ExportExcel) // 获取导出token + sysExportTemplateRouterWithoutRecord.GET("exportTemplate", exportTemplateApi.ExportTemplate) // 导出表格模板 + } + { + sysExportTemplateRouterWithoutAuth.GET("exportExcelByToken", exportTemplateApi.ExportExcelByToken) // 通过token导出表格 + sysExportTemplateRouterWithoutAuth.GET("exportTemplateByToken", exportTemplateApi.ExportTemplateByToken) // 通过token导出模板 + } +} diff --git a/router/system/sys_initdb.go b/router/system/sys_initdb.go new file mode 100644 index 0000000..3a6de50 --- /dev/null +++ b/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/router/system/sys_jwt.go b/router/system/sys_jwt.go new file mode 100644 index 0000000..4716031 --- /dev/null +++ b/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/router/system/sys_menu.go b/router/system/sys_menu.go new file mode 100644 index 0000000..5325eca --- /dev/null +++ b/router/system/sys_menu.go @@ -0,0 +1,27 @@ +package system + +import ( + "git.echol.cn/loser/lckt/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/router/system/sys_operation_record.go b/router/system/sys_operation_record.go new file mode 100644 index 0000000..11b841d --- /dev/null +++ b/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/router/system/sys_params.go b/router/system/sys_params.go new file mode 100644 index 0000000..a76bd70 --- /dev/null +++ b/router/system/sys_params.go @@ -0,0 +1,25 @@ +package system + +import ( + "git.echol.cn/loser/lckt/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/router/system/sys_system.go b/router/system/sys_system.go new file mode 100644 index 0000000..66ba706 --- /dev/null +++ b/router/system/sys_system.go @@ -0,0 +1,22 @@ +package system + +import ( + "git.echol.cn/loser/lckt/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/router/system/sys_user.go b/router/system/sys_user.go new file mode 100644 index 0000000..32e0130 --- /dev/null +++ b/router/system/sys_user.go @@ -0,0 +1,28 @@ +package system + +import ( + "git.echol.cn/loser/lckt/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) // 获取自身信息 + } +} diff --git a/service/article/article.go b/service/article/article.go new file mode 100644 index 0000000..44e67f0 --- /dev/null +++ b/service/article/article.go @@ -0,0 +1,41 @@ +package article + +import ( + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/article" + "git.echol.cn/loser/lckt/model/article/request" +) + +type ArticleService struct{} + +func (ArticleService) CreateArticle(req article.Article) (err error) { + err = global.GVA_DB.Create(&req).Error + return +} + +func (ArticleService) DeleteArticle(ids request.DeleteIds) (err error) { + err = global.GVA_DB.Delete(&article.Article{}, "id in ?", ids.Ids).Error + return +} + +func (ArticleService) UpdateArticle(req article.Article) (err error) { + err = global.GVA_DB.Model(&article.Article{}).Where("id = ?", req.ID).Updates(&req).Error + return +} +func (ArticleService) GetArticle(id string) (article article.Article, err error) { + err = global.GVA_DB.Where("id = ?", id).First(&article).Error + return +} + +func (ArticleService) GetArticleList(pageInfo request.GetList) (list []article.Article, total int64, err error) { + limit := pageInfo.PageSize + offset := pageInfo.PageSize * (pageInfo.Page - 1) + // 创建db + db := global.GVA_DB.Model(&article.Article{}) + err = db.Count(&total).Error + if err != nil { + return + } + err = db.Limit(limit).Offset(offset).Find(&list).Error + return +} diff --git a/service/article/enter.go b/service/article/enter.go new file mode 100644 index 0000000..c192b27 --- /dev/null +++ b/service/article/enter.go @@ -0,0 +1,3 @@ +package article + +type ServiceGroup struct{ ArticleService } diff --git a/service/bot/bot.go b/service/bot/bot.go new file mode 100644 index 0000000..ebc2aca --- /dev/null +++ b/service/bot/bot.go @@ -0,0 +1,74 @@ +package bot + +import ( + "context" + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/bot" + botReq "git.echol.cn/loser/lckt/model/bot/request" +) + +type BotService struct{} + +// CreateBot 创建机器人记录 +// Author [yourname](https://github.com/yourname) +func (btService *BotService) CreateBot(ctx context.Context, bt *bot.Bot) (err error) { + err = global.GVA_DB.Create(bt).Error + return err +} + +// DeleteBot 删除机器人记录 +// Author [yourname](https://github.com/yourname) +func (btService *BotService) DeleteBot(ctx context.Context, ID string) (err error) { + err = global.GVA_DB.Delete(&bot.Bot{}, "id = ?", ID).Error + return err +} + +// DeleteBotByIds 批量删除机器人记录 +// Author [yourname](https://github.com/yourname) +func (btService *BotService) DeleteBotByIds(ctx context.Context, IDs []string) (err error) { + err = global.GVA_DB.Delete(&[]bot.Bot{}, "id in ?", IDs).Error + return err +} + +// UpdateBot 更新机器人记录 +// Author [yourname](https://github.com/yourname) +func (btService *BotService) UpdateBot(ctx context.Context, bt bot.Bot) (err error) { + err = global.GVA_DB.Model(&bot.Bot{}).Where("id = ?", bt.ID).Updates(&bt).Error + return err +} + +// GetBot 根据ID获取机器人记录 +// Author [yourname](https://github.com/yourname) +func (btService *BotService) GetBot(ctx context.Context, ID string) (bt bot.Bot, err error) { + err = global.GVA_DB.Where("id = ?", ID).First(&bt).Error + return +} + +// GetBotInfoList 分页获取机器人记录 +// Author [yourname](https://github.com/yourname) +func (btService *BotService) GetBotInfoList(ctx context.Context, info botReq.BotSearch) (list []bot.Bot, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := global.GVA_DB.Model(&bot.Bot{}) + var bts []bot.Bot + // 如果有条件搜索 下方会自动创建搜索语句 + 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(&bts).Error + return bts, total, err +} +func (btService *BotService) GetBotPublic(keyWord botReq.FindKey) (bt bot.Bot, err error) { + err = global.GVA_DB.Where("keyword Like ?", "%"+keyWord.KeyWord+"%").First(&bt).Error + return +} diff --git a/service/bot/enter.go b/service/bot/enter.go new file mode 100644 index 0000000..e855adc --- /dev/null +++ b/service/bot/enter.go @@ -0,0 +1,3 @@ +package bot + +type ServiceGroup struct{ BotService } diff --git a/service/category/category.go b/service/category/category.go new file mode 100644 index 0000000..b84c622 --- /dev/null +++ b/service/category/category.go @@ -0,0 +1,85 @@ +package category + +import ( + "context" + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/category" + categoryReq "git.echol.cn/loser/lckt/model/category/request" +) + +type CategoryService struct{} + +// CreateCategory 创建类别记录 +// Author [yourname](https://github.com/yourname) +func (catService *CategoryService) CreateCategory(ctx context.Context, cat *category.Category) (err error) { + err = global.GVA_DB.Create(cat).Error + return err +} + +// DeleteCategory 删除类别记录 +// Author [yourname](https://github.com/yourname) +func (catService *CategoryService) DeleteCategory(ctx context.Context, ID string) (err error) { + err = global.GVA_DB.Delete(&category.Category{}, "id = ?", ID).Error + return err +} + +// DeleteCategoryByIds 批量删除类别记录 +// Author [yourname](https://github.com/yourname) +func (catService *CategoryService) DeleteCategoryByIds(ctx context.Context, IDs []string) (err error) { + err = global.GVA_DB.Delete(&[]category.Category{}, "id in ?", IDs).Error + return err +} + +// UpdateCategory 更新类别记录 +// Author [yourname](https://github.com/yourname) +func (catService *CategoryService) UpdateCategory(ctx context.Context, cat category.Category) (err error) { + err = global.GVA_DB.Model(&category.Category{}).Where("id = ?", cat.ID).Updates(&cat).Error + return err +} + +// GetCategory 根据ID获取类别记录 +// Author [yourname](https://github.com/yourname) +func (catService *CategoryService) GetCategory(ctx context.Context, ID string) (cat category.Category, err error) { + err = global.GVA_DB.Where("id = ?", ID).First(&cat).Error + return +} + +// GetCategoryInfoList 分页获取类别记录 +// Author [yourname](https://github.com/yourname) +func (catService *CategoryService) GetCategoryInfoList(ctx context.Context, info categoryReq.CategorySearch) (list []category.Category, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := global.GVA_DB.Model(&category.Category{}) + var cats []category.Category + // 如果有条件搜索 下方会自动创建搜索语句 + 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 + } + var OrderStr string + orderMap := make(map[string]bool) + orderMap["name"] = true + orderMap["order"] = true + if orderMap[info.Sort] { + OrderStr = info.Sort + if info.Order == "descending" { + OrderStr = OrderStr + " desc" + } + db = db.Order(OrderStr) + } + + if limit != 0 { + db = db.Limit(limit).Offset(offset) + } + + err = db.Find(&cats).Error + return cats, total, err +} +func (catService *CategoryService) GetCategoryPublic(ctx context.Context) { + // 此方法为获取数据源定义的数据 + // 请自行实现 +} diff --git a/service/category/enter.go b/service/category/enter.go new file mode 100644 index 0000000..6b165f4 --- /dev/null +++ b/service/category/enter.go @@ -0,0 +1,3 @@ +package category + +type ServiceGroup struct{ CategoryService } diff --git a/service/enter.go b/service/enter.go new file mode 100644 index 0000000..5a7f5bb --- /dev/null +++ b/service/enter.go @@ -0,0 +1,19 @@ +package service + +import ( + "git.echol.cn/loser/lckt/service/article" + "git.echol.cn/loser/lckt/service/bot" + "git.echol.cn/loser/lckt/service/category" + "git.echol.cn/loser/lckt/service/example" + "git.echol.cn/loser/lckt/service/system" +) + +var ServiceGroupApp = new(ServiceGroup) + +type ServiceGroup struct { + SystemServiceGroup system.ServiceGroup + ExampleServiceGroup example.ServiceGroup + CategoryServiceGroup category.ServiceGroup + BotServiceGroup bot.ServiceGroup + ArticleGroup article.ServiceGroup +} diff --git a/service/example/enter.go b/service/example/enter.go new file mode 100644 index 0000000..f7198da --- /dev/null +++ b/service/example/enter.go @@ -0,0 +1,7 @@ +package example + +type ServiceGroup struct { + CustomerService + FileUploadAndDownloadService + AttachmentCategoryService +} diff --git a/service/example/exa_attachment_category.go b/service/example/exa_attachment_category.go new file mode 100644 index 0000000..3f17e32 --- /dev/null +++ b/service/example/exa_attachment_category.go @@ -0,0 +1,66 @@ +package example + +import ( + "errors" + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/example" + "gorm.io/gorm" +) + +type AttachmentCategoryService struct{} + +// AddCategory 创建/更新的分类 +func (a *AttachmentCategoryService) AddCategory(req *example.ExaAttachmentCategory) (err error) { + // 检查是否已存在相同名称的分类 + if (!errors.Is(global.GVA_DB.Take(&example.ExaAttachmentCategory{}, "name = ? and pid = ?", req.Name, req.Pid).Error, gorm.ErrRecordNotFound)) { + return errors.New("分类名称已存在") + } + if req.ID > 0 { + if err = global.GVA_DB.Model(&example.ExaAttachmentCategory{}).Where("id = ?", req.ID).Updates(&example.ExaAttachmentCategory{ + Name: req.Name, + Pid: req.Pid, + }).Error; err != nil { + return err + } + } else { + if err = global.GVA_DB.Create(&example.ExaAttachmentCategory{ + Name: req.Name, + Pid: req.Pid, + }).Error; err != nil { + return err + } + } + return nil +} + +// DeleteCategory 删除分类 +func (a *AttachmentCategoryService) DeleteCategory(id *int) error { + var childCount int64 + global.GVA_DB.Model(&example.ExaAttachmentCategory{}).Where("pid = ?", id).Count(&childCount) + if childCount > 0 { + return errors.New("请先删除子级") + } + return global.GVA_DB.Where("id = ?", id).Unscoped().Delete(&example.ExaAttachmentCategory{}).Error +} + +// GetCategoryList 分类列表 +func (a *AttachmentCategoryService) GetCategoryList() (res []*example.ExaAttachmentCategory, err error) { + var fileLists []example.ExaAttachmentCategory + err = global.GVA_DB.Model(&example.ExaAttachmentCategory{}).Find(&fileLists).Error + if err != nil { + return res, err + } + return a.getChildrenList(fileLists, 0), nil +} + +// getChildrenList 子类 +func (a *AttachmentCategoryService) getChildrenList(categories []example.ExaAttachmentCategory, parentID uint) []*example.ExaAttachmentCategory { + var tree []*example.ExaAttachmentCategory + for _, category := range categories { + if category.Pid == parentID { + category.Children = a.getChildrenList(categories, category.ID) + tree = append(tree, &category) + } + } + return tree +} diff --git a/service/example/exa_breakpoint_continue.go b/service/example/exa_breakpoint_continue.go new file mode 100644 index 0000000..35785d7 --- /dev/null +++ b/service/example/exa_breakpoint_continue.go @@ -0,0 +1,71 @@ +package example + +import ( + "errors" + + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/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/service/example/exa_customer.go b/service/example/exa_customer.go new file mode 100644 index 0000000..941376b --- /dev/null +++ b/service/example/exa_customer.go @@ -0,0 +1,87 @@ +package example + +import ( + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/common/request" + "git.echol.cn/loser/lckt/model/example" + "git.echol.cn/loser/lckt/model/system" + systemService "git.echol.cn/loser/lckt/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/service/example/exa_file_upload_download.go b/service/example/exa_file_upload_download.go new file mode 100644 index 0000000..ebacb1e --- /dev/null +++ b/service/example/exa_file_upload_download.go @@ -0,0 +1,123 @@ +package example + +import ( + "errors" + "mime/multipart" + "strings" + + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/example" + "git.echol.cn/loser/lckt/model/example/request" + "git.echol.cn/loser/lckt/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.ExaAttachmentCategorySearch +//@return: list interface{}, total int64, err error + +func (e *FileUploadAndDownloadService) GetFileRecordInfoList(info request.ExaAttachmentCategorySearch) (list []example.ExaFileUploadAndDownload, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + db := global.GVA_DB.Model(&example.ExaFileUploadAndDownload{}) + + if len(info.Keyword) > 0 { + db = db.Where("name LIKE ?", "%"+info.Keyword+"%") + } + + if info.ClassId > 0 { + db = db.Where("class_id = ?", info.ClassId) + } + + err = db.Count(&total).Error + if err != nil { + return + } + err = db.Limit(limit).Offset(offset).Order("id desc").Find(&list).Error + return list, 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, classId int) (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, + ClassId: classId, + 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/service/system/auto_code_history.go b/service/system/auto_code_history.go new file mode 100644 index 0000000..28a0430 --- /dev/null +++ b/service/system/auto_code_history.go @@ -0,0 +1,217 @@ +package system + +import ( + "context" + "encoding/json" + "fmt" + "git.echol.cn/loser/lckt/utils/ast" + "github.com/pkg/errors" + "path" + "path/filepath" + "strconv" + "strings" + "time" + + "git.echol.cn/loser/lckt/global" + common "git.echol.cn/loser/lckt/model/common/request" + model "git.echol.cn/loser/lckt/model/system" + request "git.echol.cn/loser/lckt/model/system/request" + "git.echol.cn/loser/lckt/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, abbreviation, Package string) bool { + var count int64 + global.GVA_DB.Model(&model.SysAutoCodeHistory{}).Where("business_db = ? and (struct_name = ? OR abbreviation = ?) and package = ? and flag = ?", businessDB, structName, abbreviation, Package, 0).Count(&count).Debug() + 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/service/system/auto_code_package.go b/service/system/auto_code_package.go new file mode 100644 index 0000000..8a99fab --- /dev/null +++ b/service/system/auto_code_package.go @@ -0,0 +1,680 @@ +package system + +import ( + "context" + "fmt" + "go/token" + "os" + "path/filepath" + "strings" + "text/template" + + "git.echol.cn/loser/lckt/global" + common "git.echol.cn/loser/lckt/model/common/request" + model "git.echol.cn/loser/lckt/model/system" + "git.echol.cn/loser/lckt/model/system/request" + "git.echol.cn/loser/lckt/utils" + "git.echol.cn/loser/lckt/utils/ast" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +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, true) + 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, isPackage bool) (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": + if !info.GenerateServer && !isPackage { + break + } + 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": + if !info.GenerateWeb && !isPackage { + break + } + 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/service/system/auto_code_package_test.go b/service/system/auto_code_package_test.go new file mode 100644 index 0000000..3956b58 --- /dev/null +++ b/service/system/auto_code_package_test.go @@ -0,0 +1,105 @@ +package system + +import ( + "context" + model "git.echol.cn/loser/lckt/model/system" + "git.echol.cn/loser/lckt/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/service/system/auto_code_plugin.go b/service/system/auto_code_plugin.go new file mode 100644 index 0000000..64c5a79 --- /dev/null +++ b/service/system/auto_code_plugin.go @@ -0,0 +1,249 @@ +package system + +import ( + "bytes" + "context" + "fmt" + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/system" + "git.echol.cn/loser/lckt/model/system/request" + "git.echol.cn/loser/lckt/utils" + "git.echol.cn/loser/lckt/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.Archive{ + 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/service/system/auto_code_template.go b/service/system/auto_code_template.go new file mode 100644 index 0000000..b02a6b3 --- /dev/null +++ b/service/system/auto_code_template.go @@ -0,0 +1,452 @@ +package system + +import ( + "context" + "encoding/json" + "fmt" + "go/ast" + "go/format" + "go/parser" + "go/token" + "os" + "path/filepath" + "strings" + "text/template" + + "git.echol.cn/loser/lckt/global" + model "git.echol.cn/loser/lckt/model/system" + "git.echol.cn/loser/lckt/model/system/request" + utilsAst "git.echol.cn/loser/lckt/utils/ast" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +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.Abbreviation, 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, "查询包失败!") + } + // 增加判断: 重复创建struct 或者重复的简称 + if AutocodeHistory.Repeat(info.BusinessDB, info.StructName, info.Abbreviation, info.Package) && !info.IsAdd { + return nil, errors.New("已经创建过此数据结构或重复简称,请勿重复创建!") + } + + 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, false) + 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 + } + return s.addTemplateToAst("router", info) +} + +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) + if err != nil { + return err + } + + fileSet := token.NewFileSet() + astFile, err := parser.ParseFile(fileSet, "", src, 0) + if err != nil { + return 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/service/system/auto_code_template_test.go b/service/system/auto_code_template_test.go new file mode 100644 index 0000000..be429e5 --- /dev/null +++ b/service/system/auto_code_template_test.go @@ -0,0 +1,84 @@ +package system + +import ( + "context" + "encoding/json" + "git.echol.cn/loser/lckt/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/service/system/enter.go b/service/system/enter.go new file mode 100644 index 0000000..634cd00 --- /dev/null +++ b/service/system/enter.go @@ -0,0 +1,24 @@ +package system + +type ServiceGroup struct { + JwtService + ApiService + MenuService + UserService + CasbinService + InitDBService + AutoCodeService + BaseMenuService + AuthorityService + DictionaryService + SystemConfigService + OperationRecordService + DictionaryDetailService + AuthorityBtnService + SysExportTemplateService + SysParamsService + AutoCodePlugin autoCodePlugin + AutoCodePackage autoCodePackage + AutoCodeHistory autoCodeHistory + AutoCodeTemplate autoCodeTemplate +} diff --git a/service/system/jwt_black_list.go b/service/system/jwt_black_list.go new file mode 100644 index 0000000..7b0094e --- /dev/null +++ b/service/system/jwt_black_list.go @@ -0,0 +1,84 @@ +package system + +import ( + "context" + + "go.uber.org/zap" + + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/system" + "git.echol.cn/loser/lckt/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/service/system/sys_api.go b/service/system/sys_api.go new file mode 100644 index 0000000..88a628c --- /dev/null +++ b/service/system/sys_api.go @@ -0,0 +1,326 @@ +package system + +import ( + "errors" + "fmt" + "strings" + + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/common/request" + "git.echol.cn/loser/lckt/model/system" + systemRes "git.echol.cn/loser/lckt/model/system/response" + "gorm.io/gorm" +) + +//@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 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/service/system/sys_authority.go b/service/system/sys_authority.go new file mode 100644 index 0000000..5ef1a0b --- /dev/null +++ b/service/system/sys_authority.go @@ -0,0 +1,330 @@ +package system + +import ( + "errors" + "strconv" + + systemReq "git.echol.cn/loser/lckt/model/system/request" + + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/common/request" + "git.echol.cn/loser/lckt/model/system" + "git.echol.cn/loser/lckt/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) + childrenList, err := authorityService.GetStructAuthorityList(authorities[k].AuthorityId) + if err == nil { + list = append(list, childrenList...) + } + } + } + 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/service/system/sys_authority_btn.go b/service/system/sys_authority_btn.go new file mode 100644 index 0000000..5e07651 --- /dev/null +++ b/service/system/sys_authority_btn.go @@ -0,0 +1,60 @@ +package system + +import ( + "errors" + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/system" + "git.echol.cn/loser/lckt/model/system/request" + "git.echol.cn/loser/lckt/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/service/system/sys_auto_code_interface.go b/service/system/sys_auto_code_interface.go new file mode 100644 index 0000000..19cd957 --- /dev/null +++ b/service/system/sys_auto_code_interface.go @@ -0,0 +1,55 @@ +package system + +import ( + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/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/service/system/sys_auto_code_mssql.go b/service/system/sys_auto_code_mssql.go new file mode 100644 index 0000000..a7a164c --- /dev/null +++ b/service/system/sys_auto_code_mssql.go @@ -0,0 +1,83 @@ +package system + +import ( + "fmt" + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/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 sys.databases;" + 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/service/system/sys_auto_code_mysql.go b/service/system/sys_auto_code_mysql.go new file mode 100644 index 0000000..5b03d34 --- /dev/null +++ b/service/system/sys_auto_code_mysql.go @@ -0,0 +1,83 @@ +package system + +import ( + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/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/service/system/sys_auto_code_oracle.go b/service/system/sys_auto_code_oracle.go new file mode 100644 index 0000000..a8abb86 --- /dev/null +++ b/service/system/sys_auto_code_oracle.go @@ -0,0 +1,72 @@ +package system + +import ( + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/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/service/system/sys_auto_code_pgsql.go b/service/system/sys_auto_code_pgsql.go new file mode 100644 index 0000000..6e8717b --- /dev/null +++ b/service/system/sys_auto_code_pgsql.go @@ -0,0 +1,135 @@ +package system + +import ( + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/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/service/system/sys_auto_code_sqlite.go b/service/system/sys_auto_code_sqlite.go new file mode 100644 index 0000000..ded430c --- /dev/null +++ b/service/system/sys_auto_code_sqlite.go @@ -0,0 +1,84 @@ +package system + +import ( + "fmt" + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/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/service/system/sys_base_menu.go b/service/system/sys_base_menu.go new file mode 100644 index 0000000..f51870e --- /dev/null +++ b/service/system/sys_base_menu.go @@ -0,0 +1,146 @@ +package system + +import ( + "errors" + + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/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/service/system/sys_casbin.go b/service/system/sys_casbin.go new file mode 100644 index 0000000..7aa8b93 --- /dev/null +++ b/service/system/sys_casbin.go @@ -0,0 +1,221 @@ +package system + +import ( + "errors" + "strconv" + "sync" + + "gorm.io/gorm" + + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/system/request" + "github.com/casbin/casbin/v2" + "github.com/casbin/casbin/v2/model" + gormadapter "github.com/casbin/gorm-adapter/v3" + _ "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 + if err != nil { + return err + } + + e := casbinService.Casbin() + return e.LoadPolicy() +} + +//@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/service/system/sys_dictionary.go b/service/system/sys_dictionary.go new file mode 100644 index 0000000..238c066 --- /dev/null +++ b/service/system/sys_dictionary.go @@ -0,0 +1,112 @@ +package system + +import ( + "errors" + + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/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/service/system/sys_dictionary_detail.go b/service/system/sys_dictionary_detail.go new file mode 100644 index 0000000..b486f7c --- /dev/null +++ b/service/system/sys_dictionary_detail.go @@ -0,0 +1,118 @@ +package system + +import ( + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/system" + "git.echol.cn/loser/lckt/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/service/system/sys_export_template.go b/service/system/sys_export_template.go new file mode 100644 index 0000000..158c250 --- /dev/null +++ b/service/system/sys_export_template.go @@ -0,0 +1,424 @@ +package system + +import ( + "bytes" + "encoding/json" + "fmt" + "mime/multipart" + "net/url" + "strconv" + "strings" + "time" + + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/common/request" + "git.echol.cn/loser/lckt/model/system" + systemReq "git.echol.cn/loser/lckt/model/system/request" + "git.echol.cn/loser/lckt/utils" + "github.com/xuri/excelize/v2" + "gorm.io/gorm" +) + +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, 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/service/system/sys_initdb.go b/service/system/sys_initdb.go new file mode 100644 index 0000000..0a4d096 --- /dev/null +++ b/service/system/sys_initdb.go @@ -0,0 +1,189 @@ +package system + +import ( + "context" + "database/sql" + "errors" + "fmt" + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/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/service/system/sys_initdb_mssql.go b/service/system/sys_initdb_mssql.go new file mode 100644 index 0000000..9dad13a --- /dev/null +++ b/service/system/sys_initdb_mssql.go @@ -0,0 +1,92 @@ +package system + +import ( + "context" + "errors" + "git.echol.cn/loser/lckt/config" + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/system/request" + "git.echol.cn/loser/lckt/utils" + "github.com/google/uuid" + "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.New().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/service/system/sys_initdb_mysql.go b/service/system/sys_initdb_mysql.go new file mode 100644 index 0000000..508fe7d --- /dev/null +++ b/service/system/sys_initdb_mysql.go @@ -0,0 +1,97 @@ +package system + +import ( + "context" + "errors" + "fmt" + "path/filepath" + + "git.echol.cn/loser/lckt/config" + "github.com/gookit/color" + + "git.echol.cn/loser/lckt/utils" + + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/system/request" + "github.com/google/uuid" + "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.New().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/service/system/sys_initdb_pgsql.go b/service/system/sys_initdb_pgsql.go new file mode 100644 index 0000000..3ca0990 --- /dev/null +++ b/service/system/sys_initdb_pgsql.go @@ -0,0 +1,101 @@ +package system + +import ( + "context" + "errors" + "fmt" + "path/filepath" + + "git.echol.cn/loser/lckt/config" + "github.com/gookit/color" + + "git.echol.cn/loser/lckt/utils" + + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/system/request" + "github.com/google/uuid" + "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 + global.GVA_CONFIG.JWT.SigningKey = uuid.New().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 创建数据库并初始化 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() + var createSql string + if conf.Template != "" { + createSql = fmt.Sprintf("CREATE DATABASE %s WITH TEMPLATE %s;", c.Dbname, conf.Template) + } else { + 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 +} diff --git a/service/system/sys_initdb_sqlite.go b/service/system/sys_initdb_sqlite.go new file mode 100644 index 0000000..57d862e --- /dev/null +++ b/service/system/sys_initdb_sqlite.go @@ -0,0 +1,88 @@ +package system + +import ( + "context" + "errors" + "github.com/glebarez/sqlite" + "github.com/google/uuid" + "github.com/gookit/color" + "gorm.io/gorm" + "path/filepath" + + "git.echol.cn/loser/lckt/config" + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/system/request" + "git.echol.cn/loser/lckt/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.New().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/service/system/sys_menu.go b/service/system/sys_menu.go new file mode 100644 index 0000000..cfaaed8 --- /dev/null +++ b/service/system/sys_menu.go @@ -0,0 +1,289 @@ +package system + +import ( + "errors" + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/common/request" + "git.echol.cn/loser/lckt/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/service/system/sys_operation_record.go b/service/system/sys_operation_record.go new file mode 100644 index 0000000..c96ba68 --- /dev/null +++ b/service/system/sys_operation_record.go @@ -0,0 +1,88 @@ +package system + +import ( + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/common/request" + "git.echol.cn/loser/lckt/model/system" + systemReq "git.echol.cn/loser/lckt/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/service/system/sys_params.go b/service/system/sys_params.go new file mode 100644 index 0000000..76078b5 --- /dev/null +++ b/service/system/sys_params.go @@ -0,0 +1,82 @@ +package system + +import ( + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/system" + systemReq "git.echol.cn/loser/lckt/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/service/system/sys_system.go b/service/system/sys_system.go new file mode 100644 index 0000000..d0ab947 --- /dev/null +++ b/service/system/sys_system.go @@ -0,0 +1,62 @@ +package system + +import ( + "git.echol.cn/loser/lckt/config" + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/system" + "git.echol.cn/loser/lckt/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/service/system/sys_user.go b/service/system/sys_user.go new file mode 100644 index 0000000..0790a03 --- /dev/null +++ b/service/system/sys_user.go @@ -0,0 +1,317 @@ +package system + +import ( + "errors" + "fmt" + "git.echol.cn/loser/lckt/model/common" + systemReq "git.echol.cn/loser/lckt/model/system/request" + "time" + + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/system" + "git.echol.cn/loser/lckt/utils" + "github.com/google/uuid" + "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) + +func (userService *UserService) Register(u system.SysUser) (userInter system.SysUser, err error) { + var user system.SysUser + if !errors.Is(global.GVA_DB.Where("username = ?", u.Username).First(&user).Error, gorm.ErrRecordNotFound) { // 判断用户名是否注册 + return userInter, errors.New("用户名已注册") + } + // 否则 附加uuid 密码hash加密 注册 + u.Password = utils.BcryptHash(u.Password) + u.UUID = uuid.New() + 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 = ?", u.Username).Preload("Authorities").Preload("Authority").First(&user).Error + if err == nil { + if ok := utils.BcryptCheck(u.Password, user.Password); !ok { + return nil, errors.New("密码错误") + } + 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 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").Find(&userList).Error + return userList, 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: reqUser model.SysUser +//@return: err error, user model.SysUser + +func (userService *UserService) SetUserInfo(req system.SysUser) error { + 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 +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: ResetPassword +//@description: 修改用户密码 +//@param: ID uint +//@return: err error + +func (userService *UserService) ResetPassword(ID uint) (err error) { + err = global.GVA_DB.Model(&system.SysUser{}).Where("id = ?", ID).Update("password", utils.BcryptHash("123456")).Error + return err +} diff --git a/source/example/file_upload_download.go b/source/example/file_upload_download.go new file mode 100644 index 0000000..c3313db --- /dev/null +++ b/source/example/file_upload_download.go @@ -0,0 +1,65 @@ +package example + +import ( + "context" + "git.echol.cn/loser/lckt/model/example" + "git.echol.cn/loser/lckt/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/source/system/api.go b/source/system/api.go new file mode 100644 index 0000000..fe042fe --- /dev/null +++ b/source/system/api.go @@ -0,0 +1,206 @@ +package system + +import ( + "context" + sysModel "git.echol.cn/loser/lckt/model/system" + "git.echol.cn/loser/lckt/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: "/attachmentCategory/getCategoryList", Description: "分类列表"}, + {ApiGroup: "媒体库分类", Method: "POST", Path: "/attachmentCategory/addCategory", Description: "添加/编辑分类"}, + {ApiGroup: "媒体库分类", Method: "POST", Path: "/attachmentCategory/deleteCategory", Description: "删除分类"}, + } + 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/source/system/api_ignore.go b/source/system/api_ignore.go new file mode 100644 index 0000000..cf54c24 --- /dev/null +++ b/source/system/api_ignore.go @@ -0,0 +1,77 @@ +package system + +import ( + "context" + sysModel "git.echol.cn/loser/lckt/model/system" + "git.echol.cn/loser/lckt/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/source/system/authorities_menus.go b/source/system/authorities_menus.go new file mode 100644 index 0000000..93a9eef --- /dev/null +++ b/source/system/authorities_menus.go @@ -0,0 +1,84 @@ +package system + +import ( + "context" + + sysModel "git.echol.cn/loser/lckt/model/system" + "git.echol.cn/loser/lckt/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 + } + initAuth := &initAuthority{} + authorities, ok := ctx.Value(initAuth.InitializerName()).([]sysModel.SysAuthority) + if !ok { + return ctx, errors.Wrap(system.ErrMissingDependentContext, "创建 [菜单-权限] 关联失败, 未找到权限表初始化数据") + } + menus, ok := ctx.Value(new(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); 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/source/system/authority.go b/source/system/authority.go new file mode 100644 index 0000000..4c85a7c --- /dev/null +++ b/source/system/authority.go @@ -0,0 +1,88 @@ +package system + +import ( + "context" + sysModel "git.echol.cn/loser/lckt/model/system" + "git.echol.cn/loser/lckt/service/system" + "git.echol.cn/loser/lckt/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: "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/source/system/casbin.go b/source/system/casbin.go new file mode 100644 index 0000000..f12b4f5 --- /dev/null +++ b/source/system/casbin.go @@ -0,0 +1,292 @@ +package system + +import ( + "context" + + "git.echol.cn/loser/lckt/service/system" + adapter "github.com/casbin/gorm-adapter/v3" + "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: "/attachmentCategory/getCategoryList", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/attachmentCategory/addCategory", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/attachmentCategory/deleteCategory", 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"}, + } + 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/source/system/dictionary.go b/source/system/dictionary.go new file mode 100644 index 0000000..d56ea77 --- /dev/null +++ b/source/system/dictionary.go @@ -0,0 +1,71 @@ +package system + +import ( + "context" + sysModel "git.echol.cn/loser/lckt/model/system" + "git.echol.cn/loser/lckt/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/source/system/dictionary_detail.go b/source/system/dictionary_detail.go new file mode 100644 index 0000000..ace9bde --- /dev/null +++ b/source/system/dictionary_detail.go @@ -0,0 +1,121 @@ +package system + +import ( + "context" + "fmt" + sysModel "git.echol.cn/loser/lckt/model/system" + "git.echol.cn/loser/lckt/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(new(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/source/system/excel_template.go b/source/system/excel_template.go new file mode 100644 index 0000000..4386ff1 --- /dev/null +++ b/source/system/excel_template.go @@ -0,0 +1,75 @@ +package system + +import ( + "context" + sysModel "git.echol.cn/loser/lckt/model/system" + "git.echol.cn/loser/lckt/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/source/system/menu.go b/source/system/menu.go new file mode 100644 index 0000000..cde0895 --- /dev/null +++ b/source/system/menu.go @@ -0,0 +1,102 @@ +package system + +import ( + "context" + + . "git.echol.cn/loser/lckt/model/system" + "git.echol.cn/loser/lckt/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"}}, + } + 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/source/system/user.go b/source/system/user.go new file mode 100644 index 0000000..c78ff2a --- /dev/null +++ b/source/system/user.go @@ -0,0 +1,106 @@ +package system + +import ( + "context" + sysModel "git.echol.cn/loser/lckt/model/system" + "git.echol.cn/loser/lckt/service/system" + "git.echol.cn/loser/lckt/utils" + "github.com/google/uuid" + "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.New(), + Username: "admin", + Password: adminPassword, + NickName: "Mr.奇淼", + HeaderImg: "https://qmplusimg.henrongyi.top/gva_header.jpg", + AuthorityId: 888, + Phone: "17611111111", + Email: "333333333@qq.com", + }, + { + UUID: uuid.New(), + Username: "a303176530", + Password: password, + NickName: "用户1", + HeaderImg: "https:///qmplusimg.henrongyi.top/1572075907logo.png", + AuthorityId: 9528, + Phone: "17611111111", + Email: "333333333@qq.com"}, + } + 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(new(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/task/clearTable.go b/task/clearTable.go new file mode 100644 index 0000000..b4e3137 --- /dev/null +++ b/task/clearTable.go @@ -0,0 +1,51 @@ +package task + +import ( + "errors" + "fmt" + "git.echol.cn/loser/lckt/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/uploads/file/9c97e3210d5ea26224c629c055734360_20250409105756.png b/uploads/file/9c97e3210d5ea26224c629c055734360_20250409105756.png new file mode 100644 index 0000000000000000000000000000000000000000..89191e1c6d2561a2f9a2df3d1024cd98771bf716 GIT binary patch literal 243308 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%0Dyi_7FRhW52!Z1-;*a41R0&4}) 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%0D82kYnt`&F2HYw*z4e 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 e{inrGt9*O(XY%dF!!D`psxmW%U43Eb_UK -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/utils/ast/ast_gorm.go b/utils/ast/ast_gorm.go new file mode 100644 index 0000000..0e03529 --- /dev/null +++ b/utils/ast/ast_gorm.go @@ -0,0 +1,166 @@ +package ast + +import ( + "bytes" + "fmt" + "go/ast" + "go/parser" + "go/printer" + "go/token" + "os" +) + +// AddRegisterTablesAst 自动为 gorm.go 注册一个自动迁移 +func AddRegisterTablesAst(path, funcName, pk, varName, dbName, model string) { + modelPk := fmt.Sprintf("git.echol.cn/loser/lckt/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) + } +} + +// NeedAppendModel 为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/utils/ast/ast_init_test.go b/utils/ast/ast_init_test.go new file mode 100644 index 0000000..25cb467 --- /dev/null +++ b/utils/ast/ast_init_test.go @@ -0,0 +1,11 @@ +package ast + +import ( + "git.echol.cn/loser/lckt/global" + "path/filepath" +) + +func init() { + global.GVA_CONFIG.AutoCode.Root, _ = filepath.Abs("../../../") + global.GVA_CONFIG.AutoCode.Server = "server" +} diff --git a/utils/ast/ast_rollback.go b/utils/ast/ast_rollback.go new file mode 100644 index 0000000..46620fe --- /dev/null +++ b/utils/ast/ast_rollback.go @@ -0,0 +1,173 @@ +package ast + +import ( + "bytes" + "fmt" + "git.echol.cn/loser/lckt/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 == "\"git.echol.cn/loser/lckt/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/utils/ast/ast_router.go b/utils/ast/ast_router.go new file mode 100644 index 0000000..86356b8 --- /dev/null +++ b/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/utils/ast/ast_test.go b/utils/ast/ast_test.go new file mode 100644 index 0000000..71e2456 --- /dev/null +++ b/utils/ast/ast_test.go @@ -0,0 +1,32 @@ +package ast + +import ( + "git.echol.cn/loser/lckt/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/utils/ast/ast_type.go b/utils/ast/ast_type.go new file mode 100644 index 0000000..c4e905e --- /dev/null +++ b/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/utils/ast/import.go b/utils/ast/import.go new file mode 100644 index 0000000..5de18a3 --- /dev/null +++ b/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/utils/ast/interfaces.go b/utils/ast/interfaces.go new file mode 100644 index 0000000..33ecc47 --- /dev/null +++ b/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/utils/ast/interfaces_base.go b/utils/ast/interfaces_base.go new file mode 100644 index 0000000..228296d --- /dev/null +++ b/utils/ast/interfaces_base.go @@ -0,0 +1,76 @@ +package ast + +import ( + "git.echol.cn/loser/lckt/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/utils/ast/package_enter.go b/utils/ast/package_enter.go new file mode 100644 index 0000000..f4b6305 --- /dev/null +++ b/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/utils/ast/package_enter_test.go b/utils/ast/package_enter_test.go new file mode 100644 index 0000000..bc05673 --- /dev/null +++ b/utils/ast/package_enter_test.go @@ -0,0 +1,154 @@ +package ast + +import ( + "git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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/utils/ast/package_initialize_gorm.go b/utils/ast/package_initialize_gorm.go new file mode 100644 index 0000000..594f714 --- /dev/null +++ b/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/utils/ast/package_initialize_gorm_test.go b/utils/ast/package_initialize_gorm_test.go new file mode 100644 index 0000000..58dbb6d --- /dev/null +++ b/utils/ast/package_initialize_gorm_test.go @@ -0,0 +1,171 @@ +package ast + +import ( + "git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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/utils/ast/package_initialize_router.go b/utils/ast/package_initialize_router.go new file mode 100644 index 0000000..9fe4429 --- /dev/null +++ b/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/utils/ast/package_initialize_router_test.go b/utils/ast/package_initialize_router_test.go new file mode 100644 index 0000000..84df7f3 --- /dev/null +++ b/utils/ast/package_initialize_router_test.go @@ -0,0 +1,158 @@ +package ast + +import ( + "git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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/utils/ast/package_module_enter.go b/utils/ast/package_module_enter.go new file mode 100644 index 0000000..881fb3f --- /dev/null +++ b/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/utils/ast/package_module_enter_test.go b/utils/ast/package_module_enter_test.go new file mode 100644 index 0000000..75bc96f --- /dev/null +++ b/utils/ast/package_module_enter_test.go @@ -0,0 +1,185 @@ +package ast + +import ( + "git.echol.cn/loser/lckt/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 "git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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 "git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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/utils/ast/plugin_enter.go b/utils/ast/plugin_enter.go new file mode 100644 index 0000000..df5bba4 --- /dev/null +++ b/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/utils/ast/plugin_enter_test.go b/utils/ast/plugin_enter_test.go new file mode 100644 index 0000000..6bb9ba7 --- /dev/null +++ b/utils/ast/plugin_enter_test.go @@ -0,0 +1,200 @@ +package ast + +import ( + "git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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/utils/ast/plugin_gen.go b/utils/ast/plugin_gen.go new file mode 100644 index 0000000..ed7d04f --- /dev/null +++ b/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/utils/ast/plugin_gen_test.go b/utils/ast/plugin_gen_test.go new file mode 100644 index 0000000..0d43c0c --- /dev/null +++ b/utils/ast/plugin_gen_test.go @@ -0,0 +1,127 @@ +package ast + +import ( + "git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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/utils/ast/plugin_initialize_gorm.go b/utils/ast/plugin_initialize_gorm.go new file mode 100644 index 0000000..e342251 --- /dev/null +++ b/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/utils/ast/plugin_initialize_gorm_test.go b/utils/ast/plugin_initialize_gorm_test.go new file mode 100644 index 0000000..6426e4a --- /dev/null +++ b/utils/ast/plugin_initialize_gorm_test.go @@ -0,0 +1,138 @@ +package ast + +import ( + "git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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/utils/ast/plugin_initialize_router.go b/utils/ast/plugin_initialize_router.go new file mode 100644 index 0000000..6550789 --- /dev/null +++ b/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/utils/ast/plugin_initialize_router_test.go b/utils/ast/plugin_initialize_router_test.go new file mode 100644 index 0000000..485df12 --- /dev/null +++ b/utils/ast/plugin_initialize_router_test.go @@ -0,0 +1,155 @@ +package ast + +import ( + "git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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/utils/ast/plugin_initialize_v2.go b/utils/ast/plugin_initialize_v2.go new file mode 100644 index 0000000..1befdc6 --- /dev/null +++ b/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/utils/ast/plugin_initialize_v2_test.go b/utils/ast/plugin_initialize_v2_test.go new file mode 100644 index 0000000..890e9be --- /dev/null +++ b/utils/ast/plugin_initialize_v2_test.go @@ -0,0 +1,100 @@ +package ast + +import ( + "git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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: `"git.echol.cn/loser/lckt/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/utils/breakpoint_continue.go b/utils/breakpoint_continue.go new file mode 100644 index 0000000..bce6686 --- /dev/null +++ b/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.Contains(fileName, "..") || strings.Contains(FileDir, "..") { + 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/utils/captcha/redis.go b/utils/captcha/redis.go new file mode 100644 index 0000000..5802047 --- /dev/null +++ b/utils/captcha/redis.go @@ -0,0 +1,60 @@ +package captcha + +import ( + "context" + "time" + + "git.echol.cn/loser/lckt/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/utils/claims.go b/utils/claims.go new file mode 100644 index 0000000..c08c90c --- /dev/null +++ b/utils/claims.go @@ -0,0 +1,148 @@ +package utils + +import ( + "net" + "time" + + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/system" + systemReq "git.echol.cn/loser/lckt/model/system/request" + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +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) + } else { + c.SetCookie("x-token", token, maxAge, "/", host, false, false) + } +} + +func GetToken(c *gin.Context) string { + token := c.Request.Header.Get("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)) + } + 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) { + j := NewJWT() + claims = j.CreateClaims(systemReq.BaseClaims{ + UUID: user.GetUUID(), + ID: user.GetUserId(), + NickName: user.GetNickname(), + Username: user.GetUsername(), + AuthorityId: user.GetAuthorityId(), + }) + token, err = j.CreateToken(claims) + return +} diff --git a/utils/directory.go b/utils/directory.go new file mode 100644 index 0000000..3dea13c --- /dev/null +++ b/utils/directory.go @@ -0,0 +1,124 @@ +package utils + +import ( + "errors" + "os" + "path/filepath" + "reflect" + "strings" + + "git.echol.cn/loser/lckt/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/utils/fmt_plus.go b/utils/fmt_plus.go new file mode 100644 index 0000000..d849d18 --- /dev/null +++ b/utils/fmt_plus.go @@ -0,0 +1,108 @@ +package utils + +import ( + "fmt" + "git.echol.cn/loser/lckt/model/common" + "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, "") +} + +// RandomString 随机字符串 +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) +} + +// BuildTree 用于构建一个树形结构 +func BuildTree[T common.TreeNode[T]](nodes []T) []T { + nodeMap := make(map[int]T) + // 创建一个基本map + for i := range nodes { + nodeMap[nodes[i].GetID()] = nodes[i] + } + + for i := range nodes { + if nodes[i].GetParentID() != 0 { + parent := nodeMap[nodes[i].GetParentID()] + parent.SetChildren(nodes[i]) + } + } + + var rootNodes []T + + for i := range nodeMap { + if nodeMap[i].GetParentID() == 0 { + rootNodes = append(rootNodes, nodeMap[i]) + } + } + return rootNodes +} diff --git a/utils/hash.go b/utils/hash.go new file mode 100644 index 0000000..9c3564b --- /dev/null +++ b/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/utils/human_duration.go b/utils/human_duration.go new file mode 100644 index 0000000..0cdb055 --- /dev/null +++ b/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/utils/human_duration_test.go b/utils/human_duration_test.go new file mode 100644 index 0000000..8a5294b --- /dev/null +++ b/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/utils/json.go b/utils/json.go new file mode 100644 index 0000000..8c4118c --- /dev/null +++ b/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/utils/json_test.go b/utils/json_test.go new file mode 100644 index 0000000..f21a679 --- /dev/null +++ b/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/utils/jwt.go b/utils/jwt.go new file mode 100644 index 0000000..096e2d1 --- /dev/null +++ b/utils/jwt.go @@ -0,0 +1,87 @@ +package utils + +import ( + "errors" + "time" + + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/system/request" + jwt "github.com/golang-jwt/jwt/v5" +) + +type JWT struct { + SigningKey []byte +} + +var ( + TokenValid = errors.New("未知错误") + TokenExpired = errors.New("token已过期") + TokenNotValidYet = errors.New("token尚未激活") + TokenMalformed = errors.New("这不是一个token") + TokenSignatureInvalid = errors.New("无效签名") + TokenInvalid = errors.New("无法处理此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, + BufferTime: int64(bf / time.Second), // 缓冲时间1天 缓冲时间内会获得新的token刷新令牌 此时一个用户会存在两个有效令牌 但是前端只留一个 另一个会丢失 + RegisteredClaims: jwt.RegisteredClaims{ + Audience: jwt.ClaimStrings{"GVA"}, // 受众 + NotBefore: jwt.NewNumericDate(time.Now().Add(-1000)), // 签名生效时间 + ExpiresAt: jwt.NewNumericDate(time.Now().Add(ep)), // 过期时间 7天 配置文件 + Issuer: global.GVA_CONFIG.JWT.Issuer, // 签名的发行者 + }, + } + return claims +} + +// CreateToken 创建一个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 +} + +// ParseToken 解析 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 { + switch { + case errors.Is(err, jwt.ErrTokenExpired): + return nil, TokenExpired + case errors.Is(err, jwt.ErrTokenMalformed): + return nil, TokenMalformed + case errors.Is(err, jwt.ErrTokenSignatureInvalid): + return nil, TokenSignatureInvalid + case errors.Is(err, jwt.ErrTokenNotValidYet): + return nil, TokenNotValidYet + default: + return nil, TokenInvalid + } + } + if token != nil { + if claims, ok := token.Claims.(*request.CustomClaims); ok && token.Valid { + return claims, nil + } + } + return nil, TokenValid +} diff --git a/utils/plugin/plugin.go b/utils/plugin/plugin.go new file mode 100644 index 0000000..a59d5b5 --- /dev/null +++ b/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/utils/plugin/v2/plugin.go b/utils/plugin/v2/plugin.go new file mode 100644 index 0000000..4dac0ab --- /dev/null +++ b/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/utils/reload.go b/utils/reload.go new file mode 100644 index 0000000..de5499b --- /dev/null +++ b/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/utils/request/http.go b/utils/request/http.go new file mode 100644 index 0000000..86d0d15 --- /dev/null +++ b/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/utils/server.go b/utils/server.go new file mode 100644 index 0000000..0c8cbcf --- /dev/null +++ b/utils/server.go @@ -0,0 +1,126 @@ +package utils + +import ( + "git.echol.cn/loser/lckt/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/utils/timer/timed_task.go b/utils/timer/timed_task.go new file mode 100644 index 0000000..06865d2 --- /dev/null +++ b/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 +} + +// AddTaskByFuncWithSecond 通过函数的方法使用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() + } +} + +// RemoveTask 从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/utils/timer/timed_task_test.go b/utils/timer/timed_task_test.go new file mode 100644 index 0000000..9f2c02c --- /dev/null +++ b/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/utils/upload/aliyun_oss.go b/utils/upload/aliyun_oss.go new file mode 100644 index 0000000..c515d79 --- /dev/null +++ b/utils/upload/aliyun_oss.go @@ -0,0 +1,75 @@ +package upload + +import ( + "errors" + "mime/multipart" + "time" + + "git.echol.cn/loser/lckt/global" + "github.com/aliyun/aliyun-oss-go-sdk/oss" + "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/utils/upload/aws_s3.go b/utils/upload/aws_s3.go new file mode 100644 index 0000000..29732f2 --- /dev/null +++ b/utils/upload/aws_s3.go @@ -0,0 +1,97 @@ +package upload + +import ( + "errors" + "fmt" + "mime/multipart" + "time" + + "git.echol.cn/loser/lckt/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/utils/upload/cloudflare_r2.go b/utils/upload/cloudflare_r2.go new file mode 100644 index 0000000..2de8568 --- /dev/null +++ b/utils/upload/cloudflare_r2.go @@ -0,0 +1,85 @@ +package upload + +import ( + "errors" + "fmt" + "mime/multipart" + "time" + + "git.echol.cn/loser/lckt/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 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/utils/upload/local.go b/utils/upload/local.go new file mode 100644 index 0000000..1918b56 --- /dev/null +++ b/utils/upload/local.go @@ -0,0 +1,109 @@ +package upload + +import ( + "errors" + "io" + "mime/multipart" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/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/utils/upload/minio_oss.go b/utils/upload/minio_oss.go new file mode 100644 index 0000000..02dcb95 --- /dev/null +++ b/utils/upload/minio_oss.go @@ -0,0 +1,99 @@ +package upload + +import ( + "bytes" + "context" + "errors" + "io" + "mime/multipart" + "path/filepath" + "strings" + "time" + + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/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 { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + // Delete the object from MinIO + err := m.Client.RemoveObject(ctx, m.bucket, key, minio.RemoveObjectOptions{}) + return err +} diff --git a/utils/upload/obs.go b/utils/upload/obs.go new file mode 100644 index 0000000..991f1d4 --- /dev/null +++ b/utils/upload/obs.go @@ -0,0 +1,69 @@ +package upload + +import ( + "mime/multipart" + + "git.echol.cn/loser/lckt/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/utils/upload/qiniu.go b/utils/upload/qiniu.go new file mode 100644 index 0000000..b6fba1d --- /dev/null +++ b/utils/upload/qiniu.go @@ -0,0 +1,96 @@ +package upload + +import ( + "context" + "errors" + "fmt" + "mime/multipart" + "time" + + "git.echol.cn/loser/lckt/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/utils/upload/tencent_cos.go b/utils/upload/tencent_cos.go new file mode 100644 index 0000000..5070a50 --- /dev/null +++ b/utils/upload/tencent_cos.go @@ -0,0 +1,61 @@ +package upload + +import ( + "context" + "errors" + "fmt" + "mime/multipart" + "net/http" + "net/url" + "time" + + "git.echol.cn/loser/lckt/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/utils/upload/upload.go b/utils/upload/upload.go new file mode 100644 index 0000000..6b2558d --- /dev/null +++ b/utils/upload/upload.go @@ -0,0 +1,46 @@ +package upload + +import ( + "mime/multipart" + + "git.echol.cn/loser/lckt/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/utils/validator.go b/utils/validator.go new file mode 100644 index 0000000..a56dac0 --- /dev/null +++ b/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/utils/validator_test.go b/utils/validator_test.go new file mode 100644 index 0000000..ec5c3fd --- /dev/null +++ b/utils/validator_test.go @@ -0,0 +1,37 @@ +package utils + +import ( + "git.echol.cn/loser/lckt/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/utils/verify.go b/utils/verify.go new file mode 100644 index 0000000..43a8672 --- /dev/null +++ b/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/utils/zip.go b/utils/zip.go new file mode 100644 index 0000000..ef35d10 --- /dev/null +++ b/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.Contains(f.Name, "..") { + 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 +}