From f4e166c5ee04cd94636a9c22519e3e85b5cc9a8c Mon Sep 17 00:00:00 2001
From: Echo <1711788888@qq.com>
Date: Fri, 27 Feb 2026 21:52:00 +0800
Subject: [PATCH] =?UTF-8?q?:tada:=20=E5=88=9D=E5=A7=8B=E5=8C=96=E9=A1=B9?=
=?UTF-8?q?=E7=9B=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: Echo <1711788888@qq.com>
---
.gitignore | 27 +
README.md | 93 +
server/Dockerfile | 31 +
server/README.md | 54 +
server/api/v1/app/ai_config.go | 218 +
server/api/v1/app/auth.go | 189 +
server/api/v1/app/character.go | 246 +
server/api/v1/app/conversation.go | 301 ++
server/api/v1/app/enter.go | 10 +
server/api/v1/app/preset.go | 265 +
server/api/v1/app/upload.go | 41 +
server/api/v1/enter.go | 15 +
server/api/v1/example/enter.go | 15 +
.../api/v1/example/exa_attachment_category.go | 82 +
.../api/v1/example/exa_breakpoint_continue.go | 156 +
server/api/v1/example/exa_customer.go | 176 +
.../v1/example/exa_file_upload_download.go | 136 +
server/api/v1/system/auto_code_history.go | 115 +
server/api/v1/system/auto_code_mcp.go | 145 +
server/api/v1/system/auto_code_package.go | 101 +
server/api/v1/system/auto_code_plugin.go | 218 +
server/api/v1/system/auto_code_template.go | 121 +
server/api/v1/system/enter.go | 57 +
server/api/v1/system/sys_api.go | 323 ++
server/api/v1/system/sys_api_token.go | 81 +
server/api/v1/system/sys_authority.go | 202 +
server/api/v1/system/sys_authority_btn.go | 80 +
server/api/v1/system/sys_auto_code.go | 117 +
server/api/v1/system/sys_captcha.go | 70 +
server/api/v1/system/sys_casbin.go | 69 +
server/api/v1/system/sys_dictionary.go | 191 +
server/api/v1/system/sys_dictionary_detail.go | 267 +
server/api/v1/system/sys_error.go | 199 +
server/api/v1/system/sys_export_template.go | 456 ++
server/api/v1/system/sys_initdb.go | 59 +
server/api/v1/system/sys_jwt_blacklist.go | 33 +
server/api/v1/system/sys_login_log.go | 82 +
server/api/v1/system/sys_menu.go | 265 +
server/api/v1/system/sys_operation_record.go | 124 +
server/api/v1/system/sys_params.go | 171 +
server/api/v1/system/sys_skills.go | 219 +
server/api/v1/system/sys_system.go | 89 +
server/api/v1/system/sys_user.go | 516 ++
server/api/v1/system/sys_version.go | 486 ++
server/config.docker.yaml | 285 +
server/config.yaml | 267 +
server/config/auto_code.go | 22 +
server/config/captcha.go | 9 +
server/config/config.go | 40 +
server/config/cors.go | 14 +
server/config/db_list.go | 53 +
server/config/disk.go | 9 +
server/config/email.go | 12 +
server/config/excel.go | 5 +
server/config/gorm_mssql.go | 10 +
server/config/gorm_mysql.go | 9 +
server/config/gorm_oracle.go | 18 +
server/config/gorm_pgsql.go | 17 +
server/config/gorm_sqlite.go | 13 +
server/config/jwt.go | 8 +
server/config/mcp.go | 11 +
server/config/mongo.go | 41 +
server/config/oss_aliyun.go | 10 +
server/config/oss_aws.go | 13 +
server/config/oss_cloudflare.go | 10 +
server/config/oss_huawei.go | 9 +
server/config/oss_local.go | 6 +
server/config/oss_minio.go | 11 +
server/config/oss_qiniu.go | 11 +
server/config/oss_tencent.go | 10 +
server/config/redis.go | 10 +
server/config/system.go | 16 +
server/config/zap.go | 72 +
server/core/internal/constant.go | 9 +
server/core/internal/cutter.go | 125 +
server/core/internal/zap_core.go | 133 +
server/core/server.go | 44 +
server/core/server_run.go | 60 +
server/core/viper.go | 76 +
server/core/zap.go | 36 +
server/global/global.go | 69 +
server/global/model.go | 14 +
server/global/version.go | 12 +
server/go.mod | 190 +
server/go.sum | 873 ++++
server/initialize/db_list.go | 36 +
server/initialize/ensure_tables.go | 112 +
server/initialize/fix_world_info_table.sql | 9 +
server/initialize/gorm.go | 105 +
server/initialize/gorm_biz.go | 14 +
server/initialize/gorm_mssql.go | 64 +
server/initialize/gorm_mysql.go | 48 +
server/initialize/gorm_oracle.go | 37 +
server/initialize/gorm_pgsql.go | 43 +
server/initialize/gorm_pgsql_extension.go | 48 +
server/initialize/gorm_sqlite.go | 38 +
server/initialize/init.go | 15 +
server/initialize/internal/gorm.go | 31 +
.../initialize/internal/gorm_logger_writer.go | 42 +
server/initialize/internal/mongo.go | 30 +
server/initialize/mcp.go | 25 +
server/initialize/mongo.go | 156 +
server/initialize/other.go | 33 +
server/initialize/plugin.go | 15 +
server/initialize/plugin_biz_v1.go | 36 +
server/initialize/plugin_biz_v2.go | 16 +
server/initialize/redis.go | 59 +
server/initialize/register_init.go | 10 +
server/initialize/reload.go | 45 +
server/initialize/router.go | 168 +
server/initialize/router_biz.go | 19 +
server/initialize/timer.go | 38 +
server/initialize/validator.go | 22 +
server/main.go | 51 +
server/mcp/api_creator.go | 191 +
server/mcp/api_lister.go | 168 +
server/mcp/client/client.go | 40 +
server/mcp/client/client_test.go | 133 +
server/mcp/dictionary_generator.go | 229 +
server/mcp/dictionary_query.go | 239 +
server/mcp/enter.go | 32 +
server/mcp/gva_analyze.go | 503 ++
server/mcp/gva_execute.go | 793 +++
server/mcp/gva_review.go | 170 +
server/mcp/menu_creator.go | 277 +
server/mcp/menu_lister.go | 114 +
server/mcp/requirement_analyzer.go | 199 +
server/middleware/app_jwt.go | 147 +
server/middleware/casbin_rbac.go | 33 +
server/middleware/cors.go | 74 +
server/middleware/email.go | 58 +
server/middleware/error.go | 80 +
server/middleware/jwt.go | 89 +
server/middleware/limit_ip.go | 92 +
server/middleware/loadtls.go | 27 +
server/middleware/logger.go | 89 +
server/middleware/operation.go | 129 +
server/middleware/timeout.go | 56 +
server/model/app/README.md | 213 +
server/model/app/ai_character.go | 49 +
server/model/app/ai_config.go | 31 +
server/model/app/app_user.go | 33 +
server/model/app/app_user_session.go | 25 +
server/model/app/conversation.go | 56 +
server/model/app/preset.go | 44 +
server/model/app/request/ai_config.go | 37 +
server/model/app/request/auth.go | 37 +
server/model/app/request/character.go | 52 +
server/model/app/request/conversation.go | 32 +
server/model/app/request/preset.go | 47 +
server/model/app/response/ai_config.go | 88 +
server/model/app/response/auth.go | 55 +
server/model/app/response/character.go | 108 +
server/model/app/response/conversation.go | 122 +
server/model/app/response/preset.go | 78 +
server/model/common/basetypes.go | 43 +
server/model/common/clearDB.go | 7 +
server/model/common/common.go | 21 +
server/model/common/request/common.go | 48 +
server/model/common/response/common.go | 8 +
server/model/common/response/response.go | 62 +
.../model/example/exa_attachment_category.go | 16 +
.../model/example/exa_breakpoint_continue.go | 24 +
server/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 +
server/model/example/response/exa_customer.go | 7 +
.../response/exa_file_upload_download.go | 7 +
server/model/system/request/jwt.go | 21 +
server/model/system/request/sys_api.go | 14 +
server/model/system/request/sys_api_token.go | 12 +
.../model/system/request/sys_authority_btn.go | 7 +
server/model/system/request/sys_auto_code.go | 292 ++
.../model/system/request/sys_auto_code_mcp.go | 16 +
.../system/request/sys_auto_code_package.go | 31 +
.../model/system/request/sys_auto_history.go | 57 +
server/model/system/request/sys_casbin.go | 27 +
server/model/system/request/sys_dictionary.go | 9 +
.../system/request/sys_dictionary_detail.go | 43 +
server/model/system/request/sys_error.go | 14 +
.../system/request/sys_export_template.go | 15 +
server/model/system/request/sys_init.go | 125 +
server/model/system/request/sys_login_log.go | 11 +
server/model/system/request/sys_menu.go | 27 +
.../system/request/sys_operation_record.go | 11 +
server/model/system/request/sys_params.go | 15 +
server/model/system/request/sys_skills.go | 64 +
server/model/system/request/sys_user.go | 69 +
server/model/system/request/sys_version.go | 41 +
server/model/system/response/sys_api.go | 18 +
server/model/system/response/sys_authority.go | 12 +
.../system/response/sys_authority_btn.go | 5 +
server/model/system/response/sys_auto_code.go | 27 +
server/model/system/response/sys_captcha.go | 8 +
server/model/system/response/sys_casbin.go | 9 +
server/model/system/response/sys_menu.go | 15 +
server/model/system/response/sys_system.go | 7 +
server/model/system/response/sys_user.go | 15 +
server/model/system/response/sys_version.go | 14 +
server/model/system/sys_api.go | 28 +
server/model/system/sys_api_token.go | 18 +
server/model/system/sys_authority.go | 23 +
server/model/system/sys_authority_btn.go | 8 +
server/model/system/sys_authority_menu.go | 19 +
server/model/system/sys_auto_code_history.go | 69 +
server/model/system/sys_auto_code_package.go | 18 +
server/model/system/sys_base_menu.go | 43 +
server/model/system/sys_dictionary.go | 22 +
server/model/system/sys_dictionary_detail.go | 26 +
server/model/system/sys_error.go | 21 +
server/model/system/sys_export_template.go | 46 +
server/model/system/sys_jwt_blacklist.go | 10 +
server/model/system/sys_login_log.go | 16 +
server/model/system/sys_menu_btn.go | 10 +
server/model/system/sys_operation_record.go | 24 +
server/model/system/sys_params.go | 20 +
server/model/system/sys_skills.go | 25 +
server/model/system/sys_system.go | 10 +
server/model/system/sys_user.go | 62 +
server/model/system/sys_user_authority.go | 11 +
server/model/system/sys_version.go | 20 +
server/plugin/email/README.MD | 78 +
server/plugin/email/api/enter.go | 7 +
server/plugin/email/api/sys_email.go | 53 +
server/plugin/email/config/email.go | 12 +
server/plugin/email/global/gloabl.go | 5 +
server/plugin/email/main.go | 29 +
server/plugin/email/model/response/email.go | 7 +
server/plugin/email/router/enter.go | 7 +
server/plugin/email/router/sys_email.go | 19 +
server/plugin/email/service/enter.go | 7 +
server/plugin/email/service/sys_email.go | 32 +
server/plugin/email/utils/email.go | 122 +
server/plugin/plugin-tool/utils/check.go | 138 +
server/plugin/register.go | 1 +
server/resource/function/api.go.tpl | 44 +
server/resource/function/api.js.tpl | 32 +
server/resource/function/server.go.tpl | 25 +
server/resource/mcp/tools.tpl | 56 +
server/resource/package/readme.txt.tpl | 7 +
server/resource/package/server/api/api.go.tpl | 260 +
.../resource/package/server/api/enter.go.tpl | 4 +
.../package/server/model/model.go.tpl | 75 +
.../server/model/request/request.go.tpl | 39 +
.../package/server/router/enter.go.tpl | 4 +
.../package/server/router/router.go.tpl | 42 +
.../package/server/service/enter.go.tpl | 4 +
.../package/server/service/service.go.tpl | 213 +
server/resource/package/web/api/api.js.tpl | 130 +
server/resource/package/web/view/form.vue.tpl | 274 +
.../resource/package/web/view/table.vue.tpl | 694 +++
server/resource/plugin/server/api/api.go.tpl | 255 +
.../resource/plugin/server/api/enter.go.tpl | 6 +
.../plugin/server/config/config.go.tpl | 4 +
server/resource/plugin/server/gen/gen.go.tpl | 18 +
.../plugin/server/initialize/api.go.tpl | 12 +
.../server/initialize/dictionary.go.tpl | 12 +
.../plugin/server/initialize/gorm.go.tpl | 17 +
.../plugin/server/initialize/menu.go.tpl | 12 +
.../plugin/server/initialize/router.go.tpl | 14 +
.../plugin/server/initialize/viper.go.tpl | 17 +
.../resource/plugin/server/model/model.go.tpl | 76 +
.../server/model/request/request.go.tpl | 38 +
server/resource/plugin/server/plugin.go.tpl | 33 +
.../plugin/server/plugin/plugin.go.tpl | 5 +
.../plugin/server/router/enter.go.tpl | 6 +
.../plugin/server/router/router.go.tpl | 46 +
.../plugin/server/service/enter.go.tpl | 7 +
.../plugin/server/service/service.go.tpl | 211 +
server/resource/plugin/web/api/api.js.tpl | 127 +
server/resource/plugin/web/form/form.vue.tpl | 464 ++
server/resource/plugin/web/view/view.vue.tpl | 689 +++
server/router/app/ai_config.go | 26 +
server/router/app/auth.go | 36 +
server/router/app/character.go | 25 +
server/router/app/conversation.go | 25 +
server/router/app/enter.go | 10 +
server/router/app/preset.go | 24 +
server/router/app/upload.go | 17 +
server/router/enter.go | 15 +
server/router/example/enter.go | 17 +
.../router/example/exa_attachment_category.go | 16 +
server/router/example/exa_customer.go | 22 +
.../example/exa_file_upload_and_download.go | 22 +
server/router/system/enter.go | 52 +
server/router/system/sys_api.go | 33 +
server/router/system/sys_api_token.go | 19 +
server/router/system/sys_authority.go | 23 +
server/router/system/sys_authority_btn.go | 19 +
server/router/system/sys_auto_code.go | 47 +
server/router/system/sys_auto_code_history.go | 17 +
server/router/system/sys_base.go | 16 +
server/router/system/sys_casbin.go | 19 +
server/router/system/sys_dictionary.go | 24 +
server/router/system/sys_dictionary_detail.go | 26 +
server/router/system/sys_error.go | 28 +
server/router/system/sys_export_template.go | 35 +
server/router/system/sys_initdb.go | 15 +
server/router/system/sys_jwt.go | 14 +
server/router/system/sys_login_log.go | 23 +
server/router/system/sys_menu.go | 27 +
server/router/system/sys_operation_record.go | 18 +
server/router/system/sys_params.go | 25 +
server/router/system/sys_skills.go | 29 +
server/router/system/sys_system.go | 22 +
server/router/system/sys_user.go | 28 +
server/router/system/sys_version.go | 25 +
server/service/app/ai_config.go | 316 ++
server/service/app/auth.go | 254 +
server/service/app/character.go | 366 ++
server/service/app/conversation.go | 1172 +++++
server/service/app/enter.go | 10 +
server/service/app/preset.go | 353 ++
server/service/app/upload.go | 49 +
server/service/enter.go | 15 +
server/service/example/enter.go | 7 +
.../example/exa_attachment_category.go | 67 +
.../example/exa_breakpoint_continue.go | 71 +
server/service/example/exa_customer.go | 87 +
.../example/exa_file_upload_download.go | 130 +
server/service/system/auto_code_history.go | 218 +
server/service/system/auto_code_llm.go | 52 +
server/service/system/auto_code_mcp.go | 46 +
server/service/system/auto_code_package.go | 743 +++
.../service/system/auto_code_package_test.go | 108 +
server/service/system/auto_code_plugin.go | 512 ++
server/service/system/auto_code_template.go | 454 ++
.../service/system/auto_code_template_test.go | 85 +
server/service/system/enter.go | 29 +
server/service/system/jwt_black_list.go | 52 +
server/service/system/sys_api.go | 326 ++
server/service/system/sys_api_token.go | 107 +
server/service/system/sys_authority.go | 333 ++
server/service/system/sys_authority_btn.go | 61 +
.../service/system/sys_auto_code_interface.go | 55 +
server/service/system/sys_auto_code_mssql.go | 84 +
server/service/system/sys_auto_code_mysql.go | 83 +
server/service/system/sys_auto_code_oracle.go | 72 +
server/service/system/sys_auto_code_pgsql.go | 135 +
server/service/system/sys_auto_code_sqlite.go | 85 +
server/service/system/sys_base_menu.go | 147 +
server/service/system/sys_casbin.go | 173 +
server/service/system/sys_dictionary.go | 297 ++
.../service/system/sys_dictionary_detail.go | 392 ++
server/service/system/sys_error.go | 127 +
server/service/system/sys_export_template.go | 724 +++
server/service/system/sys_initdb.go | 190 +
server/service/system/sys_initdb_mssql.go | 93 +
server/service/system/sys_initdb_mysql.go | 97 +
server/service/system/sys_initdb_pgsql.go | 101 +
server/service/system/sys_initdb_sqlite.go | 89 +
server/service/system/sys_login_log.go | 53 +
server/service/system/sys_menu.go | 332 ++
server/service/system/sys_operation_record.go | 83 +
server/service/system/sys_params.go | 82 +
server/service/system/sys_skills.go | 549 ++
server/service/system/sys_system.go | 62 +
server/service/system/sys_user.go | 318 ++
server/service/system/sys_version.go | 231 +
server/source/example/file_upload_download.go | 66 +
server/source/system/api.go | 264 +
server/source/system/api_ignore.go | 78 +
server/source/system/authorities_menus.go | 121 +
server/source/system/authority.go | 89 +
server/source/system/casbin.go | 348 ++
server/source/system/dictionary.go | 72 +
server/source/system/dictionary_detail.go | 122 +
server/source/system/excel_template.go | 76 +
server/source/system/menu.go | 138 +
server/source/system/user.go | 107 +
server/task/clearTable.go | 52 +
server/utils/app_jwt.go | 84 +
server/utils/ast/ast.go | 410 ++
server/utils/ast/ast_auto_enter.go | 47 +
server/utils/ast/ast_enter.go | 182 +
server/utils/ast/ast_gorm.go | 166 +
server/utils/ast/ast_init_test.go | 12 +
server/utils/ast/ast_rollback.go | 174 +
server/utils/ast/ast_router.go | 135 +
server/utils/ast/ast_test.go | 33 +
server/utils/ast/ast_type.go | 53 +
server/utils/ast/extract_func.go | 62 +
server/utils/ast/import.go | 94 +
server/utils/ast/interfaces.go | 17 +
server/utils/ast/interfaces_base.go | 77 +
server/utils/ast/package_enter.go | 85 +
server/utils/ast/package_enter_test.go | 155 +
server/utils/ast/package_initialize_gorm.go | 196 +
.../utils/ast/package_initialize_gorm_test.go | 172 +
server/utils/ast/package_initialize_router.go | 150 +
.../ast/package_initialize_router_test.go | 159 +
server/utils/ast/package_module_enter.go | 180 +
server/utils/ast/package_module_enter_test.go | 186 +
server/utils/ast/plugin_enter.go | 167 +
server/utils/ast/plugin_enter_test.go | 201 +
server/utils/ast/plugin_gen.go | 189 +
server/utils/ast/plugin_gen_test.go | 128 +
server/utils/ast/plugin_initialize_gorm.go | 111 +
.../utils/ast/plugin_initialize_gorm_test.go | 139 +
server/utils/ast/plugin_initialize_router.go | 124 +
.../ast/plugin_initialize_router_test.go | 156 +
server/utils/ast/plugin_initialize_v2.go | 82 +
server/utils/ast/plugin_initialize_v2_test.go | 101 +
server/utils/autocode/template_funcs.go | 714 +++
server/utils/breakpoint_continue.go | 121 +
server/utils/captcha/redis.go | 61 +
server/utils/casbin_util.go | 52 +
server/utils/character_card.go | 285 +
server/utils/claims.go | 148 +
server/utils/directory.go | 124 +
server/utils/fmt_plus.go | 126 +
server/utils/hash.go | 32 +
server/utils/human_duration.go | 29 +
server/utils/human_duration_test.go | 49 +
server/utils/json.go | 34 +
server/utils/json_test.go | 53 +
server/utils/jwt.go | 105 +
server/utils/plugin/plugin.go | 18 +
server/utils/plugin/v2/plugin.go | 11 +
server/utils/plugin/v2/registry.go | 27 +
server/utils/request/http.go | 62 +
server/utils/server.go | 127 +
server/utils/stacktrace/stacktrace.go | 79 +
server/utils/system_events.go | 34 +
server/utils/timer/timed_task.go | 230 +
server/utils/timer/timed_task_test.go | 72 +
server/utils/upload/aliyun_oss.go | 75 +
server/utils/upload/aws_s3.go | 98 +
server/utils/upload/cloudflare_r2.go | 85 +
server/utils/upload/local.go | 109 +
server/utils/upload/minio_oss.go | 106 +
server/utils/upload/obs.go | 69 +
server/utils/upload/qiniu.go | 96 +
server/utils/upload/tencent_cos.go | 61 +
server/utils/upload/upload.go | 46 +
server/utils/validator.go | 294 ++
server/utils/validator_test.go | 38 +
server/utils/verify.go | 19 +
server/utils/zip.go | 53 +
web-app/.env.development | 1 +
web-app/.env.production | 1 +
web-app/.vite/deps/_metadata.json | 8 +
web-app/.vite/deps/package.json | 3 +
web-app/.vite/deps_temp_7925bc02/package.json | 3 +
web-app/README.md | 55 +
web-app/index.html | 15 +
web-app/package-lock.json | 4652 +++++++++++++++++
web-app/package.json | 32 +
web-app/postcss.config.js | 6 +
web-app/src/App.tsx | 36 +
web-app/src/api/aiConfig.ts | 114 +
web-app/src/api/auth.ts | 92 +
web-app/src/api/character.ts | 130 +
web-app/src/api/client.ts | 76 +
web-app/src/api/conversation.ts | 109 +
web-app/src/api/preset.ts | 102 +
web-app/src/api/upload.ts | 18 +
web-app/src/components/CharacterPanel.tsx | 131 +
web-app/src/components/ChatArea.tsx | 719 +++
web-app/src/components/MessageContent.tsx | 341 ++
web-app/src/components/Navbar.tsx | 138 +
web-app/src/components/SettingsPanel.tsx | 247 +
web-app/src/components/Sidebar.tsx | 249 +
web-app/src/index.css | 27 +
web-app/src/main.tsx | 10 +
web-app/src/pages/AdminPage.tsx | 1102 ++++
web-app/src/pages/CharacterDetail.tsx | 183 +
web-app/src/pages/CharacterDetailPage.tsx | 244 +
web-app/src/pages/CharacterManagePage.tsx | 677 +++
web-app/src/pages/CharacterMarket.tsx | 206 +
web-app/src/pages/ChatPage.tsx | 208 +
web-app/src/pages/ForgotPasswordPage.tsx | 87 +
web-app/src/pages/HomePage.tsx | 132 +
web-app/src/pages/LoginPage.tsx | 142 +
web-app/src/pages/PresetManagePage.tsx | 836 +++
web-app/src/pages/ProfilePage.tsx | 295 ++
web-app/src/pages/RegisterPage.tsx | 184 +
web-app/tailwind.config.js | 20 +
web-app/tsconfig.json | 21 +
web-app/tsconfig.node.json | 10 +
web-app/vite.config.ts | 6 +
482 files changed, 55079 insertions(+)
create mode 100644 .gitignore
create mode 100644 README.md
create mode 100644 server/Dockerfile
create mode 100644 server/README.md
create mode 100644 server/api/v1/app/ai_config.go
create mode 100644 server/api/v1/app/auth.go
create mode 100644 server/api/v1/app/character.go
create mode 100644 server/api/v1/app/conversation.go
create mode 100644 server/api/v1/app/enter.go
create mode 100644 server/api/v1/app/preset.go
create mode 100644 server/api/v1/app/upload.go
create mode 100644 server/api/v1/enter.go
create mode 100644 server/api/v1/example/enter.go
create mode 100644 server/api/v1/example/exa_attachment_category.go
create mode 100644 server/api/v1/example/exa_breakpoint_continue.go
create mode 100644 server/api/v1/example/exa_customer.go
create mode 100644 server/api/v1/example/exa_file_upload_download.go
create mode 100644 server/api/v1/system/auto_code_history.go
create mode 100644 server/api/v1/system/auto_code_mcp.go
create mode 100644 server/api/v1/system/auto_code_package.go
create mode 100644 server/api/v1/system/auto_code_plugin.go
create mode 100644 server/api/v1/system/auto_code_template.go
create mode 100644 server/api/v1/system/enter.go
create mode 100644 server/api/v1/system/sys_api.go
create mode 100644 server/api/v1/system/sys_api_token.go
create mode 100644 server/api/v1/system/sys_authority.go
create mode 100644 server/api/v1/system/sys_authority_btn.go
create mode 100644 server/api/v1/system/sys_auto_code.go
create mode 100644 server/api/v1/system/sys_captcha.go
create mode 100644 server/api/v1/system/sys_casbin.go
create mode 100644 server/api/v1/system/sys_dictionary.go
create mode 100644 server/api/v1/system/sys_dictionary_detail.go
create mode 100644 server/api/v1/system/sys_error.go
create mode 100644 server/api/v1/system/sys_export_template.go
create mode 100644 server/api/v1/system/sys_initdb.go
create mode 100644 server/api/v1/system/sys_jwt_blacklist.go
create mode 100644 server/api/v1/system/sys_login_log.go
create mode 100644 server/api/v1/system/sys_menu.go
create mode 100644 server/api/v1/system/sys_operation_record.go
create mode 100644 server/api/v1/system/sys_params.go
create mode 100644 server/api/v1/system/sys_skills.go
create mode 100644 server/api/v1/system/sys_system.go
create mode 100644 server/api/v1/system/sys_user.go
create mode 100644 server/api/v1/system/sys_version.go
create mode 100644 server/config.docker.yaml
create mode 100644 server/config.yaml
create mode 100644 server/config/auto_code.go
create mode 100644 server/config/captcha.go
create mode 100644 server/config/config.go
create mode 100644 server/config/cors.go
create mode 100644 server/config/db_list.go
create mode 100644 server/config/disk.go
create mode 100644 server/config/email.go
create mode 100644 server/config/excel.go
create mode 100644 server/config/gorm_mssql.go
create mode 100644 server/config/gorm_mysql.go
create mode 100644 server/config/gorm_oracle.go
create mode 100644 server/config/gorm_pgsql.go
create mode 100644 server/config/gorm_sqlite.go
create mode 100644 server/config/jwt.go
create mode 100644 server/config/mcp.go
create mode 100644 server/config/mongo.go
create mode 100644 server/config/oss_aliyun.go
create mode 100644 server/config/oss_aws.go
create mode 100644 server/config/oss_cloudflare.go
create mode 100644 server/config/oss_huawei.go
create mode 100644 server/config/oss_local.go
create mode 100644 server/config/oss_minio.go
create mode 100644 server/config/oss_qiniu.go
create mode 100644 server/config/oss_tencent.go
create mode 100644 server/config/redis.go
create mode 100644 server/config/system.go
create mode 100644 server/config/zap.go
create mode 100644 server/core/internal/constant.go
create mode 100644 server/core/internal/cutter.go
create mode 100644 server/core/internal/zap_core.go
create mode 100644 server/core/server.go
create mode 100644 server/core/server_run.go
create mode 100644 server/core/viper.go
create mode 100644 server/core/zap.go
create mode 100644 server/global/global.go
create mode 100644 server/global/model.go
create mode 100644 server/global/version.go
create mode 100644 server/go.mod
create mode 100644 server/go.sum
create mode 100644 server/initialize/db_list.go
create mode 100644 server/initialize/ensure_tables.go
create mode 100644 server/initialize/fix_world_info_table.sql
create mode 100644 server/initialize/gorm.go
create mode 100644 server/initialize/gorm_biz.go
create mode 100644 server/initialize/gorm_mssql.go
create mode 100644 server/initialize/gorm_mysql.go
create mode 100644 server/initialize/gorm_oracle.go
create mode 100644 server/initialize/gorm_pgsql.go
create mode 100644 server/initialize/gorm_pgsql_extension.go
create mode 100644 server/initialize/gorm_sqlite.go
create mode 100644 server/initialize/init.go
create mode 100644 server/initialize/internal/gorm.go
create mode 100644 server/initialize/internal/gorm_logger_writer.go
create mode 100644 server/initialize/internal/mongo.go
create mode 100644 server/initialize/mcp.go
create mode 100644 server/initialize/mongo.go
create mode 100644 server/initialize/other.go
create mode 100644 server/initialize/plugin.go
create mode 100644 server/initialize/plugin_biz_v1.go
create mode 100644 server/initialize/plugin_biz_v2.go
create mode 100644 server/initialize/redis.go
create mode 100644 server/initialize/register_init.go
create mode 100644 server/initialize/reload.go
create mode 100644 server/initialize/router.go
create mode 100644 server/initialize/router_biz.go
create mode 100644 server/initialize/timer.go
create mode 100644 server/initialize/validator.go
create mode 100644 server/main.go
create mode 100644 server/mcp/api_creator.go
create mode 100644 server/mcp/api_lister.go
create mode 100644 server/mcp/client/client.go
create mode 100644 server/mcp/client/client_test.go
create mode 100644 server/mcp/dictionary_generator.go
create mode 100644 server/mcp/dictionary_query.go
create mode 100644 server/mcp/enter.go
create mode 100644 server/mcp/gva_analyze.go
create mode 100644 server/mcp/gva_execute.go
create mode 100644 server/mcp/gva_review.go
create mode 100644 server/mcp/menu_creator.go
create mode 100644 server/mcp/menu_lister.go
create mode 100644 server/mcp/requirement_analyzer.go
create mode 100644 server/middleware/app_jwt.go
create mode 100644 server/middleware/casbin_rbac.go
create mode 100644 server/middleware/cors.go
create mode 100644 server/middleware/email.go
create mode 100644 server/middleware/error.go
create mode 100644 server/middleware/jwt.go
create mode 100644 server/middleware/limit_ip.go
create mode 100644 server/middleware/loadtls.go
create mode 100644 server/middleware/logger.go
create mode 100644 server/middleware/operation.go
create mode 100644 server/middleware/timeout.go
create mode 100644 server/model/app/README.md
create mode 100644 server/model/app/ai_character.go
create mode 100644 server/model/app/ai_config.go
create mode 100644 server/model/app/app_user.go
create mode 100644 server/model/app/app_user_session.go
create mode 100644 server/model/app/conversation.go
create mode 100644 server/model/app/preset.go
create mode 100644 server/model/app/request/ai_config.go
create mode 100644 server/model/app/request/auth.go
create mode 100644 server/model/app/request/character.go
create mode 100644 server/model/app/request/conversation.go
create mode 100644 server/model/app/request/preset.go
create mode 100644 server/model/app/response/ai_config.go
create mode 100644 server/model/app/response/auth.go
create mode 100644 server/model/app/response/character.go
create mode 100644 server/model/app/response/conversation.go
create mode 100644 server/model/app/response/preset.go
create mode 100644 server/model/common/basetypes.go
create mode 100644 server/model/common/clearDB.go
create mode 100644 server/model/common/common.go
create mode 100644 server/model/common/request/common.go
create mode 100644 server/model/common/response/common.go
create mode 100644 server/model/common/response/response.go
create mode 100644 server/model/example/exa_attachment_category.go
create mode 100644 server/model/example/exa_breakpoint_continue.go
create mode 100644 server/model/example/exa_customer.go
create mode 100644 server/model/example/exa_file_upload_download.go
create mode 100644 server/model/example/request/exa_file_upload_and_downloads.go
create mode 100644 server/model/example/response/exa_breakpoint_continue.go
create mode 100644 server/model/example/response/exa_customer.go
create mode 100644 server/model/example/response/exa_file_upload_download.go
create mode 100644 server/model/system/request/jwt.go
create mode 100644 server/model/system/request/sys_api.go
create mode 100644 server/model/system/request/sys_api_token.go
create mode 100644 server/model/system/request/sys_authority_btn.go
create mode 100644 server/model/system/request/sys_auto_code.go
create mode 100644 server/model/system/request/sys_auto_code_mcp.go
create mode 100644 server/model/system/request/sys_auto_code_package.go
create mode 100644 server/model/system/request/sys_auto_history.go
create mode 100644 server/model/system/request/sys_casbin.go
create mode 100644 server/model/system/request/sys_dictionary.go
create mode 100644 server/model/system/request/sys_dictionary_detail.go
create mode 100644 server/model/system/request/sys_error.go
create mode 100644 server/model/system/request/sys_export_template.go
create mode 100644 server/model/system/request/sys_init.go
create mode 100644 server/model/system/request/sys_login_log.go
create mode 100644 server/model/system/request/sys_menu.go
create mode 100644 server/model/system/request/sys_operation_record.go
create mode 100644 server/model/system/request/sys_params.go
create mode 100644 server/model/system/request/sys_skills.go
create mode 100644 server/model/system/request/sys_user.go
create mode 100644 server/model/system/request/sys_version.go
create mode 100644 server/model/system/response/sys_api.go
create mode 100644 server/model/system/response/sys_authority.go
create mode 100644 server/model/system/response/sys_authority_btn.go
create mode 100644 server/model/system/response/sys_auto_code.go
create mode 100644 server/model/system/response/sys_captcha.go
create mode 100644 server/model/system/response/sys_casbin.go
create mode 100644 server/model/system/response/sys_menu.go
create mode 100644 server/model/system/response/sys_system.go
create mode 100644 server/model/system/response/sys_user.go
create mode 100644 server/model/system/response/sys_version.go
create mode 100644 server/model/system/sys_api.go
create mode 100644 server/model/system/sys_api_token.go
create mode 100644 server/model/system/sys_authority.go
create mode 100644 server/model/system/sys_authority_btn.go
create mode 100644 server/model/system/sys_authority_menu.go
create mode 100644 server/model/system/sys_auto_code_history.go
create mode 100644 server/model/system/sys_auto_code_package.go
create mode 100644 server/model/system/sys_base_menu.go
create mode 100644 server/model/system/sys_dictionary.go
create mode 100644 server/model/system/sys_dictionary_detail.go
create mode 100644 server/model/system/sys_error.go
create mode 100644 server/model/system/sys_export_template.go
create mode 100644 server/model/system/sys_jwt_blacklist.go
create mode 100644 server/model/system/sys_login_log.go
create mode 100644 server/model/system/sys_menu_btn.go
create mode 100644 server/model/system/sys_operation_record.go
create mode 100644 server/model/system/sys_params.go
create mode 100644 server/model/system/sys_skills.go
create mode 100644 server/model/system/sys_system.go
create mode 100644 server/model/system/sys_user.go
create mode 100644 server/model/system/sys_user_authority.go
create mode 100644 server/model/system/sys_version.go
create mode 100644 server/plugin/email/README.MD
create mode 100644 server/plugin/email/api/enter.go
create mode 100644 server/plugin/email/api/sys_email.go
create mode 100644 server/plugin/email/config/email.go
create mode 100644 server/plugin/email/global/gloabl.go
create mode 100644 server/plugin/email/main.go
create mode 100644 server/plugin/email/model/response/email.go
create mode 100644 server/plugin/email/router/enter.go
create mode 100644 server/plugin/email/router/sys_email.go
create mode 100644 server/plugin/email/service/enter.go
create mode 100644 server/plugin/email/service/sys_email.go
create mode 100644 server/plugin/email/utils/email.go
create mode 100644 server/plugin/plugin-tool/utils/check.go
create mode 100644 server/plugin/register.go
create mode 100644 server/resource/function/api.go.tpl
create mode 100644 server/resource/function/api.js.tpl
create mode 100644 server/resource/function/server.go.tpl
create mode 100644 server/resource/mcp/tools.tpl
create mode 100644 server/resource/package/readme.txt.tpl
create mode 100644 server/resource/package/server/api/api.go.tpl
create mode 100644 server/resource/package/server/api/enter.go.tpl
create mode 100644 server/resource/package/server/model/model.go.tpl
create mode 100644 server/resource/package/server/model/request/request.go.tpl
create mode 100644 server/resource/package/server/router/enter.go.tpl
create mode 100644 server/resource/package/server/router/router.go.tpl
create mode 100644 server/resource/package/server/service/enter.go.tpl
create mode 100644 server/resource/package/server/service/service.go.tpl
create mode 100644 server/resource/package/web/api/api.js.tpl
create mode 100644 server/resource/package/web/view/form.vue.tpl
create mode 100644 server/resource/package/web/view/table.vue.tpl
create mode 100644 server/resource/plugin/server/api/api.go.tpl
create mode 100644 server/resource/plugin/server/api/enter.go.tpl
create mode 100644 server/resource/plugin/server/config/config.go.tpl
create mode 100644 server/resource/plugin/server/gen/gen.go.tpl
create mode 100644 server/resource/plugin/server/initialize/api.go.tpl
create mode 100644 server/resource/plugin/server/initialize/dictionary.go.tpl
create mode 100644 server/resource/plugin/server/initialize/gorm.go.tpl
create mode 100644 server/resource/plugin/server/initialize/menu.go.tpl
create mode 100644 server/resource/plugin/server/initialize/router.go.tpl
create mode 100644 server/resource/plugin/server/initialize/viper.go.tpl
create mode 100644 server/resource/plugin/server/model/model.go.tpl
create mode 100644 server/resource/plugin/server/model/request/request.go.tpl
create mode 100644 server/resource/plugin/server/plugin.go.tpl
create mode 100644 server/resource/plugin/server/plugin/plugin.go.tpl
create mode 100644 server/resource/plugin/server/router/enter.go.tpl
create mode 100644 server/resource/plugin/server/router/router.go.tpl
create mode 100644 server/resource/plugin/server/service/enter.go.tpl
create mode 100644 server/resource/plugin/server/service/service.go.tpl
create mode 100644 server/resource/plugin/web/api/api.js.tpl
create mode 100644 server/resource/plugin/web/form/form.vue.tpl
create mode 100644 server/resource/plugin/web/view/view.vue.tpl
create mode 100644 server/router/app/ai_config.go
create mode 100644 server/router/app/auth.go
create mode 100644 server/router/app/character.go
create mode 100644 server/router/app/conversation.go
create mode 100644 server/router/app/enter.go
create mode 100644 server/router/app/preset.go
create mode 100644 server/router/app/upload.go
create mode 100644 server/router/enter.go
create mode 100644 server/router/example/enter.go
create mode 100644 server/router/example/exa_attachment_category.go
create mode 100644 server/router/example/exa_customer.go
create mode 100644 server/router/example/exa_file_upload_and_download.go
create mode 100644 server/router/system/enter.go
create mode 100644 server/router/system/sys_api.go
create mode 100644 server/router/system/sys_api_token.go
create mode 100644 server/router/system/sys_authority.go
create mode 100644 server/router/system/sys_authority_btn.go
create mode 100644 server/router/system/sys_auto_code.go
create mode 100644 server/router/system/sys_auto_code_history.go
create mode 100644 server/router/system/sys_base.go
create mode 100644 server/router/system/sys_casbin.go
create mode 100644 server/router/system/sys_dictionary.go
create mode 100644 server/router/system/sys_dictionary_detail.go
create mode 100644 server/router/system/sys_error.go
create mode 100644 server/router/system/sys_export_template.go
create mode 100644 server/router/system/sys_initdb.go
create mode 100644 server/router/system/sys_jwt.go
create mode 100644 server/router/system/sys_login_log.go
create mode 100644 server/router/system/sys_menu.go
create mode 100644 server/router/system/sys_operation_record.go
create mode 100644 server/router/system/sys_params.go
create mode 100644 server/router/system/sys_skills.go
create mode 100644 server/router/system/sys_system.go
create mode 100644 server/router/system/sys_user.go
create mode 100644 server/router/system/sys_version.go
create mode 100644 server/service/app/ai_config.go
create mode 100644 server/service/app/auth.go
create mode 100644 server/service/app/character.go
create mode 100644 server/service/app/conversation.go
create mode 100644 server/service/app/enter.go
create mode 100644 server/service/app/preset.go
create mode 100644 server/service/app/upload.go
create mode 100644 server/service/enter.go
create mode 100644 server/service/example/enter.go
create mode 100644 server/service/example/exa_attachment_category.go
create mode 100644 server/service/example/exa_breakpoint_continue.go
create mode 100644 server/service/example/exa_customer.go
create mode 100644 server/service/example/exa_file_upload_download.go
create mode 100644 server/service/system/auto_code_history.go
create mode 100644 server/service/system/auto_code_llm.go
create mode 100644 server/service/system/auto_code_mcp.go
create mode 100644 server/service/system/auto_code_package.go
create mode 100644 server/service/system/auto_code_package_test.go
create mode 100644 server/service/system/auto_code_plugin.go
create mode 100644 server/service/system/auto_code_template.go
create mode 100644 server/service/system/auto_code_template_test.go
create mode 100644 server/service/system/enter.go
create mode 100644 server/service/system/jwt_black_list.go
create mode 100644 server/service/system/sys_api.go
create mode 100644 server/service/system/sys_api_token.go
create mode 100644 server/service/system/sys_authority.go
create mode 100644 server/service/system/sys_authority_btn.go
create mode 100644 server/service/system/sys_auto_code_interface.go
create mode 100644 server/service/system/sys_auto_code_mssql.go
create mode 100644 server/service/system/sys_auto_code_mysql.go
create mode 100644 server/service/system/sys_auto_code_oracle.go
create mode 100644 server/service/system/sys_auto_code_pgsql.go
create mode 100644 server/service/system/sys_auto_code_sqlite.go
create mode 100644 server/service/system/sys_base_menu.go
create mode 100644 server/service/system/sys_casbin.go
create mode 100644 server/service/system/sys_dictionary.go
create mode 100644 server/service/system/sys_dictionary_detail.go
create mode 100644 server/service/system/sys_error.go
create mode 100644 server/service/system/sys_export_template.go
create mode 100644 server/service/system/sys_initdb.go
create mode 100644 server/service/system/sys_initdb_mssql.go
create mode 100644 server/service/system/sys_initdb_mysql.go
create mode 100644 server/service/system/sys_initdb_pgsql.go
create mode 100644 server/service/system/sys_initdb_sqlite.go
create mode 100644 server/service/system/sys_login_log.go
create mode 100644 server/service/system/sys_menu.go
create mode 100644 server/service/system/sys_operation_record.go
create mode 100644 server/service/system/sys_params.go
create mode 100644 server/service/system/sys_skills.go
create mode 100644 server/service/system/sys_system.go
create mode 100644 server/service/system/sys_user.go
create mode 100644 server/service/system/sys_version.go
create mode 100644 server/source/example/file_upload_download.go
create mode 100644 server/source/system/api.go
create mode 100644 server/source/system/api_ignore.go
create mode 100644 server/source/system/authorities_menus.go
create mode 100644 server/source/system/authority.go
create mode 100644 server/source/system/casbin.go
create mode 100644 server/source/system/dictionary.go
create mode 100644 server/source/system/dictionary_detail.go
create mode 100644 server/source/system/excel_template.go
create mode 100644 server/source/system/menu.go
create mode 100644 server/source/system/user.go
create mode 100644 server/task/clearTable.go
create mode 100644 server/utils/app_jwt.go
create mode 100644 server/utils/ast/ast.go
create mode 100644 server/utils/ast/ast_auto_enter.go
create mode 100644 server/utils/ast/ast_enter.go
create mode 100644 server/utils/ast/ast_gorm.go
create mode 100644 server/utils/ast/ast_init_test.go
create mode 100644 server/utils/ast/ast_rollback.go
create mode 100644 server/utils/ast/ast_router.go
create mode 100644 server/utils/ast/ast_test.go
create mode 100644 server/utils/ast/ast_type.go
create mode 100644 server/utils/ast/extract_func.go
create mode 100644 server/utils/ast/import.go
create mode 100644 server/utils/ast/interfaces.go
create mode 100644 server/utils/ast/interfaces_base.go
create mode 100644 server/utils/ast/package_enter.go
create mode 100644 server/utils/ast/package_enter_test.go
create mode 100644 server/utils/ast/package_initialize_gorm.go
create mode 100644 server/utils/ast/package_initialize_gorm_test.go
create mode 100644 server/utils/ast/package_initialize_router.go
create mode 100644 server/utils/ast/package_initialize_router_test.go
create mode 100644 server/utils/ast/package_module_enter.go
create mode 100644 server/utils/ast/package_module_enter_test.go
create mode 100644 server/utils/ast/plugin_enter.go
create mode 100644 server/utils/ast/plugin_enter_test.go
create mode 100644 server/utils/ast/plugin_gen.go
create mode 100644 server/utils/ast/plugin_gen_test.go
create mode 100644 server/utils/ast/plugin_initialize_gorm.go
create mode 100644 server/utils/ast/plugin_initialize_gorm_test.go
create mode 100644 server/utils/ast/plugin_initialize_router.go
create mode 100644 server/utils/ast/plugin_initialize_router_test.go
create mode 100644 server/utils/ast/plugin_initialize_v2.go
create mode 100644 server/utils/ast/plugin_initialize_v2_test.go
create mode 100644 server/utils/autocode/template_funcs.go
create mode 100644 server/utils/breakpoint_continue.go
create mode 100644 server/utils/captcha/redis.go
create mode 100644 server/utils/casbin_util.go
create mode 100644 server/utils/character_card.go
create mode 100644 server/utils/claims.go
create mode 100644 server/utils/directory.go
create mode 100644 server/utils/fmt_plus.go
create mode 100644 server/utils/hash.go
create mode 100644 server/utils/human_duration.go
create mode 100644 server/utils/human_duration_test.go
create mode 100644 server/utils/json.go
create mode 100644 server/utils/json_test.go
create mode 100644 server/utils/jwt.go
create mode 100644 server/utils/plugin/plugin.go
create mode 100644 server/utils/plugin/v2/plugin.go
create mode 100644 server/utils/plugin/v2/registry.go
create mode 100644 server/utils/request/http.go
create mode 100644 server/utils/server.go
create mode 100644 server/utils/stacktrace/stacktrace.go
create mode 100644 server/utils/system_events.go
create mode 100644 server/utils/timer/timed_task.go
create mode 100644 server/utils/timer/timed_task_test.go
create mode 100644 server/utils/upload/aliyun_oss.go
create mode 100644 server/utils/upload/aws_s3.go
create mode 100644 server/utils/upload/cloudflare_r2.go
create mode 100644 server/utils/upload/local.go
create mode 100644 server/utils/upload/minio_oss.go
create mode 100644 server/utils/upload/obs.go
create mode 100644 server/utils/upload/qiniu.go
create mode 100644 server/utils/upload/tencent_cos.go
create mode 100644 server/utils/upload/upload.go
create mode 100644 server/utils/validator.go
create mode 100644 server/utils/validator_test.go
create mode 100644 server/utils/verify.go
create mode 100644 server/utils/zip.go
create mode 100644 web-app/.env.development
create mode 100644 web-app/.env.production
create mode 100644 web-app/.vite/deps/_metadata.json
create mode 100644 web-app/.vite/deps/package.json
create mode 100644 web-app/.vite/deps_temp_7925bc02/package.json
create mode 100644 web-app/README.md
create mode 100644 web-app/index.html
create mode 100644 web-app/package-lock.json
create mode 100644 web-app/package.json
create mode 100644 web-app/postcss.config.js
create mode 100644 web-app/src/App.tsx
create mode 100644 web-app/src/api/aiConfig.ts
create mode 100644 web-app/src/api/auth.ts
create mode 100644 web-app/src/api/character.ts
create mode 100644 web-app/src/api/client.ts
create mode 100644 web-app/src/api/conversation.ts
create mode 100644 web-app/src/api/preset.ts
create mode 100644 web-app/src/api/upload.ts
create mode 100644 web-app/src/components/CharacterPanel.tsx
create mode 100644 web-app/src/components/ChatArea.tsx
create mode 100644 web-app/src/components/MessageContent.tsx
create mode 100644 web-app/src/components/Navbar.tsx
create mode 100644 web-app/src/components/SettingsPanel.tsx
create mode 100644 web-app/src/components/Sidebar.tsx
create mode 100644 web-app/src/index.css
create mode 100644 web-app/src/main.tsx
create mode 100644 web-app/src/pages/AdminPage.tsx
create mode 100644 web-app/src/pages/CharacterDetail.tsx
create mode 100644 web-app/src/pages/CharacterDetailPage.tsx
create mode 100644 web-app/src/pages/CharacterManagePage.tsx
create mode 100644 web-app/src/pages/CharacterMarket.tsx
create mode 100644 web-app/src/pages/ChatPage.tsx
create mode 100644 web-app/src/pages/ForgotPasswordPage.tsx
create mode 100644 web-app/src/pages/HomePage.tsx
create mode 100644 web-app/src/pages/LoginPage.tsx
create mode 100644 web-app/src/pages/PresetManagePage.tsx
create mode 100644 web-app/src/pages/ProfilePage.tsx
create mode 100644 web-app/src/pages/RegisterPage.tsx
create mode 100644 web-app/tailwind.config.js
create mode 100644 web-app/tsconfig.json
create mode 100644 web-app/tsconfig.node.json
create mode 100644 web-app/vite.config.ts
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3d7c5e9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,27 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+uploads
+docs
+.claude
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..80e8a98
--- /dev/null
+++ b/README.md
@@ -0,0 +1,93 @@
+# 云酒馆 - SillyTavern Cloud
+
+现代化的 AI 角色对话平台,采用前后端分离架构。
+
+## 项目结构
+
+```
+st-ui/
+├── web-app/ # 前端应用 (React + TypeScript + Tailwind)
+├── server/ # 后端服务 (Node.js + Express)
+├── docs/ # 项目文档
+├── .claude/ # Claude AI 配置
+└── README.md # 项目说明
+```
+
+## 快速开始
+
+### 前端开发
+
+```bash
+cd web-app
+npm install
+npm run dev
+```
+
+前端将运行在 `http://localhost:5174`
+
+### 后端开发
+
+```bash
+cd server
+npm install
+cp .env.example .env # 配置环境变量
+npm run dev
+```
+
+后端将运行在 `http://localhost:3000`
+
+## 功能特性
+
+### 前端功能
+- 🎨 现代化 Glassmorphism 设计
+- 🌙 深色主题 (OLED 优化)
+- 💬 实时聊天界面
+- 👤 角色卡管理(支持 PNG/JSON 导入导出)
+- ⚙️ 预设管理(支持多种格式)
+- 🔐 用户认证系统
+- 📱 响应式设计
+
+### 后端功能
+- 🔒 JWT 身份认证
+- 📦 角色卡存储和管理
+- 💾 对话历史持久化
+- 🤖 AI API 集成
+- 📤 文件上传处理
+
+## 技术栈
+
+### 前端
+- React 18
+- TypeScript
+- Tailwind CSS
+- Vite
+- React Router
+- Lucide Icons
+
+### 后端
+- Node.js
+- Express
+- MongoDB
+- JWT
+- Multer
+
+## 开发指南
+
+详细的开发文档请查看 [docs](./docs/) 目录。
+
+## 设计系统
+
+项目采用统一的设计系统,详见 [design-system](./docs/design-system/)。
+
+- 主色调: #7C3AED (紫色)
+- 次要色: #A78BFA (淡紫色)
+- 强调色: #F97316 (橙色)
+- 字体: Inter
+
+## 贡献
+
+欢迎提交 Issue 和 Pull Request。
+
+## 许可证
+
+MIT
diff --git a/server/Dockerfile b/server/Dockerfile
new file mode 100644
index 0000000..42c578c
--- /dev/null
+++ b/server/Dockerfile
@@ -0,0 +1,31 @@
+FROM golang:alpine as builder
+
+WORKDIR /go/src/git.echol.cn/loser/st/server
+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/st/server
+
+COPY --from=0 /go/src/git.echol.cn/loser/st/server/server ./
+COPY --from=0 /go/src/git.echol.cn/loser/st/server/resource ./resource/
+COPY --from=0 /go/src/git.echol.cn/loser/st/server/config.docker.yaml ./
+
+# 挂载目录:如果使用了sqlite数据库,容器命令示例:docker run -d -v /宿主机路径/gva.db:/go/src/git.echol.cn/loser/st/server/gva.db -p 8888:8888 --name gva-server-v1 gva-server:1.0
+# VOLUME ["/go/src/git.echol.cn/loser/st/server"]
+
+EXPOSE 8888
+ENTRYPOINT ./server -c config.docker.yaml
diff --git a/server/README.md b/server/README.md
new file mode 100644
index 0000000..9a34870
--- /dev/null
+++ b/server/README.md
@@ -0,0 +1,54 @@
+## server项目结构
+
+```shell
+├── api
+│ └── v1
+├── config
+├── core
+├── docs
+├── global
+├── initialize
+│ └── internal
+├── middleware
+├── model
+│ ├── request
+│ └── response
+├── packfile
+├── resource
+│ ├── excel
+│ ├── page
+│ └── template
+├── router
+├── service
+├── source
+└── utils
+ ├── timer
+ └── upload
+```
+
+| 文件夹 | 说明 | 描述 |
+| ------------ | ----------------------- | --------------------------- |
+| `api` | api层 | api层 |
+| `--v1` | v1版本接口 | v1版本接口 |
+| `config` | 配置包 | config.yaml对应的配置结构体 |
+| `core` | 核心文件 | 核心组件(zap, viper, server)的初始化 |
+| `docs` | swagger文档目录 | swagger文档目录 |
+| `global` | 全局对象 | 全局对象 |
+| `initialize` | 初始化 | router,redis,gorm,validator, timer的初始化 |
+| `--internal` | 初始化内部函数 | gorm 的 longger 自定义,在此文件夹的函数只能由 `initialize` 层进行调用 |
+| `middleware` | 中间件层 | 用于存放 `gin` 中间件代码 |
+| `model` | 模型层 | 模型对应数据表 |
+| `--request` | 入参结构体 | 接收前端发送到后端的数据。 |
+| `--response` | 出参结构体 | 返回给前端的数据结构体 |
+| `packfile` | 静态文件打包 | 静态文件打包 |
+| `resource` | 静态资源文件夹 | 负责存放静态文件 |
+| `--excel` | excel导入导出默认路径 | excel导入导出默认路径 |
+| `--page` | 表单生成器 | 表单生成器 打包后的dist |
+| `--template` | 模板 | 模板文件夹,存放的是代码生成器的模板 |
+| `router` | 路由层 | 路由层 |
+| `service` | service层 | 存放业务逻辑问题 |
+| `source` | source层 | 存放初始化数据的函数 |
+| `utils` | 工具包 | 工具函数封装 |
+| `--timer` | timer | 定时器接口封装 |
+| `--upload` | oss | oss接口封装 |
+
diff --git a/server/api/v1/app/ai_config.go b/server/api/v1/app/ai_config.go
new file mode 100644
index 0000000..7362182
--- /dev/null
+++ b/server/api/v1/app/ai_config.go
@@ -0,0 +1,218 @@
+package app
+
+import (
+ "strconv"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/app/request"
+ commonResponse "git.echol.cn/loser/st/server/model/common/response"
+ "git.echol.cn/loser/st/server/service"
+ "github.com/gin-gonic/gin"
+ "go.uber.org/zap"
+)
+
+type AIConfigApi struct{}
+
+// CreateAIConfig
+// @Tags AppAIConfig
+// @Summary 创建AI配置
+// @Produce application/json
+// @Param data body request.CreateAIConfigRequest true "AI配置信息"
+// @Success 200 {object} commonResponse.Response{data=response.AIConfigResponse} "创建成功"
+// @Router /app/ai-config [post]
+// @Security ApiKeyAuth
+func (a *AIConfigApi) CreateAIConfig(c *gin.Context) {
+ var req request.CreateAIConfigRequest
+ err := c.ShouldBindJSON(&req)
+ if err != nil {
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ resp, err := service.ServiceGroupApp.AppServiceGroup.AIConfigService.CreateAIConfig(&req)
+ if err != nil {
+ global.GVA_LOG.Error("创建AI配置失败", zap.Error(err))
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ commonResponse.OkWithData(resp, c)
+}
+
+// GetAIConfigList
+// @Tags AppAIConfig
+// @Summary 获取AI配置列表
+// @Produce application/json
+// @Success 200 {object} commonResponse.Response{data=response.AIConfigListResponse} "获取成功"
+// @Router /app/ai-config [get]
+// @Security ApiKeyAuth
+func (a *AIConfigApi) GetAIConfigList(c *gin.Context) {
+ resp, err := service.ServiceGroupApp.AppServiceGroup.AIConfigService.GetAIConfigList()
+ if err != nil {
+ global.GVA_LOG.Error("获取AI配置列表失败", zap.Error(err))
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ commonResponse.OkWithData(resp, c)
+}
+
+// UpdateAIConfig
+// @Tags AppAIConfig
+// @Summary 更新AI配置
+// @Produce application/json
+// @Param id path int true "配置ID"
+// @Param data body request.UpdateAIConfigRequest true "AI配置信息"
+// @Success 200 {object} commonResponse.Response{msg=string} "更新成功"
+// @Router /app/ai-config/:id [put]
+// @Security ApiKeyAuth
+func (a *AIConfigApi) UpdateAIConfig(c *gin.Context) {
+ id, err := strconv.ParseUint(c.Param("id"), 10, 32)
+ if err != nil {
+ commonResponse.FailWithMessage("无效的配置ID", c)
+ return
+ }
+
+ var req request.UpdateAIConfigRequest
+ err = c.ShouldBindJSON(&req)
+ if err != nil {
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ err = service.ServiceGroupApp.AppServiceGroup.AIConfigService.UpdateAIConfig(uint(id), &req)
+ if err != nil {
+ global.GVA_LOG.Error("更新AI配置失败", zap.Error(err))
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ commonResponse.OkWithMessage("更新成功", c)
+}
+
+// DeleteAIConfig
+// @Tags AppAIConfig
+// @Summary 删除AI配置
+// @Produce application/json
+// @Param id path int true "配置ID"
+// @Success 200 {object} commonResponse.Response{msg=string} "删除成功"
+// @Router /app/ai-config/:id [delete]
+// @Security ApiKeyAuth
+func (a *AIConfigApi) DeleteAIConfig(c *gin.Context) {
+ id, err := strconv.ParseUint(c.Param("id"), 10, 32)
+ if err != nil {
+ commonResponse.FailWithMessage("无效的配置ID", c)
+ return
+ }
+
+ err = service.ServiceGroupApp.AppServiceGroup.AIConfigService.DeleteAIConfig(uint(id))
+ if err != nil {
+ global.GVA_LOG.Error("删除AI配置失败", zap.Error(err))
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ commonResponse.OkWithMessage("删除成功", c)
+}
+
+// GetModels
+// @Tags AppAIConfig
+// @Summary 获取可用模型列表
+// @Produce application/json
+// @Param data body request.GetModelsRequest true "获取模型请求"
+// @Success 200 {object} commonResponse.Response{data=response.GetModelsResponse} "获取成功"
+// @Router /app/ai-config/models [post]
+// @Security ApiKeyAuth
+func (a *AIConfigApi) GetModels(c *gin.Context) {
+ var req request.GetModelsRequest
+ err := c.ShouldBindJSON(&req)
+ if err != nil {
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ resp, err := service.ServiceGroupApp.AppServiceGroup.AIConfigService.GetModels(&req)
+ if err != nil {
+ global.GVA_LOG.Error("获取模型列表失败", zap.Error(err))
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ commonResponse.OkWithData(resp, c)
+}
+
+// GetModelsByID
+// @Tags AppAIConfig
+// @Summary 通过配置ID获取可用模型列表
+// @Produce application/json
+// @Param id path int true "配置ID"
+// @Success 200 {object} commonResponse.Response{data=response.GetModelsResponse} "获取成功"
+// @Router /app/ai-config/:id/models [get]
+// @Security ApiKeyAuth
+func (a *AIConfigApi) GetModelsByID(c *gin.Context) {
+ id, err := strconv.ParseUint(c.Param("id"), 10, 32)
+ if err != nil {
+ commonResponse.FailWithMessage("无效的配置ID", c)
+ return
+ }
+
+ resp, err := service.ServiceGroupApp.AppServiceGroup.AIConfigService.GetModelsByID(uint(id))
+ if err != nil {
+ global.GVA_LOG.Error("获取模型列表失败", zap.Error(err))
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ commonResponse.OkWithData(resp, c)
+}
+
+// TestAIConfig
+// @Tags AppAIConfig
+// @Summary 测试AI配置
+// @Produce application/json
+// @Param data body request.TestAIConfigRequest true "测试配置请求"
+// @Success 200 {object} commonResponse.Response{data=response.TestAIConfigResponse} "测试完成"
+// @Router /app/ai-config/test [post]
+// @Security ApiKeyAuth
+func (a *AIConfigApi) TestAIConfig(c *gin.Context) {
+ var req request.TestAIConfigRequest
+ err := c.ShouldBindJSON(&req)
+ if err != nil {
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ resp, err := service.ServiceGroupApp.AppServiceGroup.AIConfigService.TestAIConfig(&req)
+ if err != nil {
+ global.GVA_LOG.Error("测试AI配置失败", zap.Error(err))
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ commonResponse.OkWithData(resp, c)
+}
+
+// TestAIConfigByID
+// @Tags AppAIConfig
+// @Summary 通过ID测试AI配置
+// @Produce application/json
+// @Param id path int true "配置ID"
+// @Success 200 {object} commonResponse.Response{data=response.TestAIConfigResponse} "测试完成"
+// @Router /app/ai-config/:id/test [post]
+// @Security ApiKeyAuth
+func (a *AIConfigApi) TestAIConfigByID(c *gin.Context) {
+ id, err := strconv.ParseUint(c.Param("id"), 10, 32)
+ if err != nil {
+ commonResponse.FailWithMessage("无效的配置ID", c)
+ return
+ }
+
+ resp, err := service.ServiceGroupApp.AppServiceGroup.AIConfigService.TestAIConfigByID(uint(id))
+ if err != nil {
+ global.GVA_LOG.Error("测试AI配置失败", zap.Error(err))
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ commonResponse.OkWithData(resp, c)
+}
diff --git a/server/api/v1/app/auth.go b/server/api/v1/app/auth.go
new file mode 100644
index 0000000..53159d6
--- /dev/null
+++ b/server/api/v1/app/auth.go
@@ -0,0 +1,189 @@
+package app
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/app/request"
+ "git.echol.cn/loser/st/server/model/common"
+ commonResponse "git.echol.cn/loser/st/server/model/common/response"
+ "git.echol.cn/loser/st/server/service"
+ "github.com/gin-gonic/gin"
+ "go.uber.org/zap"
+)
+
+type AuthApi struct{}
+
+// Register
+// @Tags AppAuth
+// @Summary 用户注册
+// @Produce application/json
+// @Param data body request.RegisterRequest true "用户注册信息"
+// @Success 200 {object} commonResponse.Response{msg=string} "注册成功"
+// @Router /app/auth/register [post]
+func (a *AuthApi) Register(c *gin.Context) {
+ var req request.RegisterRequest
+ err := c.ShouldBindJSON(&req)
+ if err != nil {
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ err = service.ServiceGroupApp.AppServiceGroup.AuthService.Register(&req)
+ if err != nil {
+ global.GVA_LOG.Error("注册失败", zap.Error(err))
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ commonResponse.OkWithMessage("注册成功", c)
+}
+
+// Login
+// @Tags AppAuth
+// @Summary 用户登录
+// @Produce application/json
+// @Param data body request.LoginRequest true "用户登录信息"
+// @Success 200 {object} commonResponse.Response{data=response.LoginResponse} "登录成功"
+// @Router /app/auth/login [post]
+func (a *AuthApi) Login(c *gin.Context) {
+ var req request.LoginRequest
+ err := c.ShouldBindJSON(&req)
+ if err != nil {
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ ip := c.ClientIP()
+ resp, err := service.ServiceGroupApp.AppServiceGroup.AuthService.Login(&req, ip)
+ if err != nil {
+ global.GVA_LOG.Error("登录失败", zap.Error(err))
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ commonResponse.OkWithData(resp, c)
+}
+
+// RefreshToken
+// @Tags AppAuth
+// @Summary 刷新Token
+// @Produce application/json
+// @Param data body request.RefreshTokenRequest true "刷新Token"
+// @Success 200 {object} commonResponse.Response{data=response.LoginResponse} "刷新成功"
+// @Router /app/auth/refresh [post]
+func (a *AuthApi) RefreshToken(c *gin.Context) {
+ var req request.RefreshTokenRequest
+ err := c.ShouldBindJSON(&req)
+ if err != nil {
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ resp, err := service.ServiceGroupApp.AppServiceGroup.AuthService.RefreshToken(&req)
+ if err != nil {
+ global.GVA_LOG.Error("刷新Token失败", zap.Error(err))
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ commonResponse.OkWithData(resp, c)
+}
+
+// Logout
+// @Tags AppAuth
+// @Summary 用户登出
+// @Produce application/json
+// @Success 200 {object} commonResponse.Response{msg=string} "登出成功"
+// @Router /app/auth/logout [post]
+// @Security ApiKeyAuth
+func (a *AuthApi) Logout(c *gin.Context) {
+ userID := common.GetAppUserID(c)
+ token := c.GetHeader("Authorization")
+ if len(token) > 7 {
+ token = token[7:] // 移除 "Bearer " 前缀
+ }
+
+ err := service.ServiceGroupApp.AppServiceGroup.AuthService.Logout(userID, token)
+ if err != nil {
+ global.GVA_LOG.Error("登出失败", zap.Error(err))
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ commonResponse.OkWithMessage("登出成功", c)
+}
+
+// GetUserInfo
+// @Tags AppAuth
+// @Summary 获取用户信息
+// @Produce application/json
+// @Success 200 {object} commonResponse.Response{data=response.AppUserResponse} "获取成功"
+// @Router /app/auth/userinfo [get]
+// @Security ApiKeyAuth
+func (a *AuthApi) GetUserInfo(c *gin.Context) {
+ userID := common.GetAppUserID(c)
+
+ resp, err := service.ServiceGroupApp.AppServiceGroup.AuthService.GetUserInfo(userID)
+ if err != nil {
+ global.GVA_LOG.Error("获取用户信息失败", zap.Error(err))
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ commonResponse.OkWithData(resp, c)
+}
+
+// UpdateProfile
+// @Tags AppAuth
+// @Summary 更新用户信息
+// @Produce application/json
+// @Param data body request.UpdateProfileRequest true "用户信息"
+// @Success 200 {object} commonResponse.Response{msg=string} "更新成功"
+// @Router /app/user/profile [put]
+// @Security ApiKeyAuth
+func (a *AuthApi) UpdateProfile(c *gin.Context) {
+ userID := common.GetAppUserID(c)
+
+ var req request.UpdateProfileRequest
+ err := c.ShouldBindJSON(&req)
+ if err != nil {
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ err = service.ServiceGroupApp.AppServiceGroup.AuthService.UpdateProfile(userID, &req)
+ if err != nil {
+ global.GVA_LOG.Error("更新用户信息失败", zap.Error(err))
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ commonResponse.OkWithMessage("更新成功", c)
+}
+
+// ChangePassword
+// @Tags AppAuth
+// @Summary 修改密码
+// @Produce application/json
+// @Param data body request.ChangePasswordRequest true "密码信息"
+// @Success 200 {object} commonResponse.Response{msg=string} "修改成功"
+// @Router /app/user/change-password [post]
+// @Security ApiKeyAuth
+func (a *AuthApi) ChangePassword(c *gin.Context) {
+ userID := common.GetAppUserID(c)
+
+ var req request.ChangePasswordRequest
+ err := c.ShouldBindJSON(&req)
+ if err != nil {
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ err = service.ServiceGroupApp.AppServiceGroup.AuthService.ChangePassword(userID, &req)
+ if err != nil {
+ global.GVA_LOG.Error("修改密码失败", zap.Error(err))
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ commonResponse.OkWithMessage("修改成功", c)
+}
diff --git a/server/api/v1/app/character.go b/server/api/v1/app/character.go
new file mode 100644
index 0000000..ff1ceec
--- /dev/null
+++ b/server/api/v1/app/character.go
@@ -0,0 +1,246 @@
+package app
+
+import (
+ "encoding/json"
+ "strconv"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/app/request"
+ "git.echol.cn/loser/st/server/model/common"
+ commonResponse "git.echol.cn/loser/st/server/model/common/response"
+ "git.echol.cn/loser/st/server/service"
+ "github.com/gin-gonic/gin"
+ "go.uber.org/zap"
+)
+
+type CharacterApi struct{}
+
+// CreateCharacter
+// @Tags AppCharacter
+// @Summary 创建角色卡
+// @Produce application/json
+// @Param data body request.CreateCharacterRequest true "角色卡信息"
+// @Success 200 {object} commonResponse.Response{data=response.CharacterResponse} "创建成功"
+// @Router /app/character [post]
+// @Security ApiKeyAuth
+func (a *CharacterApi) CreateCharacter(c *gin.Context) {
+ userID := common.GetAppUserID(c)
+
+ var req request.CreateCharacterRequest
+ err := c.ShouldBindJSON(&req)
+ if err != nil {
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ resp, err := service.ServiceGroupApp.AppServiceGroup.CharacterService.CreateCharacter(userID, &req)
+ if err != nil {
+ global.GVA_LOG.Error("创建角色卡失败", zap.Error(err))
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ commonResponse.OkWithData(resp, c)
+}
+
+// GetCharacterList
+// @Tags AppCharacter
+// @Summary 获取角色卡列表
+// @Produce application/json
+// @Param page query int false "页码"
+// @Param pageSize query int false "每页数量"
+// @Param keyword query string false "关键词"
+// @Param tag query string false "标签"
+// @Param isPublic query bool false "是否公开"
+// @Success 200 {object} commonResponse.Response{data=response.CharacterListResponse} "获取成功"
+// @Router /app/character [get]
+// @Security ApiKeyAuth
+func (a *CharacterApi) GetCharacterList(c *gin.Context) {
+ userID := common.GetAppUserID(c)
+
+ var req request.GetCharacterListRequest
+ req.Page, _ = strconv.Atoi(c.DefaultQuery("page", "1"))
+ req.PageSize, _ = strconv.Atoi(c.DefaultQuery("pageSize", "20"))
+ req.Keyword = c.Query("keyword")
+ req.Tag = c.Query("tag")
+
+ if isPublicStr := c.Query("isPublic"); isPublicStr != "" {
+ isPublic := isPublicStr == "true"
+ req.IsPublic = &isPublic
+ }
+
+ // 参数验证
+ if req.Page < 1 {
+ req.Page = 1
+ }
+ if req.PageSize < 1 || req.PageSize > 100 {
+ req.PageSize = 20
+ }
+
+ resp, err := service.ServiceGroupApp.AppServiceGroup.CharacterService.GetCharacterList(userID, &req)
+ if err != nil {
+ global.GVA_LOG.Error("获取角色卡列表失败", zap.Error(err))
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ commonResponse.OkWithData(resp, c)
+}
+
+// GetCharacterByID
+// @Tags AppCharacter
+// @Summary 获取角色卡详情
+// @Produce application/json
+// @Param id path int true "角色卡ID"
+// @Success 200 {object} commonResponse.Response{data=response.CharacterResponse} "获取成功"
+// @Router /app/character/:id [get]
+// @Security ApiKeyAuth
+func (a *CharacterApi) GetCharacterByID(c *gin.Context) {
+ userID := common.GetAppUserID(c)
+ characterID, err := strconv.ParseUint(c.Param("id"), 10, 32)
+ if err != nil {
+ commonResponse.FailWithMessage("无效的角色卡ID", c)
+ return
+ }
+
+ resp, err := service.ServiceGroupApp.AppServiceGroup.CharacterService.GetCharacterByID(userID, uint(characterID))
+ if err != nil {
+ global.GVA_LOG.Error("获取角色卡详情失败", zap.Error(err))
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ commonResponse.OkWithData(resp, c)
+}
+
+// UpdateCharacter
+// @Tags AppCharacter
+// @Summary 更新角色卡
+// @Produce application/json
+// @Param id path int true "角色卡ID"
+// @Param data body request.UpdateCharacterRequest true "角色卡信息"
+// @Success 200 {object} commonResponse.Response{msg=string} "更新成功"
+// @Router /app/character/:id [put]
+// @Security ApiKeyAuth
+func (a *CharacterApi) UpdateCharacter(c *gin.Context) {
+ userID := common.GetAppUserID(c)
+ characterID, err := strconv.ParseUint(c.Param("id"), 10, 32)
+ if err != nil {
+ commonResponse.FailWithMessage("无效的角色卡ID", c)
+ return
+ }
+
+ var req request.UpdateCharacterRequest
+ err = c.ShouldBindJSON(&req)
+ if err != nil {
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ err = service.ServiceGroupApp.AppServiceGroup.CharacterService.UpdateCharacter(userID, uint(characterID), &req)
+ if err != nil {
+ global.GVA_LOG.Error("更新角色卡失败", zap.Error(err))
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ commonResponse.OkWithMessage("更新成功", c)
+}
+
+// DeleteCharacter
+// @Tags AppCharacter
+// @Summary 删除角色卡
+// @Produce application/json
+// @Param id path int true "角色卡ID"
+// @Success 200 {object} commonResponse.Response{msg=string} "删除成功"
+// @Router /app/character/:id [delete]
+// @Security ApiKeyAuth
+func (a *CharacterApi) DeleteCharacter(c *gin.Context) {
+ userID := common.GetAppUserID(c)
+ characterID, err := strconv.ParseUint(c.Param("id"), 10, 32)
+ if err != nil {
+ commonResponse.FailWithMessage("无效的角色卡ID", c)
+ return
+ }
+
+ err = service.ServiceGroupApp.AppServiceGroup.CharacterService.DeleteCharacter(userID, uint(characterID))
+ if err != nil {
+ global.GVA_LOG.Error("删除角色卡失败", zap.Error(err))
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ commonResponse.OkWithMessage("删除成功", c)
+}
+
+// UploadCharacter
+// @Tags AppCharacter
+// @Summary 上传角色卡文件(PNG/JSON)
+// @Accept multipart/form-data
+// @Produce application/json
+// @Param file formData file true "角色卡文件"
+// @Success 200 {object} commonResponse.Response{data=response.CharacterResponse} "上传成功"
+// @Router /app/character/upload [post]
+// @Security ApiKeyAuth
+func (a *CharacterApi) UploadCharacter(c *gin.Context) {
+ userID := common.GetAppUserID(c)
+
+ file, err := c.FormFile("file")
+ if err != nil {
+ commonResponse.FailWithMessage("请选择文件", c)
+ return
+ }
+
+ // 根据文件类型选择导入方式
+ contentType := file.Header.Get("Content-Type")
+ var resp interface{}
+
+ if contentType == "image/png" || contentType == "image/x-png" {
+ resp, err = service.ServiceGroupApp.AppServiceGroup.CharacterService.ImportCharacterFromPNG(userID, file)
+ } else if contentType == "application/json" {
+ resp, err = service.ServiceGroupApp.AppServiceGroup.CharacterService.ImportCharacterFromJSON(userID, file)
+ } else {
+ commonResponse.FailWithMessage("不支持的文件类型,仅支持 PNG 和 JSON", c)
+ return
+ }
+
+ if err != nil {
+ global.GVA_LOG.Error("上传角色卡失败", zap.Error(err))
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ commonResponse.OkWithData(resp, c)
+}
+
+// ExportCharacter
+// @Tags AppCharacter
+// @Summary 导出角色卡为 JSON
+// @Produce application/json
+// @Param id path int true "角色卡ID"
+// @Success 200 {object} utils.CharacterCardV2 "导出成功"
+// @Router /app/character/:id/export [get]
+// @Security ApiKeyAuth
+func (a *CharacterApi) ExportCharacter(c *gin.Context) {
+ userID := common.GetAppUserID(c)
+ characterID, err := strconv.ParseUint(c.Param("id"), 10, 32)
+ if err != nil {
+ commonResponse.FailWithMessage("无效的角色卡ID", c)
+ return
+ }
+
+ card, err := service.ServiceGroupApp.AppServiceGroup.CharacterService.ExportCharacterToJSON(userID, uint(characterID))
+ if err != nil {
+ global.GVA_LOG.Error("导出角色卡失败", zap.Error(err))
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ // 设置下载响应头
+ c.Header("Content-Type", "application/json")
+ c.Header("Content-Disposition", "attachment; filename=character_"+c.Param("id")+".json")
+
+ // 直接返回 JSON
+ jsonData, _ := json.MarshalIndent(card, "", " ")
+ c.Data(200, "application/json", jsonData)
+}
diff --git a/server/api/v1/app/conversation.go b/server/api/v1/app/conversation.go
new file mode 100644
index 0000000..38444f0
--- /dev/null
+++ b/server/api/v1/app/conversation.go
@@ -0,0 +1,301 @@
+package app
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/app/request"
+ "git.echol.cn/loser/st/server/model/common"
+ commonResponse "git.echol.cn/loser/st/server/model/common/response"
+ "git.echol.cn/loser/st/server/service"
+ "github.com/gin-gonic/gin"
+ "go.uber.org/zap"
+)
+
+type ConversationApi struct{}
+
+// CreateConversation
+// @Tags AppConversation
+// @Summary 创建对话
+// @Produce application/json
+// @Param data body request.CreateConversationRequest true "对话信息"
+// @Success 200 {object} commonResponse.Response{data=response.ConversationResponse} "创建成功"
+// @Router /app/conversation [post]
+// @Security ApiKeyAuth
+func (a *ConversationApi) CreateConversation(c *gin.Context) {
+ userID := common.GetAppUserID(c)
+
+ var req request.CreateConversationRequest
+ err := c.ShouldBindJSON(&req)
+ if err != nil {
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ resp, err := service.ServiceGroupApp.AppServiceGroup.ConversationService.CreateConversation(userID, &req)
+ if err != nil {
+ global.GVA_LOG.Error("创建对话失败", zap.Error(err))
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ commonResponse.OkWithData(resp, c)
+}
+
+// GetConversationList
+// @Tags AppConversation
+// @Summary 获取对话列表
+// @Produce application/json
+// @Param page query int false "页码"
+// @Param pageSize query int false "每页数量"
+// @Success 200 {object} commonResponse.Response{data=response.ConversationListResponse} "获取成功"
+// @Router /app/conversation [get]
+// @Security ApiKeyAuth
+func (a *ConversationApi) GetConversationList(c *gin.Context) {
+ userID := common.GetAppUserID(c)
+
+ var req request.GetConversationListRequest
+ req.Page, _ = strconv.Atoi(c.DefaultQuery("page", "1"))
+ req.PageSize, _ = strconv.Atoi(c.DefaultQuery("pageSize", "20"))
+
+ if req.Page < 1 {
+ req.Page = 1
+ }
+ if req.PageSize < 1 || req.PageSize > 100 {
+ req.PageSize = 20
+ }
+
+ resp, err := service.ServiceGroupApp.AppServiceGroup.ConversationService.GetConversationList(userID, &req)
+ if err != nil {
+ global.GVA_LOG.Error("获取对话列表失败", zap.Error(err))
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ commonResponse.OkWithData(resp, c)
+}
+
+// GetConversationByID
+// @Tags AppConversation
+// @Summary 获取对话详情
+// @Produce application/json
+// @Param id path int true "对话ID"
+// @Success 200 {object} commonResponse.Response{data=response.ConversationResponse} "获取成功"
+// @Router /app/conversation/:id [get]
+// @Security ApiKeyAuth
+func (a *ConversationApi) GetConversationByID(c *gin.Context) {
+ userID := common.GetAppUserID(c)
+ conversationID, err := strconv.ParseUint(c.Param("id"), 10, 32)
+ if err != nil {
+ commonResponse.FailWithMessage("无效的对话ID", c)
+ return
+ }
+
+ resp, err := service.ServiceGroupApp.AppServiceGroup.ConversationService.GetConversationByID(userID, uint(conversationID))
+ if err != nil {
+ global.GVA_LOG.Error("获取对话详情失败", zap.Error(err))
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ commonResponse.OkWithData(resp, c)
+}
+
+// UpdateConversationSettings
+// @Tags AppConversation
+// @Summary 更新对话设置
+// @Produce application/json
+// @Param id path int true "对话ID"
+// @Param data body request.UpdateConversationSettingsRequest true "设置信息"
+// @Success 200 {object} commonResponse.Response{msg=string} "更新成功"
+// @Router /app/conversation/:id/settings [put]
+// @Security ApiKeyAuth
+func (a *ConversationApi) UpdateConversationSettings(c *gin.Context) {
+ userID := common.GetAppUserID(c)
+ conversationID, err := strconv.ParseUint(c.Param("id"), 10, 32)
+ if err != nil {
+ commonResponse.FailWithMessage("无效的对话ID", c)
+ return
+ }
+
+ var req request.UpdateConversationSettingsRequest
+ err = c.ShouldBindJSON(&req)
+ if err != nil {
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ err = service.ServiceGroupApp.AppServiceGroup.ConversationService.UpdateConversationSettings(userID, uint(conversationID), req.Settings)
+ if err != nil {
+ global.GVA_LOG.Error("更新对话设置失败", zap.Error(err))
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ commonResponse.OkWithMessage("更新成功", c)
+}
+
+// DeleteConversation
+// @Tags AppConversation
+// @Summary 删除对话
+// @Produce application/json
+// @Param id path int true "对话ID"
+// @Success 200 {object} commonResponse.Response{msg=string} "删除成功"
+// @Router /app/conversation/:id [delete]
+// @Security ApiKeyAuth
+func (a *ConversationApi) DeleteConversation(c *gin.Context) {
+ userID := common.GetAppUserID(c)
+ conversationID, err := strconv.ParseUint(c.Param("id"), 10, 32)
+ if err != nil {
+ commonResponse.FailWithMessage("无效的对话ID", c)
+ return
+ }
+
+ err = service.ServiceGroupApp.AppServiceGroup.ConversationService.DeleteConversation(userID, uint(conversationID))
+ if err != nil {
+ global.GVA_LOG.Error("删除对话失败", zap.Error(err))
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ commonResponse.OkWithMessage("删除成功", c)
+}
+
+// GetMessageList
+// @Tags AppConversation
+// @Summary 获取消息列表
+// @Produce application/json
+// @Param id path int true "对话ID"
+// @Param page query int false "页码"
+// @Param pageSize query int false "每页数量"
+// @Success 200 {object} commonResponse.Response{data=response.MessageListResponse} "获取成功"
+// @Router /app/conversation/:id/messages [get]
+// @Security ApiKeyAuth
+func (a *ConversationApi) GetMessageList(c *gin.Context) {
+ userID := common.GetAppUserID(c)
+ conversationID, err := strconv.ParseUint(c.Param("id"), 10, 32)
+ if err != nil {
+ commonResponse.FailWithMessage("无效的对话ID", c)
+ return
+ }
+
+ var req request.GetMessageListRequest
+ req.Page, _ = strconv.Atoi(c.DefaultQuery("page", "1"))
+ req.PageSize, _ = strconv.Atoi(c.DefaultQuery("pageSize", "50"))
+
+ if req.Page < 1 {
+ req.Page = 1
+ }
+ if req.PageSize < 1 || req.PageSize > 100 {
+ req.PageSize = 50
+ }
+
+ resp, err := service.ServiceGroupApp.AppServiceGroup.ConversationService.GetMessageList(userID, uint(conversationID), &req)
+ if err != nil {
+ global.GVA_LOG.Error("获取消息列表失败", zap.Error(err))
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ commonResponse.OkWithData(resp, c)
+}
+
+// SendMessage
+// @Tags AppConversation
+// @Summary 发送消息
+// @Produce application/json
+// @Param id path int true "对话ID"
+// @Param data body request.SendMessageRequest true "消息内容"
+// @Success 200 {object} commonResponse.Response{data=response.MessageResponse} "发送成功"
+// @Router /app/conversation/:id/message [post]
+// @Security ApiKeyAuth
+func (a *ConversationApi) SendMessage(c *gin.Context) {
+ userID := common.GetAppUserID(c)
+ conversationID, err := strconv.ParseUint(c.Param("id"), 10, 32)
+ if err != nil {
+ commonResponse.FailWithMessage("无效的对话ID", c)
+ return
+ }
+
+ var req request.SendMessageRequest
+ err = c.ShouldBindJSON(&req)
+ if err != nil {
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ // 检查是否启用流式传输
+ stream := c.Query("stream") == "true"
+
+ if stream {
+ // 流式传输
+ a.SendMessageStream(c, userID, uint(conversationID), &req)
+ } else {
+ // 普通传输
+ resp, err := service.ServiceGroupApp.AppServiceGroup.ConversationService.SendMessage(userID, uint(conversationID), &req)
+ if err != nil {
+ global.GVA_LOG.Error("发送消息失败", zap.Error(err))
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+ commonResponse.OkWithData(resp, c)
+ }
+}
+
+// SendMessageStream 流式传输消息
+func (a *ConversationApi) SendMessageStream(c *gin.Context, userID, conversationID uint, req *request.SendMessageRequest) {
+ // 设置SSE响应头
+ c.Header("Content-Type", "text/event-stream")
+ c.Header("Cache-Control", "no-cache")
+ c.Header("Connection", "keep-alive")
+ c.Header("X-Accel-Buffering", "no")
+
+ // 创建流式传输通道
+ streamChan := make(chan string, 100)
+ errorChan := make(chan error, 1)
+ doneChan := make(chan bool, 1)
+
+ // 启动流式传输
+ go func() {
+ err := service.ServiceGroupApp.AppServiceGroup.ConversationService.SendMessageStream(
+ userID, conversationID, req, streamChan, doneChan,
+ )
+ if err != nil {
+ errorChan <- err
+ }
+ }()
+
+ // 发送流式数据
+ flusher, ok := c.Writer.(http.Flusher)
+ if !ok {
+ commonResponse.FailWithMessage("不支持流式传输", c)
+ return
+ }
+
+ for {
+ select {
+ case chunk := <-streamChan:
+ // 手动写入 SSE 格式,避免 Gin 的 SSEvent 进行 JSON 序列化
+ c.Writer.Write([]byte("event: message\n"))
+ c.Writer.Write([]byte(fmt.Sprintf("data: %s\n\n", chunk)))
+ flusher.Flush()
+ case err := <-errorChan:
+ // 发送错误
+ c.Writer.Write([]byte("event: error\n"))
+ c.Writer.Write([]byte(fmt.Sprintf("data: %s\n\n", err.Error())))
+ flusher.Flush()
+ return
+ case <-doneChan:
+ // 发送完成信号
+ c.Writer.Write([]byte("event: done\n"))
+ c.Writer.Write([]byte("data: \n\n"))
+ flusher.Flush()
+ return
+ case <-c.Request.Context().Done():
+ // 客户端断开连接
+ return
+ }
+ }
+}
diff --git a/server/api/v1/app/enter.go b/server/api/v1/app/enter.go
new file mode 100644
index 0000000..303d1fe
--- /dev/null
+++ b/server/api/v1/app/enter.go
@@ -0,0 +1,10 @@
+package app
+
+type ApiGroup struct {
+ AuthApi
+ CharacterApi
+ ConversationApi
+ AIConfigApi
+ PresetApi
+ UploadApi
+}
diff --git a/server/api/v1/app/preset.go b/server/api/v1/app/preset.go
new file mode 100644
index 0000000..b4a04d6
--- /dev/null
+++ b/server/api/v1/app/preset.go
@@ -0,0 +1,265 @@
+package app
+
+import (
+ "io"
+ "strconv"
+
+ "git.echol.cn/loser/st/server/model/app/request"
+ "git.echol.cn/loser/st/server/model/app/response"
+ "git.echol.cn/loser/st/server/model/common"
+ commonResponse "git.echol.cn/loser/st/server/model/common/response"
+ "git.echol.cn/loser/st/server/service"
+ "github.com/gin-gonic/gin"
+)
+
+type PresetApi struct{}
+
+// CreatePreset 创建预设
+// @Summary 创建预设
+// @Tags Preset
+// @Accept JSON
+// @Produce JSON
+// @Param data body request.CreatePresetRequest true "预设信息"
+// @Success 200 {object} response.PresetResponse
+// @Router /app/preset [post]
+func (a *PresetApi) CreatePreset(c *gin.Context) {
+ var req request.CreatePresetRequest
+ if err := c.ShouldBindJSON(&req); err != nil {
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ userID := common.GetAppUserID(c)
+ preset, err := service.ServiceGroupApp.AppServiceGroup.PresetService.CreatePreset(userID, &req)
+ if err != nil {
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ resp := response.ToPresetResponse(preset)
+ commonResponse.OkWithData(resp, c)
+}
+
+// GetPresetList 获取预设列表
+// @Summary 获取预设列表
+// @Tags Preset
+// @Accept JSON
+// @Produce JSON
+// @Param page query int false "页码"
+// @Param pageSize query int false "每页数量"
+// @Param keyword query string false "关键词"
+// @Param isPublic query bool false "是否公开"
+// @Success 200 {object} response.PresetListResponse
+// @Router /app/preset [get]
+func (a *PresetApi) GetPresetList(c *gin.Context) {
+ var req request.GetPresetListRequest
+ if err := c.ShouldBindQuery(&req); err != nil {
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ // 设置默认值
+ if req.Page == 0 {
+ req.Page = 1
+ }
+ if req.PageSize == 0 {
+ req.PageSize = 20
+ }
+
+ userID := common.GetAppUserID(c)
+ presets, total, err := service.ServiceGroupApp.AppServiceGroup.PresetService.GetPresetList(userID, &req)
+ if err != nil {
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ // 转换为响应格式
+ list := make([]response.PresetResponse, 0, len(presets))
+ for _, preset := range presets {
+ list = append(list, response.ToPresetResponse(&preset))
+ }
+
+ resp := response.PresetListResponse{
+ List: list,
+ Total: total,
+ Page: req.Page,
+ PageSize: req.PageSize,
+ }
+
+ commonResponse.OkWithData(resp, c)
+}
+
+// GetPresetByID 根据ID获取预设
+// @Summary 根据ID获取预设
+// @Tags Preset
+// @Accept JSON
+// @Produce JSON
+// @Param id path int true "预设ID"
+// @Success 200 {object} response.PresetResponse
+// @Router /app/preset/:id [get]
+func (a *PresetApi) GetPresetByID(c *gin.Context) {
+ id, err := strconv.ParseUint(c.Param("id"), 10, 32)
+ if err != nil {
+ commonResponse.FailWithMessage("无效的预设ID", c)
+ return
+ }
+
+ userID := common.GetAppUserID(c)
+ preset, err := service.ServiceGroupApp.AppServiceGroup.PresetService.GetPresetByID(userID, uint(id))
+ if err != nil {
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ resp := response.ToPresetResponse(preset)
+ commonResponse.OkWithData(resp, c)
+}
+
+// UpdatePreset 更新预设
+// @Summary 更新预设
+// @Tags Preset
+// @Accept JSON
+// @Produce JSON
+// @Param id path int true "预设ID"
+// @Param data body request.UpdatePresetRequest true "预设信息"
+// @Success 200 {string} string "更新成功"
+// @Router /app/preset/:id [put]
+func (a *PresetApi) UpdatePreset(c *gin.Context) {
+ id, err := strconv.ParseUint(c.Param("id"), 10, 32)
+ if err != nil {
+ commonResponse.FailWithMessage("无效的预设ID", c)
+ return
+ }
+
+ var req request.UpdatePresetRequest
+ if err := c.ShouldBindJSON(&req); err != nil {
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ userID := common.GetAppUserID(c)
+ if err := service.ServiceGroupApp.AppServiceGroup.PresetService.UpdatePreset(userID, uint(id), &req); err != nil {
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ commonResponse.OkWithMessage("更新成功", c)
+}
+
+// DeletePreset 删除预设
+// @Summary 删除预设
+// @Tags Preset
+// @Accept JSON
+// @Produce JSON
+// @Param id path int true "预设ID"
+// @Success 200 {string} string "删除成功"
+// @Router /app/preset/:id [delete]
+func (a *PresetApi) DeletePreset(c *gin.Context) {
+ id, err := strconv.ParseUint(c.Param("id"), 10, 32)
+ if err != nil {
+ commonResponse.FailWithMessage("无效的预设ID", c)
+ return
+ }
+
+ userID := common.GetAppUserID(c)
+ if err := service.ServiceGroupApp.AppServiceGroup.PresetService.DeletePreset(userID, uint(id)); err != nil {
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ commonResponse.OkWithMessage("删除成功", c)
+}
+
+// SetDefaultPreset 设置默认预设
+// @Summary 设置默认预设
+// @Tags Preset
+// @Accept JSON
+// @Produce JSON
+// @Param id path int true "预设ID"
+// @Success 200 {string} string "设置成功"
+// @Router /app/preset/:id/default [post]
+func (a *PresetApi) SetDefaultPreset(c *gin.Context) {
+ id, err := strconv.ParseUint(c.Param("id"), 10, 32)
+ if err != nil {
+ commonResponse.FailWithMessage("无效的预设ID", c)
+ return
+ }
+
+ userID := common.GetAppUserID(c)
+ if err := service.ServiceGroupApp.AppServiceGroup.PresetService.SetDefaultPreset(userID, uint(id)); err != nil {
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ commonResponse.OkWithMessage("设置成功", c)
+}
+
+// ImportPreset 导入预设
+// @Summary 导入预设
+// @Tags Preset
+// @Accept multipart/form-data
+// @Produce JSON
+// @Param file formData file true "预设JSON文件"
+// @Success 200 {object} response.PresetResponse
+// @Router /app/preset/import [post]
+func (a *PresetApi) ImportPreset(c *gin.Context) {
+ file, err := c.FormFile("file")
+ if err != nil {
+ commonResponse.FailWithMessage("请上传文件", c)
+ return
+ }
+
+ // 读取文件内容
+ f, err := file.Open()
+ if err != nil {
+ commonResponse.FailWithMessage("读取文件失败", c)
+ return
+ }
+ defer f.Close()
+
+ jsonData, err := io.ReadAll(f)
+ if err != nil {
+ commonResponse.FailWithMessage("读取文件内容失败", c)
+ return
+ }
+
+ userID := common.GetAppUserID(c)
+ preset, err := service.ServiceGroupApp.AppServiceGroup.PresetService.ImportPresetFromJSON(userID, jsonData, file.Filename)
+ if err != nil {
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ resp := response.ToPresetResponse(preset)
+ commonResponse.OkWithData(resp, c)
+}
+
+// ExportPreset 导出预设
+// @Summary 导出预设
+// @Tags Preset
+// @Accept JSON
+// @Produce JSON
+// @Param id path int true "预设ID"
+// @Success 200 {file} file "预设JSON文件"
+// @Router /app/preset/:id/export [get]
+func (a *PresetApi) ExportPreset(c *gin.Context) {
+ id, err := strconv.ParseUint(c.Param("id"), 10, 32)
+ if err != nil {
+ commonResponse.FailWithMessage("无效的预设ID", c)
+ return
+ }
+
+ userID := common.GetAppUserID(c)
+ jsonData, err := service.ServiceGroupApp.AppServiceGroup.PresetService.ExportPresetToJSON(userID, uint(id))
+ if err != nil {
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ // 设置响应头
+ c.Header("Content-Type", "application/json")
+ c.Header("Content-Disposition", "attachment; filename=preset-"+c.Param("id")+".json")
+
+ // 返回JSON数据
+ c.Data(200, "application/json", jsonData)
+}
diff --git a/server/api/v1/app/upload.go b/server/api/v1/app/upload.go
new file mode 100644
index 0000000..037adc9
--- /dev/null
+++ b/server/api/v1/app/upload.go
@@ -0,0 +1,41 @@
+package app
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ commonResponse "git.echol.cn/loser/st/server/model/common/response"
+ "git.echol.cn/loser/st/server/service"
+ "github.com/gin-gonic/gin"
+ "go.uber.org/zap"
+)
+
+type UploadApi struct{}
+
+// UploadImage 上传图片
+// @Summary 上传图片
+// @Tags Upload
+// @Accept multipart/form-data
+// @Produce json
+// @Param file formData file true "图片文件"
+// @Success 200 {object} map[string]string "返回图片URL"
+// @Router /app/upload/image [post]
+func (a *UploadApi) UploadImage(c *gin.Context) {
+ _, header, err := c.Request.FormFile("file")
+ if err != nil {
+ global.GVA_LOG.Error("接收文件失败", zap.Error(err))
+ commonResponse.FailWithMessage("请上传图片文件", c)
+ return
+ }
+
+ // 上传图片到 OSS
+ imageURL, err := service.ServiceGroupApp.AppServiceGroup.UploadService.UploadImage(header)
+ if err != nil {
+ global.GVA_LOG.Error("上传图片失败", zap.Error(err))
+ commonResponse.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ // 返回图片 URL
+ commonResponse.OkWithData(gin.H{
+ "url": imageURL,
+ }, c)
+}
diff --git a/server/api/v1/enter.go b/server/api/v1/enter.go
new file mode 100644
index 0000000..7a5b528
--- /dev/null
+++ b/server/api/v1/enter.go
@@ -0,0 +1,15 @@
+package v1
+
+import (
+ "git.echol.cn/loser/st/server/api/v1/app"
+ "git.echol.cn/loser/st/server/api/v1/example"
+ "git.echol.cn/loser/st/server/api/v1/system"
+)
+
+var ApiGroupApp = new(ApiGroup)
+
+type ApiGroup struct {
+ SystemApiGroup system.ApiGroup
+ ExampleApiGroup example.ApiGroup
+ AppApiGroup app.ApiGroup
+}
diff --git a/server/api/v1/example/enter.go b/server/api/v1/example/enter.go
new file mode 100644
index 0000000..93e2d3f
--- /dev/null
+++ b/server/api/v1/example/enter.go
@@ -0,0 +1,15 @@
+package example
+
+import "git.echol.cn/loser/st/server/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/server/api/v1/example/exa_attachment_category.go b/server/api/v1/example/exa_attachment_category.go
new file mode 100644
index 0000000..41f255c
--- /dev/null
+++ b/server/api/v1/example/exa_attachment_category.go
@@ -0,0 +1,82 @@
+package example
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ common "git.echol.cn/loser/st/server/model/common/request"
+ "git.echol.cn/loser/st/server/model/common/response"
+ "git.echol.cn/loser/st/server/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/server/api/v1/example/exa_breakpoint_continue.go b/server/api/v1/example/exa_breakpoint_continue.go
new file mode 100644
index 0000000..1d429ca
--- /dev/null
+++ b/server/api/v1/example/exa_breakpoint_continue.go
@@ -0,0 +1,156 @@
+package example
+
+import (
+ "fmt"
+ "io"
+ "mime/multipart"
+ "strconv"
+ "strings"
+
+ "git.echol.cn/loser/st/server/model/example"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common/response"
+ exampleRes "git.echol.cn/loser/st/server/model/example/response"
+ "git.echol.cn/loser/st/server/utils"
+ "github.com/gin-gonic/gin"
+ "go.uber.org/zap"
+)
+
+// BreakpointContinue
+// @Tags ExaFileUploadAndDownload
+// @Summary 断点续传到服务器
+// @Security ApiKeyAuth
+// @accept multipart/form-data
+// @Produce application/json
+// @Param file formData file true "an example for breakpoint resume, 断点续传示例"
+// @Success 200 {object} response.Response{msg=string} "断点续传到服务器"
+// @Router /fileUploadAndDownload/breakpointContinue [post]
+func (b *FileUploadAndDownloadApi) BreakpointContinue(c *gin.Context) {
+ fileMd5 := c.Request.FormValue("fileMd5")
+ fileName := c.Request.FormValue("fileName")
+ chunkMd5 := c.Request.FormValue("chunkMd5")
+ chunkNumber, _ := strconv.Atoi(c.Request.FormValue("chunkNumber"))
+ chunkTotal, _ := strconv.Atoi(c.Request.FormValue("chunkTotal"))
+ _, FileHeader, err := c.Request.FormFile("file")
+ if err != nil {
+ global.GVA_LOG.Error("接收文件失败!", zap.Error(err))
+ response.FailWithMessage("接收文件失败", c)
+ return
+ }
+ f, err := FileHeader.Open()
+ if err != nil {
+ global.GVA_LOG.Error("文件读取失败!", zap.Error(err))
+ response.FailWithMessage("文件读取失败", c)
+ return
+ }
+ defer func(f multipart.File) {
+ err := f.Close()
+ if err != nil {
+ fmt.Println(err)
+ }
+ }(f)
+ cen, _ := io.ReadAll(f)
+ if !utils.CheckMd5(cen, chunkMd5) {
+ global.GVA_LOG.Error("检查md5失败!", zap.Error(err))
+ response.FailWithMessage("检查md5失败", c)
+ return
+ }
+ file, err := fileUploadAndDownloadService.FindOrCreateFile(fileMd5, fileName, chunkTotal)
+ if err != nil {
+ global.GVA_LOG.Error("查找或创建记录失败!", zap.Error(err))
+ response.FailWithMessage("查找或创建记录失败", c)
+ return
+ }
+ pathC, err := utils.BreakPointContinue(cen, fileName, chunkNumber, chunkTotal, fileMd5)
+ if err != nil {
+ global.GVA_LOG.Error("断点续传失败!", zap.Error(err))
+ response.FailWithMessage("断点续传失败", c)
+ return
+ }
+
+ if err = fileUploadAndDownloadService.CreateFileChunk(file.ID, pathC, chunkNumber); err != nil {
+ global.GVA_LOG.Error("创建文件记录失败!", zap.Error(err))
+ response.FailWithMessage("创建文件记录失败", c)
+ return
+ }
+ response.OkWithMessage("切片创建成功", c)
+}
+
+// FindFile
+// @Tags ExaFileUploadAndDownload
+// @Summary 查找文件
+// @Security ApiKeyAuth
+// @accept multipart/form-data
+// @Produce application/json
+// @Param file formData file true "Find the file, 查找文件"
+// @Success 200 {object} response.Response{data=exampleRes.FileResponse,msg=string} "查找文件,返回包括文件详情"
+// @Router /fileUploadAndDownload/findFile [get]
+func (b *FileUploadAndDownloadApi) FindFile(c *gin.Context) {
+ fileMd5 := c.Query("fileMd5")
+ fileName := c.Query("fileName")
+ chunkTotal, _ := strconv.Atoi(c.Query("chunkTotal"))
+ file, err := fileUploadAndDownloadService.FindOrCreateFile(fileMd5, fileName, chunkTotal)
+ if err != nil {
+ global.GVA_LOG.Error("查找失败!", zap.Error(err))
+ response.FailWithMessage("查找失败", c)
+ } else {
+ response.OkWithDetailed(exampleRes.FileResponse{File: file}, "查找成功", c)
+ }
+}
+
+// BreakpointContinueFinish
+// @Tags ExaFileUploadAndDownload
+// @Summary 创建文件
+// @Security ApiKeyAuth
+// @accept multipart/form-data
+// @Produce application/json
+// @Param file formData file true "上传文件完成"
+// @Success 200 {object} response.Response{data=exampleRes.FilePathResponse,msg=string} "创建文件,返回包括文件路径"
+// @Router /fileUploadAndDownload/findFile [post]
+func (b *FileUploadAndDownloadApi) BreakpointContinueFinish(c *gin.Context) {
+ fileMd5 := c.Query("fileMd5")
+ fileName := c.Query("fileName")
+ filePath, err := utils.MakeFile(fileName, fileMd5)
+ if err != nil {
+ global.GVA_LOG.Error("文件创建失败!", zap.Error(err))
+ response.FailWithDetailed(exampleRes.FilePathResponse{FilePath: filePath}, "文件创建失败", c)
+ } else {
+ response.OkWithDetailed(exampleRes.FilePathResponse{FilePath: filePath}, "文件创建成功", c)
+ }
+}
+
+// RemoveChunk
+// @Tags ExaFileUploadAndDownload
+// @Summary 删除切片
+// @Security ApiKeyAuth
+// @accept multipart/form-data
+// @Produce application/json
+// @Param file formData file true "删除缓存切片"
+// @Success 200 {object} response.Response{msg=string} "删除切片"
+// @Router /fileUploadAndDownload/removeChunk [post]
+func (b *FileUploadAndDownloadApi) RemoveChunk(c *gin.Context) {
+ var file example.ExaFile
+ err := c.ShouldBindJSON(&file)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ // 路径穿越拦截
+ if strings.Contains(file.FilePath, "..") || strings.Contains(file.FilePath, "../") || strings.Contains(file.FilePath, "./") || strings.Contains(file.FilePath, ".\\") {
+ response.FailWithMessage("非法路径,禁止删除", 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/server/api/v1/example/exa_customer.go b/server/api/v1/example/exa_customer.go
new file mode 100644
index 0000000..bd75fee
--- /dev/null
+++ b/server/api/v1/example/exa_customer.go
@@ -0,0 +1,176 @@
+package example
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common/request"
+ "git.echol.cn/loser/st/server/model/common/response"
+ "git.echol.cn/loser/st/server/model/example"
+ exampleRes "git.echol.cn/loser/st/server/model/example/response"
+ "git.echol.cn/loser/st/server/utils"
+ "github.com/gin-gonic/gin"
+ "go.uber.org/zap"
+)
+
+type CustomerApi struct{}
+
+// CreateExaCustomer
+// @Tags ExaCustomer
+// @Summary 创建客户
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body example.ExaCustomer true "客户用户名, 客户手机号码"
+// @Success 200 {object} response.Response{msg=string} "创建客户"
+// @Router /customer/customer [post]
+func (e *CustomerApi) CreateExaCustomer(c *gin.Context) {
+ var customer example.ExaCustomer
+ err := c.ShouldBindJSON(&customer)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = utils.Verify(customer, utils.CustomerVerify)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ customer.SysUserID = utils.GetUserID(c)
+ customer.SysUserAuthorityID = utils.GetUserAuthorityId(c)
+ err = customerService.CreateExaCustomer(customer)
+ if err != nil {
+ global.GVA_LOG.Error("创建失败!", zap.Error(err))
+ response.FailWithMessage("创建失败", c)
+ return
+ }
+ response.OkWithMessage("创建成功", c)
+}
+
+// DeleteExaCustomer
+// @Tags ExaCustomer
+// @Summary 删除客户
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body example.ExaCustomer true "客户ID"
+// @Success 200 {object} response.Response{msg=string} "删除客户"
+// @Router /customer/customer [delete]
+func (e *CustomerApi) DeleteExaCustomer(c *gin.Context) {
+ var customer example.ExaCustomer
+ err := c.ShouldBindJSON(&customer)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = utils.Verify(customer.GVA_MODEL, utils.IdVerify)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = customerService.DeleteExaCustomer(customer)
+ if err != nil {
+ global.GVA_LOG.Error("删除失败!", zap.Error(err))
+ response.FailWithMessage("删除失败", c)
+ return
+ }
+ response.OkWithMessage("删除成功", c)
+}
+
+// UpdateExaCustomer
+// @Tags ExaCustomer
+// @Summary 更新客户信息
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body example.ExaCustomer true "客户ID, 客户信息"
+// @Success 200 {object} response.Response{msg=string} "更新客户信息"
+// @Router /customer/customer [put]
+func (e *CustomerApi) UpdateExaCustomer(c *gin.Context) {
+ var customer example.ExaCustomer
+ err := c.ShouldBindJSON(&customer)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = utils.Verify(customer.GVA_MODEL, utils.IdVerify)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = utils.Verify(customer, utils.CustomerVerify)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = customerService.UpdateExaCustomer(&customer)
+ if err != nil {
+ global.GVA_LOG.Error("更新失败!", zap.Error(err))
+ response.FailWithMessage("更新失败", c)
+ return
+ }
+ response.OkWithMessage("更新成功", c)
+}
+
+// GetExaCustomer
+// @Tags ExaCustomer
+// @Summary 获取单一客户信息
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data query example.ExaCustomer true "客户ID"
+// @Success 200 {object} response.Response{data=exampleRes.ExaCustomerResponse,msg=string} "获取单一客户信息,返回包括客户详情"
+// @Router /customer/customer [get]
+func (e *CustomerApi) GetExaCustomer(c *gin.Context) {
+ var customer example.ExaCustomer
+ err := c.ShouldBindQuery(&customer)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = utils.Verify(customer.GVA_MODEL, utils.IdVerify)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ data, err := customerService.GetExaCustomer(customer.ID)
+ if err != nil {
+ global.GVA_LOG.Error("获取失败!", zap.Error(err))
+ response.FailWithMessage("获取失败", c)
+ return
+ }
+ response.OkWithDetailed(exampleRes.ExaCustomerResponse{Customer: data}, "获取成功", c)
+}
+
+// GetExaCustomerList
+// @Tags ExaCustomer
+// @Summary 分页获取权限客户列表
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data query request.PageInfo true "页码, 每页大小"
+// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页获取权限客户列表,返回包括列表,总数,页码,每页数量"
+// @Router /customer/customerList [get]
+func (e *CustomerApi) GetExaCustomerList(c *gin.Context) {
+ var pageInfo request.PageInfo
+ err := c.ShouldBindQuery(&pageInfo)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = utils.Verify(pageInfo, utils.PageInfoVerify)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ customerList, total, err := customerService.GetCustomerInfoList(utils.GetUserAuthorityId(c), pageInfo)
+ if err != nil {
+ global.GVA_LOG.Error("获取失败!", zap.Error(err))
+ response.FailWithMessage("获取失败"+err.Error(), c)
+ return
+ }
+ response.OkWithDetailed(response.PageResult{
+ List: customerList,
+ Total: total,
+ Page: pageInfo.Page,
+ PageSize: pageInfo.PageSize,
+ }, "获取成功", c)
+}
diff --git a/server/api/v1/example/exa_file_upload_download.go b/server/api/v1/example/exa_file_upload_download.go
new file mode 100644
index 0000000..c4f639a
--- /dev/null
+++ b/server/api/v1/example/exa_file_upload_download.go
@@ -0,0 +1,136 @@
+package example
+
+import (
+ "strconv"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common/response"
+ "git.echol.cn/loser/st/server/model/example"
+ "git.echol.cn/loser/st/server/model/example/request"
+ exampleRes "git.echol.cn/loser/st/server/model/example/response"
+ "github.com/gin-gonic/gin"
+ "go.uber.org/zap"
+)
+
+type FileUploadAndDownloadApi struct{}
+
+// UploadFile
+// @Tags ExaFileUploadAndDownload
+// @Summary 上传文件示例
+// @Security ApiKeyAuth
+// @accept multipart/form-data
+// @Produce application/json
+// @Param file formData file true "上传文件示例"
+// @Success 200 {object} response.Response{data=exampleRes.ExaFileResponse,msg=string} "上传文件示例,返回包括文件详情"
+// @Router /fileUploadAndDownload/upload [post]
+func (b *FileUploadAndDownloadApi) UploadFile(c *gin.Context) {
+ var file example.ExaFileUploadAndDownload
+ noSave := c.DefaultQuery("noSave", "0")
+ _, header, err := c.Request.FormFile("file")
+ 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/server/api/v1/system/auto_code_history.go b/server/api/v1/system/auto_code_history.go
new file mode 100644
index 0000000..abc2fc8
--- /dev/null
+++ b/server/api/v1/system/auto_code_history.go
@@ -0,0 +1,115 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ common "git.echol.cn/loser/st/server/model/common/request"
+ "git.echol.cn/loser/st/server/model/common/response"
+ request "git.echol.cn/loser/st/server/model/system/request"
+ "github.com/gin-gonic/gin"
+ "go.uber.org/zap"
+)
+
+type AutoCodeHistoryApi struct{}
+
+// First
+// @Tags AutoCode
+// @Summary 获取meta信息
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body request.GetById true "请求参数"
+// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "获取meta信息"
+// @Router /autoCode/getMeta [post]
+func (a *AutoCodeHistoryApi) First(c *gin.Context) {
+ var info common.GetById
+ err := c.ShouldBindJSON(&info)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ data, err := autoCodeHistoryService.First(c.Request.Context(), info)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ response.OkWithDetailed(gin.H{"meta": data}, "获取成功", c)
+}
+
+// Delete
+// @Tags AutoCode
+// @Summary 删除回滚记录
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body request.GetById true "请求参数"
+// @Success 200 {object} response.Response{msg=string} "删除回滚记录"
+// @Router /autoCode/delSysHistory [post]
+func (a *AutoCodeHistoryApi) Delete(c *gin.Context) {
+ var info common.GetById
+ err := c.ShouldBindJSON(&info)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = autoCodeHistoryService.Delete(c.Request.Context(), info)
+ if err != nil {
+ global.GVA_LOG.Error("删除失败!", zap.Error(err))
+ response.FailWithMessage("删除失败", c)
+ return
+ }
+ response.OkWithMessage("删除成功", c)
+}
+
+// RollBack
+// @Tags AutoCode
+// @Summary 回滚自动生成代码
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body request.SysAutoHistoryRollBack true "请求参数"
+// @Success 200 {object} response.Response{msg=string} "回滚自动生成代码"
+// @Router /autoCode/rollback [post]
+func (a *AutoCodeHistoryApi) RollBack(c *gin.Context) {
+ var info request.SysAutoHistoryRollBack
+ err := c.ShouldBindJSON(&info)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = autoCodeHistoryService.RollBack(c.Request.Context(), info)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ response.OkWithMessage("回滚成功", c)
+}
+
+// GetList
+// @Tags AutoCode
+// @Summary 查询回滚记录
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body common.PageInfo true "请求参数"
+// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "查询回滚记录,返回包括列表,总数,页码,每页数量"
+// @Router /autoCode/getSysHistory [post]
+func (a *AutoCodeHistoryApi) GetList(c *gin.Context) {
+ var info common.PageInfo
+ err := c.ShouldBindJSON(&info)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ list, total, err := autoCodeHistoryService.GetList(c.Request.Context(), info)
+ if err != nil {
+ global.GVA_LOG.Error("获取失败!", zap.Error(err))
+ response.FailWithMessage("获取失败", c)
+ return
+ }
+ response.OkWithDetailed(response.PageResult{
+ List: list,
+ Total: total,
+ Page: info.Page,
+ PageSize: info.PageSize,
+ }, "获取成功", c)
+}
diff --git a/server/api/v1/system/auto_code_mcp.go b/server/api/v1/system/auto_code_mcp.go
new file mode 100644
index 0000000..ca8fdfc
--- /dev/null
+++ b/server/api/v1/system/auto_code_mcp.go
@@ -0,0 +1,145 @@
+package system
+
+import (
+ "fmt"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/mcp/client"
+ "git.echol.cn/loser/st/server/model/common/response"
+ "git.echol.cn/loser/st/server/model/system/request"
+ "github.com/gin-gonic/gin"
+ "github.com/mark3labs/mcp-go/mcp"
+)
+
+// Create
+// @Tags mcp
+// @Summary 自动McpTool
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body request.AutoMcpTool true "创建自动代码"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}"
+// @Router /autoCode/mcp [post]
+func (a *AutoCodeTemplateApi) MCP(c *gin.Context) {
+ var info request.AutoMcpTool
+ err := c.ShouldBindJSON(&info)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ toolFilePath, err := autoCodeTemplateService.CreateMcp(c.Request.Context(), info)
+ if err != nil {
+ response.FailWithMessage("创建失败", c)
+ global.GVA_LOG.Error(err.Error())
+ return
+ }
+ response.OkWithMessage("创建成功,MCP Tool路径:"+toolFilePath, c)
+}
+
+// Create
+// @Tags mcp
+// @Summary 自动McpTool
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body request.AutoMcpTool true "创建自动代码"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}"
+// @Router /autoCode/mcpList [post]
+func (a *AutoCodeTemplateApi) MCPList(c *gin.Context) {
+
+ baseUrl := fmt.Sprintf("http://127.0.0.1:%d%s", global.GVA_CONFIG.System.Addr, global.GVA_CONFIG.MCP.SSEPath)
+
+ testClient, err := client.NewClient(baseUrl, "testClient", "v1.0.0", global.GVA_CONFIG.MCP.Name)
+ defer testClient.Close()
+ toolsRequest := mcp.ListToolsRequest{}
+
+ list, err := testClient.ListTools(c.Request.Context(), toolsRequest)
+
+ if err != nil {
+ response.FailWithMessage("创建失败", c)
+ global.GVA_LOG.Error(err.Error())
+ return
+ }
+
+ mcpServerConfig := map[string]interface{}{
+ "mcpServers": map[string]interface{}{
+ global.GVA_CONFIG.MCP.Name: map[string]string{
+ "url": baseUrl,
+ },
+ },
+ }
+ response.OkWithData(gin.H{
+ "mcpServerConfig": mcpServerConfig,
+ "list": list,
+ }, c)
+}
+
+// Create
+// @Tags mcp
+// @Summary 测试McpTool
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body object true "调用MCP Tool的参数"
+// @Success 200 {object} response.Response "{"success":true,"data":{},"msg":"测试成功"}"
+// @Router /autoCode/mcpTest [post]
+func (a *AutoCodeTemplateApi) MCPTest(c *gin.Context) {
+ // 定义接口请求结构
+ var testRequest struct {
+ Name string `json:"name" binding:"required"` // 工具名称
+ Arguments map[string]interface{} `json:"arguments" binding:"required"` // 工具参数
+ }
+
+ // 绑定JSON请求体
+ if err := c.ShouldBindJSON(&testRequest); err != nil {
+ response.FailWithMessage("参数解析失败:"+err.Error(), c)
+ return
+ }
+
+ // 创建MCP客户端
+ baseUrl := fmt.Sprintf("http://127.0.0.1:%d%s", global.GVA_CONFIG.System.Addr, global.GVA_CONFIG.MCP.SSEPath)
+ testClient, err := client.NewClient(baseUrl, "testClient", "v1.0.0", global.GVA_CONFIG.MCP.Name)
+ if err != nil {
+ response.FailWithMessage("创建MCP客户端失败:"+err.Error(), c)
+ return
+ }
+ defer testClient.Close()
+
+ ctx := c.Request.Context()
+
+ // 初始化MCP连接
+ initRequest := mcp.InitializeRequest{}
+ initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
+ initRequest.Params.ClientInfo = mcp.Implementation{
+ Name: "testClient",
+ Version: "v1.0.0",
+ }
+
+ _, err = testClient.Initialize(ctx, initRequest)
+ if err != nil {
+ response.FailWithMessage("初始化MCP连接失败:"+err.Error(), c)
+ return
+ }
+
+ // 构建工具调用请求
+ request := mcp.CallToolRequest{}
+ request.Params.Name = testRequest.Name
+ request.Params.Arguments = testRequest.Arguments
+
+ // 调用工具
+ result, err := testClient.CallTool(ctx, request)
+ if err != nil {
+ response.FailWithMessage("工具调用失败:"+err.Error(), c)
+ return
+ }
+
+ // 处理响应结果
+ if len(result.Content) == 0 {
+ response.FailWithMessage("工具未返回任何内容", c)
+ return
+ }
+
+ // 返回结果
+ response.OkWithData(result.Content, c)
+}
diff --git a/server/api/v1/system/auto_code_package.go b/server/api/v1/system/auto_code_package.go
new file mode 100644
index 0000000..b32846a
--- /dev/null
+++ b/server/api/v1/system/auto_code_package.go
@@ -0,0 +1,101 @@
+package system
+
+import (
+ "strings"
+
+ "git.echol.cn/loser/st/server/global"
+ common "git.echol.cn/loser/st/server/model/common/request"
+ "git.echol.cn/loser/st/server/model/common/response"
+ "git.echol.cn/loser/st/server/model/system/request"
+ "git.echol.cn/loser/st/server/utils"
+ "github.com/gin-gonic/gin"
+ "go.uber.org/zap"
+)
+
+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/server/api/v1/system/auto_code_plugin.go b/server/api/v1/system/auto_code_plugin.go
new file mode 100644
index 0000000..917aca6
--- /dev/null
+++ b/server/api/v1/system/auto_code_plugin.go
@@ -0,0 +1,218 @@
+package system
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common/response"
+ "git.echol.cn/loser/st/server/model/system/request"
+ systemRes "git.echol.cn/loser/st/server/model/system/response"
+ "git.echol.cn/loser/st/server/plugin/plugin-tool/utils"
+ "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)
+}
+
+// InitDictionary
+// @Tags AutoCodePlugin
+// @Summary 打包插件
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "打包插件成功"
+// @Router /autoCode/initDictionary [post]
+func (a *AutoCodePluginApi) InitDictionary(c *gin.Context) {
+ var dictInfo request.InitDictionary
+ err := c.ShouldBindJSON(&dictInfo)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = autoCodePluginService.InitDictionary(dictInfo)
+ if err != nil {
+ global.GVA_LOG.Error("创建初始化Dictionary失败!", zap.Error(err))
+ response.FailWithMessage("创建初始化Dictionary失败"+err.Error(), c)
+ return
+ }
+ response.OkWithMessage("文件变更成功", c)
+}
+
+// GetPluginList
+// @Tags AutoCodePlugin
+// @Summary 获取插件列表
+// @Security ApiKeyAuth
+// @Produce application/json
+// @Success 200 {object} response.Response{data=[]systemRes.PluginInfo} "获取插件列表成功"
+// @Router /autoCode/getPluginList [get]
+func (a *AutoCodePluginApi) GetPluginList(c *gin.Context) {
+ serverDir := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin")
+ webDir := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Web, "plugin")
+
+ serverEntries, _ := os.ReadDir(serverDir)
+ webEntries, _ := os.ReadDir(webDir)
+
+ configMap := make(map[string]string)
+
+ for _, entry := range serverEntries {
+ if entry.IsDir() {
+ configMap[entry.Name()] = "server"
+ }
+ }
+
+ for _, entry := range webEntries {
+ if entry.IsDir() {
+ if val, ok := configMap[entry.Name()]; ok {
+ if val == "server" {
+ configMap[entry.Name()] = "full"
+ }
+ } else {
+ configMap[entry.Name()] = "web"
+ }
+ }
+ }
+
+ var list []systemRes.PluginInfo
+ for k, v := range configMap {
+ apis, menus, dicts := utils.GetPluginData(k)
+ list = append(list, systemRes.PluginInfo{
+ PluginName: k,
+ PluginType: v,
+ Apis: apis,
+ Menus: menus,
+ Dictionaries: dicts,
+ })
+ }
+
+ response.OkWithDetailed(list, "获取成功", c)
+}
+
+// Remove
+// @Tags AutoCodePlugin
+// @Summary 删除插件
+// @Security ApiKeyAuth
+// @Produce application/json
+// @Param pluginName query string true "插件名称"
+// @Param pluginType query string true "插件类型"
+// @Success 200 {object} response.Response{msg=string} "删除插件成功"
+// @Router /autoCode/removePlugin [post]
+func (a *AutoCodePluginApi) Remove(c *gin.Context) {
+ pluginName := c.Query("pluginName")
+ pluginType := c.Query("pluginType")
+ err := autoCodePluginService.Remove(pluginName, pluginType)
+ if err != nil {
+ global.GVA_LOG.Error("删除失败!", zap.Error(err))
+ response.FailWithMessage("删除失败"+err.Error(), c)
+ return
+ }
+ response.OkWithMessage("删除成功", c)
+}
diff --git a/server/api/v1/system/auto_code_template.go b/server/api/v1/system/auto_code_template.go
new file mode 100644
index 0000000..b1297bc
--- /dev/null
+++ b/server/api/v1/system/auto_code_template.go
@@ -0,0 +1,121 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common/response"
+ "git.echol.cn/loser/st/server/model/system/request"
+ "git.echol.cn/loser/st/server/utils"
+ "github.com/gin-gonic/gin"
+ "go.uber.org/zap"
+)
+
+type AutoCodeTemplateApi struct{}
+
+// Preview
+// @Tags AutoCodeTemplate
+// @Summary 预览创建后的代码
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body request.AutoCode true "预览创建代码"
+// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "预览创建后的代码"
+// @Router /autoCode/preview [post]
+func (a *AutoCodeTemplateApi) Preview(c *gin.Context) {
+ var info request.AutoCode
+ err := c.ShouldBindJSON(&info)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = utils.Verify(info, utils.AutoCodeVerify)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = info.Pretreatment()
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ info.PackageT = utils.FirstUpper(info.Package)
+ autoCode, err := autoCodeTemplateService.Preview(c.Request.Context(), info)
+ if err != nil {
+ global.GVA_LOG.Error(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/server/api/v1/system/enter.go b/server/api/v1/system/enter.go
new file mode 100644
index 0000000..ee7c7bd
--- /dev/null
+++ b/server/api/v1/system/enter.go
@@ -0,0 +1,57 @@
+package system
+
+import "git.echol.cn/loser/st/server/service"
+
+type ApiGroup struct {
+ DBApi
+ JwtApi
+ BaseApi
+ SystemApi
+ CasbinApi
+ AutoCodeApi
+ SystemApiApi
+ AuthorityApi
+ DictionaryApi
+ AuthorityMenuApi
+ OperationRecordApi
+ DictionaryDetailApi
+ AuthorityBtnApi
+ SysExportTemplateApi
+ AutoCodePluginApi
+ AutoCodePackageApi
+ AutoCodeHistoryApi
+ AutoCodeTemplateApi
+ SysParamsApi
+ SysVersionApi
+ SysErrorApi
+ LoginLogApi
+ ApiTokenApi
+ SkillsApi
+}
+
+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
+ sysVersionService = service.ServiceGroupApp.SystemServiceGroup.SysVersionService
+ sysErrorService = service.ServiceGroupApp.SystemServiceGroup.SysErrorService
+ loginLogService = service.ServiceGroupApp.SystemServiceGroup.LoginLogService
+ apiTokenService = service.ServiceGroupApp.SystemServiceGroup.ApiTokenService
+ skillsService = service.ServiceGroupApp.SystemServiceGroup.SkillsService
+)
diff --git a/server/api/v1/system/sys_api.go b/server/api/v1/system/sys_api.go
new file mode 100644
index 0000000..bcc0e7d
--- /dev/null
+++ b/server/api/v1/system/sys_api.go
@@ -0,0 +1,323 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common/request"
+ "git.echol.cn/loser/st/server/model/common/response"
+ "git.echol.cn/loser/st/server/model/system"
+ systemReq "git.echol.cn/loser/st/server/model/system/request"
+ systemRes "git.echol.cn/loser/st/server/model/system/response"
+ "git.echol.cn/loser/st/server/utils"
+
+ "github.com/gin-gonic/gin"
+ "go.uber.org/zap"
+)
+
+type SystemApiApi struct{}
+
+// CreateApi
+// @Tags SysApi
+// @Summary 创建基础api
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body system.SysApi true "api路径, api中文描述, api组, 方法"
+// @Success 200 {object} response.Response{msg=string} "创建基础api"
+// @Router /api/createApi [post]
+func (s *SystemApiApi) CreateApi(c *gin.Context) {
+ var api system.SysApi
+ err := c.ShouldBindJSON(&api)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = utils.Verify(api, utils.ApiVerify)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = apiService.CreateApi(api)
+ if err != nil {
+ global.GVA_LOG.Error("创建失败!", zap.Error(err))
+ response.FailWithMessage("创建失败", c)
+ return
+ }
+ response.OkWithMessage("创建成功", c)
+}
+
+// SyncApi
+// @Tags SysApi
+// @Summary 同步API
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Success 200 {object} response.Response{msg=string} "同步API"
+// @Router /api/syncApi [get]
+func (s *SystemApiApi) SyncApi(c *gin.Context) {
+ newApis, deleteApis, ignoreApis, err := apiService.SyncApi()
+ if err != nil {
+ global.GVA_LOG.Error("同步失败!", zap.Error(err))
+ response.FailWithMessage("同步失败", c)
+ return
+ }
+ response.OkWithData(gin.H{
+ "newApis": newApis,
+ "deleteApis": deleteApis,
+ "ignoreApis": ignoreApis,
+ }, c)
+}
+
+// GetApiGroups
+// @Tags SysApi
+// @Summary 获取API分组
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Success 200 {object} response.Response{msg=string} "获取API分组"
+// @Router /api/getApiGroups [get]
+func (s *SystemApiApi) GetApiGroups(c *gin.Context) {
+ groups, apiGroupMap, err := apiService.GetApiGroups()
+ if err != nil {
+ global.GVA_LOG.Error("获取失败!", zap.Error(err))
+ response.FailWithMessage("获取失败", c)
+ return
+ }
+ response.OkWithData(gin.H{
+ "groups": groups,
+ "apiGroupMap": apiGroupMap,
+ }, c)
+}
+
+// IgnoreApi
+// @Tags IgnoreApi
+// @Summary 忽略API
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Success 200 {object} response.Response{msg=string} "同步API"
+// @Router /api/ignoreApi [post]
+func (s *SystemApiApi) IgnoreApi(c *gin.Context) {
+ var ignoreApi system.SysIgnoreApi
+ err := c.ShouldBindJSON(&ignoreApi)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = apiService.IgnoreApi(ignoreApi)
+ if err != nil {
+ global.GVA_LOG.Error("忽略失败!", zap.Error(err))
+ response.FailWithMessage("忽略失败", c)
+ return
+ }
+ response.Ok(c)
+}
+
+// EnterSyncApi
+// @Tags SysApi
+// @Summary 确认同步API
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Success 200 {object} response.Response{msg=string} "确认同步API"
+// @Router /api/enterSyncApi [post]
+func (s *SystemApiApi) EnterSyncApi(c *gin.Context) {
+ var syncApi systemRes.SysSyncApis
+ err := c.ShouldBindJSON(&syncApi)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = apiService.EnterSyncApi(syncApi)
+ if err != nil {
+ global.GVA_LOG.Error("忽略失败!", zap.Error(err))
+ response.FailWithMessage("忽略失败", c)
+ return
+ }
+ response.Ok(c)
+}
+
+// DeleteApi
+// @Tags SysApi
+// @Summary 删除api
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body system.SysApi true "ID"
+// @Success 200 {object} response.Response{msg=string} "删除api"
+// @Router /api/deleteApi [post]
+func (s *SystemApiApi) DeleteApi(c *gin.Context) {
+ var api system.SysApi
+ err := c.ShouldBindJSON(&api)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = utils.Verify(api.GVA_MODEL, utils.IdVerify)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = apiService.DeleteApi(api)
+ if err != nil {
+ global.GVA_LOG.Error("删除失败!", zap.Error(err))
+ response.FailWithMessage("删除失败", c)
+ return
+ }
+ response.OkWithMessage("删除成功", c)
+}
+
+// GetApiList
+// @Tags SysApi
+// @Summary 分页获取API列表
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body systemReq.SearchApiParams true "分页获取API列表"
+// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页获取API列表,返回包括列表,总数,页码,每页数量"
+// @Router /api/getApiList [post]
+func (s *SystemApiApi) GetApiList(c *gin.Context) {
+ var pageInfo systemReq.SearchApiParams
+ err := c.ShouldBindJSON(&pageInfo)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = utils.Verify(pageInfo.PageInfo, utils.PageInfoVerify)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ list, total, err := apiService.GetAPIInfoList(pageInfo.SysApi, pageInfo.PageInfo, pageInfo.OrderKey, pageInfo.Desc)
+ if err != nil {
+ global.GVA_LOG.Error("获取失败!", zap.Error(err))
+ response.FailWithMessage("获取失败", c)
+ return
+ }
+ response.OkWithDetailed(response.PageResult{
+ List: list,
+ Total: total,
+ Page: pageInfo.Page,
+ PageSize: pageInfo.PageSize,
+ }, "获取成功", c)
+}
+
+// GetApiById
+// @Tags SysApi
+// @Summary 根据id获取api
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body request.GetById true "根据id获取api"
+// @Success 200 {object} response.Response{data=systemRes.SysAPIResponse} "根据id获取api,返回包括api详情"
+// @Router /api/getApiById [post]
+func (s *SystemApiApi) GetApiById(c *gin.Context) {
+ var idInfo request.GetById
+ err := c.ShouldBindJSON(&idInfo)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = utils.Verify(idInfo, utils.IdVerify)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ api, err := apiService.GetApiById(idInfo.ID)
+ if err != nil {
+ global.GVA_LOG.Error("获取失败!", zap.Error(err))
+ response.FailWithMessage("获取失败", c)
+ return
+ }
+ response.OkWithDetailed(systemRes.SysAPIResponse{Api: api}, "获取成功", c)
+}
+
+// UpdateApi
+// @Tags SysApi
+// @Summary 修改基础api
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body system.SysApi true "api路径, api中文描述, api组, 方法"
+// @Success 200 {object} response.Response{msg=string} "修改基础api"
+// @Router /api/updateApi [post]
+func (s *SystemApiApi) UpdateApi(c *gin.Context) {
+ var api system.SysApi
+ err := c.ShouldBindJSON(&api)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = utils.Verify(api, utils.ApiVerify)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = apiService.UpdateApi(api)
+ if err != nil {
+ global.GVA_LOG.Error("修改失败!", zap.Error(err))
+ response.FailWithMessage("修改失败", c)
+ return
+ }
+ response.OkWithMessage("修改成功", c)
+}
+
+// GetAllApis
+// @Tags SysApi
+// @Summary 获取所有的Api 不分页
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Success 200 {object} response.Response{data=systemRes.SysAPIListResponse,msg=string} "获取所有的Api 不分页,返回包括api列表"
+// @Router /api/getAllApis [post]
+func (s *SystemApiApi) GetAllApis(c *gin.Context) {
+ authorityID := utils.GetUserAuthorityId(c)
+ apis, err := apiService.GetAllApis(authorityID)
+ if err != nil {
+ global.GVA_LOG.Error("获取失败!", zap.Error(err))
+ response.FailWithMessage("获取失败", c)
+ return
+ }
+ response.OkWithDetailed(systemRes.SysAPIListResponse{Apis: apis}, "获取成功", c)
+}
+
+// DeleteApisByIds
+// @Tags SysApi
+// @Summary 删除选中Api
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body request.IdsReq true "ID"
+// @Success 200 {object} response.Response{msg=string} "删除选中Api"
+// @Router /api/deleteApisByIds [delete]
+func (s *SystemApiApi) DeleteApisByIds(c *gin.Context) {
+ var ids request.IdsReq
+ err := c.ShouldBindJSON(&ids)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = apiService.DeleteApisByIds(ids)
+ if err != nil {
+ global.GVA_LOG.Error("删除失败!", zap.Error(err))
+ response.FailWithMessage("删除失败", c)
+ return
+ }
+ response.OkWithMessage("删除成功", c)
+}
+
+// FreshCasbin
+// @Tags SysApi
+// @Summary 刷新casbin缓存
+// @accept application/json
+// @Produce application/json
+// @Success 200 {object} response.Response{msg=string} "刷新成功"
+// @Router /api/freshCasbin [get]
+func (s *SystemApiApi) FreshCasbin(c *gin.Context) {
+ err := casbinService.FreshCasbin()
+ if err != nil {
+ global.GVA_LOG.Error("刷新失败!", zap.Error(err))
+ response.FailWithMessage("刷新失败", c)
+ return
+ }
+ response.OkWithMessage("刷新成功", c)
+}
diff --git a/server/api/v1/system/sys_api_token.go b/server/api/v1/system/sys_api_token.go
new file mode 100644
index 0000000..3f5a29d
--- /dev/null
+++ b/server/api/v1/system/sys_api_token.go
@@ -0,0 +1,81 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common/response"
+ "git.echol.cn/loser/st/server/model/system"
+ sysReq "git.echol.cn/loser/st/server/model/system/request"
+ "github.com/gin-gonic/gin"
+ "go.uber.org/zap"
+)
+
+type ApiTokenApi struct{}
+
+// CreateApiToken 签发Token
+func (s *ApiTokenApi) CreateApiToken(c *gin.Context) {
+ var req struct {
+ UserID uint `json:"userId"`
+ AuthorityID uint `json:"authorityId"`
+ Days int `json:"days"` // -1为永久, 其他为天数
+ Remark string `json:"remark"`
+ }
+ err := c.ShouldBindJSON(&req)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ token := system.SysApiToken{
+ UserID: req.UserID,
+ AuthorityID: req.AuthorityID,
+ Remark: req.Remark,
+ }
+
+ jwtStr, err := apiTokenService.CreateApiToken(token, req.Days)
+ if err != nil {
+ global.GVA_LOG.Error("签发失败!", zap.Error(err))
+ response.FailWithMessage("签发失败: "+err.Error(), c)
+ return
+ }
+
+ response.OkWithDetailed(gin.H{"token": jwtStr}, "签发成功", c)
+}
+
+// GetApiTokenList 获取列表
+func (s *ApiTokenApi) GetApiTokenList(c *gin.Context) {
+ var pageInfo sysReq.SysApiTokenSearch
+ err := c.ShouldBindJSON(&pageInfo)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ list, total, err := apiTokenService.GetApiTokenList(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)
+}
+
+// DeleteApiToken 作废Token
+func (s *ApiTokenApi) DeleteApiToken(c *gin.Context) {
+ var req system.SysApiToken
+ err := c.ShouldBindJSON(&req)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = apiTokenService.DeleteApiToken(req.ID)
+ if err != nil {
+ global.GVA_LOG.Error("作废失败!", zap.Error(err))
+ response.FailWithMessage("作废失败", c)
+ return
+ }
+ response.OkWithMessage("作废成功", c)
+}
diff --git a/server/api/v1/system/sys_authority.go b/server/api/v1/system/sys_authority.go
new file mode 100644
index 0000000..635e0dd
--- /dev/null
+++ b/server/api/v1/system/sys_authority.go
@@ -0,0 +1,202 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common/response"
+ "git.echol.cn/loser/st/server/model/system"
+ systemRes "git.echol.cn/loser/st/server/model/system/response"
+ "git.echol.cn/loser/st/server/utils"
+
+ "github.com/gin-gonic/gin"
+ "go.uber.org/zap"
+)
+
+type AuthorityApi struct{}
+
+// CreateAuthority
+// @Tags Authority
+// @Summary 创建角色
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body system.SysAuthority true "权限id, 权限名, 父角色id"
+// @Success 200 {object} response.Response{data=systemRes.SysAuthorityResponse,msg=string} "创建角色,返回包括系统角色详情"
+// @Router /authority/createAuthority [post]
+func (a *AuthorityApi) CreateAuthority(c *gin.Context) {
+ var authority, authBack system.SysAuthority
+ var err error
+
+ if err = c.ShouldBindJSON(&authority); err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ if err = utils.Verify(authority, utils.AuthorityVerify); err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ if *authority.ParentId == 0 && global.GVA_CONFIG.System.UseStrictAuth {
+ authority.ParentId = utils.Pointer(utils.GetUserAuthorityId(c))
+ }
+
+ if authBack, err = authorityService.CreateAuthority(authority); err != nil {
+ global.GVA_LOG.Error("创建失败!", zap.Error(err))
+ response.FailWithMessage("创建失败"+err.Error(), c)
+ return
+ }
+ err = casbinService.FreshCasbin()
+ if err != nil {
+ global.GVA_LOG.Error("创建成功,权限刷新失败。", zap.Error(err))
+ response.FailWithMessage("创建成功,权限刷新失败。"+err.Error(), c)
+ return
+ }
+ response.OkWithDetailed(systemRes.SysAuthorityResponse{Authority: authBack}, "创建成功", c)
+}
+
+// CopyAuthority
+// @Tags Authority
+// @Summary 拷贝角色
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body response.SysAuthorityCopyResponse true "旧角色id, 新权限id, 新权限名, 新父角色id"
+// @Success 200 {object} response.Response{data=systemRes.SysAuthorityResponse,msg=string} "拷贝角色,返回包括系统角色详情"
+// @Router /authority/copyAuthority [post]
+func (a *AuthorityApi) CopyAuthority(c *gin.Context) {
+ var copyInfo systemRes.SysAuthorityCopyResponse
+ err := c.ShouldBindJSON(©Info)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = utils.Verify(copyInfo, utils.OldAuthorityVerify)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = utils.Verify(copyInfo.Authority, utils.AuthorityVerify)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ adminAuthorityID := utils.GetUserAuthorityId(c)
+ authBack, err := authorityService.CopyAuthority(adminAuthorityID, copyInfo)
+ if err != nil {
+ global.GVA_LOG.Error("拷贝失败!", zap.Error(err))
+ response.FailWithMessage("拷贝失败"+err.Error(), c)
+ return
+ }
+ response.OkWithDetailed(systemRes.SysAuthorityResponse{Authority: authBack}, "拷贝成功", c)
+}
+
+// DeleteAuthority
+// @Tags Authority
+// @Summary 删除角色
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body system.SysAuthority true "删除角色"
+// @Success 200 {object} response.Response{msg=string} "删除角色"
+// @Router /authority/deleteAuthority [post]
+func (a *AuthorityApi) DeleteAuthority(c *gin.Context) {
+ var authority system.SysAuthority
+ var err error
+ if err = c.ShouldBindJSON(&authority); err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ if err = utils.Verify(authority, utils.AuthorityIdVerify); err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ // 删除角色之前需要判断是否有用户正在使用此角色
+ if err = authorityService.DeleteAuthority(&authority); err != nil {
+ global.GVA_LOG.Error("删除失败!", zap.Error(err))
+ response.FailWithMessage("删除失败"+err.Error(), c)
+ return
+ }
+ _ = casbinService.FreshCasbin()
+ response.OkWithMessage("删除成功", c)
+}
+
+// UpdateAuthority
+// @Tags Authority
+// @Summary 更新角色信息
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body system.SysAuthority true "权限id, 权限名, 父角色id"
+// @Success 200 {object} response.Response{data=systemRes.SysAuthorityResponse,msg=string} "更新角色信息,返回包括系统角色详情"
+// @Router /authority/updateAuthority [put]
+func (a *AuthorityApi) UpdateAuthority(c *gin.Context) {
+ var auth system.SysAuthority
+ err := c.ShouldBindJSON(&auth)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = utils.Verify(auth, utils.AuthorityVerify)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ authority, err := authorityService.UpdateAuthority(auth)
+ if err != nil {
+ global.GVA_LOG.Error("更新失败!", zap.Error(err))
+ response.FailWithMessage("更新失败"+err.Error(), c)
+ return
+ }
+ response.OkWithDetailed(systemRes.SysAuthorityResponse{Authority: authority}, "更新成功", c)
+}
+
+// GetAuthorityList
+// @Tags Authority
+// @Summary 分页获取角色列表
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body request.PageInfo true "页码, 每页大小"
+// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页获取角色列表,返回包括列表,总数,页码,每页数量"
+// @Router /authority/getAuthorityList [post]
+func (a *AuthorityApi) GetAuthorityList(c *gin.Context) {
+ authorityID := utils.GetUserAuthorityId(c)
+ list, err := authorityService.GetAuthorityInfoList(authorityID)
+ if err != nil {
+ global.GVA_LOG.Error("获取失败!", zap.Error(err))
+ response.FailWithMessage("获取失败"+err.Error(), c)
+ return
+ }
+ response.OkWithDetailed(list, "获取成功", c)
+}
+
+// SetDataAuthority
+// @Tags Authority
+// @Summary 设置角色资源权限
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body system.SysAuthority true "设置角色资源权限"
+// @Success 200 {object} response.Response{msg=string} "设置角色资源权限"
+// @Router /authority/setDataAuthority [post]
+func (a *AuthorityApi) SetDataAuthority(c *gin.Context) {
+ var auth system.SysAuthority
+ err := c.ShouldBindJSON(&auth)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = utils.Verify(auth, utils.AuthorityIdVerify)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ adminAuthorityID := utils.GetUserAuthorityId(c)
+ err = authorityService.SetDataAuthority(adminAuthorityID, auth)
+ if err != nil {
+ global.GVA_LOG.Error("设置失败!", zap.Error(err))
+ response.FailWithMessage("设置失败"+err.Error(), c)
+ return
+ }
+ response.OkWithMessage("设置成功", c)
+}
diff --git a/server/api/v1/system/sys_authority_btn.go b/server/api/v1/system/sys_authority_btn.go
new file mode 100644
index 0000000..2d09c42
--- /dev/null
+++ b/server/api/v1/system/sys_authority_btn.go
@@ -0,0 +1,80 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common/response"
+ "git.echol.cn/loser/st/server/model/system/request"
+ "github.com/gin-gonic/gin"
+ "go.uber.org/zap"
+)
+
+type AuthorityBtnApi struct{}
+
+// GetAuthorityBtn
+// @Tags AuthorityBtn
+// @Summary 获取权限按钮
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body request.SysAuthorityBtnReq true "菜单id, 角色id, 选中的按钮id"
+// @Success 200 {object} response.Response{data=response.SysAuthorityBtnRes,msg=string} "返回列表成功"
+// @Router /authorityBtn/getAuthorityBtn [post]
+func (a *AuthorityBtnApi) GetAuthorityBtn(c *gin.Context) {
+ var req request.SysAuthorityBtnReq
+ err := c.ShouldBindJSON(&req)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ res, err := authorityBtnService.GetAuthorityBtn(req)
+ if err != nil {
+ global.GVA_LOG.Error("查询失败!", zap.Error(err))
+ response.FailWithMessage("查询失败", c)
+ return
+ }
+ response.OkWithDetailed(res, "查询成功", c)
+}
+
+// SetAuthorityBtn
+// @Tags AuthorityBtn
+// @Summary 设置权限按钮
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body request.SysAuthorityBtnReq true "菜单id, 角色id, 选中的按钮id"
+// @Success 200 {object} response.Response{msg=string} "返回列表成功"
+// @Router /authorityBtn/setAuthorityBtn [post]
+func (a *AuthorityBtnApi) SetAuthorityBtn(c *gin.Context) {
+ var req request.SysAuthorityBtnReq
+ err := c.ShouldBindJSON(&req)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = authorityBtnService.SetAuthorityBtn(req)
+ if err != nil {
+ global.GVA_LOG.Error("分配失败!", zap.Error(err))
+ response.FailWithMessage("分配失败", c)
+ return
+ }
+ response.OkWithMessage("分配成功", c)
+}
+
+// CanRemoveAuthorityBtn
+// @Tags AuthorityBtn
+// @Summary 设置权限按钮
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Success 200 {object} response.Response{msg=string} "删除成功"
+// @Router /authorityBtn/canRemoveAuthorityBtn [post]
+func (a *AuthorityBtnApi) CanRemoveAuthorityBtn(c *gin.Context) {
+ id := c.Query("id")
+ err := authorityBtnService.CanRemoveAuthorityBtn(id)
+ if err != nil {
+ global.GVA_LOG.Error("删除失败!", zap.Error(err))
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ response.OkWithMessage("删除成功", c)
+}
diff --git a/server/api/v1/system/sys_auto_code.go b/server/api/v1/system/sys_auto_code.go
new file mode 100644
index 0000000..f45cb05
--- /dev/null
+++ b/server/api/v1/system/sys_auto_code.go
@@ -0,0 +1,117 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/model/common"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common/response"
+ "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
+ if err := c.ShouldBindJSON(&llm); err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ data, err := autoCodeService.LLMAuto(c.Request.Context(), llm)
+ if err != nil {
+ global.GVA_LOG.Error("大模型生成失败!", zap.Error(err))
+ response.FailWithMessage("大模型生成失败"+err.Error(), c)
+ return
+ }
+ response.OkWithData(data, c)
+}
diff --git a/server/api/v1/system/sys_captcha.go b/server/api/v1/system/sys_captcha.go
new file mode 100644
index 0000000..849c110
--- /dev/null
+++ b/server/api/v1/system/sys_captcha.go
@@ -0,0 +1,70 @@
+package system
+
+import (
+ "time"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common/response"
+ systemRes "git.echol.cn/loser/st/server/model/system/response"
+ "github.com/gin-gonic/gin"
+ "github.com/mojocn/base64Captcha"
+ "go.uber.org/zap"
+)
+
+// 当开启多服务器部署时,替换下面的配置,使用redis共享存储验证码
+// var store = captcha.NewDefaultRedisStore()
+var store = base64Captcha.DefaultMemStore
+
+type BaseApi struct{}
+
+// Captcha
+// @Tags Base
+// @Summary 生成验证码
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Success 200 {object} response.Response{data=systemRes.SysCaptchaResponse,msg=string} "生成验证码,返回包括随机数id,base64,验证码长度,是否开启验证码"
+// @Router /base/captcha [post]
+func (b *BaseApi) Captcha(c *gin.Context) {
+ // 判断验证码是否开启
+ openCaptcha := global.GVA_CONFIG.Captcha.OpenCaptcha // 是否开启防爆次数
+ openCaptchaTimeOut := global.GVA_CONFIG.Captcha.OpenCaptchaTimeOut // 缓存超时时间
+ key := c.ClientIP()
+ v, ok := global.BlackCache.Get(key)
+ if !ok {
+ global.BlackCache.Set(key, 1, time.Second*time.Duration(openCaptchaTimeOut))
+ }
+
+ var oc bool
+ if openCaptcha == 0 || openCaptcha < interfaceToInt(v) {
+ oc = true
+ }
+ // 字符,公式,验证码配置
+ // 生成默认数字的driver
+ driver := base64Captcha.NewDriverDigit(global.GVA_CONFIG.Captcha.ImgHeight, global.GVA_CONFIG.Captcha.ImgWidth, global.GVA_CONFIG.Captcha.KeyLong, 0.7, 80)
+ // cp := base64Captcha.NewCaptcha(driver, store.UseWithCtx(c)) // v8下使用redis
+ cp := base64Captcha.NewCaptcha(driver, store)
+ id, b64s, _, err := cp.Generate()
+ if err != nil {
+ global.GVA_LOG.Error("验证码获取失败!", zap.Error(err))
+ response.FailWithMessage("验证码获取失败", c)
+ return
+ }
+ response.OkWithDetailed(systemRes.SysCaptchaResponse{
+ CaptchaId: id,
+ PicPath: b64s,
+ CaptchaLength: global.GVA_CONFIG.Captcha.KeyLong,
+ OpenCaptcha: oc,
+ }, "验证码获取成功", c)
+}
+
+// 类型转换
+func interfaceToInt(v interface{}) (i int) {
+ switch v := v.(type) {
+ case int:
+ i = v
+ default:
+ i = 0
+ }
+ return
+}
diff --git a/server/api/v1/system/sys_casbin.go b/server/api/v1/system/sys_casbin.go
new file mode 100644
index 0000000..9ac19cd
--- /dev/null
+++ b/server/api/v1/system/sys_casbin.go
@@ -0,0 +1,69 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common/response"
+ "git.echol.cn/loser/st/server/model/system/request"
+ systemRes "git.echol.cn/loser/st/server/model/system/response"
+ "git.echol.cn/loser/st/server/utils"
+ "github.com/gin-gonic/gin"
+ "go.uber.org/zap"
+)
+
+type CasbinApi struct{}
+
+// UpdateCasbin
+// @Tags Casbin
+// @Summary 更新角色api权限
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body request.CasbinInReceive true "权限id, 权限模型列表"
+// @Success 200 {object} response.Response{msg=string} "更新角色api权限"
+// @Router /casbin/UpdateCasbin [post]
+func (cas *CasbinApi) UpdateCasbin(c *gin.Context) {
+ var cmr request.CasbinInReceive
+ err := c.ShouldBindJSON(&cmr)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = utils.Verify(cmr, utils.AuthorityIdVerify)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ adminAuthorityID := utils.GetUserAuthorityId(c)
+ err = casbinService.UpdateCasbin(adminAuthorityID, cmr.AuthorityId, cmr.CasbinInfos)
+ if err != nil {
+ global.GVA_LOG.Error("更新失败!", zap.Error(err))
+ response.FailWithMessage("更新失败", c)
+ return
+ }
+ response.OkWithMessage("更新成功", c)
+}
+
+// GetPolicyPathByAuthorityId
+// @Tags Casbin
+// @Summary 获取权限列表
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body request.CasbinInReceive true "权限id, 权限模型列表"
+// @Success 200 {object} response.Response{data=systemRes.PolicyPathResponse,msg=string} "获取权限列表,返回包括casbin详情列表"
+// @Router /casbin/getPolicyPathByAuthorityId [post]
+func (cas *CasbinApi) GetPolicyPathByAuthorityId(c *gin.Context) {
+ var casbin request.CasbinInReceive
+ err := c.ShouldBindJSON(&casbin)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = utils.Verify(casbin, utils.AuthorityIdVerify)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ paths := casbinService.GetPolicyPathByAuthorityId(casbin.AuthorityId)
+ response.OkWithDetailed(systemRes.PolicyPathResponse{Paths: paths}, "获取成功", c)
+}
diff --git a/server/api/v1/system/sys_dictionary.go b/server/api/v1/system/sys_dictionary.go
new file mode 100644
index 0000000..ea74696
--- /dev/null
+++ b/server/api/v1/system/sys_dictionary.go
@@ -0,0 +1,191 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common/response"
+ "git.echol.cn/loser/st/server/model/system"
+ "git.echol.cn/loser/st/server/model/system/request"
+ "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
+// @Param data query request.SysDictionarySearch true "字典 name 或者 type"
+// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页获取SysDictionary列表,返回包括列表,总数,页码,每页数量"
+// @Router /sysDictionary/getSysDictionaryList [get]
+func (s *DictionaryApi) GetSysDictionaryList(c *gin.Context) {
+ var dictionary request.SysDictionarySearch
+ err := c.ShouldBindQuery(&dictionary)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ list, err := dictionaryService.GetSysDictionaryInfoList(c, dictionary)
+ if err != nil {
+ global.GVA_LOG.Error("获取失败!", zap.Error(err))
+ response.FailWithMessage("获取失败", c)
+ return
+ }
+ response.OkWithDetailed(list, "获取成功", c)
+}
+
+// ExportSysDictionary
+// @Tags SysDictionary
+// @Summary 导出字典JSON(包含字典详情)
+// @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} "导出字典JSON"
+// @Router /sysDictionary/exportSysDictionary [get]
+func (s *DictionaryApi) ExportSysDictionary(c *gin.Context) {
+ var dictionary system.SysDictionary
+ err := c.ShouldBindQuery(&dictionary)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ if dictionary.ID == 0 {
+ response.FailWithMessage("字典ID不能为空", c)
+ return
+ }
+ exportData, err := dictionaryService.ExportSysDictionary(dictionary.ID)
+ if err != nil {
+ global.GVA_LOG.Error("导出失败!", zap.Error(err))
+ response.FailWithMessage("导出失败", c)
+ return
+ }
+ response.OkWithDetailed(exportData, "导出成功", c)
+}
+
+// ImportSysDictionary
+// @Tags SysDictionary
+// @Summary 导入字典JSON(包含字典详情)
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body request.ImportSysDictionaryRequest true "字典JSON数据"
+// @Success 200 {object} response.Response{msg=string} "导入字典"
+// @Router /sysDictionary/importSysDictionary [post]
+func (s *DictionaryApi) ImportSysDictionary(c *gin.Context) {
+ var req request.ImportSysDictionaryRequest
+ err := c.ShouldBindJSON(&req)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = dictionaryService.ImportSysDictionary(req.Json)
+ if err != nil {
+ global.GVA_LOG.Error("导入失败!", zap.Error(err))
+ response.FailWithMessage("导入失败: "+err.Error(), c)
+ return
+ }
+ response.OkWithMessage("导入成功", c)
+}
diff --git a/server/api/v1/system/sys_dictionary_detail.go b/server/api/v1/system/sys_dictionary_detail.go
new file mode 100644
index 0000000..eca655e
--- /dev/null
+++ b/server/api/v1/system/sys_dictionary_detail.go
@@ -0,0 +1,267 @@
+package system
+
+import (
+ "strconv"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common/response"
+ "git.echol.cn/loser/st/server/model/system"
+ "git.echol.cn/loser/st/server/model/system/request"
+ "git.echol.cn/loser/st/server/utils"
+ "github.com/gin-gonic/gin"
+ "go.uber.org/zap"
+)
+
+type DictionaryDetailApi struct{}
+
+// CreateSysDictionaryDetail
+// @Tags SysDictionaryDetail
+// @Summary 创建SysDictionaryDetail
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body system.SysDictionaryDetail true "SysDictionaryDetail模型"
+// @Success 200 {object} response.Response{msg=string} "创建SysDictionaryDetail"
+// @Router /sysDictionaryDetail/createSysDictionaryDetail [post]
+func (s *DictionaryDetailApi) CreateSysDictionaryDetail(c *gin.Context) {
+ var detail system.SysDictionaryDetail
+ err := c.ShouldBindJSON(&detail)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = dictionaryDetailService.CreateSysDictionaryDetail(detail)
+ if err != nil {
+ global.GVA_LOG.Error("创建失败!", zap.Error(err))
+ response.FailWithMessage("创建失败", c)
+ return
+ }
+ response.OkWithMessage("创建成功", c)
+}
+
+// DeleteSysDictionaryDetail
+// @Tags SysDictionaryDetail
+// @Summary 删除SysDictionaryDetail
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body system.SysDictionaryDetail true "SysDictionaryDetail模型"
+// @Success 200 {object} response.Response{msg=string} "删除SysDictionaryDetail"
+// @Router /sysDictionaryDetail/deleteSysDictionaryDetail [delete]
+func (s *DictionaryDetailApi) DeleteSysDictionaryDetail(c *gin.Context) {
+ var detail system.SysDictionaryDetail
+ err := c.ShouldBindJSON(&detail)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = dictionaryDetailService.DeleteSysDictionaryDetail(detail)
+ if err != nil {
+ global.GVA_LOG.Error("删除失败!", zap.Error(err))
+ response.FailWithMessage("删除失败", c)
+ return
+ }
+ response.OkWithMessage("删除成功", c)
+}
+
+// UpdateSysDictionaryDetail
+// @Tags SysDictionaryDetail
+// @Summary 更新SysDictionaryDetail
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body system.SysDictionaryDetail true "更新SysDictionaryDetail"
+// @Success 200 {object} response.Response{msg=string} "更新SysDictionaryDetail"
+// @Router /sysDictionaryDetail/updateSysDictionaryDetail [put]
+func (s *DictionaryDetailApi) UpdateSysDictionaryDetail(c *gin.Context) {
+ var detail system.SysDictionaryDetail
+ err := c.ShouldBindJSON(&detail)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = dictionaryDetailService.UpdateSysDictionaryDetail(&detail)
+ if err != nil {
+ global.GVA_LOG.Error("更新失败!", zap.Error(err))
+ response.FailWithMessage("更新失败", c)
+ return
+ }
+ response.OkWithMessage("更新成功", c)
+}
+
+// FindSysDictionaryDetail
+// @Tags SysDictionaryDetail
+// @Summary 用id查询SysDictionaryDetail
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data query system.SysDictionaryDetail true "用id查询SysDictionaryDetail"
+// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "用id查询SysDictionaryDetail"
+// @Router /sysDictionaryDetail/findSysDictionaryDetail [get]
+func (s *DictionaryDetailApi) FindSysDictionaryDetail(c *gin.Context) {
+ var detail system.SysDictionaryDetail
+ err := c.ShouldBindQuery(&detail)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = utils.Verify(detail, utils.IdVerify)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ reSysDictionaryDetail, err := dictionaryDetailService.GetSysDictionaryDetail(detail.ID)
+ if err != nil {
+ global.GVA_LOG.Error("查询失败!", zap.Error(err))
+ response.FailWithMessage("查询失败", c)
+ return
+ }
+ response.OkWithDetailed(gin.H{"reSysDictionaryDetail": reSysDictionaryDetail}, "查询成功", c)
+}
+
+// GetSysDictionaryDetailList
+// @Tags SysDictionaryDetail
+// @Summary 分页获取SysDictionaryDetail列表
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data query request.SysDictionaryDetailSearch true "页码, 每页大小, 搜索条件"
+// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页获取SysDictionaryDetail列表,返回包括列表,总数,页码,每页数量"
+// @Router /sysDictionaryDetail/getSysDictionaryDetailList [get]
+func (s *DictionaryDetailApi) GetSysDictionaryDetailList(c *gin.Context) {
+ var pageInfo request.SysDictionaryDetailSearch
+ err := c.ShouldBindQuery(&pageInfo)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ list, total, err := dictionaryDetailService.GetSysDictionaryDetailInfoList(pageInfo)
+ if err != nil {
+ global.GVA_LOG.Error("获取失败!", zap.Error(err))
+ response.FailWithMessage("获取失败", c)
+ return
+ }
+ response.OkWithDetailed(response.PageResult{
+ List: list,
+ Total: total,
+ Page: pageInfo.Page,
+ PageSize: pageInfo.PageSize,
+ }, "获取成功", c)
+}
+
+// GetDictionaryTreeList
+// @Tags SysDictionaryDetail
+// @Summary 获取字典详情树形结构
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param sysDictionaryID query int true "字典ID"
+// @Success 200 {object} response.Response{data=[]system.SysDictionaryDetail,msg=string} "获取字典详情树形结构"
+// @Router /sysDictionaryDetail/getDictionaryTreeList [get]
+func (s *DictionaryDetailApi) GetDictionaryTreeList(c *gin.Context) {
+ sysDictionaryID := c.Query("sysDictionaryID")
+ if sysDictionaryID == "" {
+ response.FailWithMessage("字典ID不能为空", c)
+ return
+ }
+
+ var id uint
+ if idUint64, err := strconv.ParseUint(sysDictionaryID, 10, 32); err != nil {
+ response.FailWithMessage("字典ID格式错误", c)
+ return
+ } else {
+ id = uint(idUint64)
+ }
+
+ list, err := dictionaryDetailService.GetDictionaryTreeList(id)
+ if err != nil {
+ global.GVA_LOG.Error("获取失败!", zap.Error(err))
+ response.FailWithMessage("获取失败", c)
+ return
+ }
+ response.OkWithDetailed(gin.H{"list": list}, "获取成功", c)
+}
+
+// GetDictionaryTreeListByType
+// @Tags SysDictionaryDetail
+// @Summary 根据字典类型获取字典详情树形结构
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param type query string true "字典类型"
+// @Success 200 {object} response.Response{data=[]system.SysDictionaryDetail,msg=string} "获取字典详情树形结构"
+// @Router /sysDictionaryDetail/getDictionaryTreeListByType [get]
+func (s *DictionaryDetailApi) GetDictionaryTreeListByType(c *gin.Context) {
+ dictType := c.Query("type")
+ if dictType == "" {
+ response.FailWithMessage("字典类型不能为空", c)
+ return
+ }
+
+ list, err := dictionaryDetailService.GetDictionaryTreeListByType(dictType)
+ if err != nil {
+ global.GVA_LOG.Error("获取失败!", zap.Error(err))
+ response.FailWithMessage("获取失败", c)
+ return
+ }
+ response.OkWithDetailed(gin.H{"list": list}, "获取成功", c)
+}
+
+// GetDictionaryDetailsByParent
+// @Tags SysDictionaryDetail
+// @Summary 根据父级ID获取字典详情
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data query request.GetDictionaryDetailsByParentRequest true "查询参数"
+// @Success 200 {object} response.Response{data=[]system.SysDictionaryDetail,msg=string} "获取字典详情列表"
+// @Router /sysDictionaryDetail/getDictionaryDetailsByParent [get]
+func (s *DictionaryDetailApi) GetDictionaryDetailsByParent(c *gin.Context) {
+ var req request.GetDictionaryDetailsByParentRequest
+ err := c.ShouldBindQuery(&req)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ list, err := dictionaryDetailService.GetDictionaryDetailsByParent(req)
+ if err != nil {
+ global.GVA_LOG.Error("获取失败!", zap.Error(err))
+ response.FailWithMessage("获取失败", c)
+ return
+ }
+ response.OkWithDetailed(gin.H{"list": list}, "获取成功", c)
+}
+
+// GetDictionaryPath
+// @Tags SysDictionaryDetail
+// @Summary 获取字典详情的完整路径
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param id query uint true "字典详情ID"
+// @Success 200 {object} response.Response{data=[]system.SysDictionaryDetail,msg=string} "获取字典详情路径"
+// @Router /sysDictionaryDetail/getDictionaryPath [get]
+func (s *DictionaryDetailApi) GetDictionaryPath(c *gin.Context) {
+ idStr := c.Query("id")
+ if idStr == "" {
+ response.FailWithMessage("字典详情ID不能为空", c)
+ return
+ }
+
+ var id uint
+ if idUint64, err := strconv.ParseUint(idStr, 10, 32); err != nil {
+ response.FailWithMessage("字典详情ID格式错误", c)
+ return
+ } else {
+ id = uint(idUint64)
+ }
+
+ path, err := dictionaryDetailService.GetDictionaryPath(id)
+ if err != nil {
+ global.GVA_LOG.Error("获取失败!", zap.Error(err))
+ response.FailWithMessage("获取失败", c)
+ return
+ }
+ response.OkWithDetailed(gin.H{"path": path}, "获取成功", c)
+}
diff --git a/server/api/v1/system/sys_error.go b/server/api/v1/system/sys_error.go
new file mode 100644
index 0000000..54de19c
--- /dev/null
+++ b/server/api/v1/system/sys_error.go
@@ -0,0 +1,199 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common/response"
+ "git.echol.cn/loser/st/server/model/system"
+ systemReq "git.echol.cn/loser/st/server/model/system/request"
+ "github.com/gin-gonic/gin"
+ "go.uber.org/zap"
+)
+
+type SysErrorApi struct{}
+
+// CreateSysError 创建错误日志
+// @Tags SysError
+// @Summary 创建错误日志
+// @Security ApiKeyAuth
+// @Accept application/json
+// @Produce application/json
+// @Param data body system.SysError true "创建错误日志"
+// @Success 200 {object} response.Response{msg=string} "创建成功"
+// @Router /sysError/createSysError [post]
+func (sysErrorApi *SysErrorApi) CreateSysError(c *gin.Context) {
+ // 创建业务用Context
+ ctx := c.Request.Context()
+
+ var sysError system.SysError
+ err := c.ShouldBindJSON(&sysError)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = sysErrorService.CreateSysError(ctx, &sysError)
+ if err != nil {
+ global.GVA_LOG.Error("创建失败!", zap.Error(err))
+ response.FailWithMessage("创建失败:"+err.Error(), c)
+ return
+ }
+ response.OkWithMessage("创建成功", c)
+}
+
+// DeleteSysError 删除错误日志
+// @Tags SysError
+// @Summary 删除错误日志
+// @Security ApiKeyAuth
+// @Accept application/json
+// @Produce application/json
+// @Param data body system.SysError true "删除错误日志"
+// @Success 200 {object} response.Response{msg=string} "删除成功"
+// @Router /sysError/deleteSysError [delete]
+func (sysErrorApi *SysErrorApi) DeleteSysError(c *gin.Context) {
+ // 创建业务用Context
+ ctx := c.Request.Context()
+
+ ID := c.Query("ID")
+ err := sysErrorService.DeleteSysError(ctx, ID)
+ if err != nil {
+ global.GVA_LOG.Error("删除失败!", zap.Error(err))
+ response.FailWithMessage("删除失败:"+err.Error(), c)
+ return
+ }
+ response.OkWithMessage("删除成功", c)
+}
+
+// DeleteSysErrorByIds 批量删除错误日志
+// @Tags SysError
+// @Summary 批量删除错误日志
+// @Security ApiKeyAuth
+// @Accept application/json
+// @Produce application/json
+// @Success 200 {object} response.Response{msg=string} "批量删除成功"
+// @Router /sysError/deleteSysErrorByIds [delete]
+func (sysErrorApi *SysErrorApi) DeleteSysErrorByIds(c *gin.Context) {
+ // 创建业务用Context
+ ctx := c.Request.Context()
+
+ IDs := c.QueryArray("IDs[]")
+ err := sysErrorService.DeleteSysErrorByIds(ctx, IDs)
+ if err != nil {
+ global.GVA_LOG.Error("批量删除失败!", zap.Error(err))
+ response.FailWithMessage("批量删除失败:"+err.Error(), c)
+ return
+ }
+ response.OkWithMessage("批量删除成功", c)
+}
+
+// UpdateSysError 更新错误日志
+// @Tags SysError
+// @Summary 更新错误日志
+// @Security ApiKeyAuth
+// @Accept application/json
+// @Produce application/json
+// @Param data body system.SysError true "更新错误日志"
+// @Success 200 {object} response.Response{msg=string} "更新成功"
+// @Router /sysError/updateSysError [put]
+func (sysErrorApi *SysErrorApi) UpdateSysError(c *gin.Context) {
+ // 从ctx获取标准context进行业务行为
+ ctx := c.Request.Context()
+
+ var sysError system.SysError
+ err := c.ShouldBindJSON(&sysError)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = sysErrorService.UpdateSysError(ctx, sysError)
+ if err != nil {
+ global.GVA_LOG.Error("更新失败!", zap.Error(err))
+ response.FailWithMessage("更新失败:"+err.Error(), c)
+ return
+ }
+ response.OkWithMessage("更新成功", c)
+}
+
+// FindSysError 用id查询错误日志
+// @Tags SysError
+// @Summary 用id查询错误日志
+// @Security ApiKeyAuth
+// @Accept application/json
+// @Produce application/json
+// @Param ID query uint true "用id查询错误日志"
+// @Success 200 {object} response.Response{data=system.SysError,msg=string} "查询成功"
+// @Router /sysError/findSysError [get]
+func (sysErrorApi *SysErrorApi) FindSysError(c *gin.Context) {
+ // 创建业务用Context
+ ctx := c.Request.Context()
+
+ ID := c.Query("ID")
+ resysError, err := sysErrorService.GetSysError(ctx, ID)
+ if err != nil {
+ global.GVA_LOG.Error("查询失败!", zap.Error(err))
+ response.FailWithMessage("查询失败:"+err.Error(), c)
+ return
+ }
+ response.OkWithData(resysError, c)
+}
+
+// GetSysErrorList 分页获取错误日志列表
+// @Tags SysError
+// @Summary 分页获取错误日志列表
+// @Security ApiKeyAuth
+// @Accept application/json
+// @Produce application/json
+// @Param data query systemReq.SysErrorSearch true "分页获取错误日志列表"
+// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功"
+// @Router /sysError/getSysErrorList [get]
+func (sysErrorApi *SysErrorApi) GetSysErrorList(c *gin.Context) {
+ // 创建业务用Context
+ ctx := c.Request.Context()
+
+ var pageInfo systemReq.SysErrorSearch
+ err := c.ShouldBindQuery(&pageInfo)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ list, total, err := sysErrorService.GetSysErrorInfoList(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)
+}
+
+// GetSysErrorSolution 触发错误日志的异步处理
+// @Tags SysError
+// @Summary 根据ID触发处理:标记为处理中,1分钟后自动改为处理完成
+// @Security ApiKeyAuth
+// @Accept application/json
+// @Produce application/json
+// @Param id query string true "错误日志ID"
+// @Success 200 {object} response.Response{msg=string} "处理已提交"
+// @Router /sysError/getSysErrorSolution [get]
+func (sysErrorApi *SysErrorApi) GetSysErrorSolution(c *gin.Context) {
+ // 创建业务用Context
+ ctx := c.Request.Context()
+
+ // 兼容 id 与 ID 两种参数
+ ID := c.Query("id")
+ if ID == "" {
+ response.FailWithMessage("缺少参数: id", c)
+ return
+ }
+
+ err := sysErrorService.GetSysErrorSolution(ctx, ID)
+ if err != nil {
+ global.GVA_LOG.Error("处理触发失败!", zap.Error(err))
+ response.FailWithMessage("处理触发失败:"+err.Error(), c)
+ return
+ }
+
+ response.OkWithMessage("已提交至AI处理", c)
+}
diff --git a/server/api/v1/system/sys_export_template.go b/server/api/v1/system/sys_export_template.go
new file mode 100644
index 0000000..2270f76
--- /dev/null
+++ b/server/api/v1/system/sys_export_template.go
@@ -0,0 +1,456 @@
+package system
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "sync"
+ "time"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common/request"
+ "git.echol.cn/loser/st/server/model/common/response"
+ "git.echol.cn/loser/st/server/model/system"
+ systemReq "git.echol.cn/loser/st/server/model/system/request"
+ "git.echol.cn/loser/st/server/service"
+ "git.echol.cn/loser/st/server/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
+
+// PreviewSQL 预览最终生成的SQL
+// @Tags SysExportTemplate
+// @Summary 预览最终生成的SQL(不执行查询,仅返回SQL字符串)
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param templateID query string true "导出模板ID"
+// @Param params query string false "查询参数编码字符串,参考 ExportExcel 组件"
+// @Success 200 {object} response.Response{data=map[string]string} "获取成功"
+// @Router /sysExportTemplate/previewSQL [get]
+func (sysExportTemplateApi *SysExportTemplateApi) PreviewSQL(c *gin.Context) {
+ templateID := c.Query("templateID")
+ if templateID == "" {
+ response.FailWithMessage("模板ID不能为空", c)
+ return
+ }
+
+ // 直接复用导出接口的参数组织方式:使用 URL Query,其中 params 为内部编码的查询字符串
+ queryParams := c.Request.URL.Query()
+
+ if sqlPreview, err := sysExportTemplateService.PreviewSQL(templateID, queryParams); err != nil {
+ global.GVA_LOG.Error("获取失败!", zap.Error(err))
+ response.FailWithMessage("获取失败", c)
+ } else {
+ response.OkWithData(gin.H{"sql": sqlPreview}, c)
+ }
+}
+
+// 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/server/api/v1/system/sys_initdb.go b/server/api/v1/system/sys_initdb.go
new file mode 100644
index 0000000..3b37384
--- /dev/null
+++ b/server/api/v1/system/sys_initdb.go
@@ -0,0 +1,59 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common/response"
+ "git.echol.cn/loser/st/server/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/server/api/v1/system/sys_jwt_blacklist.go b/server/api/v1/system/sys_jwt_blacklist.go
new file mode 100644
index 0000000..44b1a16
--- /dev/null
+++ b/server/api/v1/system/sys_jwt_blacklist.go
@@ -0,0 +1,33 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common/response"
+ "git.echol.cn/loser/st/server/model/system"
+ "git.echol.cn/loser/st/server/utils"
+ "github.com/gin-gonic/gin"
+ "go.uber.org/zap"
+)
+
+type JwtApi struct{}
+
+// JsonInBlacklist
+// @Tags Jwt
+// @Summary jwt加入黑名单
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Success 200 {object} response.Response{msg=string} "jwt加入黑名单"
+// @Router /jwt/jsonInBlacklist [post]
+func (j *JwtApi) JsonInBlacklist(c *gin.Context) {
+ token := utils.GetToken(c)
+ jwt := system.JwtBlacklist{Jwt: token}
+ err := jwtService.JsonInBlacklist(jwt)
+ if err != nil {
+ global.GVA_LOG.Error("jwt作废失败!", zap.Error(err))
+ response.FailWithMessage("jwt作废失败", c)
+ return
+ }
+ utils.ClearToken(c)
+ response.OkWithMessage("jwt作废成功", c)
+}
diff --git a/server/api/v1/system/sys_login_log.go b/server/api/v1/system/sys_login_log.go
new file mode 100644
index 0000000..4579ebc
--- /dev/null
+++ b/server/api/v1/system/sys_login_log.go
@@ -0,0 +1,82 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common/request"
+ "git.echol.cn/loser/st/server/model/common/response"
+ "git.echol.cn/loser/st/server/model/system"
+ systemReq "git.echol.cn/loser/st/server/model/system/request"
+ "github.com/gin-gonic/gin"
+ "go.uber.org/zap"
+)
+
+type LoginLogApi struct{}
+
+func (s *LoginLogApi) DeleteLoginLog(c *gin.Context) {
+ var loginLog system.SysLoginLog
+ err := c.ShouldBindJSON(&loginLog)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = loginLogService.DeleteLoginLog(loginLog)
+ if err != nil {
+ global.GVA_LOG.Error("删除失败!", zap.Error(err))
+ response.FailWithMessage("删除失败", c)
+ return
+ }
+ response.OkWithMessage("删除成功", c)
+}
+
+func (s *LoginLogApi) DeleteLoginLogByIds(c *gin.Context) {
+ var SDS request.IdsReq
+ err := c.ShouldBindJSON(&SDS)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = loginLogService.DeleteLoginLogByIds(SDS)
+ if err != nil {
+ global.GVA_LOG.Error("批量删除失败!", zap.Error(err))
+ response.FailWithMessage("批量删除失败", c)
+ return
+ }
+ response.OkWithMessage("批量删除成功", c)
+}
+
+func (s *LoginLogApi) FindLoginLog(c *gin.Context) {
+ var loginLog system.SysLoginLog
+ err := c.ShouldBindQuery(&loginLog)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ reLoginLog, err := loginLogService.GetLoginLog(loginLog.ID)
+ if err != nil {
+ global.GVA_LOG.Error("查询失败!", zap.Error(err))
+ response.FailWithMessage("查询失败", c)
+ return
+ }
+ response.OkWithDetailed(reLoginLog, "查询成功", c)
+}
+
+func (s *LoginLogApi) GetLoginLogList(c *gin.Context) {
+ var pageInfo systemReq.SysLoginLogSearch
+ err := c.ShouldBindQuery(&pageInfo)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ list, total, err := loginLogService.GetLoginLogInfoList(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/server/api/v1/system/sys_menu.go b/server/api/v1/system/sys_menu.go
new file mode 100644
index 0000000..8d2f998
--- /dev/null
+++ b/server/api/v1/system/sys_menu.go
@@ -0,0 +1,265 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common/request"
+ "git.echol.cn/loser/st/server/model/common/response"
+ "git.echol.cn/loser/st/server/model/system"
+ systemReq "git.echol.cn/loser/st/server/model/system/request"
+ systemRes "git.echol.cn/loser/st/server/model/system/response"
+ "git.echol.cn/loser/st/server/utils"
+
+ "github.com/gin-gonic/gin"
+ "go.uber.org/zap"
+)
+
+type AuthorityMenuApi struct{}
+
+// GetMenu
+// @Tags AuthorityMenu
+// @Summary 获取用户动态路由
+// @Security ApiKeyAuth
+// @Produce application/json
+// @Param data body request.Empty true "空"
+// @Success 200 {object} response.Response{data=systemRes.SysMenusResponse,msg=string} "获取用户动态路由,返回包括系统菜单详情列表"
+// @Router /menu/getMenu [post]
+func (a *AuthorityMenuApi) GetMenu(c *gin.Context) {
+ menus, err := menuService.GetMenuTree(utils.GetUserAuthorityId(c))
+ if err != nil {
+ global.GVA_LOG.Error("获取失败!", zap.Error(err))
+ response.FailWithMessage("获取失败", c)
+ return
+ }
+ if menus == nil {
+ menus = []system.SysMenu{}
+ }
+ response.OkWithDetailed(systemRes.SysMenusResponse{Menus: menus}, "获取成功", c)
+}
+
+// GetBaseMenuTree
+// @Tags AuthorityMenu
+// @Summary 获取用户动态路由
+// @Security ApiKeyAuth
+// @Produce application/json
+// @Param data body request.Empty true "空"
+// @Success 200 {object} response.Response{data=systemRes.SysBaseMenusResponse,msg=string} "获取用户动态路由,返回包括系统菜单列表"
+// @Router /menu/getBaseMenuTree [post]
+func (a *AuthorityMenuApi) GetBaseMenuTree(c *gin.Context) {
+ authority := utils.GetUserAuthorityId(c)
+ menus, err := menuService.GetBaseMenuTree(authority)
+ if err != nil {
+ global.GVA_LOG.Error("获取失败!", zap.Error(err))
+ response.FailWithMessage("获取失败", c)
+ return
+ }
+ response.OkWithDetailed(systemRes.SysBaseMenusResponse{Menus: menus}, "获取成功", c)
+}
+
+// AddMenuAuthority
+// @Tags AuthorityMenu
+// @Summary 增加menu和角色关联关系
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body systemReq.AddMenuAuthorityInfo true "角色ID"
+// @Success 200 {object} response.Response{msg=string} "增加menu和角色关联关系"
+// @Router /menu/addMenuAuthority [post]
+func (a *AuthorityMenuApi) AddMenuAuthority(c *gin.Context) {
+ var authorityMenu systemReq.AddMenuAuthorityInfo
+ err := c.ShouldBindJSON(&authorityMenu)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ if err := utils.Verify(authorityMenu, utils.AuthorityIdVerify); err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ adminAuthorityID := utils.GetUserAuthorityId(c)
+ if err := menuService.AddMenuAuthority(authorityMenu.Menus, adminAuthorityID, authorityMenu.AuthorityId); err != nil {
+ global.GVA_LOG.Error("添加失败!", zap.Error(err))
+ response.FailWithMessage("添加失败", c)
+ } else {
+ response.OkWithMessage("添加成功", c)
+ }
+}
+
+// GetMenuAuthority
+// @Tags AuthorityMenu
+// @Summary 获取指定角色menu
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body request.GetAuthorityId true "角色ID"
+// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "获取指定角色menu"
+// @Router /menu/getMenuAuthority [post]
+func (a *AuthorityMenuApi) GetMenuAuthority(c *gin.Context) {
+ var param request.GetAuthorityId
+ err := c.ShouldBindJSON(¶m)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = utils.Verify(param, utils.AuthorityIdVerify)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ menus, err := menuService.GetMenuAuthority(¶m)
+ if err != nil {
+ global.GVA_LOG.Error("获取失败!", zap.Error(err))
+ response.FailWithDetailed(systemRes.SysMenusResponse{Menus: menus}, "获取失败", c)
+ return
+ }
+ response.OkWithDetailed(gin.H{"menus": menus}, "获取成功", c)
+}
+
+// AddBaseMenu
+// @Tags Menu
+// @Summary 新增菜单
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body system.SysBaseMenu true "路由path, 父菜单ID, 路由name, 对应前端文件路径, 排序标记"
+// @Success 200 {object} response.Response{msg=string} "新增菜单"
+// @Router /menu/addBaseMenu [post]
+func (a *AuthorityMenuApi) AddBaseMenu(c *gin.Context) {
+ var menu system.SysBaseMenu
+ err := c.ShouldBindJSON(&menu)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = utils.Verify(menu, utils.MenuVerify)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = utils.Verify(menu.Meta, utils.MenuMetaVerify)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = menuService.AddBaseMenu(menu)
+ if err != nil {
+ global.GVA_LOG.Error("添加失败!", zap.Error(err))
+ response.FailWithMessage("添加失败:"+err.Error(), 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/server/api/v1/system/sys_operation_record.go b/server/api/v1/system/sys_operation_record.go
new file mode 100644
index 0000000..44058fe
--- /dev/null
+++ b/server/api/v1/system/sys_operation_record.go
@@ -0,0 +1,124 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common/request"
+ "git.echol.cn/loser/st/server/model/common/response"
+ "git.echol.cn/loser/st/server/model/system"
+ systemReq "git.echol.cn/loser/st/server/model/system/request"
+ "git.echol.cn/loser/st/server/utils"
+ "github.com/gin-gonic/gin"
+ "go.uber.org/zap"
+)
+
+type OperationRecordApi struct{}
+
+// 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/server/api/v1/system/sys_params.go b/server/api/v1/system/sys_params.go
new file mode 100644
index 0000000..b565217
--- /dev/null
+++ b/server/api/v1/system/sys_params.go
@@ -0,0 +1,171 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common/response"
+ "git.echol.cn/loser/st/server/model/system"
+ systemReq "git.echol.cn/loser/st/server/model/system/request"
+ "github.com/gin-gonic/gin"
+ "go.uber.org/zap"
+)
+
+type SysParamsApi struct{}
+
+// CreateSysParams 创建参数
+// @Tags SysParams
+// @Summary 创建参数
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body system.SysParams true "创建参数"
+// @Success 200 {object} response.Response{msg=string} "创建成功"
+// @Router /sysParams/createSysParams [post]
+func (sysParamsApi *SysParamsApi) CreateSysParams(c *gin.Context) {
+ var sysParams system.SysParams
+ err := c.ShouldBindJSON(&sysParams)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = sysParamsService.CreateSysParams(&sysParams)
+ if err != nil {
+ global.GVA_LOG.Error("创建失败!", zap.Error(err))
+ response.FailWithMessage("创建失败:"+err.Error(), c)
+ return
+ }
+ response.OkWithMessage("创建成功", c)
+}
+
+// DeleteSysParams 删除参数
+// @Tags SysParams
+// @Summary 删除参数
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body system.SysParams true "删除参数"
+// @Success 200 {object} response.Response{msg=string} "删除成功"
+// @Router /sysParams/deleteSysParams [delete]
+func (sysParamsApi *SysParamsApi) DeleteSysParams(c *gin.Context) {
+ ID := c.Query("ID")
+ err := sysParamsService.DeleteSysParams(ID)
+ if err != nil {
+ global.GVA_LOG.Error("删除失败!", zap.Error(err))
+ response.FailWithMessage("删除失败:"+err.Error(), c)
+ return
+ }
+ response.OkWithMessage("删除成功", c)
+}
+
+// DeleteSysParamsByIds 批量删除参数
+// @Tags SysParams
+// @Summary 批量删除参数
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Success 200 {object} response.Response{msg=string} "批量删除成功"
+// @Router /sysParams/deleteSysParamsByIds [delete]
+func (sysParamsApi *SysParamsApi) DeleteSysParamsByIds(c *gin.Context) {
+ IDs := c.QueryArray("IDs[]")
+ err := sysParamsService.DeleteSysParamsByIds(IDs)
+ if err != nil {
+ global.GVA_LOG.Error("批量删除失败!", zap.Error(err))
+ response.FailWithMessage("批量删除失败:"+err.Error(), c)
+ return
+ }
+ response.OkWithMessage("批量删除成功", c)
+}
+
+// UpdateSysParams 更新参数
+// @Tags SysParams
+// @Summary 更新参数
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body system.SysParams true "更新参数"
+// @Success 200 {object} response.Response{msg=string} "更新成功"
+// @Router /sysParams/updateSysParams [put]
+func (sysParamsApi *SysParamsApi) UpdateSysParams(c *gin.Context) {
+ var sysParams system.SysParams
+ err := c.ShouldBindJSON(&sysParams)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = sysParamsService.UpdateSysParams(sysParams)
+ if err != nil {
+ global.GVA_LOG.Error("更新失败!", zap.Error(err))
+ response.FailWithMessage("更新失败:"+err.Error(), c)
+ return
+ }
+ response.OkWithMessage("更新成功", c)
+}
+
+// FindSysParams 用id查询参数
+// @Tags SysParams
+// @Summary 用id查询参数
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data query system.SysParams true "用id查询参数"
+// @Success 200 {object} response.Response{data=system.SysParams,msg=string} "查询成功"
+// @Router /sysParams/findSysParams [get]
+func (sysParamsApi *SysParamsApi) FindSysParams(c *gin.Context) {
+ ID := c.Query("ID")
+ resysParams, err := sysParamsService.GetSysParams(ID)
+ if err != nil {
+ global.GVA_LOG.Error("查询失败!", zap.Error(err))
+ response.FailWithMessage("查询失败:"+err.Error(), c)
+ return
+ }
+ response.OkWithData(resysParams, c)
+}
+
+// GetSysParamsList 分页获取参数列表
+// @Tags SysParams
+// @Summary 分页获取参数列表
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data query systemReq.SysParamsSearch true "分页获取参数列表"
+// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功"
+// @Router /sysParams/getSysParamsList [get]
+func (sysParamsApi *SysParamsApi) GetSysParamsList(c *gin.Context) {
+ var pageInfo systemReq.SysParamsSearch
+ err := c.ShouldBindQuery(&pageInfo)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ list, total, err := sysParamsService.GetSysParamsInfoList(pageInfo)
+ if err != nil {
+ global.GVA_LOG.Error("获取失败!", zap.Error(err))
+ response.FailWithMessage("获取失败:"+err.Error(), c)
+ return
+ }
+ response.OkWithDetailed(response.PageResult{
+ List: list,
+ Total: total,
+ Page: pageInfo.Page,
+ PageSize: pageInfo.PageSize,
+ }, "获取成功", c)
+}
+
+// GetSysParam 根据key获取参数value
+// @Tags SysParams
+// @Summary 根据key获取参数value
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param key query string true "key"
+// @Success 200 {object} response.Response{data=system.SysParams,msg=string} "获取成功"
+// @Router /sysParams/getSysParam [get]
+func (sysParamsApi *SysParamsApi) GetSysParam(c *gin.Context) {
+ k := c.Query("key")
+ params, err := sysParamsService.GetSysParam(k)
+ if err != nil {
+ global.GVA_LOG.Error("获取失败!", zap.Error(err))
+ response.FailWithMessage("获取失败:"+err.Error(), c)
+ return
+ }
+ response.OkWithDetailed(params, "获取成功", c)
+}
diff --git a/server/api/v1/system/sys_skills.go b/server/api/v1/system/sys_skills.go
new file mode 100644
index 0000000..167dec8
--- /dev/null
+++ b/server/api/v1/system/sys_skills.go
@@ -0,0 +1,219 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common/response"
+ "git.echol.cn/loser/st/server/model/system/request"
+ "github.com/gin-gonic/gin"
+ "go.uber.org/zap"
+)
+
+type SkillsApi struct{}
+
+func (s *SkillsApi) GetTools(c *gin.Context) {
+ data, err := skillsService.Tools(c.Request.Context())
+ if err != nil {
+ global.GVA_LOG.Error("获取工具列表失败", zap.Error(err))
+ response.FailWithMessage("获取工具列表失败", c)
+ return
+ }
+ response.OkWithDetailed(gin.H{"tools": data}, "获取成功", c)
+}
+
+func (s *SkillsApi) GetSkillList(c *gin.Context) {
+ var req request.SkillToolRequest
+ _ = c.ShouldBindJSON(&req)
+ data, err := skillsService.List(c.Request.Context(), req.Tool)
+ if err != nil {
+ global.GVA_LOG.Error("获取技能列表失败", zap.Error(err))
+ response.FailWithMessage("获取技能列表失败", c)
+ return
+ }
+ response.OkWithDetailed(gin.H{"skills": data}, "获取成功", c)
+}
+
+func (s *SkillsApi) GetSkillDetail(c *gin.Context) {
+ var req request.SkillDetailRequest
+ _ = c.ShouldBindJSON(&req)
+ data, err := skillsService.Detail(c.Request.Context(), req.Tool, req.Skill)
+ if err != nil {
+ global.GVA_LOG.Error("获取技能详情失败", zap.Error(err))
+ response.FailWithMessage("获取技能详情失败", c)
+ return
+ }
+ response.OkWithDetailed(gin.H{"detail": data}, "获取成功", c)
+}
+
+func (s *SkillsApi) SaveSkill(c *gin.Context) {
+ var req request.SkillSaveRequest
+ _ = c.ShouldBindJSON(&req)
+ if err := skillsService.Save(c.Request.Context(), req); err != nil {
+ global.GVA_LOG.Error("保存技能失败", zap.Error(err))
+ response.FailWithMessage("保存技能失败", c)
+ return
+ }
+ response.OkWithMessage("保存成功", c)
+}
+
+func (s *SkillsApi) CreateScript(c *gin.Context) {
+ var req request.SkillScriptCreateRequest
+ _ = c.ShouldBindJSON(&req)
+ fileName, content, err := skillsService.CreateScript(c.Request.Context(), req)
+ if err != nil {
+ global.GVA_LOG.Error("创建脚本失败", zap.Error(err))
+ response.FailWithMessage("创建脚本失败", c)
+ return
+ }
+ response.OkWithDetailed(gin.H{"fileName": fileName, "content": content}, "创建成功", c)
+}
+
+func (s *SkillsApi) GetScript(c *gin.Context) {
+ var req request.SkillFileRequest
+ _ = c.ShouldBindJSON(&req)
+ content, err := skillsService.GetScript(c.Request.Context(), req)
+ if err != nil {
+ global.GVA_LOG.Error("读取脚本失败", zap.Error(err))
+ response.FailWithMessage("读取脚本失败", c)
+ return
+ }
+ response.OkWithDetailed(gin.H{"content": content}, "获取成功", c)
+}
+
+func (s *SkillsApi) SaveScript(c *gin.Context) {
+ var req request.SkillFileSaveRequest
+ _ = c.ShouldBindJSON(&req)
+ if err := skillsService.SaveScript(c.Request.Context(), req); err != nil {
+ global.GVA_LOG.Error("保存脚本失败", zap.Error(err))
+ response.FailWithMessage("保存脚本失败", c)
+ return
+ }
+ response.OkWithMessage("保存成功", c)
+}
+
+func (s *SkillsApi) CreateResource(c *gin.Context) {
+ var req request.SkillResourceCreateRequest
+ _ = c.ShouldBindJSON(&req)
+ fileName, content, err := skillsService.CreateResource(c.Request.Context(), req)
+ if err != nil {
+ global.GVA_LOG.Error("创建资源失败", zap.Error(err))
+ response.FailWithMessage("创建资源失败", c)
+ return
+ }
+ response.OkWithDetailed(gin.H{"fileName": fileName, "content": content}, "创建成功", c)
+}
+
+func (s *SkillsApi) GetResource(c *gin.Context) {
+ var req request.SkillFileRequest
+ _ = c.ShouldBindJSON(&req)
+ content, err := skillsService.GetResource(c.Request.Context(), req)
+ if err != nil {
+ global.GVA_LOG.Error("读取资源失败", zap.Error(err))
+ response.FailWithMessage("读取资源失败", c)
+ return
+ }
+ response.OkWithDetailed(gin.H{"content": content}, "获取成功", c)
+}
+
+func (s *SkillsApi) SaveResource(c *gin.Context) {
+ var req request.SkillFileSaveRequest
+ _ = c.ShouldBindJSON(&req)
+ if err := skillsService.SaveResource(c.Request.Context(), req); err != nil {
+ global.GVA_LOG.Error("保存资源失败", zap.Error(err))
+ response.FailWithMessage("保存资源失败", c)
+ return
+ }
+ response.OkWithMessage("保存成功", c)
+}
+
+func (s *SkillsApi) CreateReference(c *gin.Context) {
+ var req request.SkillReferenceCreateRequest
+ _ = c.ShouldBindJSON(&req)
+ fileName, content, err := skillsService.CreateReference(c.Request.Context(), req)
+ if err != nil {
+ global.GVA_LOG.Error("创建参考失败", zap.Error(err))
+ response.FailWithMessage("创建参考失败", c)
+ return
+ }
+ response.OkWithDetailed(gin.H{"fileName": fileName, "content": content}, "创建成功", c)
+}
+
+func (s *SkillsApi) GetReference(c *gin.Context) {
+ var req request.SkillFileRequest
+ _ = c.ShouldBindJSON(&req)
+ content, err := skillsService.GetReference(c.Request.Context(), req)
+ if err != nil {
+ global.GVA_LOG.Error("读取参考失败", zap.Error(err))
+ response.FailWithMessage("读取参考失败", c)
+ return
+ }
+ response.OkWithDetailed(gin.H{"content": content}, "获取成功", c)
+}
+
+func (s *SkillsApi) SaveReference(c *gin.Context) {
+ var req request.SkillFileSaveRequest
+ _ = c.ShouldBindJSON(&req)
+ if err := skillsService.SaveReference(c.Request.Context(), req); err != nil {
+ global.GVA_LOG.Error("保存参考失败", zap.Error(err))
+ response.FailWithMessage("保存参考失败", c)
+ return
+ }
+ response.OkWithMessage("保存成功", c)
+}
+
+func (s *SkillsApi) CreateTemplate(c *gin.Context) {
+ var req request.SkillTemplateCreateRequest
+ _ = c.ShouldBindJSON(&req)
+ fileName, content, err := skillsService.CreateTemplate(c.Request.Context(), req)
+ if err != nil {
+ global.GVA_LOG.Error("创建模板失败", zap.Error(err))
+ response.FailWithMessage("创建模板失败", c)
+ return
+ }
+ response.OkWithDetailed(gin.H{"fileName": fileName, "content": content}, "创建成功", c)
+}
+
+func (s *SkillsApi) GetTemplate(c *gin.Context) {
+ var req request.SkillFileRequest
+ _ = c.ShouldBindJSON(&req)
+ content, err := skillsService.GetTemplate(c.Request.Context(), req)
+ if err != nil {
+ global.GVA_LOG.Error("读取模板失败", zap.Error(err))
+ response.FailWithMessage("读取模板失败", c)
+ return
+ }
+ response.OkWithDetailed(gin.H{"content": content}, "获取成功", c)
+}
+
+func (s *SkillsApi) SaveTemplate(c *gin.Context) {
+ var req request.SkillFileSaveRequest
+ _ = c.ShouldBindJSON(&req)
+ if err := skillsService.SaveTemplate(c.Request.Context(), req); err != nil {
+ global.GVA_LOG.Error("保存模板失败", zap.Error(err))
+ response.FailWithMessage("保存模板失败", c)
+ return
+ }
+ response.OkWithMessage("保存成功", c)
+}
+
+func (s *SkillsApi) GetGlobalConstraint(c *gin.Context) {
+ var req request.SkillToolRequest
+ _ = c.ShouldBindJSON(&req)
+ content, exists, err := skillsService.GetGlobalConstraint(c.Request.Context(), req.Tool)
+ if err != nil {
+ global.GVA_LOG.Error("读取全局约束失败", zap.Error(err))
+ response.FailWithMessage("读取全局约束失败", c)
+ return
+ }
+ response.OkWithDetailed(gin.H{"content": content, "exists": exists}, "获取成功", c)
+}
+
+func (s *SkillsApi) SaveGlobalConstraint(c *gin.Context) {
+ var req request.SkillGlobalConstraintSaveRequest
+ _ = c.ShouldBindJSON(&req)
+ if err := skillsService.SaveGlobalConstraint(c.Request.Context(), req); err != nil {
+ global.GVA_LOG.Error("保存全局约束失败", zap.Error(err))
+ response.FailWithMessage("保存全局约束失败", c)
+ return
+ }
+ response.OkWithMessage("保存成功", c)
+}
diff --git a/server/api/v1/system/sys_system.go b/server/api/v1/system/sys_system.go
new file mode 100644
index 0000000..df1add4
--- /dev/null
+++ b/server/api/v1/system/sys_system.go
@@ -0,0 +1,89 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common/response"
+ "git.echol.cn/loser/st/server/model/system"
+ systemRes "git.echol.cn/loser/st/server/model/system/response"
+ "git.echol.cn/loser/st/server/utils"
+ "github.com/gin-gonic/gin"
+ "go.uber.org/zap"
+)
+
+type SystemApi struct{}
+
+// GetSystemConfig
+// @Tags System
+// @Summary 获取配置文件内容
+// @Security ApiKeyAuth
+// @Produce application/json
+// @Success 200 {object} response.Response{data=systemRes.SysConfigResponse,msg=string} "获取配置文件内容,返回包括系统配置"
+// @Router /system/getSystemConfig [post]
+func (s *SystemApi) GetSystemConfig(c *gin.Context) {
+ config, err := systemConfigService.GetSystemConfig()
+ if err != nil {
+ global.GVA_LOG.Error("获取失败!", zap.Error(err))
+ response.FailWithMessage("获取失败", c)
+ return
+ }
+ response.OkWithDetailed(systemRes.SysConfigResponse{Config: config}, "获取成功", c)
+}
+
+// SetSystemConfig
+// @Tags System
+// @Summary 设置配置文件内容
+// @Security ApiKeyAuth
+// @Produce application/json
+// @Param data body system.System true "设置配置文件内容"
+// @Success 200 {object} response.Response{data=string} "设置配置文件内容"
+// @Router /system/setSystemConfig [post]
+func (s *SystemApi) SetSystemConfig(c *gin.Context) {
+ var sys system.System
+ err := c.ShouldBindJSON(&sys)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = systemConfigService.SetSystemConfig(sys)
+ if err != nil {
+ global.GVA_LOG.Error("设置失败!", zap.Error(err))
+ response.FailWithMessage("设置失败", c)
+ return
+ }
+ response.OkWithMessage("设置成功", c)
+}
+
+// ReloadSystem
+// @Tags System
+// @Summary 重载系统
+// @Security ApiKeyAuth
+// @Produce application/json
+// @Success 200 {object} response.Response{msg=string} "重载系统"
+// @Router /system/reloadSystem [post]
+func (s *SystemApi) ReloadSystem(c *gin.Context) {
+ // 触发系统重载事件
+ err := utils.GlobalSystemEvents.TriggerReload()
+ if err != nil {
+ global.GVA_LOG.Error("重载系统失败!", zap.Error(err))
+ response.FailWithMessage("重载系统失败:"+err.Error(), 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/server/api/v1/system/sys_user.go b/server/api/v1/system/sys_user.go
new file mode 100644
index 0000000..be738b0
--- /dev/null
+++ b/server/api/v1/system/sys_user.go
@@ -0,0 +1,516 @@
+package system
+
+import (
+ "strconv"
+ "time"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common"
+ "git.echol.cn/loser/st/server/model/common/request"
+ "git.echol.cn/loser/st/server/model/common/response"
+ "git.echol.cn/loser/st/server/model/system"
+ systemReq "git.echol.cn/loser/st/server/model/system/request"
+ systemRes "git.echol.cn/loser/st/server/model/system/response"
+ "git.echol.cn/loser/st/server/utils"
+ "github.com/gin-gonic/gin"
+ "github.com/redis/go-redis/v9"
+ "go.uber.org/zap"
+)
+
+// Login
+// @Tags Base
+// @Summary 用户登录
+// @Produce application/json
+// @Param data body systemReq.Login true "用户名, 密码, 验证码"
+// @Success 200 {object} response.Response{data=systemRes.LoginResponse,msg=string} "返回包括用户信息,token,过期时间"
+// @Router /base/login [post]
+func (b *BaseApi) Login(c *gin.Context) {
+ var l systemReq.Login
+ err := c.ShouldBindJSON(&l)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = utils.Verify(l, utils.LoginVerify)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ key := c.ClientIP()
+ // 判断验证码是否开启
+ 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.Captcha == "" || l.CaptchaId == "" || !store.Verify(l.CaptchaId, l.Captcha, true)) {
+ // 验证码次数+1
+ global.BlackCache.Increment(key, 1)
+ response.FailWithMessage("验证码错误", c)
+ // 记录登录失败日志
+ loginLogService.CreateLoginLog(system.SysLoginLog{
+ Username: l.Username,
+ Ip: c.ClientIP(),
+ Agent: c.Request.UserAgent(),
+ Status: false,
+ ErrorMessage: "验证码错误",
+ })
+ return
+ }
+
+ 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)
+ // 记录登录失败日志
+ loginLogService.CreateLoginLog(system.SysLoginLog{
+ Username: l.Username,
+ Ip: c.ClientIP(),
+ Agent: c.Request.UserAgent(),
+ Status: false,
+ ErrorMessage: "用户名不存在或者密码错误",
+ })
+ return
+ }
+ if user.Enable != 1 {
+ global.GVA_LOG.Error("登陆失败! 用户被禁止登录!")
+ // 验证码次数+1
+ global.BlackCache.Increment(key, 1)
+ response.FailWithMessage("用户被禁止登录", c)
+ // 记录登录失败日志
+ loginLogService.CreateLoginLog(system.SysLoginLog{
+ Username: l.Username,
+ Ip: c.ClientIP(),
+ Agent: c.Request.UserAgent(),
+ Status: false,
+ ErrorMessage: "用户被禁止登录",
+ UserID: user.ID,
+ })
+ return
+ }
+ b.TokenNext(c, *user)
+}
+
+// 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
+ }
+ // 记录登录成功日志
+ loginLogService.CreateLoginLog(system.SysLoginLog{
+ Username: user.Username,
+ Ip: c.ClientIP(),
+ Agent: c.Request.UserAgent(),
+ Status: true,
+ UserID: user.ID,
+ ErrorMessage: "登录成功",
+ })
+ 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 := utils.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 := utils.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()))
+ 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 rps systemReq.ResetPassword
+ err := c.ShouldBindJSON(&rps)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = userService.ResetPassword(rps.ID, rps.Password)
+ if err != nil {
+ global.GVA_LOG.Error("重置失败!", zap.Error(err))
+ response.FailWithMessage("重置失败"+err.Error(), c)
+ return
+ }
+ response.OkWithMessage("重置成功", c)
+}
diff --git a/server/api/v1/system/sys_version.go b/server/api/v1/system/sys_version.go
new file mode 100644
index 0000000..1371891
--- /dev/null
+++ b/server/api/v1/system/sys_version.go
@@ -0,0 +1,486 @@
+package system
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "sort"
+ "strconv"
+ "time"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common/response"
+ "git.echol.cn/loser/st/server/model/system"
+ systemReq "git.echol.cn/loser/st/server/model/system/request"
+ systemRes "git.echol.cn/loser/st/server/model/system/response"
+ "git.echol.cn/loser/st/server/utils"
+ "github.com/gin-gonic/gin"
+ "go.uber.org/zap"
+)
+
+type SysVersionApi struct{}
+
+// buildMenuTree 构建菜单树结构
+func buildMenuTree(menus []system.SysBaseMenu) []system.SysBaseMenu {
+ // 创建菜单映射
+ menuMap := make(map[uint]*system.SysBaseMenu)
+ for i := range menus {
+ menuMap[menus[i].ID] = &menus[i]
+ }
+
+ // 构建树结构
+ var rootMenus []system.SysBaseMenu
+ for _, menu := range menus {
+ if menu.ParentId == 0 {
+ // 根菜单
+ menuData := convertMenuToStruct(menu, menuMap)
+ rootMenus = append(rootMenus, menuData)
+ }
+ }
+
+ // 按sort排序根菜单
+ sort.Slice(rootMenus, func(i, j int) bool {
+ return rootMenus[i].Sort < rootMenus[j].Sort
+ })
+
+ return rootMenus
+}
+
+// convertMenuToStruct 将菜单转换为结构体并递归处理子菜单
+func convertMenuToStruct(menu system.SysBaseMenu, menuMap map[uint]*system.SysBaseMenu) system.SysBaseMenu {
+ result := system.SysBaseMenu{
+ Path: menu.Path,
+ Name: menu.Name,
+ Hidden: menu.Hidden,
+ Component: menu.Component,
+ Sort: menu.Sort,
+ Meta: menu.Meta,
+ }
+
+ // 清理并复制参数数据
+ if len(menu.Parameters) > 0 {
+ cleanParameters := make([]system.SysBaseMenuParameter, 0, len(menu.Parameters))
+ for _, param := range menu.Parameters {
+ cleanParam := system.SysBaseMenuParameter{
+ Type: param.Type,
+ Key: param.Key,
+ Value: param.Value,
+ // 不复制 ID, CreatedAt, UpdatedAt, SysBaseMenuID
+ }
+ cleanParameters = append(cleanParameters, cleanParam)
+ }
+ result.Parameters = cleanParameters
+ }
+
+ // 清理并复制菜单按钮数据
+ if len(menu.MenuBtn) > 0 {
+ cleanMenuBtns := make([]system.SysBaseMenuBtn, 0, len(menu.MenuBtn))
+ for _, btn := range menu.MenuBtn {
+ cleanBtn := system.SysBaseMenuBtn{
+ Name: btn.Name,
+ Desc: btn.Desc,
+ // 不复制 ID, CreatedAt, UpdatedAt, SysBaseMenuID
+ }
+ cleanMenuBtns = append(cleanMenuBtns, cleanBtn)
+ }
+ result.MenuBtn = cleanMenuBtns
+ }
+
+ // 查找并处理子菜单
+ var children []system.SysBaseMenu
+ for _, childMenu := range menuMap {
+ if childMenu.ParentId == menu.ID {
+ childData := convertMenuToStruct(*childMenu, menuMap)
+ children = append(children, childData)
+ }
+ }
+
+ // 按sort排序子菜单
+ if len(children) > 0 {
+ sort.Slice(children, func(i, j int) bool {
+ return children[i].Sort < children[j].Sort
+ })
+ result.Children = children
+ }
+
+ return result
+}
+
+// DeleteSysVersion 删除版本管理
+// @Tags SysVersion
+// @Summary 删除版本管理
+// @Security ApiKeyAuth
+// @Accept application/json
+// @Produce application/json
+// @Param data body system.SysVersion true "删除版本管理"
+// @Success 200 {object} response.Response{msg=string} "删除成功"
+// @Router /sysVersion/deleteSysVersion [delete]
+func (sysVersionApi *SysVersionApi) DeleteSysVersion(c *gin.Context) {
+ // 创建业务用Context
+ ctx := c.Request.Context()
+
+ ID := c.Query("ID")
+ err := sysVersionService.DeleteSysVersion(ctx, ID)
+ if err != nil {
+ global.GVA_LOG.Error("删除失败!", zap.Error(err))
+ response.FailWithMessage("删除失败:"+err.Error(), c)
+ return
+ }
+ response.OkWithMessage("删除成功", c)
+}
+
+// DeleteSysVersionByIds 批量删除版本管理
+// @Tags SysVersion
+// @Summary 批量删除版本管理
+// @Security ApiKeyAuth
+// @Accept application/json
+// @Produce application/json
+// @Success 200 {object} response.Response{msg=string} "批量删除成功"
+// @Router /sysVersion/deleteSysVersionByIds [delete]
+func (sysVersionApi *SysVersionApi) DeleteSysVersionByIds(c *gin.Context) {
+ // 创建业务用Context
+ ctx := c.Request.Context()
+
+ IDs := c.QueryArray("IDs[]")
+ err := sysVersionService.DeleteSysVersionByIds(ctx, IDs)
+ if err != nil {
+ global.GVA_LOG.Error("批量删除失败!", zap.Error(err))
+ response.FailWithMessage("批量删除失败:"+err.Error(), c)
+ return
+ }
+ response.OkWithMessage("批量删除成功", c)
+}
+
+// FindSysVersion 用id查询版本管理
+// @Tags SysVersion
+// @Summary 用id查询版本管理
+// @Security ApiKeyAuth
+// @Accept application/json
+// @Produce application/json
+// @Param ID query uint true "用id查询版本管理"
+// @Success 200 {object} response.Response{data=system.SysVersion,msg=string} "查询成功"
+// @Router /sysVersion/findSysVersion [get]
+func (sysVersionApi *SysVersionApi) FindSysVersion(c *gin.Context) {
+ // 创建业务用Context
+ ctx := c.Request.Context()
+
+ ID := c.Query("ID")
+ resysVersion, err := sysVersionService.GetSysVersion(ctx, ID)
+ if err != nil {
+ global.GVA_LOG.Error("查询失败!", zap.Error(err))
+ response.FailWithMessage("查询失败:"+err.Error(), c)
+ return
+ }
+ response.OkWithData(resysVersion, c)
+}
+
+// GetSysVersionList 分页获取版本管理列表
+// @Tags SysVersion
+// @Summary 分页获取版本管理列表
+// @Security ApiKeyAuth
+// @Accept application/json
+// @Produce application/json
+// @Param data query systemReq.SysVersionSearch true "分页获取版本管理列表"
+// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功"
+// @Router /sysVersion/getSysVersionList [get]
+func (sysVersionApi *SysVersionApi) GetSysVersionList(c *gin.Context) {
+ // 创建业务用Context
+ ctx := c.Request.Context()
+
+ var pageInfo systemReq.SysVersionSearch
+ err := c.ShouldBindQuery(&pageInfo)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ list, total, err := sysVersionService.GetSysVersionInfoList(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)
+}
+
+// GetSysVersionPublic 不需要鉴权的版本管理接口
+// @Tags SysVersion
+// @Summary 不需要鉴权的版本管理接口
+// @Accept application/json
+// @Produce application/json
+// @Success 200 {object} response.Response{data=object,msg=string} "获取成功"
+// @Router /sysVersion/getSysVersionPublic [get]
+func (sysVersionApi *SysVersionApi) GetSysVersionPublic(c *gin.Context) {
+ // 创建业务用Context
+ ctx := c.Request.Context()
+
+ // 此接口不需要鉴权
+ // 示例为返回了一个固定的消息接口,一般本接口用于C端服务,需要自己实现业务逻辑
+ sysVersionService.GetSysVersionPublic(ctx)
+ response.OkWithDetailed(gin.H{
+ "info": "不需要鉴权的版本管理接口信息",
+ }, "获取成功", c)
+}
+
+// ExportVersion 创建发版数据
+// @Tags SysVersion
+// @Summary 创建发版数据
+// @Security ApiKeyAuth
+// @Accept application/json
+// @Produce application/json
+// @Param data body systemReq.ExportVersionRequest true "创建发版数据"
+// @Success 200 {object} response.Response{msg=string} "创建成功"
+// @Router /sysVersion/exportVersion [post]
+func (sysVersionApi *SysVersionApi) ExportVersion(c *gin.Context) {
+ ctx := c.Request.Context()
+
+ var req systemReq.ExportVersionRequest
+ err := c.ShouldBindJSON(&req)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+
+ // 获取选中的菜单数据
+ var menuData []system.SysBaseMenu
+ if len(req.MenuIds) > 0 {
+ menuData, err = sysVersionService.GetMenusByIds(ctx, req.MenuIds)
+ if err != nil {
+ global.GVA_LOG.Error("获取菜单数据失败!", zap.Error(err))
+ response.FailWithMessage("获取菜单数据失败:"+err.Error(), c)
+ return
+ }
+ }
+
+ // 获取选中的API数据
+ var apiData []system.SysApi
+ if len(req.ApiIds) > 0 {
+ apiData, err = sysVersionService.GetApisByIds(ctx, req.ApiIds)
+ if err != nil {
+ global.GVA_LOG.Error("获取API数据失败!", zap.Error(err))
+ response.FailWithMessage("获取API数据失败:"+err.Error(), c)
+ return
+ }
+ }
+
+ // 获取选中的字典数据
+ var dictData []system.SysDictionary
+ if len(req.DictIds) > 0 {
+ dictData, err = sysVersionService.GetDictionariesByIds(ctx, req.DictIds)
+ if err != nil {
+ global.GVA_LOG.Error("获取字典数据失败!", zap.Error(err))
+ response.FailWithMessage("获取字典数据失败:"+err.Error(), c)
+ return
+ }
+ }
+
+ // 处理菜单数据,构建递归的children结构
+ processedMenus := buildMenuTree(menuData)
+
+ // 处理API数据,清除ID和时间戳字段
+ processedApis := make([]system.SysApi, 0, len(apiData))
+ for _, api := range apiData {
+ cleanApi := system.SysApi{
+ Path: api.Path,
+ Description: api.Description,
+ ApiGroup: api.ApiGroup,
+ Method: api.Method,
+ }
+ processedApis = append(processedApis, cleanApi)
+ }
+
+ // 处理字典数据,清除ID和时间戳字段,包含字典详情
+ processedDicts := make([]system.SysDictionary, 0, len(dictData))
+ for _, dict := range dictData {
+ cleanDict := system.SysDictionary{
+ Name: dict.Name,
+ Type: dict.Type,
+ Status: dict.Status,
+ Desc: dict.Desc,
+ }
+
+ // 处理字典详情数据,清除ID和时间戳字段
+ cleanDetails := make([]system.SysDictionaryDetail, 0, len(dict.SysDictionaryDetails))
+ for _, detail := range dict.SysDictionaryDetails {
+ cleanDetail := system.SysDictionaryDetail{
+ Label: detail.Label,
+ Value: detail.Value,
+ Extend: detail.Extend,
+ Status: detail.Status,
+ Sort: detail.Sort,
+ // 不复制 ID, CreatedAt, UpdatedAt, SysDictionaryID
+ }
+ cleanDetails = append(cleanDetails, cleanDetail)
+ }
+ cleanDict.SysDictionaryDetails = cleanDetails
+
+ processedDicts = append(processedDicts, cleanDict)
+ }
+
+ // 构建导出数据
+ exportData := systemRes.ExportVersionResponse{
+ Version: systemReq.VersionInfo{
+ Name: req.VersionName,
+ Code: req.VersionCode,
+ Description: req.Description,
+ ExportTime: time.Now().Format("2006-01-02 15:04:05"),
+ },
+ Menus: processedMenus,
+ Apis: processedApis,
+ Dictionaries: processedDicts,
+ }
+
+ // 转换为JSON
+ jsonData, err := json.MarshalIndent(exportData, "", " ")
+ if err != nil {
+ global.GVA_LOG.Error("JSON序列化失败!", zap.Error(err))
+ response.FailWithMessage("JSON序列化失败:"+err.Error(), c)
+ return
+ }
+
+ // 保存版本记录
+ version := system.SysVersion{
+ VersionName: utils.Pointer(req.VersionName),
+ VersionCode: utils.Pointer(req.VersionCode),
+ Description: utils.Pointer(req.Description),
+ VersionData: utils.Pointer(string(jsonData)),
+ }
+
+ err = sysVersionService.CreateSysVersion(ctx, &version)
+ if err != nil {
+ global.GVA_LOG.Error("保存版本记录失败!", zap.Error(err))
+ response.FailWithMessage("保存版本记录失败:"+err.Error(), c)
+ return
+ }
+
+ response.OkWithMessage("创建发版成功", c)
+}
+
+// DownloadVersionJson 下载版本JSON数据
+// @Tags SysVersion
+// @Summary 下载版本JSON数据
+// @Security ApiKeyAuth
+// @Accept application/json
+// @Produce application/json
+// @Param ID query string true "版本ID"
+// @Success 200 {object} response.Response{data=object,msg=string} "下载成功"
+// @Router /sysVersion/downloadVersionJson [get]
+func (sysVersionApi *SysVersionApi) DownloadVersionJson(c *gin.Context) {
+ ctx := c.Request.Context()
+
+ ID := c.Query("ID")
+ if ID == "" {
+ response.FailWithMessage("版本ID不能为空", c)
+ return
+ }
+
+ // 获取版本记录
+ version, err := sysVersionService.GetSysVersion(ctx, ID)
+ if err != nil {
+ global.GVA_LOG.Error("获取版本记录失败!", zap.Error(err))
+ response.FailWithMessage("获取版本记录失败:"+err.Error(), c)
+ return
+ }
+
+ // 构建JSON数据
+ var jsonData []byte
+ if version.VersionData != nil && *version.VersionData != "" {
+ jsonData = []byte(*version.VersionData)
+ } else {
+ // 如果没有存储的JSON数据,构建一个基本的结构
+ basicData := systemRes.ExportVersionResponse{
+ Version: systemReq.VersionInfo{
+ Name: *version.VersionName,
+ Code: *version.VersionCode,
+ Description: *version.Description,
+ ExportTime: version.CreatedAt.Format("2006-01-02 15:04:05"),
+ },
+ Menus: []system.SysBaseMenu{},
+ Apis: []system.SysApi{},
+ }
+ jsonData, _ = json.MarshalIndent(basicData, "", " ")
+ }
+
+ // 设置下载响应头
+ filename := fmt.Sprintf("version_%s_%s.json", *version.VersionCode, time.Now().Format("20060102150405"))
+ c.Header("Content-Type", "application/json")
+ c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
+ c.Header("Content-Length", strconv.Itoa(len(jsonData)))
+
+ c.Data(http.StatusOK, "application/json", jsonData)
+}
+
+// ImportVersion 导入版本数据
+// @Tags SysVersion
+// @Summary 导入版本数据
+// @Security ApiKeyAuth
+// @Accept application/json
+// @Produce application/json
+// @Param data body systemReq.ImportVersionRequest true "版本JSON数据"
+// @Success 200 {object} response.Response{msg=string} "导入成功"
+// @Router /sysVersion/importVersion [post]
+func (sysVersionApi *SysVersionApi) ImportVersion(c *gin.Context) {
+ ctx := c.Request.Context()
+
+ // 获取JSON数据
+ var importData systemReq.ImportVersionRequest
+ err := c.ShouldBindJSON(&importData)
+ if err != nil {
+ response.FailWithMessage("解析JSON数据失败:"+err.Error(), c)
+ return
+ }
+
+ // 验证数据格式
+ if importData.VersionInfo.Name == "" || importData.VersionInfo.Code == "" {
+ response.FailWithMessage("版本信息格式错误", c)
+ return
+ }
+
+ // 导入菜单数据
+ if len(importData.ExportMenu) > 0 {
+ if err := sysVersionService.ImportMenus(ctx, importData.ExportMenu); err != nil {
+ global.GVA_LOG.Error("导入菜单失败!", zap.Error(err))
+ response.FailWithMessage("导入菜单失败: "+err.Error(), c)
+ return
+ }
+ }
+
+ // 导入API数据
+ if len(importData.ExportApi) > 0 {
+ if err := sysVersionService.ImportApis(importData.ExportApi); err != nil {
+ global.GVA_LOG.Error("导入API失败!", zap.Error(err))
+ response.FailWithMessage("导入API失败: "+err.Error(), c)
+ return
+ }
+ }
+
+ // 导入字典数据
+ if len(importData.ExportDictionary) > 0 {
+ if err := sysVersionService.ImportDictionaries(importData.ExportDictionary); err != nil {
+ global.GVA_LOG.Error("导入字典失败!", zap.Error(err))
+ response.FailWithMessage("导入字典失败: "+err.Error(), c)
+ return
+ }
+ }
+
+ // 创建导入记录
+ jsonData, _ := json.Marshal(importData)
+ version := system.SysVersion{
+ VersionName: utils.Pointer(importData.VersionInfo.Name),
+ VersionCode: utils.Pointer(fmt.Sprintf("%s_imported_%s", importData.VersionInfo.Code, time.Now().Format("20060102150405"))),
+ Description: utils.Pointer(fmt.Sprintf("导入版本: %s", importData.VersionInfo.Description)),
+ VersionData: utils.Pointer(string(jsonData)),
+ }
+
+ err = sysVersionService.CreateSysVersion(ctx, &version)
+ if err != nil {
+ global.GVA_LOG.Error("保存导入记录失败!", zap.Error(err))
+ // 这里不返回错误,因为数据已经导入成功
+ }
+
+ response.OkWithMessage("导入成功", c)
+}
diff --git a/server/config.docker.yaml b/server/config.docker.yaml
new file mode 100644
index 0000000..8dc0980
--- /dev/null
+++ b/server/config.docker.yaml
@@ -0,0 +1,285 @@
+# git.echol.cn/loser/st/server 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/st/server]"
+ director: log
+ show-line: true
+ encode-level: LowercaseColorLevelEncoder
+ stacktrace-key: stacktrace
+ log-in-console: true
+ retention-day: -1
+
+# redis configuration
+redis:
+ #是否使用redis集群模式
+ useCluster: false
+ #使用集群模式addr和db默认无效
+ addr: 177.7.0.14:6379
+ password: ""
+ db: 0
+ clusterAddrs:
+ - "177.7.0.14:7000"
+ - "177.7.0.15:7001"
+ - "177.7.0.13:7002"
+
+# redis-list configuration
+redis-list:
+ - name: cache # 数据库的名称,注意: name 需要在 redis-list 中唯一
+ useCluster: false # 是否使用redis集群模式
+ addr: 177.7.0.14:6379 # 使用集群模式addr和db默认无效
+ password: ""
+ db: 0
+ clusterAddrs:
+ - "177.7.0.14:7000"
+ - "177.7.0.15:7001"
+ - "177.7.0.13:7002"
+
+# mongo configuration
+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: ''
+
+# 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: local # 修改为public可以关闭路由日志输出
+ 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
+ # 路由全局前缀
+ router-prefix: ""
+ # 严格角色模式 打开后权限将会存在上下级关系
+ use-strict-auth: false
+
+# 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
+oracle:
+ path: ""
+ port: ""
+ config: ""
+ db-name: ""
+ username: ""
+ password: ""
+ max-idle-conns: 10
+ max-open-conns: 100
+ log-mode: ""
+ log-zap: false
+mssql:
+ path: ""
+ port: ""
+ config: ""
+ db-name: ""
+ username: ""
+ password: ""
+ max-idle-conns: 10
+ max-open-conns: 100
+ log-mode: ""
+ log-zap: false
+sqlite:
+ 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、mssql、oracle
+ 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:
+ web: web/src
+ root: "" # root 自动适配项目根目录, 请不要手动配置,他会在项目加载的时候识别出根路径
+ server: server
+ module: 'git.echol.cn/loser/st/server'
+ ai-path: "" # AI服务路径
+
+# qiniu configuration (请自行七牛申请对应的 公钥 私钥 bucket 和 域名地址)
+qiniu:
+ zone: ZoneHuaDong
+ bucket: ""
+ img-path: ""
+ use-https: false
+ access-key: ""
+ secret-key: ""
+ use-cdn-domains: false
+
+# minio oss configuration
+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"
+
+# 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/st/server
+
+# 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/st/server
+
+# cloudflare r2 configuration
+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
+
+# 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/
+
+# disk usage configuration
+disk-list:
+ - mount-point: "/"
+
+# 跨域配置
+# 需要配合 server/initialize/router.go -> `Router.Use(middleware.CorsByRules())` 使用
+cors:
+ mode: strict-whitelist # 放行模式: allow-all, 放行全部; whitelist, 白名单模式, 来自白名单内域名的请求添加 cors 头; strict-whitelist 严格白名单模式, 白名单外的请求一律拒绝
+ whitelist:
+ - allow-origin: example1.com
+ allow-headers: Content-Type,AccessToken,X-CSRF-Token, Authorization, Token,X-Token,X-User-Id
+ allow-methods: POST, GET
+ 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 # 布尔值
+mcp:
+ name: GVA_MCP
+ version: v1.0.0
+ sse_path: /sse
+ message_path: /message
+ url_prefix: ''
+ addr: 8889
+ separate: false
diff --git a/server/config.yaml b/server/config.yaml
new file mode 100644
index 0000000..bc3147a
--- /dev/null
+++ b/server/config.yaml
@@ -0,0 +1,267 @@
+aliyun-oss:
+ endpoint: oss-cn-hangzhou.aliyuncs.com
+ access-key-id: LTAI5tB3Mn5Y7mVo8h3zkf46
+ access-key-secret: FtuHdFy4NFdVItEiNBnTun3Ddi8BMK
+ bucket-name: lckt
+ bucket-url: https://lckt.oss-cn-hangzhou.aliyuncs.com
+ base-path: st
+autocode:
+ web: web/src
+ root: /Users/en/GolandProjects/st
+ server: server
+ module: git.echol.cn/loser/st/server
+ ai-path: ""
+aws-s3:
+ bucket: xxxxx-10005608
+ region: ap-shanghai
+ endpoint: ""
+ secret-id: your-secret-id
+ secret-key: your-secret-key
+ base-url: https://gin.vue.admin
+ path-prefix: git.echol.cn/loser/st/server
+ 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
+ is-loginauth: false
+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
+ use-ssl: false
+jwt:
+ signing-key: 53d59b59-dba8-4f83-886e-e5bd1bf3cbda
+ expires-time: 7d
+ buffer-time: 1d
+ issuer: qmPlus
+local:
+ path: http://localhost:8888/uploads/file
+ store-path: uploads/file
+mcp:
+ name: GVA_MCP
+ version: v1.0.0
+ sse_path: /sse
+ message_path: /message
+ url_prefix: ""
+ addr: 8889
+ separate: false
+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: ""
+ config: ""
+ db-name: ""
+ username: ""
+ password: ""
+ path: ""
+ engine: ""
+ log-mode: ""
+ 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: "5432"
+# config: sslmode=disable TimeZone=Asia/Shanghai
+# db-name: st_dev
+# username: postgres
+# password: loser765911.
+# path: 149.88.74.188
+# engine: ""
+# log-mode: error
+# max-idle-conns: 10
+# max-open-conns: 100
+# singular: false
+# log-zap: false
+pgsql:
+ prefix: ""
+ port: "5432"
+ config: sslmode=disable TimeZone=Asia/Shanghai
+ db-name: st2
+ username: postgres
+ password: e5zse3Adrja7PNfA
+ path: 219.152.55.29
+ engine: ""
+ log-mode: error
+ max-idle-conns: 10
+ max-open-conns: 100
+ singular: false
+ log-zap: true
+qiniu:
+ zone: ZoneHuaDong
+ bucket: ""
+ img-path: ""
+ access-key: ""
+ secret-key: ""
+ use-https: false
+ use-cdn-domains: false
+redis:
+ name: "sys-cache"
+ addr: 219.152.55.29:6379
+ password: "THBA@6688"
+ db: 7
+ useCluster: false
+ clusterAddrs:
+ - 172.21.0.3:7000
+ - 172.21.0.4:7001
+ - 172.21.0.2:7002
+redis-list:
+ - name: app-cache
+ addr: 219.152.55.29:6379
+ password: "THBA@6688"
+ db: 6
+ 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: pgsql
+ oss-type: aliyun-oss
+ router-prefix: ""
+ addr: 8888
+ iplimit-count: 15000
+ iplimit-time: 3600
+ use-multipoint: false
+ use-redis: true
+ use-mongo: false
+ use-strict-auth: false
+ disable-auto-migrate: false
+ data-dir: data
+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/st/server
+zap:
+ level: info
+ prefix: '[git.echol.cn/loser/st/server]'
+ format: console
+ director: log
+ encode-level: LowercaseColorLevelEncoder
+ stacktrace-key: stacktrace
+ show-line: true
+ log-in-console: true
+ retention-day: -1
diff --git a/server/config/auto_code.go b/server/config/auto_code.go
new file mode 100644
index 0000000..ade79a0
--- /dev/null
+++ b/server/config/auto_code.go
@@ -0,0 +1,22 @@
+package config
+
+import (
+ "path/filepath"
+ "strings"
+)
+
+type Autocode struct {
+ Web string `mapstructure:"web" json:"web" yaml:"web"`
+ Root string `mapstructure:"root" json:"root" yaml:"root"`
+ Server string `mapstructure:"server" json:"server" yaml:"server"`
+ Module string `mapstructure:"module" json:"module" yaml:"module"`
+ AiPath string `mapstructure:"ai-path" json:"ai-path" yaml:"ai-path"`
+}
+
+func (a *Autocode) WebRoot() string {
+ webs := strings.Split(a.Web, "/")
+ if len(webs) == 0 {
+ webs = strings.Split(a.Web, "\\")
+ }
+ return filepath.Join(webs...)
+}
diff --git a/server/config/captcha.go b/server/config/captcha.go
new file mode 100644
index 0000000..d678a41
--- /dev/null
+++ b/server/config/captcha.go
@@ -0,0 +1,9 @@
+package config
+
+type Captcha struct {
+ KeyLong int `mapstructure:"key-long" json:"key-long" yaml:"key-long"` // 验证码长度
+ ImgWidth int `mapstructure:"img-width" json:"img-width" yaml:"img-width"` // 验证码宽度
+ ImgHeight int `mapstructure:"img-height" json:"img-height" yaml:"img-height"` // 验证码高度
+ OpenCaptcha int `mapstructure:"open-captcha" json:"open-captcha" yaml:"open-captcha"` // 防爆破验证码开启此数,0代表每次登录都需要验证码,其他数字代表错误密码次数,如3代表错误三次后出现验证码
+ OpenCaptchaTimeOut int `mapstructure:"open-captcha-timeout" json:"open-captcha-timeout" yaml:"open-captcha-timeout"` // 防爆破验证码超时时间,单位:s(秒)
+}
diff --git a/server/config/config.go b/server/config/config.go
new file mode 100644
index 0000000..3abac5a
--- /dev/null
+++ b/server/config/config.go
@@ -0,0 +1,40 @@
+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"`
+
+ // MCP配置
+ MCP MCP `mapstructure:"mcp" json:"mcp" yaml:"mcp"`
+}
diff --git a/server/config/cors.go b/server/config/cors.go
new file mode 100644
index 0000000..7fba993
--- /dev/null
+++ b/server/config/cors.go
@@ -0,0 +1,14 @@
+package config
+
+type CORS struct {
+ Mode string `mapstructure:"mode" json:"mode" yaml:"mode"`
+ Whitelist []CORSWhitelist `mapstructure:"whitelist" json:"whitelist" yaml:"whitelist"`
+}
+
+type CORSWhitelist struct {
+ AllowOrigin string `mapstructure:"allow-origin" json:"allow-origin" yaml:"allow-origin"`
+ AllowMethods string `mapstructure:"allow-methods" json:"allow-methods" yaml:"allow-methods"`
+ AllowHeaders string `mapstructure:"allow-headers" json:"allow-headers" yaml:"allow-headers"`
+ ExposeHeaders string `mapstructure:"expose-headers" json:"expose-headers" yaml:"expose-headers"`
+ AllowCredentials bool `mapstructure:"allow-credentials" json:"allow-credentials" yaml:"allow-credentials"`
+}
diff --git a/server/config/db_list.go b/server/config/db_list.go
new file mode 100644
index 0000000..17674b7
--- /dev/null
+++ b/server/config/db_list.go
@@ -0,0 +1,53 @@
+package config
+
+import (
+ "strings"
+
+ "gorm.io/gorm/logger"
+)
+
+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":
+ return logger.Silent
+ case "error":
+ return logger.Error
+ case "warn":
+ return logger.Warn
+ case "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/server/config/disk.go b/server/config/disk.go
new file mode 100644
index 0000000..59a6332
--- /dev/null
+++ b/server/config/disk.go
@@ -0,0 +1,9 @@
+package config
+
+type Disk struct {
+ MountPoint string `mapstructure:"mount-point" json:"mount-point" yaml:"mount-point"`
+}
+
+type DiskList struct {
+ Disk `yaml:",inline" mapstructure:",squash"`
+}
diff --git a/server/config/email.go b/server/config/email.go
new file mode 100644
index 0000000..9fd7642
--- /dev/null
+++ b/server/config/email.go
@@ -0,0 +1,12 @@
+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
+ IsLoginAuth bool `mapstructure:"is-loginauth" json:"is-loginauth" yaml:"is-loginauth"` // 是否LoginAuth 是否使用LoginAuth认证方式(适用于IBM、微软邮箱服务器等)
+}
diff --git a/server/config/excel.go b/server/config/excel.go
new file mode 100644
index 0000000..13caab7
--- /dev/null
+++ b/server/config/excel.go
@@ -0,0 +1,5 @@
+package config
+
+type Excel struct {
+ Dir string `mapstructure:"dir" json:"dir" yaml:"dir"`
+}
diff --git a/server/config/gorm_mssql.go b/server/config/gorm_mssql.go
new file mode 100644
index 0000000..d187119
--- /dev/null
+++ b/server/config/gorm_mssql.go
@@ -0,0 +1,10 @@
+package config
+
+type Mssql struct {
+ GeneralDB `yaml:",inline" mapstructure:",squash"`
+}
+
+// Dsn "sqlserver://gorm:LoremIpsum86@localhost:9930?database=gorm"
+func (m *Mssql) Dsn() string {
+ return "sqlserver://" + m.Username + ":" + m.Password + "@" + m.Path + ":" + m.Port + "?database=" + m.Dbname + "&encrypt=disable"
+}
diff --git a/server/config/gorm_mysql.go b/server/config/gorm_mysql.go
new file mode 100644
index 0000000..77e0245
--- /dev/null
+++ b/server/config/gorm_mysql.go
@@ -0,0 +1,9 @@
+package config
+
+type Mysql struct {
+ GeneralDB `yaml:",inline" mapstructure:",squash"`
+}
+
+func (m *Mysql) Dsn() string {
+ return m.Username + ":" + m.Password + "@tcp(" + m.Path + ":" + m.Port + ")/" + m.Dbname + "?" + m.Config
+}
diff --git a/server/config/gorm_oracle.go b/server/config/gorm_oracle.go
new file mode 100644
index 0000000..52cf21c
--- /dev/null
+++ b/server/config/gorm_oracle.go
@@ -0,0 +1,18 @@
+package config
+
+import (
+ "fmt"
+ "net"
+ "net/url"
+)
+
+type Oracle struct {
+ GeneralDB `yaml:",inline" mapstructure:",squash"`
+}
+
+func (m *Oracle) Dsn() string {
+ dsn := fmt.Sprintf("oracle://%s:%s@%s/%s?%s", url.PathEscape(m.Username), url.PathEscape(m.Password),
+ net.JoinHostPort(m.Path, m.Port), url.PathEscape(m.Dbname), m.Config)
+ return dsn
+
+}
diff --git a/server/config/gorm_pgsql.go b/server/config/gorm_pgsql.go
new file mode 100644
index 0000000..29fe03f
--- /dev/null
+++ b/server/config/gorm_pgsql.go
@@ -0,0 +1,17 @@
+package config
+
+type Pgsql struct {
+ GeneralDB `yaml:",inline" mapstructure:",squash"`
+}
+
+// Dsn 基于配置文件获取 dsn
+// Author [SliverHorn](https://github.com/SliverHorn)
+func (p *Pgsql) Dsn() string {
+ return "host=" + p.Path + " user=" + p.Username + " password=" + p.Password + " dbname=" + p.Dbname + " port=" + p.Port + " " + p.Config
+}
+
+// LinkDsn 根据 dbname 生成 dsn
+// Author [SliverHorn](https://github.com/SliverHorn)
+func (p *Pgsql) LinkDsn(dbname string) string {
+ return "host=" + p.Path + " user=" + p.Username + " password=" + p.Password + " dbname=" + dbname + " port=" + p.Port + " " + p.Config
+}
diff --git a/server/config/gorm_sqlite.go b/server/config/gorm_sqlite.go
new file mode 100644
index 0000000..46f2e19
--- /dev/null
+++ b/server/config/gorm_sqlite.go
@@ -0,0 +1,13 @@
+package config
+
+import (
+ "path/filepath"
+)
+
+type Sqlite struct {
+ GeneralDB `yaml:",inline" mapstructure:",squash"`
+}
+
+func (s *Sqlite) Dsn() string {
+ return filepath.Join(s.Path, s.Dbname+".db")
+}
diff --git a/server/config/jwt.go b/server/config/jwt.go
new file mode 100644
index 0000000..c95d30d
--- /dev/null
+++ b/server/config/jwt.go
@@ -0,0 +1,8 @@
+package config
+
+type JWT struct {
+ SigningKey string `mapstructure:"signing-key" json:"signing-key" yaml:"signing-key"` // jwt签名
+ ExpiresTime string `mapstructure:"expires-time" json:"expires-time" yaml:"expires-time"` // 过期时间
+ BufferTime string `mapstructure:"buffer-time" json:"buffer-time" yaml:"buffer-time"` // 缓冲时间
+ Issuer string `mapstructure:"issuer" json:"issuer" yaml:"issuer"` // 签发者
+}
diff --git a/server/config/mcp.go b/server/config/mcp.go
new file mode 100644
index 0000000..15a7876
--- /dev/null
+++ b/server/config/mcp.go
@@ -0,0 +1,11 @@
+package config
+
+type MCP struct {
+ Name string `mapstructure:"name" json:"name" yaml:"name"` // MCP名称
+ Version string `mapstructure:"version" json:"version" yaml:"version"` // MCP版本
+ SSEPath string `mapstructure:"sse_path" json:"sse_path" yaml:"sse_path"` // SSE路径
+ MessagePath string `mapstructure:"message_path" json:"message_path" yaml:"message_path"` // 消息路径
+ UrlPrefix string `mapstructure:"url_prefix" json:"url_prefix" yaml:"url_prefix"` // URL前缀
+ Addr int `mapstructure:"addr" json:"addr" yaml:"addr"` // 独立MCP服务端口
+ Separate bool `mapstructure:"separate" json:"separate" yaml:"separate"` // 是否独立运行MCP服务
+}
diff --git a/server/config/mongo.go b/server/config/mongo.go
new file mode 100644
index 0000000..2034a3f
--- /dev/null
+++ b/server/config/mongo.go
@@ -0,0 +1,41 @@
+package config
+
+import (
+ "fmt"
+ "strings"
+)
+
+type Mongo struct {
+ Coll string `json:"coll" yaml:"coll" mapstructure:"coll"` // collection name
+ Options string `json:"options" yaml:"options" mapstructure:"options"` // mongodb options
+ Database string `json:"database" yaml:"database" mapstructure:"database"` // database name
+ Username string `json:"username" yaml:"username" mapstructure:"username"` // 用户名
+ Password string `json:"password" yaml:"password" mapstructure:"password"` // 密码
+ AuthSource string `json:"auth-source" yaml:"auth-source" mapstructure:"auth-source"` // 验证数据库
+ MinPoolSize uint64 `json:"min-pool-size" yaml:"min-pool-size" mapstructure:"min-pool-size"` // 最小连接池
+ MaxPoolSize uint64 `json:"max-pool-size" yaml:"max-pool-size" mapstructure:"max-pool-size"` // 最大连接池
+ SocketTimeoutMs int64 `json:"socket-timeout-ms" yaml:"socket-timeout-ms" mapstructure:"socket-timeout-ms"` // socket超时时间
+ ConnectTimeoutMs int64 `json:"connect-timeout-ms" yaml:"connect-timeout-ms" mapstructure:"connect-timeout-ms"` // 连接超时时间
+ IsZap bool `json:"is-zap" yaml:"is-zap" mapstructure:"is-zap"` // 是否开启zap日志
+ Hosts []*MongoHost `json:"hosts" yaml:"hosts" mapstructure:"hosts"` // 主机列表
+}
+
+type MongoHost struct {
+ Host string `json:"host" yaml:"host" mapstructure:"host"` // ip地址
+ Port string `json:"port" yaml:"port" mapstructure:"port"` // 端口
+}
+
+// Uri .
+func (x *Mongo) Uri() string {
+ length := len(x.Hosts)
+ hosts := make([]string, 0, length)
+ for i := 0; i < length; i++ {
+ if x.Hosts[i].Host != "" && x.Hosts[i].Port != "" {
+ hosts = append(hosts, x.Hosts[i].Host+":"+x.Hosts[i].Port)
+ }
+ }
+ if x.Options != "" {
+ return fmt.Sprintf("mongodb://%s/%s?%s", strings.Join(hosts, ","), x.Database, x.Options)
+ }
+ return fmt.Sprintf("mongodb://%s/%s", strings.Join(hosts, ","), x.Database)
+}
diff --git a/server/config/oss_aliyun.go b/server/config/oss_aliyun.go
new file mode 100644
index 0000000..934bd78
--- /dev/null
+++ b/server/config/oss_aliyun.go
@@ -0,0 +1,10 @@
+package config
+
+type AliyunOSS struct {
+ Endpoint string `mapstructure:"endpoint" json:"endpoint" yaml:"endpoint"`
+ AccessKeyId string `mapstructure:"access-key-id" json:"access-key-id" yaml:"access-key-id"`
+ AccessKeySecret string `mapstructure:"access-key-secret" json:"access-key-secret" yaml:"access-key-secret"`
+ BucketName string `mapstructure:"bucket-name" json:"bucket-name" yaml:"bucket-name"`
+ BucketUrl string `mapstructure:"bucket-url" json:"bucket-url" yaml:"bucket-url"`
+ BasePath string `mapstructure:"base-path" json:"base-path" yaml:"base-path"`
+}
diff --git a/server/config/oss_aws.go b/server/config/oss_aws.go
new file mode 100644
index 0000000..7ec6acc
--- /dev/null
+++ b/server/config/oss_aws.go
@@ -0,0 +1,13 @@
+package config
+
+type AwsS3 struct {
+ Bucket string `mapstructure:"bucket" json:"bucket" yaml:"bucket"`
+ Region string `mapstructure:"region" json:"region" yaml:"region"`
+ Endpoint string `mapstructure:"endpoint" json:"endpoint" yaml:"endpoint"`
+ SecretID string `mapstructure:"secret-id" json:"secret-id" yaml:"secret-id"`
+ SecretKey string `mapstructure:"secret-key" json:"secret-key" yaml:"secret-key"`
+ BaseURL string `mapstructure:"base-url" json:"base-url" yaml:"base-url"`
+ PathPrefix string `mapstructure:"path-prefix" json:"path-prefix" yaml:"path-prefix"`
+ S3ForcePathStyle bool `mapstructure:"s3-force-path-style" json:"s3-force-path-style" yaml:"s3-force-path-style"`
+ DisableSSL bool `mapstructure:"disable-ssl" json:"disable-ssl" yaml:"disable-ssl"`
+}
diff --git a/server/config/oss_cloudflare.go b/server/config/oss_cloudflare.go
new file mode 100644
index 0000000..ab7a393
--- /dev/null
+++ b/server/config/oss_cloudflare.go
@@ -0,0 +1,10 @@
+package config
+
+type CloudflareR2 struct {
+ Bucket string `mapstructure:"bucket" json:"bucket" yaml:"bucket"`
+ BaseURL string `mapstructure:"base-url" json:"base-url" yaml:"base-url"`
+ Path string `mapstructure:"path" json:"path" yaml:"path"`
+ AccountID string `mapstructure:"account-id" json:"account-id" yaml:"account-id"`
+ AccessKeyID string `mapstructure:"access-key-id" json:"access-key-id" yaml:"access-key-id"`
+ SecretAccessKey string `mapstructure:"secret-access-key" json:"secret-access-key" yaml:"secret-access-key"`
+}
diff --git a/server/config/oss_huawei.go b/server/config/oss_huawei.go
new file mode 100644
index 0000000..45dfbcd
--- /dev/null
+++ b/server/config/oss_huawei.go
@@ -0,0 +1,9 @@
+package config
+
+type HuaWeiObs struct {
+ Path string `mapstructure:"path" json:"path" yaml:"path"`
+ Bucket string `mapstructure:"bucket" json:"bucket" yaml:"bucket"`
+ Endpoint string `mapstructure:"endpoint" json:"endpoint" yaml:"endpoint"`
+ AccessKey string `mapstructure:"access-key" json:"access-key" yaml:"access-key"`
+ SecretKey string `mapstructure:"secret-key" json:"secret-key" yaml:"secret-key"`
+}
diff --git a/server/config/oss_local.go b/server/config/oss_local.go
new file mode 100644
index 0000000..7038d4a
--- /dev/null
+++ b/server/config/oss_local.go
@@ -0,0 +1,6 @@
+package config
+
+type Local struct {
+ Path string `mapstructure:"path" json:"path" yaml:"path"` // 本地文件访问路径
+ StorePath string `mapstructure:"store-path" json:"store-path" yaml:"store-path"` // 本地文件存储路径
+}
diff --git a/server/config/oss_minio.go b/server/config/oss_minio.go
new file mode 100644
index 0000000..a0faac7
--- /dev/null
+++ b/server/config/oss_minio.go
@@ -0,0 +1,11 @@
+package config
+
+type Minio struct {
+ Endpoint string `mapstructure:"endpoint" json:"endpoint" yaml:"endpoint"`
+ AccessKeyId string `mapstructure:"access-key-id" json:"access-key-id" yaml:"access-key-id"`
+ AccessKeySecret string `mapstructure:"access-key-secret" json:"access-key-secret" yaml:"access-key-secret"`
+ BucketName string `mapstructure:"bucket-name" json:"bucket-name" yaml:"bucket-name"`
+ UseSSL bool `mapstructure:"use-ssl" json:"use-ssl" yaml:"use-ssl"`
+ BasePath string `mapstructure:"base-path" json:"base-path" yaml:"base-path"`
+ BucketUrl string `mapstructure:"bucket-url" json:"bucket-url" yaml:"bucket-url"`
+}
diff --git a/server/config/oss_qiniu.go b/server/config/oss_qiniu.go
new file mode 100644
index 0000000..298fe2d
--- /dev/null
+++ b/server/config/oss_qiniu.go
@@ -0,0 +1,11 @@
+package config
+
+type Qiniu struct {
+ Zone string `mapstructure:"zone" json:"zone" yaml:"zone"` // 存储区域
+ Bucket string `mapstructure:"bucket" json:"bucket" yaml:"bucket"` // 空间名称
+ ImgPath string `mapstructure:"img-path" json:"img-path" yaml:"img-path"` // CDN加速域名
+ AccessKey string `mapstructure:"access-key" json:"access-key" yaml:"access-key"` // 秘钥AK
+ SecretKey string `mapstructure:"secret-key" json:"secret-key" yaml:"secret-key"` // 秘钥SK
+ UseHTTPS bool `mapstructure:"use-https" json:"use-https" yaml:"use-https"` // 是否使用https
+ UseCdnDomains bool `mapstructure:"use-cdn-domains" json:"use-cdn-domains" yaml:"use-cdn-domains"` // 上传是否使用CDN上传加速
+}
diff --git a/server/config/oss_tencent.go b/server/config/oss_tencent.go
new file mode 100644
index 0000000..39a29d1
--- /dev/null
+++ b/server/config/oss_tencent.go
@@ -0,0 +1,10 @@
+package config
+
+type TencentCOS struct {
+ Bucket string `mapstructure:"bucket" json:"bucket" yaml:"bucket"`
+ Region string `mapstructure:"region" json:"region" yaml:"region"`
+ SecretID string `mapstructure:"secret-id" json:"secret-id" yaml:"secret-id"`
+ SecretKey string `mapstructure:"secret-key" json:"secret-key" yaml:"secret-key"`
+ BaseURL string `mapstructure:"base-url" json:"base-url" yaml:"base-url"`
+ PathPrefix string `mapstructure:"path-prefix" json:"path-prefix" yaml:"path-prefix"`
+}
diff --git a/server/config/redis.go b/server/config/redis.go
new file mode 100644
index 0000000..94b5bf6
--- /dev/null
+++ b/server/config/redis.go
@@ -0,0 +1,10 @@
+package config
+
+type Redis struct {
+ Name string `mapstructure:"name" json:"name" yaml:"name"` // 代表当前实例的名字
+ Addr string `mapstructure:"addr" json:"addr" yaml:"addr"` // 服务器地址:端口
+ Password string `mapstructure:"password" json:"password" yaml:"password"` // 密码
+ DB int `mapstructure:"db" json:"db" yaml:"db"` // 单实例模式下redis的哪个数据库
+ UseCluster bool `mapstructure:"useCluster" json:"useCluster" yaml:"useCluster"` // 是否使用集群模式
+ ClusterAddrs []string `mapstructure:"clusterAddrs" json:"clusterAddrs" yaml:"clusterAddrs"` // 集群模式下的节点地址列表
+}
diff --git a/server/config/system.go b/server/config/system.go
new file mode 100644
index 0000000..4f09773
--- /dev/null
+++ b/server/config/system.go
@@ -0,0 +1,16 @@
+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"` // 使用树形角色分配模式
+ DisableAutoMigrate bool `mapstructure:"disable-auto-migrate" json:"disable-auto-migrate" yaml:"disable-auto-migrate"` // 自动迁移数据库表结构,生产环境建议设为false,手动迁移
+ DataDir string `mapstructure:"data-dir" json:"data-dir" yaml:"data-dir"` // 数据目录
+}
diff --git a/server/config/zap.go b/server/config/zap.go
new file mode 100644
index 0000000..6beb238
--- /dev/null
+++ b/server/config/zap.go
@@ -0,0 +1,72 @@
+package config
+
+import (
+ "time"
+
+ "go.uber.org/zap/zapcore"
+)
+
+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/server/core/internal/constant.go b/server/core/internal/constant.go
new file mode 100644
index 0000000..b22362c
--- /dev/null
+++ b/server/core/internal/constant.go
@@ -0,0 +1,9 @@
+package internal
+
+const (
+ ConfigEnv = "GVA_CONFIG"
+ ConfigDefaultFile = "config.yaml"
+ ConfigTestFile = "config.test.yaml"
+ ConfigDebugFile = "config.debug.yaml"
+ ConfigReleaseFile = "config.release.yaml"
+)
diff --git a/server/core/internal/cutter.go b/server/core/internal/cutter.go
new file mode 100644
index 0000000..2873b7c
--- /dev/null
+++ b/server/core/internal/cutter.go
@@ -0,0 +1,125 @@
+package internal
+
+import (
+ "fmt"
+ "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
+ }
+ defer func() {
+ err := removeNDaysFolders(c.director, c.retentionDay)
+ if err != nil {
+ fmt.Println("清理过期日志失败", 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/server/core/internal/zap_core.go b/server/core/internal/zap_core.go
new file mode 100644
index 0000000..d722a33
--- /dev/null
+++ b/server/core/internal/zap_core.go
@@ -0,0 +1,133 @@
+package internal
+
+import (
+ "context"
+ "fmt"
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system"
+ "git.echol.cn/loser/st/server/service"
+ astutil "git.echol.cn/loser/st/server/utils/ast"
+ "git.echol.cn/loser/st/server/utils/stacktrace"
+ "go.uber.org/zap"
+ "go.uber.org/zap/zapcore"
+ "os"
+ "strings"
+ "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)
+ }
+ }
+ // 先写入原日志目标
+ err := z.Core.Write(entry, fields)
+
+ // 捕捉 Error 及以上级别日志并入库,且可提取 zap.Error(err) 的错误内容
+ if entry.Level >= zapcore.ErrorLevel {
+ // 避免与 GORM zap 写入互相递归:跳过由 gorm logger writer 触发的日志
+ if strings.Contains(entry.Caller.File, "gorm_logger_writer.go") {
+ return err
+ }
+
+ form := "后端"
+ level := entry.Level.String()
+ // 生成基础信息
+ info := entry.Message
+
+ // 提取 zap.Error(err) 内容
+ var errStr string
+ for i := 0; i < len(fields); i++ {
+ f := fields[i]
+ if f.Type == zapcore.ErrorType || f.Key == "error" || f.Key == "err" {
+ if f.Interface != nil {
+ errStr = fmt.Sprintf("%v", f.Interface)
+ } else if f.String != "" {
+ errStr = f.String
+ }
+ break
+ }
+ }
+ if errStr != "" {
+ info = fmt.Sprintf("%s | 错误: %s", info, errStr)
+ }
+
+ // 附加来源与堆栈信息
+ if entry.Caller.File != "" {
+ info = fmt.Sprintf("%s \n 源文件:%s:%d", info, entry.Caller.File, entry.Caller.Line)
+ }
+ stack := entry.Stack
+ if stack != "" {
+ info = fmt.Sprintf("%s \n 调用栈:%s", info, stack)
+ // 解析最终业务调用方,并提取其方法源码
+ if frame, ok := stacktrace.FindFinalCaller(stack); ok {
+ fnName, fnSrc, sLine, eLine, exErr := astutil.ExtractFuncSourceByPosition(frame.File, frame.Line)
+ if exErr == nil {
+ info = fmt.Sprintf("%s \n 最终调用方法:%s:%d (%s lines %d-%d)\n----- 产生日志的方法代码如下 -----\n%s", info, frame.File, frame.Line, fnName, sLine, eLine, fnSrc)
+ } else {
+ info = fmt.Sprintf("%s \n 最终调用方法:%s:%d (%s) | extract_err=%v", info, frame.File, frame.Line, fnName, exErr)
+ }
+ }
+ }
+
+ // 使用后台上下文,避免依赖 gin.Context
+ ctx := context.Background()
+ _ = service.ServiceGroupApp.SystemServiceGroup.SysErrorService.CreateSysError(ctx, &system.SysError{
+ Form: &form,
+ Info: &info,
+ Level: level,
+ })
+ }
+ return err
+}
+
+func (z *ZapCore) Sync() error {
+ return z.Core.Sync()
+}
diff --git a/server/core/server.go b/server/core/server.go
new file mode 100644
index 0000000..213ae09
--- /dev/null
+++ b/server/core/server.go
@@ -0,0 +1,44 @@
+package core
+
+import (
+ "fmt"
+ "time"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/initialize"
+ "git.echol.cn/loser/st/server/service/system"
+ "go.uber.org/zap"
+)
+
+func RunServer() {
+ if global.GVA_CONFIG.System.UseRedis {
+ // 初始化redis服务
+ initialize.Redis()
+ if global.GVA_CONFIG.System.UseMultipoint {
+ 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)
+
+ fmt.Printf(`
+ 默认自动化文档地址:http://127.0.0.1%s/swagger/index.html
+ 默认MCP SSE地址:http://127.0.0.1%s%s
+ 默认MCP Message地址:http://127.0.0.1%s%s
+ 默认前端文件运行地址:http://127.0.0.1:8080
+`, address, address, global.GVA_CONFIG.MCP.SSEPath, address, global.GVA_CONFIG.MCP.MessagePath)
+ initServer(address, Router, 10*time.Minute, 10*time.Minute)
+}
diff --git a/server/core/server_run.go b/server/core/server_run.go
new file mode 100644
index 0000000..067ce6b
--- /dev/null
+++ b/server/core/server_run.go
@@ -0,0 +1,60 @@
+package core
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "os"
+ "os/signal"
+ "syscall"
+ "time"
+
+ "github.com/gin-gonic/gin"
+ "go.uber.org/zap"
+)
+
+type server interface {
+ ListenAndServe() error
+ Shutdown(context.Context) error
+}
+
+// initServer 启动服务并实现优雅关闭
+func initServer(address string, router *gin.Engine, readTimeout, writeTimeout time.Duration) {
+ // 创建服务
+ srv := &http.Server{
+ Addr: address,
+ Handler: router,
+ ReadTimeout: readTimeout,
+ WriteTimeout: writeTimeout,
+ MaxHeaderBytes: 1 << 20,
+ }
+
+ // 在goroutine中启动服务
+ go func() {
+ if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
+ fmt.Printf("listen: %s\n", err)
+ zap.L().Error("server启动失败", zap.Error(err))
+ os.Exit(1)
+ }
+ }()
+
+ // 等待中断信号以优雅地关闭服务器
+ quit := make(chan os.Signal, 1)
+ // kill (无参数) 默认发送 syscall.SIGTERM
+ // kill -2 发送 syscall.SIGINT
+ // kill -9 发送 syscall.SIGKILL,但是无法被捕获,所以不需要添加
+ signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
+ <-quit
+ zap.L().Info("关闭WEB服务...")
+
+ // 设置5秒的超时时间
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+
+ defer cancel()
+
+ if err := srv.Shutdown(ctx); err != nil {
+ zap.L().Fatal("WEB服务关闭异常", zap.Error(err))
+ }
+
+ zap.L().Info("WEB服务已关闭")
+}
diff --git a/server/core/viper.go b/server/core/viper.go
new file mode 100644
index 0000000..ac310d1
--- /dev/null
+++ b/server/core/viper.go
@@ -0,0 +1,76 @@
+package core
+
+import (
+ "flag"
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "git.echol.cn/loser/st/server/core/internal"
+ "git.echol.cn/loser/st/server/global"
+ "github.com/fsnotify/fsnotify"
+ "github.com/gin-gonic/gin"
+ "github.com/spf13/viper"
+)
+
+// Viper 配置
+func Viper() *viper.Viper {
+ config := getConfigPath()
+
+ v := viper.New()
+ v.SetConfigFile(config)
+ v.SetConfigType("yaml")
+ err := v.ReadInConfig()
+ if err != nil {
+ panic(fmt.Errorf("fatal error config file: %w", 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(fmt.Errorf("fatal error unmarshal config: %w", err))
+ }
+
+ // root 适配性 根据root位置去找到对应迁移位置,保证root路径有效
+ global.GVA_CONFIG.AutoCode.Root, _ = filepath.Abs("..")
+ return v
+}
+
+// getConfigPath 获取配置文件路径, 优先级: 命令行 > 环境变量 > 默认值
+func getConfigPath() (config string) {
+ // `-c` flag parse
+ flag.StringVar(&config, "c", "", "choose config file.")
+ flag.Parse()
+ if config != "" { // 命令行参数不为空 将值赋值于config
+ fmt.Printf("您正在使用命令行的 '-c' 参数传递的值, config 的路径为 %s\n", config)
+ return
+ }
+ if env := os.Getenv(internal.ConfigEnv); env != "" { // 判断环境变量 GVA_CONFIG
+ config = env
+ fmt.Printf("您正在使用 %s 环境变量, config 的路径为 %s\n", internal.ConfigEnv, config)
+ return
+ }
+
+ switch gin.Mode() { // 根据 gin 模式文件名
+ case gin.DebugMode:
+ config = internal.ConfigDebugFile
+ case gin.ReleaseMode:
+ config = internal.ConfigReleaseFile
+ case gin.TestMode:
+ config = internal.ConfigTestFile
+ }
+ fmt.Printf("您正在使用 gin 的 %s 模式运行, config 的路径为 %s\n", gin.Mode(), config)
+
+ _, err := os.Stat(config)
+ if err != nil || os.IsNotExist(err) {
+ config = internal.ConfigDefaultFile
+ fmt.Printf("配置文件路径不存在, 使用默认配置文件路径: %s\n", config)
+ }
+
+ return
+}
diff --git a/server/core/zap.go b/server/core/zap.go
new file mode 100644
index 0000000..f7a27cb
--- /dev/null
+++ b/server/core/zap.go
@@ -0,0 +1,36 @@
+package core
+
+import (
+ "fmt"
+ "git.echol.cn/loser/st/server/core/internal"
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/utils"
+ "go.uber.org/zap"
+ "go.uber.org/zap/zapcore"
+ "os"
+)
+
+// Zap 获取 zap.Logger
+// Author [SliverHorn](https://github.com/SliverHorn)
+func Zap() (logger *zap.Logger) {
+ if ok, _ := utils.PathExists(global.GVA_CONFIG.Zap.Director); !ok { // 判断是否有Director文件夹
+ fmt.Printf("create %v directory\n", global.GVA_CONFIG.Zap.Director)
+ _ = os.Mkdir(global.GVA_CONFIG.Zap.Director, os.ModePerm)
+ }
+ levels := global.GVA_CONFIG.Zap.Levels()
+ length := len(levels)
+ cores := make([]zapcore.Core, 0, length)
+ for i := 0; i < length; i++ {
+ core := internal.NewZapCore(levels[i])
+ cores = append(cores, core)
+ }
+ // 构建基础 logger(错误级别的入库逻辑已在自定义 ZapCore 中处理)
+ logger = zap.New(zapcore.NewTee(cores...))
+ // 启用 Error 及以上级别的堆栈捕捉,确保 entry.Stack 可用
+ opts := []zap.Option{zap.AddStacktrace(zapcore.ErrorLevel)}
+ if global.GVA_CONFIG.Zap.ShowLine {
+ opts = append(opts, zap.AddCaller())
+ }
+ logger = logger.WithOptions(opts...)
+ return logger
+}
diff --git a/server/global/global.go b/server/global/global.go
new file mode 100644
index 0000000..002bf4c
--- /dev/null
+++ b/server/global/global.go
@@ -0,0 +1,69 @@
+package global
+
+import (
+ "fmt"
+ "sync"
+
+ "github.com/mark3labs/mcp-go/server"
+
+ "github.com/gin-gonic/gin"
+ "github.com/qiniu/qmgo"
+
+ "git.echol.cn/loser/st/server/utils/timer"
+ "github.com/songzhibin97/gkit/cache/local_cache"
+
+ "golang.org/x/sync/singleflight"
+
+ "go.uber.org/zap"
+
+ "git.echol.cn/loser/st/server/config"
+
+ "github.com/redis/go-redis/v9"
+ "github.com/spf13/viper"
+ "gorm.io/gorm"
+)
+
+var (
+ GVA_DB *gorm.DB
+ GVA_DBList map[string]*gorm.DB
+ GVA_REDIS redis.UniversalClient
+ GVA_REDISList map[string]redis.UniversalClient
+ GVA_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
+ GVA_MCP_SERVER *server.MCPServer
+ 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/server/global/model.go b/server/global/model.go
new file mode 100644
index 0000000..9772eb3
--- /dev/null
+++ b/server/global/model.go
@@ -0,0 +1,14 @@
+package global
+
+import (
+ "time"
+
+ "gorm.io/gorm"
+)
+
+type GVA_MODEL struct {
+ ID uint `gorm:"primarykey" json:"ID"` // 主键ID
+ CreatedAt time.Time // 创建时间
+ UpdatedAt time.Time // 更新时间
+ DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` // 删除时间
+}
diff --git a/server/global/version.go b/server/global/version.go
new file mode 100644
index 0000000..544d423
--- /dev/null
+++ b/server/global/version.go
@@ -0,0 +1,12 @@
+package global
+
+// Version 版本信息
+// 目前只有Version正式使用 其余为预留
+const (
+ // Version 当前版本号
+ Version = "v2.8.9"
+ // AppName 应用名称
+ AppName = "Gin-Vue-Admin"
+ // Description 应用描述
+ Description = "使用gin+vue进行极速开发的全栈开发基础平台"
+)
diff --git a/server/go.mod b/server/go.mod
new file mode 100644
index 0000000..90468ce
--- /dev/null
+++ b/server/go.mod
@@ -0,0 +1,190 @@
+module git.echol.cn/loser/st/server
+
+go 1.24.0
+
+toolchain go1.24.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/dzwvip/gorm-oracle v0.1.2
+ github.com/fsnotify/fsnotify v1.8.0
+ 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.2
+ 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/lib/pq v1.10.9
+ github.com/mark3labs/mcp-go v0.41.1
+ github.com/mholt/archives v0.1.1
+ 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/pgvector/pgvector-go v0.3.0
+ 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.37.0
+ golang.org/x/sync v0.13.0
+ golang.org/x/text v0.24.0
+ gopkg.in/yaml.v3 v3.0.1
+ 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/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.2.1 // indirect
+ github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 // indirect
+ github.com/andybalholm/brotli v1.1.1 // indirect
+ github.com/bahlo/generic-list-go v0.2.0 // 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/buger/jsonparser v1.1.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/emirpasic/gods v1.12.0 // 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/invopop/jsonschema v0.13.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.18.0 // 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/mattn/go-sqlite3 v1.14.16 // indirect
+ github.com/microsoft/go-mssqldb v1.8.0 // indirect
+ github.com/minio/md5-simd v1.1.2 // indirect
+ github.com/minio/minlz v1.0.0 // 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.1.0 // 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/sijms/go-ora/v2 v2.7.17 // 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/thoas/go-funk v0.7.0 // 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/wk8/go-ordered-map/v2 v2.1.8 // 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/yosida95/uritemplate/v3 v3.0.2 // 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/net v0.37.0 // indirect
+ golang.org/x/sys v0.32.0 // indirect
+ golang.org/x/time v0.9.0 // indirect
+ golang.org/x/tools v0.31.0 // indirect
+ google.golang.org/protobuf v1.36.6 // indirect
+ gopkg.in/ini.v1 v1.67.0 // indirect
+ gorm.io/driver/sqlite v1.5.0 // 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/server/go.sum b/server/go.sum
new file mode 100644
index 0000000..0361ac4
--- /dev/null
+++ b/server/go.sum
@@ -0,0 +1,873 @@
+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=
+entgo.io/ent v0.14.3 h1:wokAV/kIlH9TeklJWGGS7AYJdVckr0DloWjIcO9iIIQ=
+entgo.io/ent v0.14.3/go.mod h1:aDPE/OziPEu8+OWbzy4UlvWmD2/kbRuWfK2A40hcxJM=
+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.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg=
+github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4=
+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/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
+github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
+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/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
+github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
+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/dzwvip/gorm-oracle v0.1.2 h1:811aFDY7oDfKWHc0Z0lHdXzzr89EmKBSwc/jLJ8GU5g=
+github.com/dzwvip/gorm-oracle v0.1.2/go.mod h1:TbF7idnO9UgGpJ0qJpDZby1/wGquzP5GYof88ScBITE=
+github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
+github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/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/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-pg/pg/v10 v10.11.0 h1:CMKJqLgTrfpE/aOVeLdybezR2om071Vh38OLZjsyMI0=
+github.com/go-pg/pg/v10 v10.11.0/go.mod h1:4BpHRoxE61y4Onpof3x1a2SQvi9c+q1dJnrNdMjsroA=
+github.com/go-pg/zerochecker v0.2.0 h1:pp7f72c3DobMWOb2ErtZsnrPaSvHd2W4o9//8HtF4mU=
+github.com/go-pg/zerochecker v0.2.0/go.mod h1:NJZ4wKL0NmTtz0GKCoJ8kym6Xn/EQzXRl2OnAe7MmDo=
+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/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
+github.com/golang-jwt/jwt/v5 v5.2.2/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/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+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/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
+github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
+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.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+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/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
+github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
+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.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
+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.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+github.com/lib/pq v1.10.9/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/mark3labs/mcp-go v0.41.1 h1:w78eWfiQam2i8ICL7AL0WFiq7KHNJQ6UB53ZVtH4KGA=
+github.com/mark3labs/mcp-go v0.41.1/go.mod h1:T7tUa2jO6MavG+3P25Oy/jR7iCeJPHImCZHRymCn39g=
+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/archives v0.1.1 h1:c7J3qXN1FB54y0qiUXiq9Bxk4eCUc8pdXWwOhZdRzeY=
+github.com/mholt/archives v0.1.1/go.mod h1:FQVz01Q2uXKB/35CXeW/QFO23xT+hSCGZHVtha78U4I=
+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/minio/minlz v1.0.0 h1:Kj7aJZ1//LlTP1DM8Jm7lNKvvJS2m74gyyXXn3+uJWQ=
+github.com/minio/minlz v1.0.0/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
+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.1.0 h1:JQl9ZoBPDy+nIZGb1mx8+anfHp/LV3NE2MjMiv0ct/U=
+github.com/nwaples/rardecode/v2 v2.1.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
+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/pgvector/pgvector-go v0.3.0 h1:Ij+Yt78R//uYqs3Zk35evZFvr+G0blW0OUN+Q2D1RWc=
+github.com/pgvector/pgvector-go v0.3.0/go.mod h1:duFy+PXWfW7QQd5ibqutBO4GxLsUZ9RVXhFZGIBsWSA=
+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/sijms/go-ora/v2 v2.7.17 h1:M/pYIqjaMUeBxyzOWp2oj4ntF6fHSBloJWGNH9vbmsU=
+github.com/sijms/go-ora/v2 v2.7.17/go.mod h1:EHxlY6x7y9HAsdfumurRfTd+v8NrEOTR3Xl4FWlH6xk=
+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/thoas/go-funk v0.7.0 h1:GmirKrs6j6zJbhJIficOsz2aAI7700KsU/5YrdHRM1Y=
+github.com/thoas/go-funk v0.7.0/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q=
+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/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
+github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
+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/uptrace/bun v1.1.12 h1:sOjDVHxNTuM6dNGaba0wUuz7KvDE1BmNu9Gqs2gJSXQ=
+github.com/uptrace/bun v1.1.12/go.mod h1:NPG6JGULBeQ9IU6yHp7YGELRa5Agmd7ATZdz4tGZ6z0=
+github.com/uptrace/bun/dialect/pgdialect v1.1.12 h1:m/CM1UfOkoBTglGO5CUTKnIKKOApOYxkcP2qn0F9tJk=
+github.com/uptrace/bun/dialect/pgdialect v1.1.12/go.mod h1:Ij6WIxQILxLlL2frUBxUBOZJtLElD2QQNDcu/PWDHTc=
+github.com/uptrace/bun/driver/pgdriver v1.1.12 h1:3rRWB1GK0psTJrHwxzNfEij2MLibggiLdTqjTtfHc1w=
+github.com/uptrace/bun/driver/pgdriver v1.1.12/go.mod h1:ssYUP+qwSEgeDDS1xm2XBip9el1y9Mi5mTAvLoiADLM=
+github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94=
+github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ=
+github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
+github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
+github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc=
+github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
+github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
+github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
+github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
+github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
+github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
+github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
+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/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
+github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
+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.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
+golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
+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.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
+golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
+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.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
+golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
+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/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
+golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+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.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
+golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+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/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
+golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
+golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
+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.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
+golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
+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.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
+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/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
+gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/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/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=
+mellium.im/sasl v0.3.1 h1:wE0LW6g7U83vhvxjC1IY8DnXM+EU095yeo8XClvCdfo=
+mellium.im/sasl v0.3.1/go.mod h1:xm59PUYpZHhgQ9ZqoJ5QaCqzWMi8IeS49dhp6plPCzw=
+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/server/initialize/db_list.go b/server/initialize/db_list.go
new file mode 100644
index 0000000..5d65c0a
--- /dev/null
+++ b/server/initialize/db_list.go
@@ -0,0 +1,36 @@
+package initialize
+
+import (
+ "git.echol.cn/loser/st/server/config"
+ "git.echol.cn/loser/st/server/global"
+ "gorm.io/gorm"
+)
+
+const sys = "system"
+
+func DBList() {
+ dbMap := make(map[string]*gorm.DB)
+ for _, info := range global.GVA_CONFIG.DBList {
+ if info.Disable {
+ continue
+ }
+ switch info.Type {
+ case "mysql":
+ dbMap[info.AliasName] = GormMysqlByConfig(config.Mysql{GeneralDB: info.GeneralDB})
+ case "mssql":
+ dbMap[info.AliasName] = GormMssqlByConfig(config.Mssql{GeneralDB: info.GeneralDB})
+ case "pgsql":
+ dbMap[info.AliasName] = GormPgSqlByConfig(config.Pgsql{GeneralDB: info.GeneralDB})
+ case "oracle":
+ dbMap[info.AliasName] = GormOracleByConfig(config.Oracle{GeneralDB: info.GeneralDB})
+ default:
+ continue
+ }
+ }
+ // 做特殊判断,是否有迁移
+ // 适配低版本迁移多数据库版本
+ if sysDB, ok := dbMap[sys]; ok {
+ global.GVA_DB = sysDB
+ }
+ global.GVA_DBList = dbMap
+}
diff --git a/server/initialize/ensure_tables.go b/server/initialize/ensure_tables.go
new file mode 100644
index 0000000..022b917
--- /dev/null
+++ b/server/initialize/ensure_tables.go
@@ -0,0 +1,112 @@
+package initialize
+
+import (
+ "context"
+
+ "git.echol.cn/loser/st/server/model/example"
+ sysModel "git.echol.cn/loser/st/server/model/system"
+ "git.echol.cn/loser/st/server/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{},
+ sysModel.SysVersion{},
+ sysModel.SysError{},
+ sysModel.SysLoginLog{},
+ sysModel.SysApiToken{},
+ adapter.CasbinRule{},
+
+ example.ExaFile{},
+ example.ExaCustomer{},
+ example.ExaFileChunk{},
+ example.ExaFileUploadAndDownload{},
+ example.ExaAttachmentCategory{},
+ }
+ 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{},
+ }
+ yes := true
+ for _, t := range tables {
+ yes = yes && db.Migrator().HasTable(t)
+ }
+ return yes
+}
diff --git a/server/initialize/fix_world_info_table.sql b/server/initialize/fix_world_info_table.sql
new file mode 100644
index 0000000..5fd8bf3
--- /dev/null
+++ b/server/initialize/fix_world_info_table.sql
@@ -0,0 +1,9 @@
+-- 修复 ai_world_info 表结构
+-- 如果表存在旧的 name 字段,需要删除并重新创建
+
+-- 删除旧表(如果存在)
+DROP TABLE IF EXISTS ai_character_world_info CASCADE;
+DROP TABLE IF EXISTS ai_world_info CASCADE;
+
+-- 表将由 Gorm AutoMigrate 自动创建
+-- 重启服务器即可
diff --git a/server/initialize/gorm.go b/server/initialize/gorm.go
new file mode 100644
index 0000000..af5c610
--- /dev/null
+++ b/server/initialize/gorm.go
@@ -0,0 +1,105 @@
+package initialize
+
+import (
+ "os"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/app"
+ "git.echol.cn/loser/st/server/model/example"
+ "git.echol.cn/loser/st/server/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() {
+ if global.GVA_CONFIG.System.DisableAutoMigrate {
+ global.GVA_LOG.Info("auto-migrate is disabled, skipping table registration")
+ return
+ }
+
+ // 初始化 PostgreSQL 扩展(仅创建 pgvector 扩展)
+ InitPgSQLExtension()
+
+ db := global.GVA_DB
+ err := db.AutoMigrate(
+
+ // System tables (管理后台表 - 不修改)
+ 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{},
+ system.SysVersion{},
+ system.SysError{},
+ system.SysApiToken{},
+ system.SysLoginLog{},
+
+ // Example tables
+ example.ExaFile{},
+ example.ExaCustomer{},
+ example.ExaFileChunk{},
+ example.ExaFileUploadAndDownload{},
+ example.ExaAttachmentCategory{},
+
+ // App tables (前台应用表 - 新增)
+ app.AppUser{},
+ app.AppUserSession{},
+ app.AICharacter{},
+ app.Conversation{},
+ app.Message{},
+ app.AIConfig{},
+ app.AIPreset{},
+ )
+ if err != nil {
+ global.GVA_LOG.Error("register table failed", zap.Error(err))
+ os.Exit(0)
+ }
+
+ // 创建向量索引(必须在 AutoMigrate 之后)
+ CreateVectorIndexes()
+
+ 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/server/initialize/gorm_biz.go b/server/initialize/gorm_biz.go
new file mode 100644
index 0000000..1df3094
--- /dev/null
+++ b/server/initialize/gorm_biz.go
@@ -0,0 +1,14 @@
+package initialize
+
+import (
+ "git.echol.cn/loser/st/server/global"
+)
+
+func bizModel() error {
+ db := global.GVA_DB
+ err := db.AutoMigrate()
+ if err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/server/initialize/gorm_mssql.go b/server/initialize/gorm_mssql.go
new file mode 100644
index 0000000..6a6e4c4
--- /dev/null
+++ b/server/initialize/gorm_mssql.go
@@ -0,0 +1,64 @@
+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/st/server/config"
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/initialize/internal"
+ "gorm.io/driver/sqlserver"
+ "gorm.io/gorm"
+)
+
+// GormMssql 初始化Mssql数据库
+// Author [LouisZhang](191180776@qq.com)
+func GormMssql() *gorm.DB {
+ m := global.GVA_CONFIG.Mssql
+ if m.Dbname == "" {
+ return nil
+ }
+ mssqlConfig := sqlserver.Config{
+ DSN: m.Dsn(), // DSN data source name
+ DefaultStringSize: 191, // string 类型字段的默认长度
+ }
+ // 数据库配置
+ general := m.GeneralDB
+ if db, err := gorm.Open(sqlserver.New(mssqlConfig), internal.Gorm.Config(general)); 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 类型字段的默认长度
+ }
+ // 数据库配置
+ general := m.GeneralDB
+ if db, err := gorm.Open(sqlserver.New(mssqlConfig), internal.Gorm.Config(general)); 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/server/initialize/gorm_mysql.go b/server/initialize/gorm_mysql.go
new file mode 100644
index 0000000..e3222b2
--- /dev/null
+++ b/server/initialize/gorm_mysql.go
@@ -0,0 +1,48 @@
+package initialize
+
+import (
+ "git.echol.cn/loser/st/server/config"
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/initialize/internal"
+ _ "github.com/go-sql-driver/mysql"
+ "gorm.io/driver/mysql"
+ "gorm.io/gorm"
+)
+
+// GormMysql 初始化Mysql数据库
+// Author [piexlmax](https://github.com/piexlmax)
+// Author [SliverHorn](https://github.com/SliverHorn)
+// Author [ByteZhou-2018](https://github.com/ByteZhou-2018)
+func GormMysql() *gorm.DB {
+ m := global.GVA_CONFIG.Mysql
+ return initMysqlDatabase(m)
+}
+
+// GormMysqlByConfig 通过传入配置初始化Mysql数据库
+func GormMysqlByConfig(m config.Mysql) *gorm.DB {
+ return initMysqlDatabase(m)
+}
+
+// initMysqlDatabase 初始化Mysql数据库的辅助函数
+func initMysqlDatabase(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, // 根据版本自动配置
+ }
+ // 数据库配置
+ general := m.GeneralDB
+ if db, err := gorm.Open(mysql.New(mysqlConfig), internal.Gorm.Config(general)); err != nil {
+ panic(err)
+ } else {
+ db.InstanceSet("gorm:table_options", "ENGINE="+m.Engine)
+ sqlDB, _ := db.DB()
+ sqlDB.SetMaxIdleConns(m.MaxIdleConns)
+ sqlDB.SetMaxOpenConns(m.MaxOpenConns)
+ return db
+ }
+}
diff --git a/server/initialize/gorm_oracle.go b/server/initialize/gorm_oracle.go
new file mode 100644
index 0000000..ab4f476
--- /dev/null
+++ b/server/initialize/gorm_oracle.go
@@ -0,0 +1,37 @@
+package initialize
+
+import (
+ "git.echol.cn/loser/st/server/config"
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/initialize/internal"
+ oracle "github.com/dzwvip/gorm-oracle"
+ "gorm.io/gorm"
+)
+
+// GormOracle 初始化oracle数据库
+func GormOracle() *gorm.DB {
+ m := global.GVA_CONFIG.Oracle
+ return initOracleDatabase(m)
+}
+
+// GormOracleByConfig 初始化Oracle数据库用过传入配置
+func GormOracleByConfig(m config.Oracle) *gorm.DB {
+ return initOracleDatabase(m)
+}
+
+// initOracleDatabase 初始化Oracle数据库的辅助函数
+func initOracleDatabase(m config.Oracle) *gorm.DB {
+ if m.Dbname == "" {
+ return nil
+ }
+ // 数据库配置
+ general := m.GeneralDB
+ if db, err := gorm.Open(oracle.Open(m.Dsn()), internal.Gorm.Config(general)); err != nil {
+ panic(err)
+ } else {
+ sqlDB, _ := db.DB()
+ sqlDB.SetMaxIdleConns(m.MaxIdleConns)
+ sqlDB.SetMaxOpenConns(m.MaxOpenConns)
+ return db
+ }
+}
diff --git a/server/initialize/gorm_pgsql.go b/server/initialize/gorm_pgsql.go
new file mode 100644
index 0000000..47ea3f5
--- /dev/null
+++ b/server/initialize/gorm_pgsql.go
@@ -0,0 +1,43 @@
+package initialize
+
+import (
+ "git.echol.cn/loser/st/server/config"
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/initialize/internal"
+ "gorm.io/driver/postgres"
+ "gorm.io/gorm"
+)
+
+// GormPgSql 初始化 Postgresql 数据库
+// Author [piexlmax](https://github.com/piexlmax)
+// Author [SliverHorn](https://github.com/SliverHorn)
+func GormPgSql() *gorm.DB {
+ p := global.GVA_CONFIG.Pgsql
+ return initPgSqlDatabase(p)
+}
+
+// GormPgSqlByConfig 初始化 Postgresql 数据库 通过指定参数
+func GormPgSqlByConfig(p config.Pgsql) *gorm.DB {
+ return initPgSqlDatabase(p)
+}
+
+// initPgSqlDatabase 初始化 Postgresql 数据库的辅助函数
+func initPgSqlDatabase(p config.Pgsql) *gorm.DB {
+ if p.Dbname == "" {
+ return nil
+ }
+ pgsqlConfig := postgres.Config{
+ DSN: p.Dsn(), // DSN data source name
+ PreferSimpleProtocol: false,
+ }
+ // 数据库配置
+ general := p.GeneralDB
+ if db, err := gorm.Open(postgres.New(pgsqlConfig), internal.Gorm.Config(general)); err != nil {
+ panic(err)
+ } else {
+ sqlDB, _ := db.DB()
+ sqlDB.SetMaxIdleConns(p.MaxIdleConns)
+ sqlDB.SetMaxOpenConns(p.MaxOpenConns)
+ return db
+ }
+}
diff --git a/server/initialize/gorm_pgsql_extension.go b/server/initialize/gorm_pgsql_extension.go
new file mode 100644
index 0000000..ccf4a2b
--- /dev/null
+++ b/server/initialize/gorm_pgsql_extension.go
@@ -0,0 +1,48 @@
+package initialize
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ "go.uber.org/zap"
+)
+
+// InitPgSQLExtension 初始化 PostgreSQL 扩展(仅创建 pgvector 扩展)
+// 必须在 AutoMigrate 之前调用
+func InitPgSQLExtension() {
+ if global.GVA_CONFIG.System.DbType != "pgsql" {
+ return
+ }
+
+ db := global.GVA_DB
+
+ // 安装 pgvector 扩展(用于向量存储)
+ if err := db.Exec("CREATE EXTENSION IF NOT EXISTS vector").Error; err != nil {
+ global.GVA_LOG.Error("failed to create pgvector extension", zap.Error(err))
+ global.GVA_LOG.Warn("请确保 PostgreSQL 已安装 pgvector 扩展")
+ } else {
+ global.GVA_LOG.Info("pgvector extension is ready")
+ }
+}
+
+// CreateVectorIndexes 创建向量索引
+// 必须在 AutoMigrate 之后调用(确保表已存在)
+func CreateVectorIndexes() {
+ if global.GVA_CONFIG.System.DbType != "pgsql" {
+ return
+ }
+
+ db := global.GVA_DB
+
+ // 为 ai_memory_vectors 表创建 HNSW 索引(余弦相似度)
+ sql := `
+ CREATE INDEX IF NOT EXISTS idx_memory_vectors_embedding
+ ON ai_memory_vectors
+ USING hnsw (embedding vector_cosine_ops)
+ `
+
+ if err := db.Exec(sql).Error; err != nil {
+ global.GVA_LOG.Error("failed to create vector indexes", zap.Error(err))
+ return
+ }
+
+ global.GVA_LOG.Info("vector indexes created successfully")
+}
diff --git a/server/initialize/gorm_sqlite.go b/server/initialize/gorm_sqlite.go
new file mode 100644
index 0000000..0423d56
--- /dev/null
+++ b/server/initialize/gorm_sqlite.go
@@ -0,0 +1,38 @@
+package initialize
+
+import (
+ "git.echol.cn/loser/st/server/config"
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/initialize/internal"
+ "github.com/glebarez/sqlite"
+ "gorm.io/gorm"
+)
+
+// GormSqlite 初始化Sqlite数据库
+func GormSqlite() *gorm.DB {
+ s := global.GVA_CONFIG.Sqlite
+ return initSqliteDatabase(s)
+}
+
+// GormSqliteByConfig 初始化Sqlite数据库用过传入配置
+func GormSqliteByConfig(s config.Sqlite) *gorm.DB {
+ return initSqliteDatabase(s)
+}
+
+// initSqliteDatabase 初始化Sqlite数据库辅助函数
+func initSqliteDatabase(s config.Sqlite) *gorm.DB {
+ if s.Dbname == "" {
+ return nil
+ }
+
+ // 数据库配置
+ general := s.GeneralDB
+ if db, err := gorm.Open(sqlite.Open(s.Dsn()), internal.Gorm.Config(general)); err != nil {
+ panic(err)
+ } else {
+ sqlDB, _ := db.DB()
+ sqlDB.SetMaxIdleConns(s.MaxIdleConns)
+ sqlDB.SetMaxOpenConns(s.MaxOpenConns)
+ return db
+ }
+}
diff --git a/server/initialize/init.go b/server/initialize/init.go
new file mode 100644
index 0000000..097c152
--- /dev/null
+++ b/server/initialize/init.go
@@ -0,0 +1,15 @@
+// 假设这是初始化逻辑的一部分
+
+package initialize
+
+import (
+ "git.echol.cn/loser/st/server/utils"
+)
+
+// 初始化全局函数
+func SetupHandlers() {
+ // 注册系统重载处理函数
+ utils.GlobalSystemEvents.RegisterReloadHandler(func() error {
+ return Reload()
+ })
+}
diff --git a/server/initialize/internal/gorm.go b/server/initialize/internal/gorm.go
new file mode 100644
index 0000000..e398cfa
--- /dev/null
+++ b/server/initialize/internal/gorm.go
@@ -0,0 +1,31 @@
+package internal
+
+import (
+ "time"
+
+ "git.echol.cn/loser/st/server/config"
+ "gorm.io/gorm"
+ "gorm.io/gorm/logger"
+ "gorm.io/gorm/schema"
+)
+
+var Gorm = new(_gorm)
+
+type _gorm struct{}
+
+// Config gorm 自定义配置
+// Author [SliverHorn](https://github.com/SliverHorn)
+func (g *_gorm) Config(general config.GeneralDB) *gorm.Config {
+ return &gorm.Config{
+ Logger: logger.New(NewWriter(general), logger.Config{
+ SlowThreshold: 200 * time.Millisecond,
+ LogLevel: general.LogLevel(),
+ Colorful: true,
+ }),
+ NamingStrategy: schema.NamingStrategy{
+ TablePrefix: general.Prefix,
+ SingularTable: general.Singular,
+ },
+ DisableForeignKeyConstraintWhenMigrating: true,
+ }
+}
diff --git a/server/initialize/internal/gorm_logger_writer.go b/server/initialize/internal/gorm_logger_writer.go
new file mode 100644
index 0000000..3d8562a
--- /dev/null
+++ b/server/initialize/internal/gorm_logger_writer.go
@@ -0,0 +1,42 @@
+package internal
+
+import (
+ "fmt"
+
+ "git.echol.cn/loser/st/server/config"
+ "git.echol.cn/loser/st/server/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/server/initialize/internal/mongo.go b/server/initialize/internal/mongo.go
new file mode 100644
index 0000000..56ea61a
--- /dev/null
+++ b/server/initialize/internal/mongo.go
@@ -0,0 +1,30 @@
+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/server/initialize/mcp.go b/server/initialize/mcp.go
new file mode 100644
index 0000000..282a516
--- /dev/null
+++ b/server/initialize/mcp.go
@@ -0,0 +1,25 @@
+package initialize
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ mcpTool "git.echol.cn/loser/st/server/mcp"
+ "github.com/mark3labs/mcp-go/server"
+)
+
+func McpRun() *server.SSEServer {
+ config := global.GVA_CONFIG.MCP
+
+ s := server.NewMCPServer(
+ config.Name,
+ config.Version,
+ )
+
+ global.GVA_MCP_SERVER = s
+
+ mcpTool.RegisterAllTools(s)
+
+ return server.NewSSEServer(s,
+ server.WithSSEEndpoint(config.SSEPath),
+ server.WithMessageEndpoint(config.MessagePath),
+ server.WithBaseURL(config.UrlPrefix))
+}
diff --git a/server/initialize/mongo.go b/server/initialize/mongo.go
new file mode 100644
index 0000000..1512a8f
--- /dev/null
+++ b/server/initialize/mongo.go
@@ -0,0 +1,156 @@
+package initialize
+
+import (
+ "context"
+ "fmt"
+ "sort"
+ "strings"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/initialize/internal"
+ "git.echol.cn/loser/st/server/utils"
+ "github.com/pkg/errors"
+ "github.com/qiniu/qmgo"
+ "github.com/qiniu/qmgo/options"
+ "go.mongodb.org/mongo-driver/bson"
+ option "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+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/server/initialize/other.go b/server/initialize/other.go
new file mode 100644
index 0000000..21394cc
--- /dev/null
+++ b/server/initialize/other.go
@@ -0,0 +1,33 @@
+package initialize
+
+import (
+ "bufio"
+ "os"
+ "strings"
+
+ "github.com/songzhibin97/gkit/cache/local_cache"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/utils"
+)
+
+func OtherInit() {
+ dr, err := utils.ParseDuration(global.GVA_CONFIG.JWT.ExpiresTime)
+ if err != nil {
+ panic(err)
+ }
+ _, err = utils.ParseDuration(global.GVA_CONFIG.JWT.BufferTime)
+ if err != nil {
+ panic(err)
+ }
+
+ global.BlackCache = local_cache.NewCache(
+ local_cache.SetDefaultExpire(dr),
+ )
+ file, err := os.Open("go.mod")
+ if err == nil && global.GVA_CONFIG.AutoCode.Module == "" {
+ scanner := bufio.NewScanner(file)
+ scanner.Scan()
+ global.GVA_CONFIG.AutoCode.Module = strings.TrimPrefix(scanner.Text(), "module ")
+ }
+}
diff --git a/server/initialize/plugin.go b/server/initialize/plugin.go
new file mode 100644
index 0000000..470e057
--- /dev/null
+++ b/server/initialize/plugin.go
@@ -0,0 +1,15 @@
+package initialize
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ "github.com/gin-gonic/gin"
+)
+
+func InstallPlugin(PrivateGroup *gin.RouterGroup, PublicRouter *gin.RouterGroup, engine *gin.Engine) {
+ if global.GVA_DB == nil {
+ global.GVA_LOG.Info("项目暂未初始化,无法安装插件,初始化后重启项目即可完成插件安装")
+ return
+ }
+ bizPluginV1(PrivateGroup, PublicRouter)
+ bizPluginV2(engine)
+}
diff --git a/server/initialize/plugin_biz_v1.go b/server/initialize/plugin_biz_v1.go
new file mode 100644
index 0000000..97de009
--- /dev/null
+++ b/server/initialize/plugin_biz_v1.go
@@ -0,0 +1,36 @@
+package initialize
+
+import (
+ "fmt"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/plugin/email"
+ "git.echol.cn/loser/st/server/utils/plugin"
+ "github.com/gin-gonic/gin"
+)
+
+func PluginInit(group *gin.RouterGroup, Plugin ...plugin.Plugin) {
+ for i := range Plugin {
+ fmt.Println(Plugin[i].RouterPath(), "注册开始!")
+ PluginGroup := group.Group(Plugin[i].RouterPath())
+ Plugin[i].Register(PluginGroup)
+ fmt.Println(Plugin[i].RouterPath(), "注册成功!")
+ }
+}
+
+func bizPluginV1(group ...*gin.RouterGroup) {
+ private := group[0]
+ public := group[1]
+ // 添加跟角色挂钩权限的插件 示例 本地示例模式于在线仓库模式注意上方的import 可以自行切换 效果相同
+ PluginInit(private, email.CreateEmailPlug(
+ global.GVA_CONFIG.Email.To,
+ global.GVA_CONFIG.Email.From,
+ global.GVA_CONFIG.Email.Host,
+ global.GVA_CONFIG.Email.Secret,
+ global.GVA_CONFIG.Email.Nickname,
+ global.GVA_CONFIG.Email.Port,
+ global.GVA_CONFIG.Email.IsSSL,
+ global.GVA_CONFIG.Email.IsLoginAuth,
+ ))
+ holder(public, private)
+}
diff --git a/server/initialize/plugin_biz_v2.go b/server/initialize/plugin_biz_v2.go
new file mode 100644
index 0000000..57e6474
--- /dev/null
+++ b/server/initialize/plugin_biz_v2.go
@@ -0,0 +1,16 @@
+package initialize
+
+import (
+ _ "git.echol.cn/loser/st/server/plugin"
+ "git.echol.cn/loser/st/server/utils/plugin/v2"
+ "github.com/gin-gonic/gin"
+)
+
+func PluginInitV2(group *gin.Engine, plugins ...plugin.Plugin) {
+ for i := 0; i < len(plugins); i++ {
+ plugins[i].Register(group)
+ }
+}
+func bizPluginV2(engine *gin.Engine) {
+ PluginInitV2(engine, plugin.Registered()...)
+}
diff --git a/server/initialize/redis.go b/server/initialize/redis.go
new file mode 100644
index 0000000..5e09d59
--- /dev/null
+++ b/server/initialize/redis.go
@@ -0,0 +1,59 @@
+package initialize
+
+import (
+ "context"
+
+ "git.echol.cn/loser/st/server/config"
+ "git.echol.cn/loser/st/server/global"
+
+ "github.com/redis/go-redis/v9"
+ "go.uber.org/zap"
+)
+
+func initRedisClient(redisCfg config.Redis) (redis.UniversalClient, error) {
+ var client redis.UniversalClient
+ // 使用集群模式
+ if redisCfg.UseCluster {
+ client = redis.NewClusterClient(&redis.ClusterOptions{
+ Addrs: redisCfg.ClusterAddrs,
+ Password: redisCfg.Password,
+ })
+ } else {
+ // 使用单例模式
+ client = redis.NewClient(&redis.Options{
+ Addr: redisCfg.Addr,
+ Password: redisCfg.Password,
+ DB: redisCfg.DB,
+ })
+ }
+ pong, err := client.Ping(context.Background()).Result()
+ if err != nil {
+ global.GVA_LOG.Error("redis connect ping failed, err:", zap.String("name", redisCfg.Name), zap.Error(err))
+ return nil, err
+ }
+
+ global.GVA_LOG.Info("redis connect ping response:", zap.String("name", redisCfg.Name), zap.String("pong", pong))
+ return client, nil
+}
+
+func Redis() {
+ redisClient, err := initRedisClient(global.GVA_CONFIG.Redis)
+ if err != nil {
+ panic(err)
+ }
+ global.GVA_REDIS = redisClient
+}
+
+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/server/initialize/register_init.go b/server/initialize/register_init.go
new file mode 100644
index 0000000..0a945f8
--- /dev/null
+++ b/server/initialize/register_init.go
@@ -0,0 +1,10 @@
+package initialize
+
+import (
+ _ "git.echol.cn/loser/st/server/source/example"
+ _ "git.echol.cn/loser/st/server/source/system"
+)
+
+func init() {
+ // do nothing,only import source package so that inits can be registered
+}
diff --git a/server/initialize/reload.go b/server/initialize/reload.go
new file mode 100644
index 0000000..4a461d3
--- /dev/null
+++ b/server/initialize/reload.go
@@ -0,0 +1,45 @@
+package initialize
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ "go.uber.org/zap"
+)
+
+// Reload 优雅地重新加载系统配置
+func Reload() error {
+ global.GVA_LOG.Info("正在重新加载系统配置...")
+
+ // 重新加载配置文件
+ if err := global.GVA_VP.ReadInConfig(); err != nil {
+ global.GVA_LOG.Error("重新读取配置文件失败!", zap.Error(err))
+ return err
+ }
+
+ // 重新初始化数据库连接
+ if global.GVA_DB != nil {
+ db, _ := global.GVA_DB.DB()
+ err := db.Close()
+ if err != nil {
+ global.GVA_LOG.Error("关闭原数据库连接失败!", zap.Error(err))
+ return err
+ }
+ }
+
+ // 重新建立数据库连接
+ global.GVA_DB = Gorm()
+
+ // 重新初始化其他配置
+ OtherInit()
+ DBList()
+
+ if global.GVA_DB != nil {
+ // 确保数据库表结构是最新的
+ RegisterTables()
+ }
+
+ // 重新初始化定时任务
+ Timer()
+
+ global.GVA_LOG.Info("系统配置重新加载完成")
+ return nil
+}
diff --git a/server/initialize/router.go b/server/initialize/router.go
new file mode 100644
index 0000000..8a6d4d0
--- /dev/null
+++ b/server/initialize/router.go
@@ -0,0 +1,168 @@
+package initialize
+
+import (
+ "net/http"
+ "os"
+
+ "git.echol.cn/loser/st/server/docs"
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/middleware"
+ "git.echol.cn/loser/st/server/router"
+ "github.com/gin-gonic/gin"
+ swaggerFiles "github.com/swaggo/files"
+ ginSwagger "github.com/swaggo/gin-swagger"
+)
+
+type justFilesFilesystem struct {
+ fs http.FileSystem
+}
+
+func (fs justFilesFilesystem) Open(name string) (http.File, error) {
+ f, err := fs.fs.Open(name)
+ if err != nil {
+ return nil, err
+ }
+
+ stat, err := f.Stat()
+ if stat.IsDir() {
+ return nil, os.ErrPermission
+ }
+
+ return f, nil
+}
+
+// 初始化总路由
+
+func Routers() *gin.Engine {
+ Router := gin.New()
+
+ // 设置文件上传大小限制(10MB)
+ Router.MaxMultipartMemory = 10 << 20 // 10 MB
+
+ // 使用自定义的 Recovery 中间件,记录 panic 并入库
+ Router.Use(middleware.GinRecovery(true))
+ if gin.Mode() == gin.DebugMode {
+ Router.Use(gin.Logger())
+ }
+
+ // 跨域配置(前台应用需要)
+ // 必须在静态文件路由之前注册,否则静态文件跨域会失败
+ Router.Use(middleware.Cors())
+ global.GVA_LOG.Info("use middleware cors")
+
+ if !global.GVA_CONFIG.MCP.Separate {
+
+ sseServer := McpRun()
+
+ // 注册mcp服务
+ Router.GET(global.GVA_CONFIG.MCP.SSEPath, func(c *gin.Context) {
+ sseServer.SSEHandler().ServeHTTP(c.Writer, c.Request)
+ })
+
+ Router.POST(global.GVA_CONFIG.MCP.MessagePath, func(c *gin.Context) {
+ sseServer.MessageHandler().ServeHTTP(c.Writer, c.Request)
+ })
+ }
+
+ systemRouter := router.RouterGroupApp.System
+ exampleRouter := router.RouterGroupApp.Example
+ appRouter := router.RouterGroupApp.App // 前台应用路由
+
+ // SillyTavern 核心脚本静态文件服务
+ // 所有核心文件存储在 data/st-core-scripts/ 下,完全独立于 web-app/ 目录
+ stCorePath := "data/st-core-scripts"
+ if _, err := os.Stat(stCorePath); err == nil {
+ Router.Static("/scripts", stCorePath+"/scripts")
+ Router.Static("/css", stCorePath+"/css")
+ Router.Static("/img", stCorePath+"/img")
+ Router.Static("/webfonts", stCorePath+"/webfonts")
+ Router.Static("/lib", stCorePath+"/lib") // SillyTavern 依赖的第三方库
+ Router.Static("/locales", stCorePath+"/locales") // 国际化文件
+ Router.StaticFile("/script.js", stCorePath+"/script.js") // SillyTavern 主入口
+ Router.StaticFile("/lib.js", stCorePath+"/lib.js") // Webpack 编译后的 lib.js
+ global.GVA_LOG.Info("SillyTavern 核心脚本服务已启动: " + stCorePath)
+ } else {
+ global.GVA_LOG.Warn("SillyTavern 核心脚本目录不存在: " + stCorePath)
+ }
+
+ // 管理后台前端静态文件(web)
+ // 如果想要不使用nginx代理前端网页,可以修改 web/.env.production 下的
+ // VUE_APP_BASE_API = /
+ // VUE_APP_BASE_PATH = http://localhost
+ // 然后执行打包命令 npm run build。在打开下面3行注释
+ // Router.StaticFile("/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文件")
+
+ 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.InitSysVersionRouter(PrivateGroup) // 发版相关路由
+ 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) // 参数管理
+ systemRouter.InitSysErrorRouter(PrivateGroup, PublicGroup) // 错误日志
+ systemRouter.InitLoginLogRouter(PrivateGroup) // 登录日志
+ systemRouter.InitApiTokenRouter(PrivateGroup) // apiToken签发
+ systemRouter.InitSkillsRouter(PrivateGroup) // Skills 定义器
+ exampleRouter.InitCustomerRouter(PrivateGroup) // 客户路由
+ exampleRouter.InitFileUploadAndDownloadRouter(PrivateGroup) // 文件上传下载功能路由
+ exampleRouter.InitAttachmentCategoryRouterRouter(PrivateGroup) // 文件上传下载分类
+
+ }
+
+ // 前台应用路由(新增)
+ {
+ appGroup := PublicGroup.Group("app") // 统一使用 /app 前缀
+ appRouter.InitAuthRouter(appGroup) // 认证路由:/app/auth/* 和 /app/user/*
+ appRouter.InitCharacterRouter(appGroup) // 角色卡路由:/app/character/*
+ appRouter.InitConversationRouter(appGroup) // 对话路由:/app/conversation/*
+ appRouter.InitAIConfigRouter(appGroup) // AI配置路由:/app/ai-config/*
+ appRouter.InitPresetRouter(appGroup) // 预设路由:/app/preset/*
+ appRouter.InitUploadRouter(appGroup) // 上传路由:/app/upload/*
+ }
+
+ //插件路由安装
+ InstallPlugin(PrivateGroup, PublicGroup, Router)
+
+ // 注册业务路由
+ initBizRouter(PrivateGroup, PublicGroup)
+
+ global.GVA_ROUTERS = Router.Routes()
+
+ global.GVA_LOG.Info("router register success")
+ return Router
+}
diff --git a/server/initialize/router_biz.go b/server/initialize/router_biz.go
new file mode 100644
index 0000000..2d83f48
--- /dev/null
+++ b/server/initialize/router_biz.go
@@ -0,0 +1,19 @@
+package initialize
+
+import (
+ "git.echol.cn/loser/st/server/router"
+ "github.com/gin-gonic/gin"
+)
+
+// 占位方法,保证文件可以正确加载,避免go空变量检测报错,请勿删除。
+func holder(routers ...*gin.RouterGroup) {
+ _ = routers
+ _ = router.RouterGroupApp
+}
+
+func initBizRouter(routers ...*gin.RouterGroup) {
+ privateGroup := routers[0]
+ publicGroup := routers[1]
+
+ holder(publicGroup, privateGroup)
+}
diff --git a/server/initialize/timer.go b/server/initialize/timer.go
new file mode 100644
index 0000000..e0f68f3
--- /dev/null
+++ b/server/initialize/timer.go
@@ -0,0 +1,38 @@
+package initialize
+
+import (
+ "fmt"
+
+ "git.echol.cn/loser/st/server/task"
+
+ "github.com/robfig/cron/v3"
+
+ "git.echol.cn/loser/st/server/global"
+)
+
+func Timer() {
+ go func() {
+ var option []cron.Option
+ option = append(option, cron.WithSeconds())
+ // 清理DB定时任务
+ _, err := global.GVA_Timer.AddTaskByFunc("ClearDB", "@daily", func() {
+ err := task.ClearTable(global.GVA_DB) // 定时任务方法定在task文件包中
+ if err != nil {
+ fmt.Println("timer error:", err)
+ }
+ }, "定时清理数据库【日志,黑名单】内容", option...)
+ if err != nil {
+ fmt.Println("add timer error:", err)
+ }
+
+ // 其他定时任务定在这里 参考上方使用方法
+
+ //_, err := global.GVA_Timer.AddTaskByFunc("定时任务标识", "corn表达式", func() {
+ // 具体执行内容...
+ // ......
+ //}, option...)
+ //if err != nil {
+ // fmt.Println("add timer error:", err)
+ //}
+ }()
+}
diff --git a/server/initialize/validator.go b/server/initialize/validator.go
new file mode 100644
index 0000000..7b927d2
--- /dev/null
+++ b/server/initialize/validator.go
@@ -0,0 +1,22 @@
+package initialize
+
+import "git.echol.cn/loser/st/server/utils"
+
+func init() {
+ _ = utils.RegisterRule("PageVerify",
+ utils.Rules{
+ "Page": {utils.NotEmpty()},
+ "PageSize": {utils.NotEmpty()},
+ },
+ )
+ _ = utils.RegisterRule("IdVerify",
+ utils.Rules{
+ "Id": {utils.NotEmpty()},
+ },
+ )
+ _ = utils.RegisterRule("AuthorityIdVerify",
+ utils.Rules{
+ "AuthorityId": {utils.NotEmpty()},
+ },
+ )
+}
diff --git a/server/main.go b/server/main.go
new file mode 100644
index 0000000..dead586
--- /dev/null
+++ b/server/main.go
@@ -0,0 +1,51 @@
+package main
+
+import (
+ "git.echol.cn/loser/st/server/core"
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/initialize"
+ _ "go.uber.org/automaxprocs"
+ "go.uber.org/zap"
+)
+
+//go:generate go env -w GO111MODULE=on
+//go:generate go env -w GOPROXY=https://goproxy.cn,direct
+//go:generate go mod tidy
+//go:generate go mod download
+
+// 这部分 @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.9
+// @description 使用gin+vue进行极速开发的全栈开发基础平台
+// @securityDefinitions.apikey ApiKeyAuth
+// @in header
+// @name x-token
+// @BasePath /
+func main() {
+ // 初始化系统
+ initializeSystem()
+ // 运行服务器
+ core.RunServer()
+}
+
+// initializeSystem 初始化系统所有组件
+// 提取为单独函数以便于系统重载时调用
+func initializeSystem() {
+ 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()
+ initialize.SetupHandlers() // 注册全局函数
+ if global.GVA_DB != nil {
+ initialize.RegisterTables() // 初始化表
+ }
+}
diff --git a/server/mcp/api_creator.go b/server/mcp/api_creator.go
new file mode 100644
index 0000000..1459092
--- /dev/null
+++ b/server/mcp/api_creator.go
@@ -0,0 +1,191 @@
+package mcpTool
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system"
+ "git.echol.cn/loser/st/server/service"
+ "github.com/mark3labs/mcp-go/mcp"
+ "go.uber.org/zap"
+)
+
+// 注册工具
+func init() {
+ RegisterTool(&ApiCreator{})
+}
+
+// ApiCreateRequest API创建请求结构
+type ApiCreateRequest struct {
+ Path string `json:"path"` // API路径
+ Description string `json:"description"` // API中文描述
+ ApiGroup string `json:"apiGroup"` // API组
+ Method string `json:"method"` // HTTP方法
+}
+
+// ApiCreateResponse API创建响应结构
+type ApiCreateResponse struct {
+ Success bool `json:"success"`
+ Message string `json:"message"`
+ ApiID uint `json:"apiId"`
+ Path string `json:"path"`
+ Method string `json:"method"`
+}
+
+// ApiCreator API创建工具
+type ApiCreator struct{}
+
+// New 创建API创建工具
+func (a *ApiCreator) New() mcp.Tool {
+ return mcp.NewTool("create_api",
+ mcp.WithDescription(`创建后端API记录,用于AI编辑器自动添加API接口时自动创建对应的API权限记录。
+
+**重要限制:**
+- 当使用gva_auto_generate工具且needCreatedModules=true时,模块创建会自动生成API权限,不应调用此工具
+- 仅在以下情况使用:1) 单独创建API(不涉及模块创建);2) AI编辑器自动添加API;3) router下的文件产生路径变化时`),
+ mcp.WithString("path",
+ mcp.Required(),
+ mcp.Description("API路径,如:/user/create"),
+ ),
+ mcp.WithString("description",
+ mcp.Required(),
+ mcp.Description("API中文描述,如:创建用户"),
+ ),
+ mcp.WithString("apiGroup",
+ mcp.Required(),
+ mcp.Description("API组名称,用于分类管理,如:用户管理"),
+ ),
+ mcp.WithString("method",
+ mcp.Description("HTTP方法"),
+ mcp.DefaultString("POST"),
+ ),
+ mcp.WithString("apis",
+ mcp.Description("批量创建API的JSON字符串,格式:[{\"path\":\"/user/create\",\"description\":\"创建用户\",\"apiGroup\":\"用户管理\",\"method\":\"POST\"}]"),
+ ),
+ )
+}
+
+// Handle 处理API创建请求
+func (a *ApiCreator) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ args := request.GetArguments()
+
+ var apis []ApiCreateRequest
+
+ // 检查是否是批量创建
+ if apisStr, ok := args["apis"].(string); ok && apisStr != "" {
+ if err := json.Unmarshal([]byte(apisStr), &apis); err != nil {
+ return nil, fmt.Errorf("apis 参数格式错误: %v", err)
+ }
+ } else {
+ // 单个API创建
+ path, ok := args["path"].(string)
+ if !ok || path == "" {
+ return nil, errors.New("path 参数是必需的")
+ }
+
+ description, ok := args["description"].(string)
+ if !ok || description == "" {
+ return nil, errors.New("description 参数是必需的")
+ }
+
+ apiGroup, ok := args["apiGroup"].(string)
+ if !ok || apiGroup == "" {
+ return nil, errors.New("apiGroup 参数是必需的")
+ }
+
+ method := "POST"
+ if val, ok := args["method"].(string); ok && val != "" {
+ method = val
+ }
+
+ apis = append(apis, ApiCreateRequest{
+ Path: path,
+ Description: description,
+ ApiGroup: apiGroup,
+ Method: method,
+ })
+ }
+
+ if len(apis) == 0 {
+ return nil, errors.New("没有要创建的API")
+ }
+
+ // 创建API记录
+ apiService := service.ServiceGroupApp.SystemServiceGroup.ApiService
+ var responses []ApiCreateResponse
+ successCount := 0
+
+ for _, apiReq := range apis {
+ api := system.SysApi{
+ Path: apiReq.Path,
+ Description: apiReq.Description,
+ ApiGroup: apiReq.ApiGroup,
+ Method: apiReq.Method,
+ }
+
+ err := apiService.CreateApi(api)
+ if err != nil {
+ global.GVA_LOG.Warn("创建API失败",
+ zap.String("path", apiReq.Path),
+ zap.String("method", apiReq.Method),
+ zap.Error(err))
+
+ responses = append(responses, ApiCreateResponse{
+ Success: false,
+ Message: fmt.Sprintf("创建API失败: %v", err),
+ Path: apiReq.Path,
+ Method: apiReq.Method,
+ })
+ } else {
+ // 获取创建的API ID
+ var createdApi system.SysApi
+ err = global.GVA_DB.Where("path = ? AND method = ?", apiReq.Path, apiReq.Method).First(&createdApi).Error
+ if err != nil {
+ global.GVA_LOG.Warn("获取创建的API ID失败", zap.Error(err))
+ }
+
+ responses = append(responses, ApiCreateResponse{
+ Success: true,
+ Message: fmt.Sprintf("成功创建API %s %s", apiReq.Method, apiReq.Path),
+ ApiID: createdApi.ID,
+ Path: apiReq.Path,
+ Method: apiReq.Method,
+ })
+ successCount++
+ }
+ }
+
+ // 构建总体响应
+ var resultMessage string
+ if len(apis) == 1 {
+ resultMessage = responses[0].Message
+ } else {
+ resultMessage = fmt.Sprintf("批量创建API完成,成功 %d 个,失败 %d 个", successCount, len(apis)-successCount)
+ }
+
+ result := map[string]interface{}{
+ "success": successCount > 0,
+ "message": resultMessage,
+ "totalCount": len(apis),
+ "successCount": successCount,
+ "failedCount": len(apis) - successCount,
+ "details": responses,
+ }
+
+ resultJSON, err := json.MarshalIndent(result, "", " ")
+ if err != nil {
+ return nil, fmt.Errorf("序列化结果失败: %v", err)
+ }
+
+ return &mcp.CallToolResult{
+ Content: []mcp.Content{
+ mcp.TextContent{
+ Type: "text",
+ Text: fmt.Sprintf("API创建结果:\n\n%s", string(resultJSON)),
+ },
+ },
+ }, nil
+}
diff --git a/server/mcp/api_lister.go b/server/mcp/api_lister.go
new file mode 100644
index 0000000..c9ef696
--- /dev/null
+++ b/server/mcp/api_lister.go
@@ -0,0 +1,168 @@
+package mcpTool
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system"
+ "github.com/mark3labs/mcp-go/mcp"
+ "go.uber.org/zap"
+)
+
+// 注册工具
+func init() {
+ // 注册工具将在enter.go中统一处理
+ RegisterTool(&ApiLister{})
+}
+
+// ApiInfo API信息结构
+type ApiInfo struct {
+ ID uint `json:"id,omitempty"` // 数据库ID(仅数据库API有)
+ Path string `json:"path"` // API路径
+ Description string `json:"description,omitempty"` // API描述
+ ApiGroup string `json:"apiGroup,omitempty"` // API组
+ Method string `json:"method"` // HTTP方法
+ Source string `json:"source"` // 来源:database 或 gin
+}
+
+// ApiListResponse API列表响应结构
+type ApiListResponse struct {
+ Success bool `json:"success"`
+ Message string `json:"message"`
+ DatabaseApis []ApiInfo `json:"databaseApis"` // 数据库中的API
+ GinApis []ApiInfo `json:"ginApis"` // gin框架中的API
+ TotalCount int `json:"totalCount"` // 总数量
+}
+
+// ApiLister API列表工具
+type ApiLister struct{}
+
+// New 创建API列表工具
+func (a *ApiLister) New() mcp.Tool {
+ return mcp.NewTool("list_all_apis",
+ mcp.WithDescription(`获取系统中所有的API接口,分为两组:
+
+**功能说明:**
+- 返回数据库中已注册的API列表
+- 返回gin框架中实际注册的路由API列表
+- 帮助前端判断是使用现有API还是需要创建新的API,如果api在前端未使用且需要前端调用的时候,请到api文件夹下对应模块的js中添加方法并暴露给当前业务调用
+
+**返回数据结构:**
+- databaseApis: 数据库中的API记录(包含ID、描述、分组等完整信息)
+- ginApis: gin路由中的API(仅包含路径和方法),需要AI根据路径自行揣摩路径的业务含义,例如:/api/user/:id 表示根据用户ID获取用户信息`),
+ mcp.WithString("_placeholder",
+ mcp.Description("占位符,防止json schema校验失败"),
+ ),
+ )
+}
+
+// Handle 处理API列表请求
+func (a *ApiLister) Handle(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+
+ // 获取数据库中的API
+ databaseApis, err := a.getDatabaseApis()
+ if err != nil {
+ global.GVA_LOG.Error("获取数据库API失败", zap.Error(err))
+ errorResponse := ApiListResponse{
+ Success: false,
+ Message: "获取数据库API失败: " + err.Error(),
+ }
+ resultJSON, _ := json.Marshal(errorResponse)
+ return &mcp.CallToolResult{
+ Content: []mcp.Content{
+ mcp.TextContent{
+ Type: "text",
+ Text: string(resultJSON),
+ },
+ },
+ }, nil
+ }
+
+ // 获取gin路由中的API
+ ginApis, err := a.getGinApis()
+ if err != nil {
+ global.GVA_LOG.Error("获取gin路由API失败", zap.Error(err))
+ errorResponse := ApiListResponse{
+ Success: false,
+ Message: "获取gin路由API失败: " + err.Error(),
+ }
+ resultJSON, _ := json.Marshal(errorResponse)
+ return &mcp.CallToolResult{
+ Content: []mcp.Content{
+ mcp.TextContent{
+ Type: "text",
+ Text: string(resultJSON),
+ },
+ },
+ }, nil
+ }
+
+ // 构建响应
+ response := ApiListResponse{
+ Success: true,
+ Message: "获取API列表成功",
+ DatabaseApis: databaseApis,
+ GinApis: ginApis,
+ TotalCount: len(databaseApis) + len(ginApis),
+ }
+
+ global.GVA_LOG.Info("API列表获取成功",
+ zap.Int("数据库API数量", len(databaseApis)),
+ zap.Int("gin路由API数量", len(ginApis)),
+ zap.Int("总数量", response.TotalCount))
+
+ resultJSON, err := json.Marshal(response)
+ if err != nil {
+ return nil, fmt.Errorf("序列化结果失败: %v", err)
+ }
+
+ return &mcp.CallToolResult{
+ Content: []mcp.Content{
+ mcp.TextContent{
+ Type: "text",
+ Text: string(resultJSON),
+ },
+ },
+ }, nil
+}
+
+// getDatabaseApis 获取数据库中的所有API
+func (a *ApiLister) getDatabaseApis() ([]ApiInfo, error) {
+ var apis []system.SysApi
+ err := global.GVA_DB.Model(&system.SysApi{}).Order("api_group ASC, path ASC").Find(&apis).Error
+ if err != nil {
+ return nil, err
+ }
+
+ // 转换为ApiInfo格式
+ var result []ApiInfo
+ for _, api := range apis {
+ result = append(result, ApiInfo{
+ ID: api.ID,
+ Path: api.Path,
+ Description: api.Description,
+ ApiGroup: api.ApiGroup,
+ Method: api.Method,
+ Source: "database",
+ })
+ }
+
+ return result, nil
+}
+
+// getGinApis 获取gin路由中的所有API(包含被忽略的API)
+func (a *ApiLister) getGinApis() ([]ApiInfo, error) {
+ // 从gin路由信息中获取所有API
+ var result []ApiInfo
+ for _, route := range global.GVA_ROUTERS {
+ result = append(result, ApiInfo{
+ Path: route.Path,
+ Method: route.Method,
+ Source: "gin",
+ })
+ }
+
+ return result, nil
+}
diff --git a/server/mcp/client/client.go b/server/mcp/client/client.go
new file mode 100644
index 0000000..3f1a385
--- /dev/null
+++ b/server/mcp/client/client.go
@@ -0,0 +1,40 @@
+package client
+
+import (
+ "context"
+ "errors"
+
+ mcpClient "github.com/mark3labs/mcp-go/client"
+ "github.com/mark3labs/mcp-go/mcp"
+)
+
+func NewClient(baseUrl, name, version, serverName string) (*mcpClient.Client, error) {
+ client, err := mcpClient.NewSSEMCPClient(baseUrl)
+ if err != nil {
+ return nil, err
+ }
+
+ ctx := context.Background()
+
+ // 启动client
+ if err := client.Start(ctx); err != nil {
+ return nil, err
+ }
+
+ // 初始化
+ initRequest := mcp.InitializeRequest{}
+ initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
+ initRequest.Params.ClientInfo = mcp.Implementation{
+ Name: name,
+ Version: version,
+ }
+
+ result, err := client.Initialize(ctx, initRequest)
+ if err != nil {
+ return nil, err
+ }
+ if result.ServerInfo.Name != serverName {
+ return nil, errors.New("server name mismatch")
+ }
+ return client, nil
+}
diff --git a/server/mcp/client/client_test.go b/server/mcp/client/client_test.go
new file mode 100644
index 0000000..a0b2122
--- /dev/null
+++ b/server/mcp/client/client_test.go
@@ -0,0 +1,133 @@
+package client
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ "github.com/mark3labs/mcp-go/mcp"
+)
+
+// 测试 MCP 客户端连接
+func TestMcpClientConnection(t *testing.T) {
+ c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-vue-admin MCP服务")
+ defer c.Close()
+ if err != nil {
+ t.Fatalf(err.Error())
+ }
+}
+
+func TestTools(t *testing.T) {
+ t.Run("currentTime", func(t *testing.T) {
+ c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-vue-admin MCP服务")
+ defer c.Close()
+ if err != nil {
+ t.Fatalf("Failed to create client: %v", err)
+ }
+ ctx := context.Background()
+
+ request := mcp.CallToolRequest{}
+ request.Params.Name = "currentTime"
+ request.Params.Arguments = map[string]interface{}{
+ "timezone": "UTC+8",
+ }
+
+ result, err := c.CallTool(ctx, request)
+ if err != nil {
+ t.Fatalf("方法调用错误: %v", err)
+ }
+
+ if len(result.Content) != 1 {
+ t.Errorf("应该有且仅返回1条信息,但是现在有 %d", len(result.Content))
+ }
+ if content, ok := result.Content[0].(mcp.TextContent); ok {
+ t.Logf("成功返回信息%s", content.Text)
+ } else {
+ t.Logf("返回为止类型信息%+v", content)
+ }
+ })
+
+ t.Run("getNickname", func(t *testing.T) {
+
+ c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-vue-admin MCP服务")
+ defer c.Close()
+ if err != nil {
+ t.Fatalf("Failed to create client: %v", err)
+ }
+ ctx := context.Background()
+
+ // Initialize
+ initRequest := mcp.InitializeRequest{}
+ initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
+ initRequest.Params.ClientInfo = mcp.Implementation{
+ Name: "test-client",
+ Version: "1.0.0",
+ }
+
+ _, err = c.Initialize(ctx, initRequest)
+ if err != nil {
+ t.Fatalf("初始化失败: %v", err)
+ }
+
+ request := mcp.CallToolRequest{}
+ request.Params.Name = "getNickname"
+ request.Params.Arguments = map[string]interface{}{
+ "username": "admin",
+ }
+
+ result, err := c.CallTool(ctx, request)
+ if err != nil {
+ t.Fatalf("方法调用错误: %v", err)
+ }
+
+ if len(result.Content) != 1 {
+ t.Errorf("应该有且仅返回1条信息,但是现在有 %d", len(result.Content))
+ }
+ if content, ok := result.Content[0].(mcp.TextContent); ok {
+ t.Logf("成功返回信息%s", content.Text)
+ } else {
+ t.Logf("返回为止类型信息%+v", content)
+ }
+ })
+}
+
+func TestGetTools(t *testing.T) {
+ c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-vue-admin MCP服务")
+ defer c.Close()
+ if err != nil {
+ t.Fatalf("Failed to create client: %v", err)
+ }
+ ctx := context.Background()
+
+ toolsRequest := mcp.ListToolsRequest{}
+
+ toolListResult, err := c.ListTools(ctx, toolsRequest)
+ if err != nil {
+ t.Fatalf("获取工具列表失败: %v", err)
+ }
+ for i := range toolListResult.Tools {
+ tool := toolListResult.Tools[i]
+ fmt.Printf("工具名称: %s\n", tool.Name)
+ fmt.Printf("工具描述: %s\n", tool.Description)
+
+ // 打印参数信息
+ if tool.InputSchema.Properties != nil {
+ fmt.Println("参数列表:")
+ for paramName, prop := range tool.InputSchema.Properties {
+ required := "否"
+ // 检查参数是否在必填列表中
+ for _, reqField := range tool.InputSchema.Required {
+ if reqField == paramName {
+ required = "是"
+ break
+ }
+ }
+ fmt.Printf(" - %s (类型: %s, 描述: %s, 必填: %s)\n",
+ paramName, prop.(map[string]any)["type"], prop.(map[string]any)["description"], required)
+ }
+ } else {
+ fmt.Println("该工具没有参数")
+ }
+ fmt.Println("-------------------")
+ }
+}
diff --git a/server/mcp/dictionary_generator.go b/server/mcp/dictionary_generator.go
new file mode 100644
index 0000000..821adc6
--- /dev/null
+++ b/server/mcp/dictionary_generator.go
@@ -0,0 +1,229 @@
+package mcpTool
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system"
+ "git.echol.cn/loser/st/server/service"
+ "github.com/mark3labs/mcp-go/mcp"
+ "go.uber.org/zap"
+ "gorm.io/gorm"
+)
+
+func init() {
+ RegisterTool(&DictionaryOptionsGenerator{})
+}
+
+// DictionaryOptionsGenerator 字典选项生成器
+type DictionaryOptionsGenerator struct{}
+
+// DictionaryOption 字典选项结构
+type DictionaryOption struct {
+ Label string `json:"label"`
+ Value string `json:"value"`
+ Sort int `json:"sort"`
+}
+
+// DictionaryGenerateRequest 字典生成请求
+type DictionaryGenerateRequest struct {
+ DictType string `json:"dictType"` // 字典类型
+ FieldDesc string `json:"fieldDesc"` // 字段描述
+ Options []DictionaryOption `json:"options"` // AI生成的字典选项
+ DictName string `json:"dictName"` // 字典名称(可选)
+ Description string `json:"description"` // 字典描述(可选)
+}
+
+// DictionaryGenerateResponse 字典生成响应
+type DictionaryGenerateResponse struct {
+ Success bool `json:"success"`
+ Message string `json:"message"`
+ DictType string `json:"dictType"`
+ OptionsCount int `json:"optionsCount"`
+}
+
+// New 返回工具注册信息
+func (d *DictionaryOptionsGenerator) New() mcp.Tool {
+ return mcp.NewTool("generate_dictionary_options",
+ mcp.WithDescription("智能生成字典选项并自动创建字典和字典详情"),
+ mcp.WithString("dictType",
+ mcp.Required(),
+ mcp.Description("字典类型,用于标识字典的唯一性"),
+ ),
+ mcp.WithString("fieldDesc",
+ mcp.Required(),
+ mcp.Description("字段描述,用于AI理解字段含义"),
+ ),
+ mcp.WithString("options",
+ mcp.Required(),
+ mcp.Description("字典选项JSON字符串,格式:[{\"label\":\"显示名\",\"value\":\"值\",\"sort\":1}]"),
+ ),
+ mcp.WithString("dictName",
+ mcp.Description("字典名称,如果不提供将自动生成"),
+ ),
+ mcp.WithString("description",
+ mcp.Description("字典描述"),
+ ),
+ )
+}
+
+// Handle 处理工具调用
+func (d *DictionaryOptionsGenerator) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ // 解析请求参数
+ args := request.GetArguments()
+
+ dictType, ok := args["dictType"].(string)
+ if !ok || dictType == "" {
+ return nil, errors.New("dictType 参数是必需的")
+ }
+
+ fieldDesc, ok := args["fieldDesc"].(string)
+ if !ok || fieldDesc == "" {
+ return nil, errors.New("fieldDesc 参数是必需的")
+ }
+
+ optionsStr, ok := args["options"].(string)
+ if !ok || optionsStr == "" {
+ return nil, errors.New("options 参数是必需的")
+ }
+
+ // 解析options JSON字符串
+ var options []DictionaryOption
+ if err := json.Unmarshal([]byte(optionsStr), &options); err != nil {
+ return nil, fmt.Errorf("options 参数格式错误: %v", err)
+ }
+
+ if len(options) == 0 {
+ return nil, errors.New("options 不能为空")
+ }
+
+ dictName, _ := args["dictName"].(string)
+ description, _ := args["description"].(string)
+
+ // 构建请求对象
+ req := &DictionaryGenerateRequest{
+ DictType: dictType,
+ FieldDesc: fieldDesc,
+ Options: options,
+ DictName: dictName,
+ Description: description,
+ }
+
+ // 创建字典
+ response, err := d.createDictionaryWithOptions(ctx, req)
+ if err != nil {
+ return nil, err
+ }
+
+ // 构建响应
+ resultJSON, err := json.MarshalIndent(response, "", " ")
+ if err != nil {
+ return nil, fmt.Errorf("序列化结果失败: %v", err)
+ }
+
+ return &mcp.CallToolResult{
+ Content: []mcp.Content{
+ mcp.TextContent{
+ Type: "text",
+ Text: fmt.Sprintf("字典选项生成结果:\n\n%s", string(resultJSON)),
+ },
+ },
+ }, nil
+}
+
+// createDictionaryWithOptions 创建字典和字典选项
+func (d *DictionaryOptionsGenerator) createDictionaryWithOptions(ctx context.Context, req *DictionaryGenerateRequest) (*DictionaryGenerateResponse, error) {
+ // 检查字典是否已存在
+ exists, err := d.checkDictionaryExists(req.DictType)
+ if err != nil {
+ return nil, fmt.Errorf("检查字典是否存在失败: %v", err)
+ }
+
+ if exists {
+ return &DictionaryGenerateResponse{
+ Success: false,
+ Message: fmt.Sprintf("字典 %s 已存在,跳过创建", req.DictType),
+ DictType: req.DictType,
+ OptionsCount: 0,
+ }, nil
+ }
+
+ // 生成字典名称
+ dictName := req.DictName
+ if dictName == "" {
+ dictName = d.generateDictionaryName(req.DictType, req.FieldDesc)
+ }
+
+ // 创建字典
+ dictionaryService := service.ServiceGroupApp.SystemServiceGroup.DictionaryService
+ dictionary := system.SysDictionary{
+ Name: dictName,
+ Type: req.DictType,
+ Status: &[]bool{true}[0], // 默认启用
+ Desc: req.Description,
+ }
+
+ err = dictionaryService.CreateSysDictionary(dictionary)
+ if err != nil {
+ return nil, fmt.Errorf("创建字典失败: %v", err)
+ }
+
+ // 获取刚创建的字典ID
+ var createdDict system.SysDictionary
+ err = global.GVA_DB.Where("type = ?", req.DictType).First(&createdDict).Error
+ if err != nil {
+ return nil, fmt.Errorf("获取创建的字典失败: %v", err)
+ }
+
+ // 创建字典详情项
+ dictionaryDetailService := service.ServiceGroupApp.SystemServiceGroup.DictionaryDetailService
+ successCount := 0
+
+ for _, option := range req.Options {
+ dictionaryDetail := system.SysDictionaryDetail{
+ Label: option.Label,
+ Value: option.Value,
+ Status: &[]bool{true}[0], // 默认启用
+ Sort: option.Sort,
+ SysDictionaryID: int(createdDict.ID),
+ }
+
+ err = dictionaryDetailService.CreateSysDictionaryDetail(dictionaryDetail)
+ if err != nil {
+ global.GVA_LOG.Warn("创建字典详情项失败", zap.Error(err))
+ } else {
+ successCount++
+ }
+ }
+
+ return &DictionaryGenerateResponse{
+ Success: true,
+ Message: fmt.Sprintf("成功创建字典 %s,包含 %d 个选项", req.DictType, successCount),
+ DictType: req.DictType,
+ OptionsCount: successCount,
+ }, nil
+}
+
+// checkDictionaryExists 检查字典是否存在
+func (d *DictionaryOptionsGenerator) checkDictionaryExists(dictType string) (bool, error) {
+ var dictionary system.SysDictionary
+ err := global.GVA_DB.Where("type = ?", dictType).First(&dictionary).Error
+ if err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return false, nil // 字典不存在
+ }
+ return false, err // 其他错误
+ }
+ return true, nil // 字典存在
+}
+
+// generateDictionaryName 生成字典名称
+func (d *DictionaryOptionsGenerator) generateDictionaryName(dictType, fieldDesc string) string {
+ if fieldDesc != "" {
+ return fmt.Sprintf("%s字典", fieldDesc)
+ }
+ return fmt.Sprintf("%s字典", dictType)
+}
diff --git a/server/mcp/dictionary_query.go b/server/mcp/dictionary_query.go
new file mode 100644
index 0000000..d5eb745
--- /dev/null
+++ b/server/mcp/dictionary_query.go
@@ -0,0 +1,239 @@
+package mcpTool
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system"
+ "git.echol.cn/loser/st/server/service"
+ "github.com/mark3labs/mcp-go/mcp"
+ "go.uber.org/zap"
+ "gorm.io/gorm"
+)
+
+// 注册工具
+func init() {
+ RegisterTool(&DictionaryQuery{})
+}
+
+type DictionaryPre struct {
+ Type string `json:"type"` // 字典名(英)
+ Desc string `json:"desc"` // 描述
+}
+
+// DictionaryInfo 字典信息结构
+type DictionaryInfo struct {
+ ID uint `json:"id"`
+ Name string `json:"name"` // 字典名(中)
+ Type string `json:"type"` // 字典名(英)
+ Status *bool `json:"status"` // 状态
+ Desc string `json:"desc"` // 描述
+ Details []DictionaryDetailInfo `json:"details"` // 字典详情
+}
+
+// DictionaryDetailInfo 字典详情信息结构
+type DictionaryDetailInfo struct {
+ ID uint `json:"id"`
+ Label string `json:"label"` // 展示值
+ Value string `json:"value"` // 字典值
+ Extend string `json:"extend"` // 扩展值
+ Status *bool `json:"status"` // 启用状态
+ Sort int `json:"sort"` // 排序标记
+}
+
+// DictionaryQueryResponse 字典查询响应结构
+type DictionaryQueryResponse struct {
+ Success bool `json:"success"`
+ Message string `json:"message"`
+ Total int `json:"total"`
+ Dictionaries []DictionaryInfo `json:"dictionaries"`
+}
+
+// DictionaryQuery 字典查询工具
+type DictionaryQuery struct{}
+
+// New 创建字典查询工具
+func (d *DictionaryQuery) New() mcp.Tool {
+ return mcp.NewTool("query_dictionaries",
+ mcp.WithDescription("查询系统中所有的字典和字典属性,用于AI生成逻辑时了解可用的字典选项"),
+ mcp.WithString("dictType",
+ mcp.Description("可选:指定字典类型进行精确查询,如果不提供则返回所有字典"),
+ ),
+ mcp.WithBoolean("includeDisabled",
+ mcp.Description("是否包含已禁用的字典和字典项,默认为false(只返回启用的)"),
+ ),
+ mcp.WithBoolean("detailsOnly",
+ mcp.Description("是否只返回字典详情信息(不包含字典基本信息),默认为false"),
+ ),
+ )
+}
+
+// Handle 处理字典查询请求
+func (d *DictionaryQuery) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ args := request.GetArguments()
+
+ // 获取参数
+ dictType := ""
+ if val, ok := args["dictType"].(string); ok {
+ dictType = val
+ }
+
+ includeDisabled := false
+ if val, ok := args["includeDisabled"].(bool); ok {
+ includeDisabled = val
+ }
+
+ detailsOnly := false
+ if val, ok := args["detailsOnly"].(bool); ok {
+ detailsOnly = val
+ }
+
+ // 获取字典服务
+ dictionaryService := service.ServiceGroupApp.SystemServiceGroup.DictionaryService
+
+ var dictionaries []DictionaryInfo
+ var err error
+
+ if dictType != "" {
+ // 查询指定类型的字典
+ var status *bool
+ if !includeDisabled {
+ status = &[]bool{true}[0]
+ }
+
+ sysDictionary, err := dictionaryService.GetSysDictionary(dictType, 0, status)
+ if err != nil {
+ global.GVA_LOG.Error("查询字典失败", zap.Error(err))
+ return &mcp.CallToolResult{
+ Content: []mcp.Content{
+ mcp.NewTextContent(fmt.Sprintf(`{"success": false, "message": "查询字典失败: %v", "total": 0, "dictionaries": []}`, err.Error())),
+ },
+ }, nil
+ }
+
+ // 转换为响应格式
+ dictInfo := DictionaryInfo{
+ ID: sysDictionary.ID,
+ Name: sysDictionary.Name,
+ Type: sysDictionary.Type,
+ Status: sysDictionary.Status,
+ Desc: sysDictionary.Desc,
+ }
+
+ // 获取字典详情
+ for _, detail := range sysDictionary.SysDictionaryDetails {
+ if includeDisabled || (detail.Status != nil && *detail.Status) {
+ dictInfo.Details = append(dictInfo.Details, DictionaryDetailInfo{
+ ID: detail.ID,
+ Label: detail.Label,
+ Value: detail.Value,
+ Extend: detail.Extend,
+ Status: detail.Status,
+ Sort: detail.Sort,
+ })
+ }
+ }
+
+ dictionaries = append(dictionaries, dictInfo)
+ } else {
+ // 查询所有字典
+ var sysDictionaries []system.SysDictionary
+ db := global.GVA_DB.Model(&system.SysDictionary{})
+
+ if !includeDisabled {
+ db = db.Where("status = ?", true)
+ }
+
+ err = db.Preload("SysDictionaryDetails", func(db *gorm.DB) *gorm.DB {
+ if includeDisabled {
+ return db.Order("sort")
+ } else {
+ return db.Where("status = ?", true).Order("sort")
+ }
+ }).Find(&sysDictionaries).Error
+
+ if err != nil {
+ global.GVA_LOG.Error("查询字典列表失败", zap.Error(err))
+ return &mcp.CallToolResult{
+ Content: []mcp.Content{
+ mcp.NewTextContent(fmt.Sprintf(`{"success": false, "message": "查询字典列表失败: %v", "total": 0, "dictionaries": []}`, err.Error())),
+ },
+ }, nil
+ }
+
+ // 转换为响应格式
+ for _, dict := range sysDictionaries {
+ dictInfo := DictionaryInfo{
+ ID: dict.ID,
+ Name: dict.Name,
+ Type: dict.Type,
+ Status: dict.Status,
+ Desc: dict.Desc,
+ }
+
+ // 获取字典详情
+ for _, detail := range dict.SysDictionaryDetails {
+ if includeDisabled || (detail.Status != nil && *detail.Status) {
+ dictInfo.Details = append(dictInfo.Details, DictionaryDetailInfo{
+ ID: detail.ID,
+ Label: detail.Label,
+ Value: detail.Value,
+ Extend: detail.Extend,
+ Status: detail.Status,
+ Sort: detail.Sort,
+ })
+ }
+ }
+
+ dictionaries = append(dictionaries, dictInfo)
+ }
+ }
+
+ // 如果只需要详情信息,则提取所有详情
+ if detailsOnly {
+ var allDetails []DictionaryDetailInfo
+ for _, dict := range dictionaries {
+ allDetails = append(allDetails, dict.Details...)
+ }
+
+ response := map[string]interface{}{
+ "success": true,
+ "message": "查询字典详情成功",
+ "total": len(allDetails),
+ "details": allDetails,
+ }
+
+ responseJSON, _ := json.Marshal(response)
+ return &mcp.CallToolResult{
+ Content: []mcp.Content{
+ mcp.NewTextContent(string(responseJSON)),
+ },
+ }, nil
+ }
+
+ // 构建响应
+ response := DictionaryQueryResponse{
+ Success: true,
+ Message: "查询字典成功",
+ Total: len(dictionaries),
+ Dictionaries: dictionaries,
+ }
+
+ responseJSON, err := json.Marshal(response)
+ if err != nil {
+ global.GVA_LOG.Error("序列化响应失败", zap.Error(err))
+ return &mcp.CallToolResult{
+ Content: []mcp.Content{
+ mcp.NewTextContent(fmt.Sprintf(`{"success": false, "message": "序列化响应失败: %v", "total": 0, "dictionaries": []}`, err.Error())),
+ },
+ }, nil
+ }
+
+ return &mcp.CallToolResult{
+ Content: []mcp.Content{
+ mcp.NewTextContent(string(responseJSON)),
+ },
+ }, nil
+}
diff --git a/server/mcp/enter.go b/server/mcp/enter.go
new file mode 100644
index 0000000..7445525
--- /dev/null
+++ b/server/mcp/enter.go
@@ -0,0 +1,32 @@
+package mcpTool
+
+import (
+ "context"
+
+ "github.com/mark3labs/mcp-go/mcp"
+ "github.com/mark3labs/mcp-go/server"
+)
+
+// McpTool 定义了MCP工具必须实现的接口
+type McpTool interface {
+ // Handle 返回工具调用信息
+ Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error)
+ // New 返回工具注册信息
+ New() mcp.Tool
+}
+
+// 工具注册表
+var toolRegister = make(map[string]McpTool)
+
+// RegisterTool 供工具在init时调用,将自己注册到工具注册表中
+func RegisterTool(tool McpTool) {
+ mcpTool := tool.New()
+ toolRegister[mcpTool.Name] = tool
+}
+
+// RegisterAllTools 将所有注册的工具注册到MCP服务中
+func RegisterAllTools(mcpServer *server.MCPServer) {
+ for _, tool := range toolRegister {
+ mcpServer.AddTool(tool.New(), tool.Handle)
+ }
+}
diff --git a/server/mcp/gva_analyze.go b/server/mcp/gva_analyze.go
new file mode 100644
index 0000000..e983a2f
--- /dev/null
+++ b/server/mcp/gva_analyze.go
@@ -0,0 +1,503 @@
+package mcpTool
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ model "git.echol.cn/loser/st/server/model/system"
+
+ "git.echol.cn/loser/st/server/global"
+ "github.com/mark3labs/mcp-go/mcp"
+)
+
+// 注册工具
+func init() {
+ RegisterTool(&GVAAnalyzer{})
+}
+
+// GVAAnalyzer GVA分析器 - 用于分析当前功能是否需要创建独立的package和module
+type GVAAnalyzer struct{}
+
+// AnalyzeRequest 分析请求结构体
+type AnalyzeRequest struct {
+ Requirement string `json:"requirement" binding:"required"` // 用户需求描述
+}
+
+// AnalyzeResponse 分析响应结构体
+type AnalyzeResponse struct {
+ ExistingPackages []PackageInfo `json:"existingPackages"` // 现有包信息
+ PredesignedModules []PredesignedModuleInfo `json:"predesignedModules"` // 预设计模块信息
+ Dictionaries []DictionaryPre `json:"dictionaries"` // 字典信息
+ CleanupInfo *CleanupInfo `json:"cleanupInfo"` // 清理信息(如果有)
+}
+
+// ModuleInfo 模块信息
+type ModuleInfo struct {
+ ModuleName string `json:"moduleName"` // 模块名称
+ PackageName string `json:"packageName"` // 包名
+ Template string `json:"template"` // 模板类型
+ StructName string `json:"structName"` // 结构体名称
+ TableName string `json:"tableName"` // 表名
+ Description string `json:"description"` // 描述
+ FilePaths []string `json:"filePaths"` // 相关文件路径
+}
+
+// PackageInfo 包信息
+type PackageInfo struct {
+ PackageName string `json:"packageName"` // 包名
+ Template string `json:"template"` // 模板类型
+ Label string `json:"label"` // 标签
+ Desc string `json:"desc"` // 描述
+ Module string `json:"module"` // 模块
+ IsEmpty bool `json:"isEmpty"` // 是否为空包
+}
+
+// PredesignedModuleInfo 预设计模块信息
+type PredesignedModuleInfo struct {
+ ModuleName string `json:"moduleName"` // 模块名称
+ PackageName string `json:"packageName"` // 包名
+ Template string `json:"template"` // 模板类型
+ FilePaths []string `json:"filePaths"` // 文件路径列表
+ Description string `json:"description"` // 描述
+}
+
+// CleanupInfo 清理信息
+type CleanupInfo struct {
+ DeletedPackages []string `json:"deletedPackages"` // 已删除的包
+ DeletedModules []string `json:"deletedModules"` // 已删除的模块
+ CleanupMessage string `json:"cleanupMessage"` // 清理消息
+}
+
+// New 创建GVA分析器工具
+func (g *GVAAnalyzer) New() mcp.Tool {
+ return mcp.NewTool("gva_analyze",
+ mcp.WithDescription("返回当前系统中有效的包和模块信息,并分析用户需求是否需要创建新的包、模块和字典。同时检查并清理空包,确保系统整洁。"),
+ mcp.WithString("requirement",
+ mcp.Description("用户需求描述,用于分析是否需要创建新的包和模块"),
+ mcp.Required(),
+ ),
+ )
+}
+
+// Handle 处理分析请求
+func (g *GVAAnalyzer) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ // 解析请求参数
+ requirementStr, ok := request.GetArguments()["requirement"].(string)
+ if !ok || requirementStr == "" {
+ return nil, errors.New("参数错误:requirement 必须是非空字符串")
+ }
+
+ // 创建分析请求
+ analyzeReq := AnalyzeRequest{
+ Requirement: requirementStr,
+ }
+
+ // 执行分析逻辑
+ response, err := g.performAnalysis(ctx, analyzeReq)
+ if err != nil {
+ return nil, fmt.Errorf("分析失败: %v", err)
+ }
+
+ // 序列化响应
+ responseJSON, err := json.Marshal(response)
+ if err != nil {
+ return nil, fmt.Errorf("序列化响应失败: %v", err)
+ }
+
+ return &mcp.CallToolResult{
+ Content: []mcp.Content{
+ mcp.NewTextContent(string(responseJSON)),
+ },
+ }, nil
+}
+
+// performAnalysis 执行分析逻辑
+func (g *GVAAnalyzer) performAnalysis(ctx context.Context, req AnalyzeRequest) (*AnalyzeResponse, error) {
+ // 1. 获取数据库中的包信息
+ var packages []model.SysAutoCodePackage
+ if err := global.GVA_DB.Find(&packages).Error; err != nil {
+ return nil, fmt.Errorf("获取包信息失败: %v", err)
+ }
+
+ // 2. 获取历史记录
+ var histories []model.SysAutoCodeHistory
+ if err := global.GVA_DB.Find(&histories).Error; err != nil {
+ return nil, fmt.Errorf("获取历史记录失败: %v", err)
+ }
+
+ // 3. 检查空包并进行清理
+ cleanupInfo := &CleanupInfo{
+ DeletedPackages: []string{},
+ DeletedModules: []string{},
+ }
+
+ var validPackages []model.SysAutoCodePackage
+ var emptyPackageHistoryIDs []uint
+
+ for _, pkg := range packages {
+ isEmpty, err := g.isPackageFolderEmpty(pkg.PackageName, pkg.Template)
+ if err != nil {
+ global.GVA_LOG.Warn(fmt.Sprintf("检查包 %s 是否为空时出错: %v", pkg.PackageName, err))
+ continue
+ }
+
+ if isEmpty {
+ // 删除空包文件夹
+ if err := g.removeEmptyPackageFolder(pkg.PackageName, pkg.Template); err != nil {
+ global.GVA_LOG.Warn(fmt.Sprintf("删除空包文件夹 %s 失败: %v", pkg.PackageName, err))
+ } else {
+ cleanupInfo.DeletedPackages = append(cleanupInfo.DeletedPackages, pkg.PackageName)
+ }
+
+ // 删除数据库记录
+ if err := global.GVA_DB.Delete(&pkg).Error; err != nil {
+ global.GVA_LOG.Warn(fmt.Sprintf("删除包数据库记录 %s 失败: %v", pkg.PackageName, err))
+ }
+
+ // 收集相关的历史记录ID
+ for _, history := range histories {
+ if history.Package == pkg.PackageName {
+ emptyPackageHistoryIDs = append(emptyPackageHistoryIDs, history.ID)
+ cleanupInfo.DeletedModules = append(cleanupInfo.DeletedModules, history.StructName)
+ }
+ }
+ } else {
+ validPackages = append(validPackages, pkg)
+ }
+ }
+
+ // 5. 清理空包相关的历史记录和脏历史记录
+ var dirtyHistoryIDs []uint
+ for _, history := range histories {
+ // 检查是否为空包相关的历史记录
+ for _, emptyID := range emptyPackageHistoryIDs {
+ if history.ID == emptyID {
+ dirtyHistoryIDs = append(dirtyHistoryIDs, history.ID)
+ break
+ }
+ }
+ }
+
+ // 删除脏历史记录
+ if len(dirtyHistoryIDs) > 0 {
+ if err := global.GVA_DB.Delete(&model.SysAutoCodeHistory{}, "id IN ?", dirtyHistoryIDs).Error; err != nil {
+ global.GVA_LOG.Warn(fmt.Sprintf("删除脏历史记录失败: %v", err))
+ } else {
+ global.GVA_LOG.Info(fmt.Sprintf("成功删除 %d 条脏历史记录", len(dirtyHistoryIDs)))
+ }
+
+ // 清理相关的API和菜单记录
+ if err := g.cleanupRelatedApiAndMenus(dirtyHistoryIDs); err != nil {
+ global.GVA_LOG.Warn(fmt.Sprintf("清理相关API和菜单记录失败: %v", err))
+ }
+ }
+
+ // 6. 扫描预设计模块
+ predesignedModules, err := g.scanPredesignedModules()
+ if err != nil {
+ global.GVA_LOG.Warn(fmt.Sprintf("扫描预设计模块失败: %v", err))
+ predesignedModules = []PredesignedModuleInfo{} // 设置为空列表,不影响主流程
+ }
+
+ // 7. 过滤掉与已删除包相关的模块
+ filteredModules := []PredesignedModuleInfo{}
+ for _, module := range predesignedModules {
+ isDeleted := false
+ for _, deletedPkg := range cleanupInfo.DeletedPackages {
+ if module.PackageName == deletedPkg {
+ isDeleted = true
+ break
+ }
+ }
+ if !isDeleted {
+ filteredModules = append(filteredModules, module)
+ }
+ }
+
+ // 8. 构建分析结果消息
+ var analysisMessage strings.Builder
+ if len(cleanupInfo.DeletedPackages) > 0 || len(cleanupInfo.DeletedModules) > 0 {
+ analysisMessage.WriteString("**系统清理完成**\n\n")
+ if len(cleanupInfo.DeletedPackages) > 0 {
+ analysisMessage.WriteString(fmt.Sprintf("- 删除了 %d 个空包: %s\n", len(cleanupInfo.DeletedPackages), strings.Join(cleanupInfo.DeletedPackages, ", ")))
+ }
+ if len(cleanupInfo.DeletedModules) > 0 {
+ analysisMessage.WriteString(fmt.Sprintf("- 删除了 %d 个相关模块: %s\n", len(cleanupInfo.DeletedModules), strings.Join(cleanupInfo.DeletedModules, ", ")))
+ }
+ analysisMessage.WriteString("\n")
+ cleanupInfo.CleanupMessage = analysisMessage.String()
+ }
+
+ analysisMessage.WriteString(" **分析结果**\n\n")
+ analysisMessage.WriteString(fmt.Sprintf("- **现有包数量**: %d\n", len(validPackages)))
+ analysisMessage.WriteString(fmt.Sprintf("- **预设计模块数量**: %d\n\n", len(filteredModules)))
+
+ // 9. 转换包信息
+ existingPackages := make([]PackageInfo, len(validPackages))
+ for i, pkg := range validPackages {
+ existingPackages[i] = PackageInfo{
+ PackageName: pkg.PackageName,
+ Template: pkg.Template,
+ Label: pkg.Label,
+ Desc: pkg.Desc,
+ Module: pkg.Module,
+ IsEmpty: false, // 已经过滤掉空包
+ }
+ }
+
+ dictionaries := []DictionaryPre{} // 这里可以根据需要填充字典信息
+ err = global.GVA_DB.Table("sys_dictionaries").Find(&dictionaries, "deleted_at is null").Error
+ if err != nil {
+ global.GVA_LOG.Warn(fmt.Sprintf("获取字典信息失败: %v", err))
+ dictionaries = []DictionaryPre{} // 设置为空列表,不影响主流程
+ }
+
+ // 10. 构建响应
+ response := &AnalyzeResponse{
+ ExistingPackages: existingPackages,
+ PredesignedModules: filteredModules,
+ Dictionaries: dictionaries,
+ }
+
+ return response, nil
+}
+
+// isPackageFolderEmpty 检查包文件夹是否为空
+func (g *GVAAnalyzer) isPackageFolderEmpty(packageName, template string) (bool, error) {
+ // 根据模板类型确定基础路径
+ var basePath string
+ if template == "plugin" {
+ basePath = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", packageName)
+ } else {
+ basePath = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", packageName)
+ }
+
+ // 检查文件夹是否存在
+ if _, err := os.Stat(basePath); os.IsNotExist(err) {
+ return true, nil // 文件夹不存在,视为空
+ } else if err != nil {
+ return false, err // 其他错误
+ }
+ // 递归检查是否有.go文件
+ return g.hasGoFilesRecursive(basePath)
+}
+
+// hasGoFilesRecursive 递归检查目录及其子目录中是否有.go文件
+func (g *GVAAnalyzer) hasGoFilesRecursive(dirPath string) (bool, error) {
+ entries, err := os.ReadDir(dirPath)
+ if err != nil {
+ return true, err // 读取失败,返回空
+ }
+
+ // 检查当前目录下的.go文件
+ for _, entry := range entries {
+ if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".go") {
+ return false, nil // 找到.go文件,不为空
+ }
+ }
+
+ // 递归检查子目录
+ for _, entry := range entries {
+ if entry.IsDir() {
+ subDirPath := filepath.Join(dirPath, entry.Name())
+ isEmpty, err := g.hasGoFilesRecursive(subDirPath)
+ if err != nil {
+ continue // 忽略子目录的错误,继续检查其他目录
+ }
+ if !isEmpty {
+ return false, nil // 子目录中找到.go文件,不为空
+ }
+ }
+ }
+
+ return true, nil // 没有找到.go文件,为空
+}
+
+// removeEmptyPackageFolder 删除空包文件夹
+func (g *GVAAnalyzer) removeEmptyPackageFolder(packageName, template string) error {
+ var basePath string
+ if template == "plugin" {
+ basePath = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", packageName)
+ } else {
+ // 对于package类型,需要删除多个目录
+ paths := []string{
+ filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", packageName),
+ filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "model", packageName),
+ filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", packageName),
+ filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", packageName),
+ }
+ for _, path := range paths {
+ if err := g.removeDirectoryIfExists(path); err != nil {
+ return err
+ }
+ }
+ return nil
+ }
+
+ return g.removeDirectoryIfExists(basePath)
+}
+
+// removeDirectoryIfExists 删除目录(如果存在)
+func (g *GVAAnalyzer) removeDirectoryIfExists(dirPath string) error {
+ if _, err := os.Stat(dirPath); os.IsNotExist(err) {
+ return nil // 目录不存在,无需删除
+ } else if err != nil {
+ return err // 其他错误
+ }
+
+ // 检查目录中是否包含go文件
+ noGoFiles, err := g.hasGoFilesRecursive(dirPath)
+ if err != nil {
+ return err
+ }
+ // hasGoFilesRecursive 返回 false 表示发现了 go 文件
+ if noGoFiles {
+ return os.RemoveAll(dirPath)
+ }
+ return nil
+}
+
+// cleanupRelatedApiAndMenus 清理相关的API和菜单记录
+func (g *GVAAnalyzer) cleanupRelatedApiAndMenus(historyIDs []uint) error {
+ if len(historyIDs) == 0 {
+ return nil
+ }
+
+ // 这里可以根据需要实现具体的API和菜单清理逻辑
+ // 由于涉及到具体的业务逻辑,这里只做日志记录
+ global.GVA_LOG.Info(fmt.Sprintf("清理历史记录ID %v 相关的API和菜单记录", historyIDs))
+
+ // 可以调用service层的相关方法进行清理
+ // 例如:service.ServiceGroupApp.SystemApiService.DeleteApisByIds(historyIDs)
+ // 例如:service.ServiceGroupApp.MenuService.DeleteMenusByIds(historyIDs)
+
+ return nil
+}
+
+// scanPredesignedModules 扫描预设计模块
+func (g *GVAAnalyzer) scanPredesignedModules() ([]PredesignedModuleInfo, error) {
+ // 获取autocode配置路径
+ autocodeRoot := global.GVA_CONFIG.AutoCode.Root
+ if autocodeRoot == "" {
+ return nil, errors.New("autocode根路径未配置")
+ }
+
+ var modules []PredesignedModuleInfo
+
+ // 扫描plugin目录
+ pluginModules, err := g.scanPluginModules(filepath.Join(autocodeRoot, global.GVA_CONFIG.AutoCode.Server, "plugin"))
+ if err != nil {
+ global.GVA_LOG.Warn(fmt.Sprintf("扫描plugin模块失败: %v", err))
+ } else {
+ modules = append(modules, pluginModules...)
+ }
+
+ // 扫描model目录
+ modelModules, err := g.scanModelModules(filepath.Join(autocodeRoot, global.GVA_CONFIG.AutoCode.Server, "model"))
+ if err != nil {
+ global.GVA_LOG.Warn(fmt.Sprintf("扫描model模块失败: %v", err))
+ } else {
+ modules = append(modules, modelModules...)
+ }
+
+ return modules, nil
+}
+
+// scanPluginModules 扫描插件模块
+func (g *GVAAnalyzer) scanPluginModules(pluginDir string) ([]PredesignedModuleInfo, error) {
+ var modules []PredesignedModuleInfo
+
+ if _, err := os.Stat(pluginDir); os.IsNotExist(err) {
+ return modules, nil // 目录不存在,返回空列表
+ }
+
+ entries, err := os.ReadDir(pluginDir)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, entry := range entries {
+ if entry.IsDir() {
+ pluginName := entry.Name()
+ pluginPath := filepath.Join(pluginDir, pluginName)
+
+ // 查找model目录
+ modelDir := filepath.Join(pluginPath, "model")
+ if _, err := os.Stat(modelDir); err == nil {
+ // 扫描model目录下的模块
+ pluginModules, err := g.scanModulesInDirectory(modelDir, pluginName, "plugin")
+ if err != nil {
+ global.GVA_LOG.Warn(fmt.Sprintf("扫描插件 %s 的模块失败: %v", pluginName, err))
+ continue
+ }
+ modules = append(modules, pluginModules...)
+ }
+ }
+ }
+
+ return modules, nil
+}
+
+// scanModelModules 扫描模型模块
+func (g *GVAAnalyzer) scanModelModules(modelDir string) ([]PredesignedModuleInfo, error) {
+ var modules []PredesignedModuleInfo
+
+ if _, err := os.Stat(modelDir); os.IsNotExist(err) {
+ return modules, nil // 目录不存在,返回空列表
+ }
+
+ entries, err := os.ReadDir(modelDir)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, entry := range entries {
+ if entry.IsDir() {
+ packageName := entry.Name()
+ packagePath := filepath.Join(modelDir, packageName)
+
+ // 扫描包目录下的模块
+ packageModules, err := g.scanModulesInDirectory(packagePath, packageName, "package")
+ if err != nil {
+ global.GVA_LOG.Warn(fmt.Sprintf("扫描包 %s 的模块失败: %v", packageName, err))
+ continue
+ }
+ modules = append(modules, packageModules...)
+ }
+ }
+
+ return modules, nil
+}
+
+// scanModulesInDirectory 扫描目录中的模块
+func (g *GVAAnalyzer) scanModulesInDirectory(dir, packageName, template string) ([]PredesignedModuleInfo, error) {
+ var modules []PredesignedModuleInfo
+
+ entries, err := os.ReadDir(dir)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, entry := range entries {
+ if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".go") {
+ moduleName := strings.TrimSuffix(entry.Name(), ".go")
+ filePath := filepath.Join(dir, entry.Name())
+
+ module := PredesignedModuleInfo{
+ ModuleName: moduleName,
+ PackageName: packageName,
+ Template: template,
+ FilePaths: []string{filePath},
+ Description: fmt.Sprintf("%s模块中的%s", packageName, moduleName),
+ }
+ modules = append(modules, module)
+ }
+ }
+
+ return modules, nil
+}
diff --git a/server/mcp/gva_execute.go b/server/mcp/gva_execute.go
new file mode 100644
index 0000000..fbbeecf
--- /dev/null
+++ b/server/mcp/gva_execute.go
@@ -0,0 +1,793 @@
+package mcpTool
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "strings"
+
+ model "git.echol.cn/loser/st/server/model/system"
+ "git.echol.cn/loser/st/server/utils"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system/request"
+
+ "git.echol.cn/loser/st/server/service"
+ "github.com/mark3labs/mcp-go/mcp"
+ "go.uber.org/zap"
+)
+
+// 注册工具
+func init() {
+ RegisterTool(&GVAExecutor{})
+}
+
+// GVAExecutor GVA代码生成器
+type GVAExecutor struct{}
+
+// ExecuteRequest 执行请求结构
+type ExecuteRequest struct {
+ ExecutionPlan ExecutionPlan `json:"executionPlan"` // 执行计划
+ Requirement string `json:"requirement"` // 原始需求(可选,用于日志记录)
+}
+
+// ExecuteResponse 执行响应结构
+type ExecuteResponse struct {
+ Success bool `json:"success"`
+ Message string `json:"message"`
+ PackageID uint `json:"packageId,omitempty"`
+ HistoryID uint `json:"historyId,omitempty"`
+ Paths map[string]string `json:"paths,omitempty"`
+ GeneratedPaths []string `json:"generatedPaths,omitempty"`
+ NextActions []string `json:"nextActions,omitempty"`
+}
+
+// ExecutionPlan 执行计划结构
+type ExecutionPlan struct {
+ PackageName string `json:"packageName"`
+ PackageType string `json:"packageType"` // "plugin" 或 "package"
+ NeedCreatedPackage bool `json:"needCreatedPackage"`
+ NeedCreatedModules bool `json:"needCreatedModules"`
+ NeedCreatedDictionaries bool `json:"needCreatedDictionaries"`
+ PackageInfo *request.SysAutoCodePackageCreate `json:"packageInfo,omitempty"`
+ ModulesInfo []*request.AutoCode `json:"modulesInfo,omitempty"`
+ Paths map[string]string `json:"paths,omitempty"`
+ DictionariesInfo []*DictionaryGenerateRequest `json:"dictionariesInfo,omitempty"`
+}
+
+// New 创建GVA代码生成执行器工具
+func (g *GVAExecutor) New() mcp.Tool {
+ return mcp.NewTool("gva_execute",
+ mcp.WithDescription(`**GVA代码生成执行器:直接执行代码生成,无需确认步骤**
+
+**核心功能:**
+根据需求分析和当前的包信息判断是否调用,直接生成代码。支持批量创建多个模块、自动创建包、模块、字典等。
+
+**使用场景:**
+在gva_analyze获取了当前的包信息和字典信息之后,如果已经包含了可以使用的包和模块,那就不要调用本mcp。根据分析结果直接生成代码,适用于自动化代码生成流程。
+
+**重要提示:**
+- 当needCreatedModules=true时,模块创建会自动生成API和菜单,不应再调用api_creator和menu_creator工具
+- 字段使用字典类型时,系统会自动检查并创建字典
+- 字典创建会在模块创建之前执行
+- 当字段配置了dataSource且association=2(一对多关联)时,系统会自动将fieldType修改为'array'`),
+ mcp.WithObject("executionPlan",
+ mcp.Description("执行计划,包含包信息、模块与字典信息"),
+ mcp.Required(),
+ mcp.Properties(map[string]interface{}{
+ "packageName": map[string]interface{}{
+ "type": "string",
+ "description": "包名(小写开头)",
+ },
+ "packageType": map[string]interface{}{
+ "type": "string",
+ "description": "package 或 plugin,如果用户提到了使用插件则创建plugin,如果用户没有特定说明则一律选用package",
+ "enum": []string{"package", "plugin"},
+ },
+ "needCreatedPackage": map[string]interface{}{
+ "type": "boolean",
+ "description": "是否需要创建包,为true时packageInfo必需",
+ },
+ "needCreatedModules": map[string]interface{}{
+ "type": "boolean",
+ "description": "是否需要创建模块,为true时modulesInfo必需",
+ },
+ "needCreatedDictionaries": map[string]interface{}{
+ "type": "boolean",
+ "description": "是否需要创建字典,为true时dictionariesInfo必需",
+ },
+ "packageInfo": map[string]interface{}{
+ "type": "object",
+ "description": "包创建信息,当needCreatedPackage=true时必需",
+ "properties": map[string]interface{}{
+ "desc": map[string]interface{}{"type": "string", "description": "包描述"},
+ "label": map[string]interface{}{"type": "string", "description": "展示名"},
+ "template": map[string]interface{}{"type": "string", "description": "package 或 plugin,如果用户提到了使用插件则创建plugin,如果用户没有特定说明则一律选用package", "enum": []string{"package", "plugin"}},
+ "packageName": map[string]interface{}{"type": "string", "description": "包名"},
+ },
+ },
+ "modulesInfo": map[string]interface{}{
+ "type": "array",
+ "description": "模块配置列表,支持批量创建多个模块",
+ "items": map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "package": map[string]interface{}{"type": "string", "description": "包名(小写开头,示例: userInfo)"},
+ "tableName": map[string]interface{}{"type": "string", "description": "数据库表名(蛇形命名法,示例:user_info)"},
+ "businessDB": map[string]interface{}{"type": "string", "description": "业务数据库(可留空表示默认)"},
+ "structName": map[string]interface{}{"type": "string", "description": "结构体名(大驼峰示例:UserInfo)"},
+ "packageName": map[string]interface{}{"type": "string", "description": "文件名称"},
+ "description": map[string]interface{}{"type": "string", "description": "中文描述"},
+ "abbreviation": map[string]interface{}{"type": "string", "description": "简称"},
+ "humpPackageName": map[string]interface{}{"type": "string", "description": "文件名称(小驼峰),一般是结构体名的小驼峰示例:userInfo"},
+ "gvaModel": map[string]interface{}{"type": "boolean", "description": "是否使用GVA模型(固定为true),自动包含ID、CreatedAt、UpdatedAt、DeletedAt字段"},
+ "autoMigrate": map[string]interface{}{"type": "boolean", "description": "是否自动迁移数据库"},
+ "autoCreateResource": map[string]interface{}{"type": "boolean", "description": "是否创建资源(默认为false)"},
+ "autoCreateApiToSql": map[string]interface{}{"type": "boolean", "description": "是否创建API(默认为true)"},
+ "autoCreateMenuToSql": map[string]interface{}{"type": "boolean", "description": "是否创建菜单(默认为true)"},
+ "autoCreateBtnAuth": map[string]interface{}{"type": "boolean", "description": "是否创建按钮权限(默认为false)"},
+ "onlyTemplate": map[string]interface{}{"type": "boolean", "description": "是否仅模板(默认为false)"},
+ "isTree": map[string]interface{}{"type": "boolean", "description": "是否树形结构(默认为false)"},
+ "treeJson": map[string]interface{}{"type": "string", "description": "树形JSON字段"},
+ "isAdd": map[string]interface{}{"type": "boolean", "description": "是否新增(固定为false)"},
+ "generateWeb": map[string]interface{}{"type": "boolean", "description": "是否生成前端代码"},
+ "generateServer": map[string]interface{}{"type": "boolean", "description": "是否生成后端代码"},
+ "fields": map[string]interface{}{
+ "type": "array",
+ "description": "字段列表",
+ "items": map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "fieldName": map[string]interface{}{"type": "string", "description": "字段名(必须大写开头示例:UserName)"},
+ "fieldDesc": map[string]interface{}{"type": "string", "description": "字段描述"},
+ "fieldType": map[string]interface{}{"type": "string", "description": "字段类型:string(字符串)、richtext(富文本)、int(整型)、bool(布尔值)、float64(浮点型)、time.Time(时间)、enum(枚举)、picture(单图片)、pictures(多图片)、video(视频)、file(文件)、json(JSON)、array(数组)"},
+ "fieldJson": map[string]interface{}{"type": "string", "description": "JSON标签,示例: userName"},
+ "dataTypeLong": map[string]interface{}{"type": "string", "description": "数据长度"},
+ "comment": map[string]interface{}{"type": "string", "description": "注释"},
+ "columnName": map[string]interface{}{"type": "string", "description": "数据库列名,示例: user_name"},
+ "fieldSearchType": map[string]interface{}{"type": "string", "description": "搜索类型:=、!=、>、>=、<、<=、LIKE、BETWEEN、IN、NOT IN、NOT BETWEEN"},
+ "fieldSearchHide": map[string]interface{}{"type": "boolean", "description": "是否隐藏搜索"},
+ "dictType": map[string]interface{}{"type": "string", "description": "字典类型,使用字典类型时系统会自动检查并创建字典"},
+ "form": map[string]interface{}{"type": "boolean", "description": "表单显示"},
+ "table": map[string]interface{}{"type": "boolean", "description": "表格显示"},
+ "desc": map[string]interface{}{"type": "boolean", "description": "详情显示"},
+ "excel": map[string]interface{}{"type": "boolean", "description": "导入导出"},
+ "require": map[string]interface{}{"type": "boolean", "description": "是否必填"},
+ "defaultValue": map[string]interface{}{"type": "string", "description": "默认值"},
+ "errorText": map[string]interface{}{"type": "string", "description": "错误提示"},
+ "clearable": map[string]interface{}{"type": "boolean", "description": "是否可清空"},
+ "sort": map[string]interface{}{"type": "boolean", "description": "是否排序"},
+ "primaryKey": map[string]interface{}{"type": "boolean", "description": "是否主键(gvaModel=false时必须有一个字段为true)"},
+ "dataSource": map[string]interface{}{
+ "type": "object",
+ "description": "数据源配置,用于配置字段的关联表信息。获取表名提示:可在 server/model 和 plugin/xxx/model 目录下查看对应模块的 TableName() 接口实现获取实际表名(如 SysUser 的表名为 sys_users)。获取数据库名提示:主数据库通常使用 gva(默认数据库标识),多数据库可在 config.yaml 的 db-list 配置中查看可用数据库的 alias-name 字段,如果用户未提及关联多数据库信息则使用默认数据库,默认数据库的情况下 dbName填写为空",
+ "properties": map[string]interface{}{
+ "dbName": map[string]interface{}{"type": "string", "description": "关联的数据库名称(默认数据库留空)"},
+ "table": map[string]interface{}{"type": "string", "description": "关联的表名"},
+ "label": map[string]interface{}{"type": "string", "description": "用于显示的字段名(如name、title等)"},
+ "value": map[string]interface{}{"type": "string", "description": "用于存储的值字段名(通常是id)"},
+ "association": map[string]interface{}{"type": "integer", "description": "关联关系类型:1=一对一关联,2=一对多关联。一对一和一对多的前面的一是当前的实体,如果他只能关联另一个实体的一个则选用一对一,如果他需要关联多个他的关联实体则选用一对多"},
+ "hasDeletedAt": map[string]interface{}{"type": "boolean", "description": "关联表是否有软删除字段"},
+ },
+ },
+ "checkDataSource": map[string]interface{}{"type": "boolean", "description": "是否检查数据源,启用后会验证关联表的存在性"},
+ "fieldIndexType": map[string]interface{}{"type": "string", "description": "索引类型"},
+ },
+ },
+ },
+ },
+ },
+ },
+ "paths": map[string]interface{}{
+ "type": "object",
+ "description": "生成的文件路径映射",
+ "additionalProperties": map[string]interface{}{"type": "string"},
+ },
+ "dictionariesInfo": map[string]interface{}{
+ "type": "array",
+ "description": "字典创建信息,字典创建会在模块创建之前执行",
+ "items": map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "dictType": map[string]interface{}{"type": "string", "description": "字典类型,用于标识字典的唯一性"},
+ "dictName": map[string]interface{}{"type": "string", "description": "字典名称,必须生成,字典的中文名称"},
+ "description": map[string]interface{}{"type": "string", "description": "字典描述,字典的用途说明"},
+ "status": map[string]interface{}{"type": "boolean", "description": "字典状态:true启用,false禁用"},
+ "fieldDesc": map[string]interface{}{"type": "string", "description": "字段描述,用于AI理解字段含义并生成合适的选项"},
+ "options": map[string]interface{}{
+ "type": "array",
+ "description": "字典选项列表(可选,如果不提供将根据fieldDesc自动生成默认选项)",
+ "items": map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "label": map[string]interface{}{"type": "string", "description": "显示名称,用户看到的选项名"},
+ "value": map[string]interface{}{"type": "string", "description": "选项值,实际存储的值"},
+ "sort": map[string]interface{}{"type": "integer", "description": "排序号,数字越小越靠前"},
+ },
+ },
+ },
+ },
+ },
+ },
+ }),
+ mcp.AdditionalProperties(false),
+ ),
+ mcp.WithString("requirement",
+ mcp.Description("原始需求描述(可选,用于日志记录)"),
+ ),
+ )
+}
+
+// Handle 处理执行请求(移除确认步骤)
+func (g *GVAExecutor) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ executionPlanData, ok := request.GetArguments()["executionPlan"]
+ if !ok {
+ return nil, errors.New("参数错误:executionPlan 必须提供")
+ }
+
+ // 解析执行计划
+ planJSON, err := json.Marshal(executionPlanData)
+ if err != nil {
+ return nil, fmt.Errorf("解析执行计划失败: %v", err)
+ }
+
+ var plan ExecutionPlan
+ err = json.Unmarshal(planJSON, &plan)
+ if err != nil {
+ return nil, fmt.Errorf("解析执行计划失败: %v\n\n请确保ExecutionPlan格式正确,参考工具描述中的结构体格式要求", err)
+ }
+
+ // 验证执行计划的完整性
+ if err := g.validateExecutionPlan(&plan); err != nil {
+ return nil, fmt.Errorf("执行计划验证失败: %v", err)
+ }
+
+ // 获取原始需求(可选)
+ var originalRequirement string
+ if reqData, ok := request.GetArguments()["requirement"]; ok {
+ if reqStr, ok := reqData.(string); ok {
+ originalRequirement = reqStr
+ }
+ }
+
+ // 直接执行创建操作(无确认步骤)
+ result := g.executeCreation(ctx, &plan)
+
+ // 如果执行成功且有原始需求,提供代码复检建议
+ var reviewMessage string
+ if result.Success && originalRequirement != "" {
+ global.GVA_LOG.Info("执行完成,返回生成的文件路径供AI进行代码复检...")
+
+ // 构建文件路径信息供AI使用
+ var pathsInfo []string
+ for _, path := range result.GeneratedPaths {
+ pathsInfo = append(pathsInfo, fmt.Sprintf("- %s", path))
+ }
+
+ reviewMessage = fmt.Sprintf("\n\n📁 已生成以下文件:\n%s\n\n💡 提示:可以检查生成的代码是否满足原始需求。", strings.Join(pathsInfo, "\n"))
+ } else if originalRequirement == "" {
+ reviewMessage = "\n\n💡 提示:如需代码复检,请提供原始需求描述。"
+ }
+
+ // 序列化响应
+ response := ExecuteResponse{
+ Success: result.Success,
+ Message: result.Message,
+ PackageID: result.PackageID,
+ HistoryID: result.HistoryID,
+ Paths: result.Paths,
+ GeneratedPaths: result.GeneratedPaths,
+ NextActions: result.NextActions,
+ }
+
+ responseJSON, err := json.MarshalIndent(response, "", " ")
+ if err != nil {
+ return nil, fmt.Errorf("序列化结果失败: %v", err)
+ }
+
+ return &mcp.CallToolResult{
+ Content: []mcp.Content{
+ mcp.NewTextContent(fmt.Sprintf("执行结果:\n\n%s%s", string(responseJSON), reviewMessage)),
+ },
+ }, nil
+}
+
+// validateExecutionPlan 验证执行计划的完整性
+func (g *GVAExecutor) validateExecutionPlan(plan *ExecutionPlan) error {
+ // 验证基本字段
+ if plan.PackageName == "" {
+ return errors.New("packageName 不能为空")
+ }
+ if plan.PackageType != "package" && plan.PackageType != "plugin" {
+ return errors.New("packageType 必须是 'package' 或 'plugin'")
+ }
+
+ // 验证packageType和template字段的一致性
+ if plan.NeedCreatedPackage && plan.PackageInfo != nil {
+ if plan.PackageType != plan.PackageInfo.Template {
+ return errors.New("packageType 和 packageInfo.template 必须保持一致")
+ }
+ }
+
+ // 验证包信息
+ if plan.NeedCreatedPackage {
+ if plan.PackageInfo == nil {
+ return errors.New("当 needCreatedPackage=true 时,packageInfo 不能为空")
+ }
+ if plan.PackageInfo.PackageName == "" {
+ return errors.New("packageInfo.packageName 不能为空")
+ }
+ if plan.PackageInfo.Template != "package" && plan.PackageInfo.Template != "plugin" {
+ return errors.New("packageInfo.template 必须是 'package' 或 'plugin'")
+ }
+ if plan.PackageInfo.Label == "" {
+ return errors.New("packageInfo.label 不能为空")
+ }
+ if plan.PackageInfo.Desc == "" {
+ return errors.New("packageInfo.desc 不能为空")
+ }
+ }
+
+ // 验证模块信息(批量验证)
+ if plan.NeedCreatedModules {
+ if len(plan.ModulesInfo) == 0 {
+ return errors.New("当 needCreatedModules=true 时,modulesInfo 不能为空")
+ }
+
+ // 遍历验证每个模块
+ for moduleIndex, moduleInfo := range plan.ModulesInfo {
+ if moduleInfo.Package == "" {
+ return fmt.Errorf("模块 %d 的 package 不能为空", moduleIndex+1)
+ }
+ if moduleInfo.StructName == "" {
+ return fmt.Errorf("模块 %d 的 structName 不能为空", moduleIndex+1)
+ }
+ if moduleInfo.TableName == "" {
+ return fmt.Errorf("模块 %d 的 tableName 不能为空", moduleIndex+1)
+ }
+ if moduleInfo.Description == "" {
+ return fmt.Errorf("模块 %d 的 description 不能为空", moduleIndex+1)
+ }
+ if moduleInfo.Abbreviation == "" {
+ return fmt.Errorf("模块 %d 的 abbreviation 不能为空", moduleIndex+1)
+ }
+ if moduleInfo.PackageName == "" {
+ return fmt.Errorf("模块 %d 的 packageName 不能为空", moduleIndex+1)
+ }
+ if moduleInfo.HumpPackageName == "" {
+ return fmt.Errorf("模块 %d 的 humpPackageName 不能为空", moduleIndex+1)
+ }
+
+ // 验证字段信息
+ if len(moduleInfo.Fields) == 0 {
+ return fmt.Errorf("模块 %d 的 fields 不能为空,至少需要一个字段", moduleIndex+1)
+ }
+
+ for i, field := range moduleInfo.Fields {
+ if field.FieldName == "" {
+ return fmt.Errorf("模块 %d 字段 %d 的 fieldName 不能为空", moduleIndex+1, i+1)
+ }
+
+ // 确保字段名首字母大写
+ if len(field.FieldName) > 0 {
+ firstChar := string(field.FieldName[0])
+ if firstChar >= "a" && firstChar <= "z" {
+ moduleInfo.Fields[i].FieldName = strings.ToUpper(firstChar) + field.FieldName[1:]
+ }
+ }
+ if field.FieldDesc == "" {
+ return fmt.Errorf("模块 %d 字段 %d 的 fieldDesc 不能为空", moduleIndex+1, i+1)
+ }
+ if field.FieldType == "" {
+ return fmt.Errorf("模块 %d 字段 %d 的 fieldType 不能为空", moduleIndex+1, i+1)
+ }
+ if field.FieldJson == "" {
+ return fmt.Errorf("模块 %d 字段 %d 的 fieldJson 不能为空", moduleIndex+1, i+1)
+ }
+ if field.ColumnName == "" {
+ return fmt.Errorf("模块 %d 字段 %d 的 columnName 不能为空", moduleIndex+1, i+1)
+ }
+
+ // 验证字段类型
+ validFieldTypes := []string{"string", "int", "int64", "float64", "bool", "time.Time", "enum", "picture", "video", "file", "pictures", "array", "richtext", "json"}
+ validType := false
+ for _, validFieldType := range validFieldTypes {
+ if field.FieldType == validFieldType {
+ validType = true
+ break
+ }
+ }
+ if !validType {
+ return fmt.Errorf("模块 %d 字段 %d 的 fieldType '%s' 不支持,支持的类型:%v", moduleIndex+1, i+1, field.FieldType, validFieldTypes)
+ }
+
+ // 验证搜索类型(如果设置了)
+ if field.FieldSearchType != "" {
+ validSearchTypes := []string{"=", "!=", ">", ">=", "<", "<=", "LIKE", "BETWEEN", "IN", "NOT IN"}
+ validSearchType := false
+ for _, validType := range validSearchTypes {
+ if field.FieldSearchType == validType {
+ validSearchType = true
+ break
+ }
+ }
+ if !validSearchType {
+ return fmt.Errorf("模块 %d 字段 %d 的 fieldSearchType '%s' 不支持,支持的类型:%v", moduleIndex+1, i+1, field.FieldSearchType, validSearchTypes)
+ }
+ }
+
+ // 验证 dataSource 字段配置
+ if field.DataSource != nil {
+ associationValue := field.DataSource.Association
+ // 当 association 为 2(一对多关联)时,强制修改 fieldType 为 array
+ if associationValue == 2 {
+ if field.FieldType != "array" {
+ global.GVA_LOG.Info(fmt.Sprintf("模块 %d 字段 %d:检测到一对多关联(association=2),自动将 fieldType 从 '%s' 修改为 'array'", moduleIndex+1, i+1, field.FieldType))
+ moduleInfo.Fields[i].FieldType = "array"
+ }
+ }
+
+ // 验证 association 值的有效性
+ if associationValue != 1 && associationValue != 2 {
+ return fmt.Errorf("模块 %d 字段 %d 的 dataSource.association 必须是 1(一对一)或 2(一对多)", moduleIndex+1, i+1)
+ }
+ }
+ }
+
+ // 验证主键设置
+ if !moduleInfo.GvaModel {
+ // 当不使用GVA模型时,必须有且仅有一个字段设置为主键
+ primaryKeyCount := 0
+ for _, field := range moduleInfo.Fields {
+ if field.PrimaryKey {
+ primaryKeyCount++
+ }
+ }
+ if primaryKeyCount == 0 {
+ return fmt.Errorf("模块 %d:当 gvaModel=false 时,必须有一个字段的 primaryKey=true", moduleIndex+1)
+ }
+ if primaryKeyCount > 1 {
+ return fmt.Errorf("模块 %d:当 gvaModel=false 时,只能有一个字段的 primaryKey=true", moduleIndex+1)
+ }
+ } else {
+ // 当使用GVA模型时,所有字段的primaryKey都应该为false
+ for i, field := range moduleInfo.Fields {
+ if field.PrimaryKey {
+ return fmt.Errorf("模块 %d:当 gvaModel=true 时,字段 %d 的 primaryKey 应该为 false,系统会自动创建ID主键", moduleIndex+1, i+1)
+ }
+ }
+ }
+ }
+ }
+
+ return nil
+}
+
+// executeCreation 执行创建操作
+func (g *GVAExecutor) executeCreation(ctx context.Context, plan *ExecutionPlan) *ExecuteResponse {
+ result := &ExecuteResponse{
+ Success: false,
+ Paths: make(map[string]string),
+ GeneratedPaths: []string{}, // 初始化生成文件路径列表
+ }
+
+ // 无论如何都先构建目录结构信息,确保paths始终返回
+ result.Paths = g.buildDirectoryStructure(plan)
+
+ // 记录预期生成的文件路径
+ result.GeneratedPaths = g.collectExpectedFilePaths(plan)
+
+ if !plan.NeedCreatedModules {
+ result.Success = true
+ result.Message += "已列出当前功能所涉及的目录结构信息; 请在paths中查看; 并且在对应指定文件中实现相关的业务逻辑; "
+ return result
+ }
+
+ // 创建包(如果需要)
+ if plan.NeedCreatedPackage && plan.PackageInfo != nil {
+ packageService := service.ServiceGroupApp.SystemServiceGroup.AutoCodePackage
+ err := packageService.Create(ctx, plan.PackageInfo)
+ if err != nil {
+ result.Message = fmt.Sprintf("创建包失败: %v", err)
+ // 即使创建包失败,也要返回paths信息
+ return result
+ }
+ result.Message += "包创建成功; "
+ }
+
+ // 创建指定字典(如果需要)
+ if plan.NeedCreatedDictionaries && len(plan.DictionariesInfo) > 0 {
+ dictResult := g.createDictionariesFromInfo(ctx, plan.DictionariesInfo)
+ result.Message += dictResult
+ }
+
+ // 批量创建字典和模块(如果需要)
+ if plan.NeedCreatedModules && len(plan.ModulesInfo) > 0 {
+ templateService := service.ServiceGroupApp.SystemServiceGroup.AutoCodeTemplate
+
+ // 遍历所有模块进行创建
+ for _, moduleInfo := range plan.ModulesInfo {
+
+ // 创建模块
+ err := moduleInfo.Pretreatment()
+ if err != nil {
+ result.Message += fmt.Sprintf("模块 %s 信息预处理失败: %v; ", moduleInfo.StructName, err)
+ continue // 继续处理下一个模块
+ }
+
+ err = templateService.Create(ctx, *moduleInfo)
+ if err != nil {
+ result.Message += fmt.Sprintf("创建模块 %s 失败: %v; ", moduleInfo.StructName, err)
+ continue // 继续处理下一个模块
+ }
+ result.Message += fmt.Sprintf("模块 %s 创建成功; ", moduleInfo.StructName)
+ }
+
+ result.Message += fmt.Sprintf("批量创建完成,共处理 %d 个模块; ", len(plan.ModulesInfo))
+
+ // 添加重要提醒:不要使用其他MCP工具
+ result.Message += "\n\n⚠️ 重要提醒:\n"
+ result.Message += "模块创建已完成,API和菜单已自动生成。请不要再调用以下MCP工具:\n"
+ result.Message += "- api_creator:API权限已在模块创建时自动生成\n"
+ result.Message += "- menu_creator:前端菜单已在模块创建时自动生成\n"
+ result.Message += "如需修改API或菜单,请直接在系统管理界面中进行配置。\n"
+ }
+
+ result.Message += "已构建目录结构信息; "
+ result.Success = true
+
+ if result.Message == "" {
+ result.Message = "执行计划完成"
+ }
+
+ return result
+}
+
+// buildDirectoryStructure 构建目录结构信息
+func (g *GVAExecutor) buildDirectoryStructure(plan *ExecutionPlan) map[string]string {
+ paths := make(map[string]string)
+
+ // 获取配置信息
+ autoCodeConfig := global.GVA_CONFIG.AutoCode
+
+ // 构建基础路径
+ rootPath := autoCodeConfig.Root
+ serverPath := autoCodeConfig.Server
+ webPath := autoCodeConfig.Web
+ moduleName := autoCodeConfig.Module
+
+ // 如果计划中有包名,使用计划中的包名,否则使用默认
+ packageName := "example"
+ if plan.PackageName != "" {
+ packageName = plan.PackageName
+ }
+
+ // 如果计划中有模块信息,获取第一个模块的结构名作为默认值
+ structName := "ExampleStruct"
+ if len(plan.ModulesInfo) > 0 && plan.ModulesInfo[0].StructName != "" {
+ structName = plan.ModulesInfo[0].StructName
+ }
+
+ // 根据包类型构建不同的路径结构
+ packageType := plan.PackageType
+ if packageType == "" {
+ packageType = "package" // 默认为package模式
+ }
+
+ // 构建服务端路径
+ if serverPath != "" {
+ serverBasePath := fmt.Sprintf("%s/%s", rootPath, serverPath)
+
+ if packageType == "plugin" {
+ // Plugin 模式:所有文件都在 /plugin/packageName/ 目录下
+ plugingBasePath := fmt.Sprintf("%s/plugin/%s", serverBasePath, packageName)
+
+ // API 路径
+ paths["api"] = fmt.Sprintf("%s/api", plugingBasePath)
+
+ // Service 路径
+ paths["service"] = fmt.Sprintf("%s/service", plugingBasePath)
+
+ // Model 路径
+ paths["model"] = fmt.Sprintf("%s/model", plugingBasePath)
+
+ // Router 路径
+ paths["router"] = fmt.Sprintf("%s/router", plugingBasePath)
+
+ // Request 路径
+ paths["request"] = fmt.Sprintf("%s/model/request", plugingBasePath)
+
+ // Response 路径
+ paths["response"] = fmt.Sprintf("%s/model/response", plugingBasePath)
+
+ // Plugin 特有文件
+ paths["plugin_main"] = fmt.Sprintf("%s/main.go", plugingBasePath)
+ paths["plugin_config"] = fmt.Sprintf("%s/plugin.go", plugingBasePath)
+ paths["plugin_initialize"] = fmt.Sprintf("%s/initialize", plugingBasePath)
+ } else {
+ // Package 模式:传统的目录结构
+ // API 路径
+ paths["api"] = fmt.Sprintf("%s/api/v1/%s", serverBasePath, packageName)
+
+ // Service 路径
+ paths["service"] = fmt.Sprintf("%s/service/%s", serverBasePath, packageName)
+
+ // Model 路径
+ paths["model"] = fmt.Sprintf("%s/model/%s", serverBasePath, packageName)
+
+ // Router 路径
+ paths["router"] = fmt.Sprintf("%s/router/%s", serverBasePath, packageName)
+
+ // Request 路径
+ paths["request"] = fmt.Sprintf("%s/model/%s/request", serverBasePath, packageName)
+
+ // Response 路径
+ paths["response"] = fmt.Sprintf("%s/model/%s/response", serverBasePath, packageName)
+ }
+ }
+
+ // 构建前端路径(两种模式相同)
+ if webPath != "" {
+ webBasePath := fmt.Sprintf("%s/%s", rootPath, webPath)
+
+ if packageType == "plugin" {
+ // Plugin 模式:前端文件也在 /plugin/packageName/ 目录下
+ pluginWebBasePath := fmt.Sprintf("%s/plugin/%s", webBasePath, packageName)
+
+ // Vue 页面路径
+ paths["vue_page"] = fmt.Sprintf("%s/view", pluginWebBasePath)
+
+ // API 路径
+ paths["vue_api"] = fmt.Sprintf("%s/api", pluginWebBasePath)
+ } else {
+ // Package 模式:传统的目录结构
+ // Vue 页面路径
+ paths["vue_page"] = fmt.Sprintf("%s/view/%s", webBasePath, packageName)
+
+ // API 路径
+ paths["vue_api"] = fmt.Sprintf("%s/api/%s", webBasePath, packageName)
+ }
+ }
+
+ // 添加模块信息
+ paths["module"] = moduleName
+ paths["package_name"] = packageName
+ paths["package_type"] = packageType
+ paths["struct_name"] = structName
+ paths["root_path"] = rootPath
+ paths["server_path"] = serverPath
+ paths["web_path"] = webPath
+
+ return paths
+}
+
+// collectExpectedFilePaths 收集预期生成的文件路径
+func (g *GVAExecutor) collectExpectedFilePaths(plan *ExecutionPlan) []string {
+ var paths []string
+
+ // 获取目录结构
+ dirPaths := g.buildDirectoryStructure(plan)
+
+ // 如果需要创建模块,添加预期的文件路径
+ if plan.NeedCreatedModules && len(plan.ModulesInfo) > 0 {
+ for _, moduleInfo := range plan.ModulesInfo {
+ structName := moduleInfo.StructName
+
+ // 后端文件
+ if apiPath, ok := dirPaths["api"]; ok {
+ paths = append(paths, fmt.Sprintf("%s/%s.go", apiPath, strings.ToLower(structName)))
+ }
+ if servicePath, ok := dirPaths["service"]; ok {
+ paths = append(paths, fmt.Sprintf("%s/%s.go", servicePath, strings.ToLower(structName)))
+ }
+ if modelPath, ok := dirPaths["model"]; ok {
+ paths = append(paths, fmt.Sprintf("%s/%s.go", modelPath, strings.ToLower(structName)))
+ }
+ if routerPath, ok := dirPaths["router"]; ok {
+ paths = append(paths, fmt.Sprintf("%s/%s.go", routerPath, strings.ToLower(structName)))
+ }
+ if requestPath, ok := dirPaths["request"]; ok {
+ paths = append(paths, fmt.Sprintf("%s/%s.go", requestPath, strings.ToLower(structName)))
+ }
+ if responsePath, ok := dirPaths["response"]; ok {
+ paths = append(paths, fmt.Sprintf("%s/%s.go", responsePath, strings.ToLower(structName)))
+ }
+
+ // 前端文件
+ if vuePage, ok := dirPaths["vue_page"]; ok {
+ paths = append(paths, fmt.Sprintf("%s/%s.vue", vuePage, strings.ToLower(structName)))
+ }
+ if vueApi, ok := dirPaths["vue_api"]; ok {
+ paths = append(paths, fmt.Sprintf("%s/%s.js", vueApi, strings.ToLower(structName)))
+ }
+ }
+ }
+
+ return paths
+}
+
+// checkDictionaryExists 检查字典是否存在
+func (g *GVAExecutor) checkDictionaryExists(dictType string) (bool, error) {
+ dictionaryService := service.ServiceGroupApp.SystemServiceGroup.DictionaryService
+ _, err := dictionaryService.GetSysDictionary(dictType, 0, nil)
+ if err != nil {
+ // 如果是记录不存在的错误,返回false
+ if strings.Contains(err.Error(), "record not found") {
+ return false, nil
+ }
+ // 其他错误返回错误信息
+ return false, err
+ }
+ return true, nil
+}
+
+// createDictionariesFromInfo 根据 DictionariesInfo 创建字典
+func (g *GVAExecutor) createDictionariesFromInfo(ctx context.Context, dictionariesInfo []*DictionaryGenerateRequest) string {
+ var messages []string
+ dictionaryService := service.ServiceGroupApp.SystemServiceGroup.DictionaryService
+ dictionaryDetailService := service.ServiceGroupApp.SystemServiceGroup.DictionaryDetailService
+
+ messages = append(messages, fmt.Sprintf("开始创建 %d 个指定字典: ", len(dictionariesInfo)))
+
+ for _, dictInfo := range dictionariesInfo {
+ // 检查字典是否存在
+ exists, err := g.checkDictionaryExists(dictInfo.DictType)
+ if err != nil {
+ messages = append(messages, fmt.Sprintf("检查字典 %s 时出错: %v; ", dictInfo.DictType, err))
+ continue
+ }
+
+ if !exists {
+ // 字典不存在,创建字典
+ dictionary := model.SysDictionary{
+ Name: dictInfo.DictName,
+ Type: dictInfo.DictType,
+ Status: utils.Pointer(true),
+ Desc: dictInfo.Description,
+ }
+
+ err = dictionaryService.CreateSysDictionary(dictionary)
+ if err != nil {
+ messages = append(messages, fmt.Sprintf("创建字典 %s 失败: %v; ", dictInfo.DictType, err))
+ continue
+ }
+
+ messages = append(messages, fmt.Sprintf("成功创建字典 %s (%s); ", dictInfo.DictType, dictInfo.DictName))
+
+ // 获取刚创建的字典ID
+ var createdDict model.SysDictionary
+ err = global.GVA_DB.Where("type = ?", dictInfo.DictType).First(&createdDict).Error
+ if err != nil {
+ messages = append(messages, fmt.Sprintf("获取创建的字典失败: %v; ", err))
+ continue
+ }
+
+ // 创建字典选项
+ if len(dictInfo.Options) > 0 {
+ successCount := 0
+ for _, option := range dictInfo.Options {
+ dictionaryDetail := model.SysDictionaryDetail{
+ Label: option.Label,
+ Value: option.Value,
+ Status: &[]bool{true}[0], // 默认启用
+ Sort: option.Sort,
+ SysDictionaryID: int(createdDict.ID),
+ }
+
+ err = dictionaryDetailService.CreateSysDictionaryDetail(dictionaryDetail)
+ if err != nil {
+ global.GVA_LOG.Warn("创建字典详情项失败", zap.Error(err))
+ } else {
+ successCount++
+ }
+ }
+ messages = append(messages, fmt.Sprintf("创建了 %d 个字典选项; ", successCount))
+ }
+ } else {
+ messages = append(messages, fmt.Sprintf("字典 %s 已存在,跳过创建; ", dictInfo.DictType))
+ }
+ }
+
+ return strings.Join(messages, "")
+}
diff --git a/server/mcp/gva_review.go b/server/mcp/gva_review.go
new file mode 100644
index 0000000..a32a544
--- /dev/null
+++ b/server/mcp/gva_review.go
@@ -0,0 +1,170 @@
+package mcpTool
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "strings"
+
+ "github.com/mark3labs/mcp-go/mcp"
+)
+
+// GVAReviewer GVA代码审查工具
+type GVAReviewer struct{}
+
+// init 注册工具
+func init() {
+ RegisterTool(&GVAReviewer{})
+}
+
+// ReviewRequest 审查请求结构
+type ReviewRequest struct {
+ UserRequirement string `json:"userRequirement"` // 经过requirement_analyze后的用户需求
+ GeneratedFiles []string `json:"generatedFiles"` // gva_execute创建的文件列表
+}
+
+// ReviewResponse 审查响应结构
+type ReviewResponse struct {
+ Success bool `json:"success"` // 是否审查成功
+ Message string `json:"message"` // 审查结果消息
+ AdjustmentPrompt string `json:"adjustmentPrompt"` // 调整代码的提示
+ ReviewDetails string `json:"reviewDetails"` // 详细的审查结果
+}
+
+// New 创建GVA代码审查工具
+func (g *GVAReviewer) New() mcp.Tool {
+ return mcp.NewTool("gva_review",
+ mcp.WithDescription(`**GVA代码审查工具 - 在gva_execute调用后使用**
+
+**核心功能:**
+- 接收经过requirement_analyze处理的用户需求和gva_execute生成的文件列表
+- 分析生成的代码是否满足用户的原始需求
+- 检查是否涉及到关联、交互等复杂功能
+- 如果代码不满足需求,提供调整建议和新的prompt
+
+**使用场景:**
+- 在gva_execute成功执行后调用
+- 用于验证生成的代码是否完整满足用户需求
+- 检查模块间的关联关系是否正确实现
+- 发现缺失的交互功能或业务逻辑
+
+**工作流程:**
+1. 接收用户原始需求和生成的文件列表
+2. 分析需求中的关键功能点
+3. 检查生成的文件是否覆盖所有功能
+4. 识别缺失的关联关系、交互功能等
+5. 生成调整建议和新的开发prompt
+
+**输出内容:**
+- 审查结果和是否需要调整
+- 详细的缺失功能分析
+- 针对性的代码调整建议
+- 可直接使用的开发prompt
+
+**重要提示:**
+- 本工具专门用于代码质量审查,不执行实际的代码修改
+- 重点关注模块间关联、用户交互、业务流程完整性
+- 提供的调整建议应该具体可执行`),
+ mcp.WithString("userRequirement",
+ mcp.Description("经过requirement_analyze处理后的用户需求描述,包含详细的功能要求和字段信息"),
+ mcp.Required(),
+ ),
+ mcp.WithString("generatedFiles",
+ mcp.Description("gva_execute创建的文件列表,JSON字符串格式,包含所有生成的后端和前端文件路径"),
+ mcp.Required(),
+ ),
+ )
+}
+
+// Handle 处理审查请求
+func (g *GVAReviewer) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ // 获取用户需求
+ userRequirementData, ok := request.GetArguments()["userRequirement"]
+ if !ok {
+ return nil, errors.New("参数错误:userRequirement 必须提供")
+ }
+
+ userRequirement, ok := userRequirementData.(string)
+ if !ok {
+ return nil, errors.New("参数错误:userRequirement 必须是字符串类型")
+ }
+
+ // 获取生成的文件列表
+ generatedFilesData, ok := request.GetArguments()["generatedFiles"]
+ if !ok {
+ return nil, errors.New("参数错误:generatedFiles 必须提供")
+ }
+
+ generatedFilesStr, ok := generatedFilesData.(string)
+ if !ok {
+ return nil, errors.New("参数错误:generatedFiles 必须是JSON字符串")
+ }
+
+ // 解析JSON字符串为字符串数组
+ var generatedFiles []string
+ err := json.Unmarshal([]byte(generatedFilesStr), &generatedFiles)
+ if err != nil {
+ return nil, fmt.Errorf("解析generatedFiles失败: %v", err)
+ }
+
+ if len(generatedFiles) == 0 {
+ return nil, errors.New("参数错误:generatedFiles 不能为空")
+ }
+
+ // 直接生成调整提示,不进行复杂分析
+ adjustmentPrompt := g.generateAdjustmentPrompt(userRequirement, generatedFiles)
+
+ // 构建简化的审查详情
+ reviewDetails := fmt.Sprintf("📋 **代码审查报告**\n\n **用户原始需求:**\n%s\n\n **已生成文件数量:** %d\n\n **建议进行代码优化和完善**", userRequirement, len(generatedFiles))
+
+ // 构建审查结果
+ reviewResult := &ReviewResponse{
+ Success: true,
+ Message: "代码审查完成",
+ AdjustmentPrompt: adjustmentPrompt,
+ ReviewDetails: reviewDetails,
+ }
+
+ // 序列化响应
+ responseJSON, err := json.MarshalIndent(reviewResult, "", " ")
+ if err != nil {
+ return nil, fmt.Errorf("序列化审查结果失败: %v", err)
+ }
+
+ return &mcp.CallToolResult{
+ Content: []mcp.Content{
+ mcp.NewTextContent(fmt.Sprintf("代码审查结果:\n\n%s", string(responseJSON))),
+ },
+ }, nil
+}
+
+// generateAdjustmentPrompt 生成调整代码的提示
+func (g *GVAReviewer) generateAdjustmentPrompt(userRequirement string, generatedFiles []string) string {
+ var prompt strings.Builder
+
+ prompt.WriteString("🔧 **代码调整指导 Prompt:**\n\n")
+ prompt.WriteString(fmt.Sprintf("**用户的原始需求为:** %s\n\n", userRequirement))
+ prompt.WriteString("**经过GVA生成后的文件有如下内容:**\n")
+ for _, file := range generatedFiles {
+ prompt.WriteString(fmt.Sprintf("- %s\n", file))
+ }
+ prompt.WriteString("\n")
+
+ prompt.WriteString("**请帮我优化和完善代码,确保:**\n")
+ prompt.WriteString("1. 代码完全满足用户的原始需求\n")
+ prompt.WriteString("2. 完善模块间的关联关系,确保数据一致性\n")
+ prompt.WriteString("3. 实现所有必要的用户交互功能\n")
+ prompt.WriteString("4. 保持代码的完整性和可维护性\n")
+ prompt.WriteString("5. 遵循GVA框架的开发规范和最佳实践\n")
+ prompt.WriteString("6. 确保前后端功能完整对接\n")
+ prompt.WriteString("7. 添加必要的错误处理和数据验证\n\n")
+ prompt.WriteString("8. 如果需要vue路由跳转,请使用 menu_lister获取完整路由表,并且路由跳转使用 router.push({\"name\":从menu_lister中获取的name})\n\n")
+ prompt.WriteString("9. 如果当前所有的vue页面内容无法满足需求,则自行书写vue文件,并且调用 menu_creator创建菜单记录\n\n")
+ prompt.WriteString("10. 如果需要API调用,请使用 api_lister获取api表,根据需求调用对应接口\n\n")
+ prompt.WriteString("11. 如果当前所有API无法满足则自行书写接口,补全前后端代码,并使用 api_creator创建api记录\n\n")
+ prompt.WriteString("12. 无论前后端都不要随意删除import的内容\n\n")
+ prompt.WriteString("**请基于用户需求和现有文件,提供完整的代码优化方案。**")
+
+ return prompt.String()
+}
diff --git a/server/mcp/menu_creator.go b/server/mcp/menu_creator.go
new file mode 100644
index 0000000..3905606
--- /dev/null
+++ b/server/mcp/menu_creator.go
@@ -0,0 +1,277 @@
+package mcpTool
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system"
+ "git.echol.cn/loser/st/server/service"
+ "github.com/mark3labs/mcp-go/mcp"
+ "go.uber.org/zap"
+)
+
+// 注册工具
+func init() {
+ RegisterTool(&MenuCreator{})
+}
+
+// MenuCreateRequest 菜单创建请求结构
+type MenuCreateRequest struct {
+ ParentId uint `json:"parentId"` // 父菜单ID,0表示根菜单
+ Path string `json:"path"` // 路由path
+ Name string `json:"name"` // 路由name
+ Hidden bool `json:"hidden"` // 是否在列表隐藏
+ Component string `json:"component"` // 对应前端文件路径
+ Sort int `json:"sort"` // 排序标记
+ Title string `json:"title"` // 菜单名
+ Icon string `json:"icon"` // 菜单图标
+ KeepAlive bool `json:"keepAlive"` // 是否缓存
+ DefaultMenu bool `json:"defaultMenu"` // 是否是基础路由
+ CloseTab bool `json:"closeTab"` // 自动关闭tab
+ ActiveName string `json:"activeName"` // 高亮菜单
+ Parameters []MenuParameterRequest `json:"parameters"` // 路由参数
+ MenuBtn []MenuButtonRequest `json:"menuBtn"` // 菜单按钮
+}
+
+// MenuParameterRequest 菜单参数请求结构
+type MenuParameterRequest struct {
+ Type string `json:"type"` // 参数类型:params或query
+ Key string `json:"key"` // 参数key
+ Value string `json:"value"` // 参数值
+}
+
+// MenuButtonRequest 菜单按钮请求结构
+type MenuButtonRequest struct {
+ Name string `json:"name"` // 按钮名称
+ Desc string `json:"desc"` // 按钮描述
+}
+
+// MenuCreateResponse 菜单创建响应结构
+type MenuCreateResponse struct {
+ Success bool `json:"success"`
+ Message string `json:"message"`
+ MenuID uint `json:"menuId"`
+ Name string `json:"name"`
+ Path string `json:"path"`
+}
+
+// MenuCreator 菜单创建工具
+type MenuCreator struct{}
+
+// New 创建菜单创建工具
+func (m *MenuCreator) New() mcp.Tool {
+ return mcp.NewTool("create_menu",
+ mcp.WithDescription(`创建前端菜单记录,用于AI编辑器自动添加前端页面时自动创建对应的菜单项。
+
+**重要限制:**
+- 当使用gva_auto_generate工具且needCreatedModules=true时,模块创建会自动生成菜单项,不应调用此工具
+- 仅在以下情况使用:1) 单独创建菜单(不涉及模块创建);2) AI编辑器自动添加前端页面时`),
+ mcp.WithNumber("parentId",
+ mcp.Description("父菜单ID,0表示根菜单"),
+ mcp.DefaultNumber(0),
+ ),
+ mcp.WithString("path",
+ mcp.Required(),
+ mcp.Description("路由path,如:userList"),
+ ),
+ mcp.WithString("name",
+ mcp.Required(),
+ mcp.Description("路由name,用于Vue Router,如:userList"),
+ ),
+ mcp.WithBoolean("hidden",
+ mcp.Description("是否在菜单列表中隐藏"),
+ ),
+ mcp.WithString("component",
+ mcp.Required(),
+ mcp.Description("对应的前端Vue组件路径,如:view/user/list.vue"),
+ ),
+ mcp.WithNumber("sort",
+ mcp.Description("菜单排序号,数字越小越靠前"),
+ mcp.DefaultNumber(1),
+ ),
+ mcp.WithString("title",
+ mcp.Required(),
+ mcp.Description("菜单显示标题"),
+ ),
+ mcp.WithString("icon",
+ mcp.Description("菜单图标名称"),
+ mcp.DefaultString("menu"),
+ ),
+ mcp.WithBoolean("keepAlive",
+ mcp.Description("是否缓存页面"),
+ ),
+ mcp.WithBoolean("defaultMenu",
+ mcp.Description("是否是基础路由"),
+ ),
+ mcp.WithBoolean("closeTab",
+ mcp.Description("是否自动关闭tab"),
+ ),
+ mcp.WithString("activeName",
+ mcp.Description("高亮菜单名称"),
+ ),
+ mcp.WithString("parameters",
+ mcp.Description("路由参数JSON字符串,格式:[{\"type\":\"params\",\"key\":\"id\",\"value\":\"1\"}]"),
+ ),
+ mcp.WithString("menuBtn",
+ mcp.Description("菜单按钮JSON字符串,格式:[{\"name\":\"add\",\"desc\":\"新增\"}]"),
+ ),
+ )
+}
+
+// Handle 处理菜单创建请求
+func (m *MenuCreator) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ // 解析请求参数
+ args := request.GetArguments()
+
+ // 必需参数
+ path, ok := args["path"].(string)
+ if !ok || path == "" {
+ return nil, errors.New("path 参数是必需的")
+ }
+
+ name, ok := args["name"].(string)
+ if !ok || name == "" {
+ return nil, errors.New("name 参数是必需的")
+ }
+
+ component, ok := args["component"].(string)
+ if !ok || component == "" {
+ return nil, errors.New("component 参数是必需的")
+ }
+
+ title, ok := args["title"].(string)
+ if !ok || title == "" {
+ return nil, errors.New("title 参数是必需的")
+ }
+
+ // 可选参数
+ parentId := uint(0)
+ if val, ok := args["parentId"].(float64); ok {
+ parentId = uint(val)
+ }
+
+ hidden := false
+ if val, ok := args["hidden"].(bool); ok {
+ hidden = val
+ }
+
+ sort := 1
+ if val, ok := args["sort"].(float64); ok {
+ sort = int(val)
+ }
+
+ icon := "menu"
+ if val, ok := args["icon"].(string); ok && val != "" {
+ icon = val
+ }
+
+ keepAlive := false
+ if val, ok := args["keepAlive"].(bool); ok {
+ keepAlive = val
+ }
+
+ defaultMenu := false
+ if val, ok := args["defaultMenu"].(bool); ok {
+ defaultMenu = val
+ }
+
+ closeTab := false
+ if val, ok := args["closeTab"].(bool); ok {
+ closeTab = val
+ }
+
+ activeName := ""
+ if val, ok := args["activeName"].(string); ok {
+ activeName = val
+ }
+
+ // 解析参数和按钮
+ var parameters []system.SysBaseMenuParameter
+ if parametersStr, ok := args["parameters"].(string); ok && parametersStr != "" {
+ var paramReqs []MenuParameterRequest
+ if err := json.Unmarshal([]byte(parametersStr), ¶mReqs); err != nil {
+ return nil, fmt.Errorf("parameters 参数格式错误: %v", err)
+ }
+ for _, param := range paramReqs {
+ parameters = append(parameters, system.SysBaseMenuParameter{
+ Type: param.Type,
+ Key: param.Key,
+ Value: param.Value,
+ })
+ }
+ }
+
+ var menuBtn []system.SysBaseMenuBtn
+ if menuBtnStr, ok := args["menuBtn"].(string); ok && menuBtnStr != "" {
+ var btnReqs []MenuButtonRequest
+ if err := json.Unmarshal([]byte(menuBtnStr), &btnReqs); err != nil {
+ return nil, fmt.Errorf("menuBtn 参数格式错误: %v", err)
+ }
+ for _, btn := range btnReqs {
+ menuBtn = append(menuBtn, system.SysBaseMenuBtn{
+ Name: btn.Name,
+ Desc: btn.Desc,
+ })
+ }
+ }
+
+ // 构建菜单对象
+ menu := system.SysBaseMenu{
+ ParentId: parentId,
+ Path: path,
+ Name: name,
+ Hidden: hidden,
+ Component: component,
+ Sort: sort,
+ Meta: system.Meta{
+ Title: title,
+ Icon: icon,
+ KeepAlive: keepAlive,
+ DefaultMenu: defaultMenu,
+ CloseTab: closeTab,
+ ActiveName: activeName,
+ },
+ Parameters: parameters,
+ MenuBtn: menuBtn,
+ }
+
+ // 创建菜单
+ menuService := service.ServiceGroupApp.SystemServiceGroup.MenuService
+ err := menuService.AddBaseMenu(menu)
+ if err != nil {
+ return nil, fmt.Errorf("创建菜单失败: %v", err)
+ }
+
+ // 获取创建的菜单ID
+ var createdMenu system.SysBaseMenu
+ err = global.GVA_DB.Where("name = ? AND path = ?", name, path).First(&createdMenu).Error
+ if err != nil {
+ global.GVA_LOG.Warn("获取创建的菜单ID失败", zap.Error(err))
+ }
+
+ // 构建响应
+ response := &MenuCreateResponse{
+ Success: true,
+ Message: fmt.Sprintf("成功创建菜单 %s", title),
+ MenuID: createdMenu.ID,
+ Name: name,
+ Path: path,
+ }
+
+ resultJSON, err := json.MarshalIndent(response, "", " ")
+ if err != nil {
+ return nil, fmt.Errorf("序列化结果失败: %v", err)
+ }
+
+ return &mcp.CallToolResult{
+ Content: []mcp.Content{
+ mcp.TextContent{
+ Type: "text",
+ Text: fmt.Sprintf("菜单创建结果:\n\n%s", string(resultJSON)),
+ },
+ },
+ }, nil
+}
diff --git a/server/mcp/menu_lister.go b/server/mcp/menu_lister.go
new file mode 100644
index 0000000..21b6d60
--- /dev/null
+++ b/server/mcp/menu_lister.go
@@ -0,0 +1,114 @@
+package mcpTool
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system"
+ "github.com/mark3labs/mcp-go/mcp"
+ "go.uber.org/zap"
+)
+
+// 注册工具
+func init() {
+ // 注册工具将在enter.go中统一处理
+ RegisterTool(&MenuLister{})
+}
+
+// MenuListResponse 菜单列表响应结构
+type MenuListResponse struct {
+ Success bool `json:"success"`
+ Message string `json:"message"`
+ Menus []system.SysBaseMenu `json:"menus"`
+ TotalCount int `json:"totalCount"`
+ Description string `json:"description"`
+}
+
+// MenuLister 菜单列表工具
+type MenuLister struct{}
+
+// New 创建菜单列表工具
+func (m *MenuLister) New() mcp.Tool {
+ return mcp.NewTool("list_all_menus",
+ mcp.WithDescription(`获取系统中所有菜单信息,包括菜单树结构、路由信息、组件路径等,用于前端编写vue-router时正确跳转
+
+**功能说明:**
+- 返回完整的菜单树形结构
+- 包含路由配置信息(path、name、component)
+- 包含菜单元数据(title、icon、keepAlive等)
+- 包含菜单参数和按钮配置
+- 支持父子菜单关系展示
+
+**使用场景:**
+- 前端路由配置:获取所有菜单信息用于配置vue-router
+- 菜单权限管理:了解系统中所有可用的菜单项
+- 导航组件开发:构建动态导航菜单
+- 系统架构分析:了解系统的菜单结构和页面组织`),
+ mcp.WithString("_placeholder",
+ mcp.Description("占位符,防止json schema校验失败"),
+ ),
+ )
+}
+
+// Handle 处理菜单列表请求
+func (m *MenuLister) Handle(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ // 获取所有基础菜单
+ allMenus, err := m.getAllMenus()
+ if err != nil {
+ global.GVA_LOG.Error("获取菜单列表失败", zap.Error(err))
+ return &mcp.CallToolResult{
+ Content: []mcp.Content{
+ mcp.TextContent{
+ Type: "text",
+ Text: fmt.Sprintf("获取菜单列表失败: %v", err),
+ },
+ },
+ IsError: true,
+ }, nil
+ }
+
+ // 构建返回结果
+ response := MenuListResponse{
+ Success: true,
+ Message: "获取菜单列表成功",
+ Menus: allMenus,
+ TotalCount: len(allMenus),
+ Description: "系统中所有菜单信息的标准列表,包含路由配置和组件信息",
+ }
+
+ // 序列化响应
+ responseJSON, err := json.MarshalIndent(response, "", " ")
+ if err != nil {
+ global.GVA_LOG.Error("序列化菜单响应失败", zap.Error(err))
+ return &mcp.CallToolResult{
+ Content: []mcp.Content{
+ mcp.TextContent{
+ Type: "text",
+ Text: fmt.Sprintf("序列化响应失败: %v", err),
+ },
+ },
+ IsError: true,
+ }, nil
+ }
+
+ return &mcp.CallToolResult{
+ Content: []mcp.Content{
+ mcp.TextContent{
+ Type: "text",
+ Text: string(responseJSON),
+ },
+ },
+ }, nil
+}
+
+// getAllMenus 获取所有基础菜单
+func (m *MenuLister) getAllMenus() ([]system.SysBaseMenu, error) {
+ var menus []system.SysBaseMenu
+ err := global.GVA_DB.Order("sort").Preload("Parameters").Preload("MenuBtn").Find(&menus).Error
+ if err != nil {
+ return nil, err
+ }
+ return menus, nil
+}
diff --git a/server/mcp/requirement_analyzer.go b/server/mcp/requirement_analyzer.go
new file mode 100644
index 0000000..765b750
--- /dev/null
+++ b/server/mcp/requirement_analyzer.go
@@ -0,0 +1,199 @@
+package mcpTool
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+
+ "github.com/mark3labs/mcp-go/mcp"
+)
+
+func init() {
+ RegisterTool(&RequirementAnalyzer{})
+}
+
+type RequirementAnalyzer struct{}
+
+// RequirementAnalysisRequest 需求分析请求
+type RequirementAnalysisRequest struct {
+ UserRequirement string `json:"userRequirement"`
+}
+
+// RequirementAnalysisResponse 需求分析响应
+type RequirementAnalysisResponse struct {
+ AIPrompt string `json:"aiPrompt"` // 给AI的提示词
+}
+
+// New 返回工具注册信息
+func (t *RequirementAnalyzer) New() mcp.Tool {
+ return mcp.NewTool("requirement_analyzer",
+ mcp.WithDescription(`** 智能需求分析与模块设计工具 - 首选入口工具(最高优先级)**
+
+** 重要提示:这是所有MCP工具的首选入口,请优先使用!**
+
+** 核心能力:**
+作为资深系统架构师,智能分析用户需求并自动设计完整的模块架构
+
+** 核心功能:**
+1. **智能需求解构**:深度分析用户需求,识别核心业务实体、业务流程、数据关系
+2. **自动模块设计**:基于需求分析,智能确定需要多少个模块及各模块功能
+3. **字段智能推导**:为每个模块自动设计详细字段,包含数据类型、关联关系、字典需求
+4. **架构优化建议**:提供模块拆分、关联设计、扩展性等专业建议
+
+** 输出内容:**
+- 模块数量和架构设计
+- 每个模块的详细字段清单
+- 数据类型和关联关系设计
+- 字典需求和类型定义
+- 模块间关系图和扩展建议
+
+** 适用场景:**
+- 用户需求描述不完整,需要智能补全
+- 复杂业务系统的模块架构设计
+- 需要专业的数据库设计建议
+- 想要快速搭建生产级业务系统
+
+** 推荐工作流:**
+ requirement_analyzer → gva_analyze → gva_execute → 其他辅助工具
+
+ `),
+ mcp.WithString("userRequirement",
+ mcp.Required(),
+ mcp.Description("用户的需求描述,支持自然语言,如:'我要做一个猫舍管理系统,用来录入猫的信息,并且记录每只猫每天的活动信息'"),
+ ),
+ )
+}
+
+// Handle 处理工具调用
+func (t *RequirementAnalyzer) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ userRequirement, ok := request.GetArguments()["userRequirement"].(string)
+ if !ok || userRequirement == "" {
+ return nil, errors.New("参数错误:userRequirement 必须是非空字符串")
+ }
+
+ // 分析用户需求
+ analysisResponse, err := t.analyzeRequirement(userRequirement)
+ if err != nil {
+ return nil, fmt.Errorf("需求分析失败: %v", err)
+ }
+
+ // 序列化响应
+ responseData, err := json.Marshal(analysisResponse)
+ if err != nil {
+ return nil, fmt.Errorf("序列化响应失败: %v", err)
+ }
+
+ return &mcp.CallToolResult{
+ Content: []mcp.Content{
+ mcp.NewTextContent(string(responseData)),
+ },
+ }, nil
+}
+
+// analyzeRequirement 分析用户需求 - 专注于AI需求传递
+func (t *RequirementAnalyzer) analyzeRequirement(userRequirement string) (*RequirementAnalysisResponse, error) {
+ // 生成AI提示词 - 这是唯一功能
+ aiPrompt := t.generateAIPrompt(userRequirement)
+
+ return &RequirementAnalysisResponse{
+ AIPrompt: aiPrompt,
+ }, nil
+}
+
+// generateAIPrompt 生成AI提示词 - 智能分析需求并确定模块结构
+func (t *RequirementAnalyzer) generateAIPrompt(userRequirement string) string {
+ prompt := fmt.Sprintf(`# 智能需求分析与模块设计任务
+
+## 用户原始需求
+%s
+
+## 核心任务
+你需要作为一个资深的系统架构师,深度分析用户需求,智能设计出完整的模块架构。
+
+## 分析步骤
+
+### 第一步:需求解构分析
+请仔细分析用户需求,识别出:
+1. **核心业务实体**(如:用户、商品、订单、疫苗、宠物等)
+2. **业务流程**(如:注册、购买、记录、管理等)
+3. **数据关系**(实体间的关联关系)
+4. **功能模块**(需要哪些独立的管理模块)
+
+### 第二步:模块架构设计
+基于需求分析,设计出模块架构,格式如下:
+
+**模块1:[模块名称]**
+- 功能描述:[该模块的核心功能]
+- 主要字段:[列出关键字段,注明数据类型]
+- 关联关系:[与其他模块的关系,明确一对一/一对多]
+- 字典需求:[需要哪些字典类型]
+
+**模块2:[模块名称]**
+- 功能描述:[该模块的核心功能]
+- 主要字段:[列出关键字段,注明数据类型]
+- 关联关系:[与其他模块的关系]
+- 字典需求:[需要哪些字典类型]
+
+**...**
+
+### 第三步:字段详细设计
+为每个模块详细设计字段:
+
+#### 模块1字段清单:
+- 字段名1 (数据类型) - 字段描述 [是否必填] [关联信息/字典类型]
+- 字段名2 (数据类型) - 字段描述 [是否必填] [关联信息/字典类型]
+- ...
+
+#### 模块2字段清单:
+- 字段名1 (数据类型) - 字段描述 [是否必填] [关联信息/字典类型]
+- ...
+
+## 智能分析指导原则
+
+### 模块拆分原则
+1. **单一职责**:每个模块只负责一个核心业务实体
+2. **数据完整性**:相关数据应该在同一模块中
+3. **业务独立性**:模块应该能够独立完成特定业务功能
+4. **扩展性考虑**:为未来功能扩展预留空间
+
+### 字段设计原则
+1. **必要性**:只包含业务必需的字段
+2. **规范性**:遵循数据库设计规范
+3. **关联性**:正确识别实体间关系
+4. **字典化**:状态、类型等枚举值使用字典
+
+### 关联关系识别
+- **一对一**:一个实体只能关联另一个实体的一个记录
+- **一对多**:一个实体可以关联另一个实体的多个记录
+- **多对多**:通过中间表实现复杂关联
+
+## 特殊场景处理
+
+### 复杂实体识别
+当用户提到某个概念时,要判断它是否需要独立模块:
+- **字典处理**:简单的常见的状态、类型(如:开关、性别、完成状态等)
+- **独立模块**:复杂实体(如:疫苗管理、宠物档案、注射记录)
+
+## 输出要求
+
+### 必须包含的信息
+1. **模块数量**:明确需要几个模块
+2. **模块关系图**:用文字描述模块间关系
+3. **核心字段**:每个模块的关键字段(至少5-10个)
+4. **数据类型**:string、int、bool、time.Time、float64等
+5. **关联设计**:明确哪些字段是关联字段
+6. **字典需求**:列出需要创建的字典类型
+
+### 严格遵循用户输入
+- 如果用户提供了具体字段,**必须使用**用户提供的字段
+- 如果用户提供了SQL文件,**严格按照**SQL结构设计
+- **不要**随意发散,**不要**添加用户未提及的功能
+---
+
+**现在请开始深度分析用户需求:"%s"**
+
+请按照上述框架进行系统性分析,确保输出的模块设计既满足当前需求,又具备良好的扩展性。`, userRequirement, userRequirement)
+
+ return prompt
+}
diff --git a/server/middleware/app_jwt.go b/server/middleware/app_jwt.go
new file mode 100644
index 0000000..cbb4508
--- /dev/null
+++ b/server/middleware/app_jwt.go
@@ -0,0 +1,147 @@
+package middleware
+
+import (
+ "errors"
+ "strconv"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/app"
+ "git.echol.cn/loser/st/server/model/common/response"
+ "git.echol.cn/loser/st/server/utils"
+ "github.com/gin-gonic/gin"
+ "github.com/golang-jwt/jwt/v5"
+)
+
+// AppJWTAuth 前台用户 JWT 认证中间件
+func AppJWTAuth() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ token := GetToken(c)
+ if token == "" {
+ response.FailWithDetailed(gin.H{"reload": true}, "未登录或非法访问", c)
+ c.Abort()
+ return
+ }
+
+ // 解析 JWT
+ claims, err := utils.ParseAppToken(token)
+ if err != nil {
+ if errors.Is(err, jwt.ErrTokenExpired) {
+ response.FailWithDetailed(gin.H{"reload": true}, "Token 已过期", c)
+ } else {
+ response.FailWithDetailed(gin.H{"reload": true}, "Token 无效", c)
+ }
+ c.Abort()
+ return
+ }
+
+ // 验证用户类型(确保是前台用户)
+ if claims.UserType != utils.UserTypeApp {
+ response.FailWithMessage("无效的用户类型", c)
+ c.Abort()
+ return
+ }
+
+ // 查询用户是否存在
+ var user app.AppUser
+ err = global.GVA_DB.Where("id = ?", claims.UserID).First(&user).Error
+ if err != nil {
+ response.FailWithMessage("用户不存在", c)
+ c.Abort()
+ return
+ }
+
+ // 检查用户状态
+ if !user.Enable {
+ response.FailWithMessage("用户已被禁用", c)
+ c.Abort()
+ return
+ }
+
+ if user.Status != "active" {
+ response.FailWithMessage("账户状态异常", c)
+ c.Abort()
+ return
+ }
+
+ // 将用户信息存入上下文
+ c.Set("appUserId", user.ID)
+ c.Set("appUser", &user)
+ c.Set("appUsername", user.Username)
+
+ c.Next()
+ }
+}
+
+// GetAppUserID 从上下文获取前台用户 ID(需要鉴权的接口使用)
+func GetAppUserID(c *gin.Context) uint {
+ if userID, exists := c.Get("appUserId"); exists {
+ return userID.(uint)
+ }
+ return 0
+}
+
+// GetOptionalAppUserID 从上下文获取可选的前台用户 ID(公开接口使用)
+// 如果用户已登录,返回用户 ID;否则返回 nil
+func GetOptionalAppUserID(c *gin.Context) *uint {
+ // 先尝试从上下文获取(通过鉴权中间件设置)
+ if userID, exists := c.Get("appUserId"); exists {
+ if id, ok := userID.(uint); ok {
+ return &id
+ }
+ }
+
+ // 如果上下文中没有,尝试手动解析 Token(用于公开接口)
+ token := GetToken(c)
+ if token == "" {
+ return nil
+ }
+
+ claims, err := utils.ParseAppToken(token)
+ if err != nil {
+ return nil
+ }
+
+ if claims.UserType != utils.UserTypeApp {
+ return nil
+ }
+
+ return &claims.UserID
+}
+
+// GetAppUser 从上下文获取前台用户信息
+func GetAppUser(c *gin.Context) *app.AppUser {
+ if user, exists := c.Get("appUser"); exists {
+ return user.(*app.AppUser)
+ }
+ return nil
+}
+
+// GetAppUsername 从上下文获取前台用户名
+func GetAppUsername(c *gin.Context) string {
+ if username, exists := c.Get("appUsername"); exists {
+ return username.(string)
+ }
+ return ""
+}
+
+// GetToken 从请求中获取 Token
+// 优先从 Header 获取,其次从 Query 参数获取
+func GetToken(c *gin.Context) string {
+ token := c.Request.Header.Get("x-token")
+ if token == "" {
+ token = c.Request.Header.Get("Authorization")
+ if token != "" && len(token) > 7 && token[:7] == "Bearer " {
+ token = token[7:]
+ }
+ }
+ if token == "" {
+ token = c.Query("token")
+ }
+ return token
+}
+
+// SetAppUserID 设置用户 ID 到上下文(用于某些特殊场景)
+func SetAppUserID(c *gin.Context, userID uint) {
+ c.Set("appUserId", userID)
+ c.Set("appUserIdStr", strconv.Itoa(int(userID)))
+}
diff --git a/server/middleware/casbin_rbac.go b/server/middleware/casbin_rbac.go
new file mode 100644
index 0000000..3b21e51
--- /dev/null
+++ b/server/middleware/casbin_rbac.go
@@ -0,0 +1,33 @@
+package middleware
+
+import (
+ "strconv"
+ "strings"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common/response"
+ "git.echol.cn/loser/st/server/utils"
+ "github.com/gin-gonic/gin"
+)
+
+// 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 := utils.GetCasbin() // 判断策略中是否存在
+ success, _ := e.Enforce(sub, obj, act)
+ if !success {
+ response.FailWithDetailed(gin.H{}, "权限不足", c)
+ c.Abort()
+ return
+ }
+ c.Next()
+ }
+}
diff --git a/server/middleware/cors.go b/server/middleware/cors.go
new file mode 100644
index 0000000..4c2097d
--- /dev/null
+++ b/server/middleware/cors.go
@@ -0,0 +1,74 @@
+package middleware
+
+import (
+ "net/http"
+
+ "git.echol.cn/loser/st/server/config"
+ "git.echol.cn/loser/st/server/global"
+ "github.com/gin-gonic/gin"
+)
+
+// 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/server/middleware/email.go b/server/middleware/email.go
new file mode 100644
index 0000000..968294e
--- /dev/null
+++ b/server/middleware/email.go
@@ -0,0 +1,58 @@
+package middleware
+
+import (
+ "bytes"
+ "io"
+ "strconv"
+ "time"
+
+ "git.echol.cn/loser/st/server/plugin/email/utils"
+ utils2 "git.echol.cn/loser/st/server/utils"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system"
+ "github.com/gin-gonic/gin"
+ "go.uber.org/zap"
+)
+
+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"))
+ var u system.SysUser
+ err := global.GVA_DB.Where("id = ?", id).First(&u).Error
+ if err != nil {
+ username = "Unknown"
+ }
+ username = u.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/server/middleware/error.go b/server/middleware/error.go
new file mode 100644
index 0000000..9708705
--- /dev/null
+++ b/server/middleware/error.go
@@ -0,0 +1,80 @@
+package middleware
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "net/http"
+ "net/http/httputil"
+ "os"
+ "runtime/debug"
+ "strings"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system"
+ "git.echol.cn/loser/st/server/service"
+ "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 {
+ form := "后端"
+ info := fmt.Sprintf("Panic: %v\nRequest: %s\nStack: %s", err, string(httpRequest), string(debug.Stack()))
+ level := "error"
+ _ = service.ServiceGroupApp.SystemServiceGroup.SysErrorService.CreateSysError(context.Background(), &system.SysError{
+ Form: &form,
+ Info: &info,
+ Level: level,
+ })
+ global.GVA_LOG.Error("[Recovery from panic]",
+ zap.Any("error", err),
+ zap.String("request", string(httpRequest)),
+ )
+ } else {
+ form := "后端"
+ info := fmt.Sprintf("Panic: %v\nRequest: %s", err, string(httpRequest))
+ level := "error"
+ _ = service.ServiceGroupApp.SystemServiceGroup.SysErrorService.CreateSysError(context.Background(), &system.SysError{
+ Form: &form,
+ Info: &info,
+ Level: level,
+ })
+ 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/server/middleware/jwt.go b/server/middleware/jwt.go
new file mode 100644
index 0000000..f373828
--- /dev/null
+++ b/server/middleware/jwt.go
@@ -0,0 +1,89 @@
+package middleware
+
+import (
+ "errors"
+ "strconv"
+ "time"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/utils"
+ "github.com/golang-jwt/jwt/v5"
+
+ "git.echol.cn/loser/st/server/model/common/response"
+ "github.com/gin-gonic/gin"
+)
+
+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 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()/60))
+ if global.GVA_CONFIG.System.UseMultipoint {
+ // 记录新的活跃jwt
+ _ = utils.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))
+ }
+ }
+}
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@function: IsBlacklist
+//@description: 判断JWT是否在黑名单内部
+//@param: jwt string
+//@return: bool
+
+func isBlacklist(jwt string) bool {
+ _, ok := global.BlackCache.Get(jwt)
+ return ok
+}
diff --git a/server/middleware/limit_ip.go b/server/middleware/limit_ip.go
new file mode 100644
index 0000000..2988a00
--- /dev/null
+++ b/server/middleware/limit_ip.go
@@ -0,0 +1,92 @@
+package middleware
+
+import (
+ "context"
+ "errors"
+ "net/http"
+ "time"
+
+ "go.uber.org/zap"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common/response"
+ "github.com/gin-gonic/gin"
+)
+
+type LimitConfig struct {
+ // GenerationKey 根据业务生成key 下面CheckOrMark查询生成
+ GenerationKey func(c *gin.Context) string
+ // 检查函数,用户可修改具体逻辑,更加灵活
+ CheckOrMark func(key string, expire int, limit int) error
+ // Expire key 过期时间
+ Expire int
+ // Limit 周期时间
+ Limit int
+}
+
+func (l LimitConfig) LimitWithTime() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ if err := l.CheckOrMark(l.GenerationKey(c), l.Expire, l.Limit); err != nil {
+ c.JSON(http.StatusOK, gin.H{"code": response.ERROR, "msg": err.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/server/middleware/loadtls.go b/server/middleware/loadtls.go
new file mode 100644
index 0000000..a17cf65
--- /dev/null
+++ b/server/middleware/loadtls.go
@@ -0,0 +1,27 @@
+package middleware
+
+import (
+ "fmt"
+
+ "github.com/gin-gonic/gin"
+ "github.com/unrolled/secure"
+)
+
+// 用https把这个中间件在router里面use一下就好
+
+func LoadTls() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ middleware := secure.New(secure.Options{
+ SSLRedirect: true,
+ SSLHost: "localhost:443",
+ })
+ err := middleware.Process(c.Writer, c.Request)
+ if err != nil {
+ // 如果出现错误,请不要继续
+ fmt.Println(err)
+ return
+ }
+ // 继续往下处理
+ c.Next()
+ }
+}
diff --git a/server/middleware/logger.go b/server/middleware/logger.go
new file mode 100644
index 0000000..fabc334
--- /dev/null
+++ b/server/middleware/logger.go
@@ -0,0 +1,89 @@
+package middleware
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "strings"
+ "time"
+
+ "github.com/gin-gonic/gin"
+)
+
+// LogLayout 日志layout
+type LogLayout struct {
+ Time time.Time
+ Metadata map[string]interface{} // 存储自定义原数据
+ Path string // 访问路径
+ Query string // 携带query
+ Body string // 携带body数据
+ IP string // ip地址
+ UserAgent string // 代理
+ Error string // 错误
+ Cost time.Duration // 花费时间
+ Source string // 来源
+}
+
+type Logger struct {
+ // Filter 用户自定义过滤
+ Filter func(c *gin.Context) bool
+ // FilterKeyword 关键字过滤(key)
+ FilterKeyword func(layout *LogLayout) bool
+ // AuthProcess 鉴权处理
+ AuthProcess func(c *gin.Context, layout *LogLayout)
+ // 日志处理
+ Print func(LogLayout)
+ // Source 服务唯一标识
+ Source string
+}
+
+func (l Logger) SetLoggerMiddleware() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ start := time.Now()
+ path := c.Request.URL.Path
+ query := c.Request.URL.RawQuery
+ var body []byte
+ if l.Filter != nil && !l.Filter(c) {
+ body, _ = c.GetRawData()
+ // 将原body塞回去
+ c.Request.Body = io.NopCloser(bytes.NewBuffer(body))
+ }
+ c.Next()
+ cost := time.Since(start)
+ layout := LogLayout{
+ Time: time.Now(),
+ Path: path,
+ Query: query,
+ IP: c.ClientIP(),
+ UserAgent: c.Request.UserAgent(),
+ Error: strings.TrimRight(c.Errors.ByType(gin.ErrorTypePrivate).String(), "\n"),
+ Cost: cost,
+ Source: l.Source,
+ }
+ if l.Filter != nil && !l.Filter(c) {
+ layout.Body = string(body)
+ }
+ if l.AuthProcess != nil {
+ // 处理鉴权需要的信息
+ l.AuthProcess(c, &layout)
+ }
+ if l.FilterKeyword != nil {
+ // 自行判断key/value 脱敏等
+ l.FilterKeyword(&layout)
+ }
+ // 自行处理日志
+ l.Print(layout)
+ }
+}
+
+func DefaultLogger() gin.HandlerFunc {
+ return Logger{
+ Print: func(layout LogLayout) {
+ // 标准输出,k8s做收集
+ v, _ := json.Marshal(layout)
+ fmt.Println(string(v))
+ },
+ Source: "GVA",
+ }.SetLoggerMiddleware()
+}
diff --git a/server/middleware/operation.go b/server/middleware/operation.go
new file mode 100644
index 0000000..3b4ff8c
--- /dev/null
+++ b/server/middleware/operation.go
@@ -0,0 +1,129 @@
+package middleware
+
+import (
+ "bytes"
+ "encoding/json"
+ "io"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "git.echol.cn/loser/st/server/utils"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system"
+ "github.com/gin-gonic/gin"
+ "go.uber.org/zap"
+)
+
+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 := global.GVA_DB.Create(&record).Error; 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/server/middleware/timeout.go b/server/middleware/timeout.go
new file mode 100644
index 0000000..2a46ebf
--- /dev/null
+++ b/server/middleware/timeout.go
@@ -0,0 +1,56 @@
+package middleware
+
+import (
+ "context"
+ "net/http"
+ "time"
+
+ "github.com/gin-gonic/gin"
+)
+
+// TimeoutMiddleware 创建超时中间件
+// 入参 timeout 设置超时时间(例如:time.Second * 5)
+// 使用示例 xxx.Get("path",middleware.TimeoutMiddleware(30*time.Second),HandleFunc)
+func TimeoutMiddleware(timeout time.Duration) gin.HandlerFunc {
+ return func(c *gin.Context) {
+ ctx, cancel := context.WithTimeout(c.Request.Context(), timeout)
+ defer cancel()
+
+ c.Request = c.Request.WithContext(ctx)
+
+ // 使用 buffered channel 避免 goroutine 泄漏
+ done := make(chan struct{}, 1)
+ panicChan := make(chan interface{}, 1)
+
+ go func() {
+ defer func() {
+ if p := recover(); p != nil {
+ select {
+ case panicChan <- p:
+ default:
+ }
+ }
+ select {
+ case done <- struct{}{}:
+ default:
+ }
+ }()
+ c.Next()
+ }()
+
+ select {
+ case p := <-panicChan:
+ panic(p)
+ case <-done:
+ return
+ case <-ctx.Done():
+ // 确保服务器超时设置足够长
+ c.Header("Connection", "close")
+ c.AbortWithStatusJSON(http.StatusGatewayTimeout, gin.H{
+ "code": 504,
+ "msg": "请求超时",
+ })
+ return
+ }
+ }
+}
diff --git a/server/model/app/README.md b/server/model/app/README.md
new file mode 100644
index 0000000..a387e55
--- /dev/null
+++ b/server/model/app/README.md
@@ -0,0 +1,213 @@
+# App 前台应用数据模型
+
+## 📋 模型列表
+
+本目录包含所有前台用户应用相关的数据模型,与管理后台的 `system` 模块完全独立。
+
+### 1. 用户相关模型
+
+| 文件 | 模型 | 表名 | 说明 |
+|------|------|------|------|
+| `app_user.go` | `AppUser` | `app_users` | 前台用户表 |
+| `app_user_session.go` | `AppUserSession` | `app_user_sessions` | 用户会话表 |
+
+### 2. AI 角色相关模型
+
+| 文件 | 模型 | 表名 | 说明 |
+|------|------|------|------|
+| `ai_character.go` | `AICharacter` | `ai_characters` | AI 角色表 |
+| `ai_character.go` | `AppUserFavoriteCharacter` | `app_user_favorite_characters` | 用户收藏角色表 |
+
+### 3. 对话相关模型
+
+| 文件 | 模型 | 表名 | 说明 |
+|------|------|------|------|
+| `ai_chat.go` | `AIChat` | `ai_chats` | 对话表 |
+| `ai_chat.go` | `AIChatMember` | `ai_chat_members` | 群聊成员表 |
+| `ai_message.go` | `AIMessage` | `ai_messages` | 消息表 |
+| `ai_message.go` | `AIMessageSwipe` | `ai_message_swipes` | 消息变体表 |
+
+### 4. 向量记忆模型
+
+| 文件 | 模型 | 表名 | 说明 |
+|------|------|------|------|
+| `ai_memory.go` | `AIMemoryVector` | `ai_memory_vectors` | 向量记忆表(使用 pgvector) |
+
+### 5. AI 服务配置模型
+
+| 文件 | 模型 | 表名 | 说明 |
+|------|------|------|------|
+| `ai_provider.go` | `AIProvider` | `ai_providers` | AI 提供商配置表 |
+| `ai_provider.go` | `AIModel` | `ai_models` | AI 模型配置表 |
+
+### 6. 文件管理模型
+
+| 文件 | 模型 | 表名 | 说明 |
+|------|------|------|------|
+| `ai_file.go` | `AIFile` | `ai_files` | 文件表 |
+
+### 7. 其他模型
+
+| 文件 | 模型 | 表名 | 说明 |
+|------|------|------|------|
+| `ai_preset.go` | `AIPreset` | `ai_presets` | 对话预设表 |
+| `ai_world_info.go` | `AIWorldInfo` | `ai_world_info` | 世界书表 |
+| `ai_usage_stat.go` | `AIUsageStat` | `ai_usage_stats` | 使用统计表 |
+
+## 🔧 使用说明
+
+### 1. 数据库自动迁移
+
+所有模型已在 `initialize/gorm.go` 中注册,启动服务时会自动创建表:
+
+```go
+// 在 RegisterTables() 函数中已注册
+app.AppUser{},
+app.AppUserSession{},
+app.AICharacter{},
+app.AppUserFavoriteCharacter{},
+app.AIChat{},
+app.AIChatMember{},
+app.AIMessage{},
+app.AIMessageSwipe{},
+app.AIMemoryVector{},
+app.AIProvider{},
+app.AIModel{},
+app.AIFile{},
+app.AIPreset{},
+app.AIWorldInfo{},
+app.AIUsageStat{},
+```
+
+### 2. PostgreSQL 向量扩展
+
+向量记忆功能依赖 `pgvector` 扩展,已在 `initialize/gorm_pgsql_extension.go` 中自动安装:
+
+```sql
+CREATE EXTENSION IF NOT EXISTS vector;
+CREATE INDEX idx_memory_vectors_embedding ON ai_memory_vectors
+USING hnsw (embedding vector_cosine_ops);
+```
+
+### 3. 外键关系
+
+模型之间的关系已通过 GORM 标签定义:
+
+- `AppUser` ← `AppUserSession`(一对多)
+- `AppUser` ← `AICharacter`(一对多,创建者)
+- `AppUser` ← `AIChat`(一对多)
+- `AppUser` ← `AppUserFavoriteCharacter`(多对多,通过中间表)
+- `AICharacter` ← `AppUserFavoriteCharacter`(多对多,通过中间表)
+- `AICharacter` ← `AIChat`(一对多)
+- `AIChat` ← `AIMessage`(一对多)
+- `AIChat` ← `AIChatMember`(多对多,通过中间表)
+- `AICharacter` ← `AIChatMember`(多对多,通过中间表)
+- `AIMessage` ← `AIMessageSwipe`(一对多)
+- `AIProvider` ← `AIModel`(一对多)
+
+### 4. JSONB 字段
+
+以下字段使用 PostgreSQL 的 JSONB 类型:
+
+- `AppUser.AISettings` - AI 相关配置
+- `AppUser.Preferences` - 用户偏好设置
+- `AICharacter.CardData` - 角色卡片数据
+- `AICharacter.Tags` - 角色标签
+- `AICharacter.ExampleMessages` - 消息示例
+- `AIChat.Settings` - 对话设置
+- `AIMessage.GenerationParams` - AI 生成参数
+- `AIMessage.Metadata` - 消息元数据
+- `AIMemoryVector.Metadata` - 记忆元数据
+- `AIProvider.APIConfig` - API 配置
+- `AIModel.Config` - 模型配置
+- `AIFile.RelatedTo` - 文件关联对象
+- `AIFile.Metadata` - 文件元数据
+- `AIPreset.Config` - 预设配置
+- `AIWorldInfo.TriggerConfig` - 触发条件配置
+
+### 5. 向量字段
+
+`AIMemoryVector.Embedding` 使用 `pgvector.Vector` 类型,维度为 1536(OpenAI text-embedding-ada-002)。
+
+## ⚠️ 注意事项
+
+1. **不要修改 system 包**:所有管理后台相关的模型在 `model/system/` 包中,**不要修改**
+2. **表名前缀**:
+ - 前台用户相关:`app_*`
+ - AI 功能相关:`ai_*`
+ - 系统管理相关:`sys_*`(不修改)
+3. **UUID 生成**:`AppUser.UUID` 使用数据库自动生成(PostgreSQL 的 `gen_random_uuid()`)
+4. **软删除**:所有模型继承 `global.GVA_MODEL`,自动支持软删除
+5. **时间字段**:`CreatedAt`、`UpdatedAt`、`DeletedAt` 由 GORM 自动管理
+
+## 📊 ER 图关系
+
+```
+AppUser (前台用户)
+ ├── AppUserSession (会话)
+ ├── AICharacter (创建的角色)
+ ├── AIChat (对话)
+ ├── AppUserFavoriteCharacter (收藏的角色)
+ ├── AIMemoryVector (记忆)
+ ├── AIProvider (AI 提供商配置)
+ ├── AIFile (文件)
+ ├── AIPreset (预设)
+ ├── AIWorldInfo (世界书)
+ └── AIUsageStat (使用统计)
+
+AICharacter (AI 角色)
+ ├── AIChat (对话)
+ ├── AIChatMember (群聊成员)
+ ├── AppUserFavoriteCharacter (被收藏)
+ └── AIMemoryVector (记忆)
+
+AIChat (对话)
+ ├── AIMessage (消息)
+ ├── AIChatMember (群聊成员)
+ └── AIMemoryVector (记忆)
+
+AIMessage (消息)
+ └── AIMessageSwipe (消息变体)
+
+AIProvider (AI 提供商)
+ └── AIModel (AI 模型)
+```
+
+## 🚀 快速开始
+
+1. 确保 PostgreSQL 已安装 pgvector 扩展
+2. 配置 `config.yaml` 中的数据库连接
+3. 启动服务,AutoMigrate 会自动创建所有表
+4. 检查日志确认表创建成功
+
+```bash
+# 启动服务
+go run main.go
+
+# 查看日志
+# [GVA] pgvector extension is ready
+# [GVA] vector indexes created successfully
+# [GVA] register table success
+```
+
+## 📝 开发建议
+
+1. 查询时使用预加载避免 N+1 问题:
+ ```go
+ db.Preload("User").Preload("Character").Find(&chats)
+ ```
+
+2. 向量搜索示例:
+ ```go
+ db.Order("embedding <=> ?", queryVector).Limit(10).Find(&memories)
+ ```
+
+3. JSONB 查询示例:
+ ```go
+ db.Where("ai_settings->>'model' = ?", "gpt-4").Find(&users)
+ ```
+
+---
+
+**创建日期**: 2026-02-10
+**维护者**: 开发团队
diff --git a/server/model/app/ai_character.go b/server/model/app/ai_character.go
new file mode 100644
index 0000000..3164fcd
--- /dev/null
+++ b/server/model/app/ai_character.go
@@ -0,0 +1,49 @@
+package app
+
+import (
+ "time"
+
+ "gorm.io/datatypes"
+ "gorm.io/gorm"
+)
+
+// AICharacter 角色卡模型
+type AICharacter struct {
+ ID uint `gorm:"primarykey" json:"id"`
+ CreatedAt time.Time `json:"createdAt"`
+ UpdatedAt time.Time `json:"updatedAt"`
+ DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
+
+ // 基本信息
+ UserID uint `gorm:"index;not null" json:"userId"` // 所属用户ID
+ Name string `gorm:"type:varchar(100);not null" json:"name"` // 角色名称
+ Avatar string `gorm:"type:text" json:"avatar"` // 头像URL或Base64
+ Creator string `gorm:"type:varchar(100)" json:"creator"` // 创建者
+ Version string `gorm:"type:varchar(50)" json:"version"` // 角色版本
+ IsPublic bool `gorm:"default:false" json:"isPublic"` // 是否公开
+
+ // SillyTavern V2 格式字段
+ Description string `gorm:"type:text" json:"description"` // 角色描述
+ Personality string `gorm:"type:text" json:"personality"` // 性格特征
+ Scenario string `gorm:"type:text" json:"scenario"` // 场景设定
+ FirstMes string `gorm:"type:text" json:"firstMes"` // 第一条消息
+ MesExample string `gorm:"type:text" json:"mesExample"` // 消息示例
+ CreatorNotes string `gorm:"type:text" json:"creatorNotes"` // 创建者备注
+ SystemPrompt string `gorm:"type:text" json:"systemPrompt"` // 系统提示词
+ PostHistoryInstructions string `gorm:"type:text" json:"postHistoryInstructions"` // 历史后指令
+ Tags datatypes.JSON `gorm:"type:jsonb" json:"tags"` // 标签数组
+ AlternateGreetings datatypes.JSON `gorm:"type:jsonb" json:"alternateGreetings"` // 备用问候语
+ CharacterBook datatypes.JSON `gorm:"type:jsonb" json:"characterBook"` // 角色书
+ Extensions datatypes.JSON `gorm:"type:jsonb" json:"extensions"` // 扩展数据
+ Spec string `gorm:"type:varchar(50);default:'chara_card_v2'" json:"spec"` // 规范名称
+ SpecVersion string `gorm:"type:varchar(50);default:'2.0'" json:"specVersion"` // 规范版本
+
+ // 统计信息
+ UseCount int `gorm:"default:0" json:"useCount"` // 使用次数
+ FavoriteCount int `gorm:"default:0" json:"favoriteCount"` // 收藏次数
+}
+
+// TableName 指定表名
+func (AICharacter) TableName() string {
+ return "ai_characters"
+}
diff --git a/server/model/app/ai_config.go b/server/model/app/ai_config.go
new file mode 100644
index 0000000..295656d
--- /dev/null
+++ b/server/model/app/ai_config.go
@@ -0,0 +1,31 @@
+package app
+
+import (
+ "time"
+
+ "gorm.io/datatypes"
+ "gorm.io/gorm"
+)
+
+// AIConfig AI配置模型
+type AIConfig struct {
+ ID uint `gorm:"primarykey" json:"id"`
+ CreatedAt time.Time `json:"createdAt"`
+ UpdatedAt time.Time `json:"updatedAt"`
+ DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
+
+ Name string `gorm:"type:varchar(100);not null" json:"name"` // 配置名称
+ Provider string `gorm:"type:varchar(50);not null" json:"provider"` // 提供商 (openai/anthropic/custom)
+ BaseURL string `gorm:"type:varchar(500)" json:"baseUrl"` // API Base URL
+ APIKey string `gorm:"type:varchar(500)" json:"apiKey"` // API Key (加密存储)
+ Models datatypes.JSON `gorm:"type:jsonb" json:"models"` // 可用模型列表
+ DefaultModel string `gorm:"type:varchar(100)" json:"defaultModel"` // 默认模型
+ Settings datatypes.JSON `gorm:"type:jsonb" json:"settings"` // 其他设置 (temperature等)
+ IsActive bool `gorm:"default:false" json:"isActive"` // 是否激活
+ IsDefault bool `gorm:"default:false" json:"isDefault"` // 是否为默认配置
+}
+
+// TableName 指定表名
+func (AIConfig) TableName() string {
+ return "ai_configs"
+}
diff --git a/server/model/app/app_user.go b/server/model/app/app_user.go
new file mode 100644
index 0000000..2fdb5c3
--- /dev/null
+++ b/server/model/app/app_user.go
@@ -0,0 +1,33 @@
+package app
+
+import (
+ "time"
+
+ "git.echol.cn/loser/st/server/global"
+ "gorm.io/datatypes"
+)
+
+// AppUser 前台用户模型(与 sys_users 独立)
+type AppUser struct {
+ global.GVA_MODEL
+ UUID string `json:"uuid" gorm:"type:uuid;uniqueIndex;comment:用户UUID"`
+ Username string `json:"username" gorm:"uniqueIndex;comment:用户登录名"`
+ Password string `json:"-" gorm:"comment:用户登录密码"`
+ NickName string `json:"nickName" gorm:"comment:用户昵称"`
+ Email string `json:"email" gorm:"index;comment:用户邮箱"`
+ Phone string `json:"phone" gorm:"comment:用户手机号"`
+ Avatar string `json:"avatar" gorm:"type:varchar(1024);comment:用户头像"`
+ Status string `json:"status" gorm:"type:varchar(50);default:active;comment:账户状态"`
+ Enable bool `json:"enable" gorm:"default:true;comment:用户是否启用"`
+ IsAdmin bool `json:"isAdmin" gorm:"default:false;comment:是否为管理员"`
+ LastLoginAt *time.Time `json:"lastLoginAt" gorm:"comment:最后登录时间"`
+ LastLoginIP string `json:"lastLoginIp" gorm:"type:varchar(100);comment:最后登录IP"`
+ AISettings datatypes.JSON `json:"aiSettings" gorm:"type:jsonb;comment:AI配置"`
+ Preferences datatypes.JSON `json:"preferences" gorm:"type:jsonb;comment:用户偏好"`
+ ChatCount int `json:"chatCount" gorm:"default:0;comment:对话数量"`
+ MessageCount int `json:"messageCount" gorm:"default:0;comment:消息数量"`
+}
+
+func (AppUser) TableName() string {
+ return "app_users"
+}
diff --git a/server/model/app/app_user_session.go b/server/model/app/app_user_session.go
new file mode 100644
index 0000000..17ac6cb
--- /dev/null
+++ b/server/model/app/app_user_session.go
@@ -0,0 +1,25 @@
+package app
+
+import (
+ "time"
+
+ "git.echol.cn/loser/st/server/global"
+ "gorm.io/datatypes"
+)
+
+// AppUserSession 前台用户会话
+type AppUserSession struct {
+ global.GVA_MODEL
+ UserID uint `json:"userId" gorm:"index;comment:用户ID"`
+ SessionToken string `json:"sessionToken" gorm:"type:varchar(500);uniqueIndex;comment:会话Token"`
+ RefreshToken string `json:"refreshToken" gorm:"type:varchar(500);comment:刷新Token"`
+ ExpiresAt time.Time `json:"expiresAt" gorm:"index;comment:过期时间"`
+ RefreshExpiresAt *time.Time `json:"refreshExpiresAt" gorm:"comment:刷新Token过期时间"`
+ IPAddress string `json:"ipAddress" gorm:"type:varchar(100);comment:IP地址"`
+ UserAgent string `json:"userAgent" gorm:"type:text;comment:用户代理"`
+ DeviceInfo datatypes.JSON `json:"deviceInfo" gorm:"type:jsonb;comment:设备信息"`
+}
+
+func (AppUserSession) TableName() string {
+ return "app_user_sessions"
+}
diff --git a/server/model/app/conversation.go b/server/model/app/conversation.go
new file mode 100644
index 0000000..1e3128f
--- /dev/null
+++ b/server/model/app/conversation.go
@@ -0,0 +1,56 @@
+package app
+
+import (
+ "time"
+
+ "gorm.io/datatypes"
+ "gorm.io/gorm"
+)
+
+// Conversation 对话会话模型
+type Conversation struct {
+ ID uint `gorm:"primarykey" json:"id"`
+ CreatedAt time.Time `json:"createdAt"`
+ UpdatedAt time.Time `json:"updatedAt"`
+ DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
+
+ UserID uint `gorm:"index;not null" json:"userId"` // 所属用户ID
+ CharacterID uint `gorm:"index;not null" json:"characterId"` // 角色卡ID
+ Title string `gorm:"type:varchar(200)" json:"title"` // 对话标题
+
+ // 对话配置
+ PresetID *uint `gorm:"index" json:"presetId"` // 使用的预设ID
+ AIProvider string `gorm:"type:varchar(50)" json:"aiProvider"` // AI提供商 (openai/anthropic)
+ Model string `gorm:"type:varchar(100)" json:"model"` // 使用的模型
+ Settings datatypes.JSON `gorm:"type:jsonb" json:"settings"` // 对话设置 (temperature等)
+
+ // 统计信息
+ MessageCount int `gorm:"default:0" json:"messageCount"` // 消息数量
+ TokenCount int `gorm:"default:0" json:"tokenCount"` // Token使用量
+}
+
+// TableName 指定表名
+func (Conversation) TableName() string {
+ return "conversations"
+}
+
+// Message 消息模型
+type Message struct {
+ ID uint `gorm:"primarykey" json:"id"`
+ CreatedAt time.Time `json:"createdAt"`
+ UpdatedAt time.Time `json:"updatedAt"`
+ DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
+
+ ConversationID uint `gorm:"index;not null" json:"conversationId"` // 所属对话ID
+ Role string `gorm:"type:varchar(20);not null" json:"role"` // 角色 (user/assistant/system)
+ Content string `gorm:"type:text;not null" json:"content"` // 消息内容
+
+ // 元数据
+ TokenCount int `gorm:"default:0" json:"tokenCount"` // Token数量
+ Metadata datatypes.JSON `gorm:"type:jsonb" json:"metadata"` // 额外元数据
+}
+
+// TableName 指定表名
+func (Message) TableName() string {
+ return "messages"
+}
diff --git a/server/model/app/preset.go b/server/model/app/preset.go
new file mode 100644
index 0000000..f1bb414
--- /dev/null
+++ b/server/model/app/preset.go
@@ -0,0 +1,44 @@
+package app
+
+import (
+ "gorm.io/datatypes"
+ "gorm.io/gorm"
+)
+
+// AIPreset AI预设配置模型
+type AIPreset struct {
+ gorm.Model
+ UserID uint `json:"userId" gorm:"not null;index:idx_preset_user"`
+ Name string `json:"name" gorm:"not null;size:100"`
+ Description string `json:"description" gorm:"size:500"`
+ IsPublic bool `json:"isPublic" gorm:"default:false;index:idx_preset_public"`
+ IsDefault bool `json:"isDefault" gorm:"default:false"`
+
+ // Sampling Parameters (stored as individual fields for query efficiency)
+ Temperature float64 `json:"temperature" gorm:"default:1.0"`
+ TopP float64 `json:"topP" gorm:"default:1.0"`
+ TopK int `json:"topK" gorm:"default:0"`
+ FrequencyPenalty float64 `json:"frequencyPenalty" gorm:"default:0"`
+ PresencePenalty float64 `json:"presencePenalty" gorm:"default:0"`
+ MaxTokens int `json:"maxTokens" gorm:"default:2000"`
+ RepetitionPenalty float64 `json:"repetitionPenalty" gorm:"default:1.0"`
+ MinP float64 `json:"minP" gorm:"default:0"`
+ TopA float64 `json:"topA" gorm:"default:0"`
+
+ // Prompt Configuration (stored as JSON for flexibility)
+ SystemPrompt string `json:"systemPrompt" gorm:"type:text"`
+ StopSequences datatypes.JSON `json:"stopSequences" gorm:"type:jsonb"`
+
+ // SillyTavern Extensions (for full compatibility)
+ Extensions datatypes.JSON `json:"extensions" gorm:"type:jsonb"`
+
+ // Usage Statistics
+ UseCount int `json:"useCount" gorm:"default:0"`
+
+ DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
+}
+
+// TableName 指定表名
+func (AIPreset) TableName() string {
+ return "ai_presets"
+}
diff --git a/server/model/app/request/ai_config.go b/server/model/app/request/ai_config.go
new file mode 100644
index 0000000..7f05ac6
--- /dev/null
+++ b/server/model/app/request/ai_config.go
@@ -0,0 +1,37 @@
+package request
+
+// CreateAIConfigRequest 创建AI配置请求
+type CreateAIConfigRequest struct {
+ Name string `json:"name" binding:"required,max=100"`
+ Provider string `json:"provider" binding:"required,oneof=openai anthropic custom"`
+ BaseURL string `json:"baseUrl" binding:"required,url"`
+ APIKey string `json:"apiKey" binding:"required"`
+ DefaultModel string `json:"defaultModel"`
+ Settings map[string]interface{} `json:"settings"`
+}
+
+// UpdateAIConfigRequest 更新AI配置请求
+type UpdateAIConfigRequest struct {
+ Name string `json:"name" binding:"max=100"`
+ BaseURL string `json:"baseUrl" binding:"omitempty,url"`
+ APIKey string `json:"apiKey"`
+ DefaultModel string `json:"defaultModel"`
+ Settings map[string]interface{} `json:"settings"`
+ IsActive *bool `json:"isActive"`
+ IsDefault *bool `json:"isDefault"`
+}
+
+// TestAIConfigRequest 测试AI配置请求
+type TestAIConfigRequest struct {
+ Provider string `json:"provider" binding:"required,oneof=openai anthropic custom"`
+ BaseURL string `json:"baseUrl" binding:"required,url"`
+ APIKey string `json:"apiKey" binding:"required"`
+ Model string `json:"model"`
+}
+
+// GetModelsRequest 获取模型列表请求
+type GetModelsRequest struct {
+ Provider string `json:"provider" binding:"required,oneof=openai anthropic custom"`
+ BaseURL string `json:"baseUrl" binding:"required,url"`
+ APIKey string `json:"apiKey" binding:"required"`
+}
diff --git a/server/model/app/request/auth.go b/server/model/app/request/auth.go
new file mode 100644
index 0000000..d718f77
--- /dev/null
+++ b/server/model/app/request/auth.go
@@ -0,0 +1,37 @@
+package request
+
+// RegisterRequest 用户注册请求
+type RegisterRequest struct {
+ Username string `json:"username" binding:"required,min=3,max=32"`
+ Password string `json:"password" binding:"required,min=6,max=32"`
+ NickName string `json:"nickName" binding:"max=50"`
+ Email string `json:"email" binding:"omitempty,email"`
+ Phone string `json:"phone" binding:"omitempty"`
+}
+
+// LoginRequest 用户登录请求
+type LoginRequest struct {
+ Username string `json:"username" binding:"required"`
+ Password string `json:"password" binding:"required"`
+}
+
+// RefreshTokenRequest 刷新 Token 请求
+type RefreshTokenRequest struct {
+ RefreshToken string `json:"refreshToken" binding:"required"`
+}
+
+// ChangePasswordRequest 修改密码请求
+type ChangePasswordRequest struct {
+ OldPassword string `json:"oldPassword" binding:"required"`
+ NewPassword string `json:"newPassword" binding:"required,min=6,max=32"`
+}
+
+// UpdateProfileRequest 更新用户信息请求
+type UpdateProfileRequest struct {
+ NickName string `json:"nickName" binding:"max=50"`
+ Email string `json:"email" binding:"omitempty,email"`
+ Phone string `json:"phone"`
+ Avatar string `json:"avatar"`
+ Preferences string `json:"preferences"` // JSON 字符串
+ AISettings string `json:"aiSettings"` // JSON 字符串
+}
diff --git a/server/model/app/request/character.go b/server/model/app/request/character.go
new file mode 100644
index 0000000..3d70ba0
--- /dev/null
+++ b/server/model/app/request/character.go
@@ -0,0 +1,52 @@
+package request
+
+// CreateCharacterRequest 创建角色卡请求
+type CreateCharacterRequest struct {
+ Name string `json:"name" binding:"required,max=100"`
+ Avatar string `json:"avatar"`
+ Creator string `json:"creator" binding:"max=100"`
+ Version string `json:"version" binding:"max=50"`
+ Description string `json:"description"`
+ Personality string `json:"personality"`
+ Scenario string `json:"scenario"`
+ FirstMes string `json:"firstMes"`
+ MesExample string `json:"mesExample"`
+ CreatorNotes string `json:"creatorNotes"`
+ SystemPrompt string `json:"systemPrompt"`
+ PostHistoryInstructions string `json:"postHistoryInstructions"`
+ Tags []string `json:"tags"`
+ AlternateGreetings []string `json:"alternateGreetings"`
+ CharacterBook map[string]interface{} `json:"characterBook"`
+ Extensions map[string]interface{} `json:"extensions"`
+ IsPublic bool `json:"isPublic"`
+}
+
+// UpdateCharacterRequest 更新角色卡请求
+type UpdateCharacterRequest struct {
+ Name string `json:"name" binding:"max=100"`
+ Avatar string `json:"avatar"`
+ Creator string `json:"creator" binding:"max=100"`
+ Version string `json:"version" binding:"max=50"`
+ Description string `json:"description"`
+ Personality string `json:"personality"`
+ Scenario string `json:"scenario"`
+ FirstMes string `json:"firstMes"`
+ MesExample string `json:"mesExample"`
+ CreatorNotes string `json:"creatorNotes"`
+ SystemPrompt string `json:"systemPrompt"`
+ PostHistoryInstructions string `json:"postHistoryInstructions"`
+ Tags []string `json:"tags"`
+ AlternateGreetings []string `json:"alternateGreetings"`
+ CharacterBook map[string]interface{} `json:"characterBook"`
+ Extensions map[string]interface{} `json:"extensions"`
+ IsPublic bool `json:"isPublic"`
+}
+
+// GetCharacterListRequest 获取角色卡列表请求
+type GetCharacterListRequest struct {
+ Page int `form:"page" binding:"min=1"`
+ PageSize int `form:"pageSize" binding:"min=1,max=100"`
+ Keyword string `form:"keyword"`
+ Tag string `form:"tag"`
+ IsPublic *bool `form:"isPublic"`
+}
diff --git a/server/model/app/request/conversation.go b/server/model/app/request/conversation.go
new file mode 100644
index 0000000..a179690
--- /dev/null
+++ b/server/model/app/request/conversation.go
@@ -0,0 +1,32 @@
+package request
+
+// CreateConversationRequest 创建对话请求
+type CreateConversationRequest struct {
+ CharacterID uint `json:"characterId" binding:"required"`
+ Title string `json:"title" binding:"max=200"`
+ PresetID *uint `json:"presetId"`
+ AIProvider string `json:"aiProvider" binding:"omitempty,oneof=openai anthropic"`
+ Model string `json:"model"`
+}
+
+// SendMessageRequest 发送消息请求
+type SendMessageRequest struct {
+ Content string `json:"content" binding:"required"`
+}
+
+// GetConversationListRequest 获取对话列表请求
+type GetConversationListRequest struct {
+ Page int `form:"page" binding:"min=1"`
+ PageSize int `form:"pageSize" binding:"min=1,max=100"`
+}
+
+// GetMessageListRequest 获取消息列表请求
+type GetMessageListRequest struct {
+ Page int `form:"page" binding:"min=1"`
+ PageSize int `form:"pageSize" binding:"min=1,max=100"`
+}
+
+// UpdateConversationSettingsRequest 更新对话设置请求
+type UpdateConversationSettingsRequest struct {
+ Settings map[string]interface{} `json:"settings" binding:"required"`
+}
diff --git a/server/model/app/request/preset.go b/server/model/app/request/preset.go
new file mode 100644
index 0000000..bf574b4
--- /dev/null
+++ b/server/model/app/request/preset.go
@@ -0,0 +1,47 @@
+package request
+
+// CreatePresetRequest 创建预设请求
+type CreatePresetRequest struct {
+ Name string `json:"name" binding:"required,min=1,max=100"`
+ Description string `json:"description" binding:"max=500"`
+ IsPublic bool `json:"isPublic"`
+ Temperature float64 `json:"temperature" binding:"min=0,max=2"`
+ TopP float64 `json:"topP" binding:"min=0,max=1"`
+ TopK int `json:"topK" binding:"min=0"`
+ FrequencyPenalty float64 `json:"frequencyPenalty" binding:"min=-2,max=2"`
+ PresencePenalty float64 `json:"presencePenalty" binding:"min=-2,max=2"`
+ MaxTokens int `json:"maxTokens" binding:"min=1,max=32000"`
+ RepetitionPenalty float64 `json:"repetitionPenalty"`
+ MinP float64 `json:"minP"`
+ TopA float64 `json:"topA"`
+ SystemPrompt string `json:"systemPrompt"`
+ StopSequences []string `json:"stopSequences"`
+ Extensions map[string]interface{} `json:"extensions"`
+}
+
+// UpdatePresetRequest 更新预设请求
+type UpdatePresetRequest struct {
+ Name string `json:"name" binding:"min=1,max=100"`
+ Description string `json:"description" binding:"max=500"`
+ IsPublic *bool `json:"isPublic"`
+ Temperature *float64 `json:"temperature" binding:"omitempty,min=0,max=2"`
+ TopP *float64 `json:"topP" binding:"omitempty,min=0,max=1"`
+ TopK *int `json:"topK" binding:"omitempty,min=0"`
+ FrequencyPenalty *float64 `json:"frequencyPenalty" binding:"omitempty,min=-2,max=2"`
+ PresencePenalty *float64 `json:"presencePenalty" binding:"omitempty,min=-2,max=2"`
+ MaxTokens *int `json:"maxTokens" binding:"omitempty,min=1,max=32000"`
+ RepetitionPenalty *float64 `json:"repetitionPenalty"`
+ MinP *float64 `json:"minP"`
+ TopA *float64 `json:"topA"`
+ SystemPrompt *string `json:"systemPrompt"`
+ StopSequences []string `json:"stopSequences"`
+ Extensions map[string]interface{} `json:"extensions"`
+}
+
+// GetPresetListRequest 获取预设列表请求
+type GetPresetListRequest struct {
+ Page int `form:"page" binding:"min=1"`
+ PageSize int `form:"pageSize" binding:"min=1,max=100"`
+ Keyword string `form:"keyword"`
+ IsPublic *bool `form:"isPublic"`
+}
diff --git a/server/model/app/response/ai_config.go b/server/model/app/response/ai_config.go
new file mode 100644
index 0000000..7c52e09
--- /dev/null
+++ b/server/model/app/response/ai_config.go
@@ -0,0 +1,88 @@
+package response
+
+import (
+ "encoding/json"
+ "time"
+
+ "git.echol.cn/loser/st/server/model/app"
+)
+
+// AIConfigResponse AI配置响应
+type AIConfigResponse struct {
+ ID uint `json:"id"`
+ Name string `json:"name"`
+ Provider string `json:"provider"`
+ BaseURL string `json:"baseUrl"`
+ APIKey string `json:"apiKey"` // 前端显示时应该脱敏
+ Models []string `json:"models"`
+ DefaultModel string `json:"defaultModel"`
+ Settings map[string]interface{} `json:"settings"`
+ IsActive bool `json:"isActive"`
+ IsDefault bool `json:"isDefault"`
+ CreatedAt time.Time `json:"createdAt"`
+ UpdatedAt time.Time `json:"updatedAt"`
+}
+
+// AIConfigListResponse AI配置列表响应
+type AIConfigListResponse struct {
+ List []AIConfigResponse `json:"list"`
+ Total int64 `json:"total"`
+}
+
+// TestAIConfigResponse 测试AI配置响应
+type TestAIConfigResponse struct {
+ Success bool `json:"success"`
+ Message string `json:"message"`
+ Latency int64 `json:"latency"` // 响应延迟(ms)
+}
+
+// GetModelsResponse 获取模型列表响应
+type GetModelsResponse struct {
+ Models []ModelInfo `json:"models"`
+}
+
+// ModelInfo 模型信息
+type ModelInfo struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ OwnedBy string `json:"ownedBy"`
+}
+
+// ToAIConfigResponse 转换为AI配置响应结构
+func ToAIConfigResponse(config *app.AIConfig) AIConfigResponse {
+ resp := AIConfigResponse{
+ ID: config.ID,
+ Name: config.Name,
+ Provider: config.Provider,
+ BaseURL: config.BaseURL,
+ APIKey: maskAPIKey(config.APIKey),
+ DefaultModel: config.DefaultModel,
+ IsActive: config.IsActive,
+ IsDefault: config.IsDefault,
+ CreatedAt: config.CreatedAt,
+ UpdatedAt: config.UpdatedAt,
+ }
+
+ // 解析 JSON 字段
+ if len(config.Models) > 0 {
+ var models []string
+ json.Unmarshal(config.Models, &models)
+ resp.Models = models
+ }
+
+ if len(config.Settings) > 0 {
+ var settings map[string]interface{}
+ json.Unmarshal(config.Settings, &settings)
+ resp.Settings = settings
+ }
+
+ return resp
+}
+
+// maskAPIKey 脱敏API Key
+func maskAPIKey(apiKey string) string {
+ if len(apiKey) <= 8 {
+ return "****"
+ }
+ return apiKey[:4] + "****" + apiKey[len(apiKey)-4:]
+}
diff --git a/server/model/app/response/auth.go b/server/model/app/response/auth.go
new file mode 100644
index 0000000..f86f177
--- /dev/null
+++ b/server/model/app/response/auth.go
@@ -0,0 +1,55 @@
+package response
+
+import (
+ "time"
+
+ "git.echol.cn/loser/st/server/model/app"
+)
+
+// AppUserResponse 用户信息响应
+type AppUserResponse struct {
+ ID uint `json:"id"`
+ UUID string `json:"uuid"`
+ Username string `json:"username"`
+ NickName string `json:"nickName"`
+ Email string `json:"email"`
+ Phone string `json:"phone"`
+ Avatar string `json:"avatar"`
+ Status string `json:"status"`
+ Enable bool `json:"enable"`
+ IsAdmin bool `json:"isAdmin"`
+ LastLoginAt *time.Time `json:"lastLoginAt"`
+ LastLoginIP string `json:"lastLoginIp"`
+ ChatCount int `json:"chatCount"`
+ MessageCount int `json:"messageCount"`
+ CreatedAt time.Time `json:"createdAt"`
+}
+
+// LoginResponse 登录响应
+type LoginResponse struct {
+ User AppUserResponse `json:"user"`
+ Token string `json:"token"`
+ RefreshToken string `json:"refreshToken"`
+ ExpiresAt int64 `json:"expiresAt"`
+}
+
+// ToAppUserResponse 转换为用户响应结构
+func ToAppUserResponse(user *app.AppUser) AppUserResponse {
+ return AppUserResponse{
+ ID: user.ID,
+ UUID: user.UUID,
+ Username: user.Username,
+ NickName: user.NickName,
+ Email: user.Email,
+ Phone: user.Phone,
+ Avatar: user.Avatar,
+ Status: user.Status,
+ Enable: user.Enable,
+ IsAdmin: user.IsAdmin,
+ LastLoginAt: user.LastLoginAt,
+ LastLoginIP: user.LastLoginIP,
+ ChatCount: user.ChatCount,
+ MessageCount: user.MessageCount,
+ CreatedAt: user.CreatedAt,
+ }
+}
diff --git a/server/model/app/response/character.go b/server/model/app/response/character.go
new file mode 100644
index 0000000..e58ffa0
--- /dev/null
+++ b/server/model/app/response/character.go
@@ -0,0 +1,108 @@
+package response
+
+import (
+ "encoding/json"
+ "time"
+
+ "git.echol.cn/loser/st/server/model/app"
+)
+
+// CharacterResponse 角色卡响应
+type CharacterResponse struct {
+ ID uint `json:"id"`
+ Name string `json:"name"`
+ Avatar string `json:"avatar"`
+ Creator string `json:"creator"`
+ Version string `json:"version"`
+ Description string `json:"description"`
+ Personality string `json:"personality"`
+ Scenario string `json:"scenario"`
+ FirstMes string `json:"firstMes"`
+ MesExample string `json:"mesExample"`
+ CreatorNotes string `json:"creatorNotes"`
+ SystemPrompt string `json:"systemPrompt"`
+ PostHistoryInstructions string `json:"postHistoryInstructions"`
+ Tags []string `json:"tags"`
+ AlternateGreetings []string `json:"alternateGreetings"`
+ CharacterBook map[string]interface{} `json:"characterBook"`
+ Extensions map[string]interface{} `json:"extensions"`
+ Spec string `json:"spec"`
+ SpecVersion string `json:"specVersion"`
+ IsPublic bool `json:"isPublic"`
+ UseCount int `json:"useCount"`
+ FavoriteCount int `json:"favoriteCount"`
+ CreatedAt time.Time `json:"createdAt"`
+ UpdatedAt time.Time `json:"updatedAt"`
+}
+
+// CharacterSimpleResponse 角色卡简化响应(用于列表)
+type CharacterSimpleResponse struct {
+ ID uint `json:"id"`
+ Name string `json:"name"`
+ Avatar string `json:"avatar"`
+ Description string `json:"description"`
+ CreatedAt time.Time `json:"createdAt"`
+ UpdatedAt time.Time `json:"updatedAt"`
+}
+
+// CharacterListResponse 角色卡列表响应
+type CharacterListResponse struct {
+ List []CharacterResponse `json:"list"`
+ Total int64 `json:"total"`
+ Page int `json:"page"`
+ PageSize int `json:"pageSize"`
+}
+
+// ToCharacterResponse 转换为角色卡响应结构
+func ToCharacterResponse(character *app.AICharacter) CharacterResponse {
+ resp := CharacterResponse{
+ ID: character.ID,
+ Name: character.Name,
+ Avatar: character.Avatar,
+ Creator: character.Creator,
+ Version: character.Version,
+ Description: character.Description,
+ Personality: character.Personality,
+ Scenario: character.Scenario,
+ FirstMes: character.FirstMes,
+ MesExample: character.MesExample,
+ CreatorNotes: character.CreatorNotes,
+ SystemPrompt: character.SystemPrompt,
+ PostHistoryInstructions: character.PostHistoryInstructions,
+ Spec: character.Spec,
+ SpecVersion: character.SpecVersion,
+ IsPublic: character.IsPublic,
+ UseCount: character.UseCount,
+ FavoriteCount: character.FavoriteCount,
+ CreatedAt: character.CreatedAt,
+ UpdatedAt: character.UpdatedAt,
+ }
+
+ // 解析 JSON 字段
+ if len(character.Tags) > 0 {
+ json.Unmarshal(character.Tags, &resp.Tags)
+ }
+ if len(character.AlternateGreetings) > 0 {
+ json.Unmarshal(character.AlternateGreetings, &resp.AlternateGreetings)
+ }
+ if len(character.CharacterBook) > 0 {
+ json.Unmarshal(character.CharacterBook, &resp.CharacterBook)
+ }
+ if len(character.Extensions) > 0 {
+ json.Unmarshal(character.Extensions, &resp.Extensions)
+ }
+
+ return resp
+}
+
+// ToCharacterSimpleResponse 转换为角色卡简化响应结构(用于列表)
+func ToCharacterSimpleResponse(character *app.AICharacter) CharacterSimpleResponse {
+ return CharacterSimpleResponse{
+ ID: character.ID,
+ Name: character.Name,
+ Avatar: character.Avatar,
+ Description: character.Description,
+ CreatedAt: character.CreatedAt,
+ UpdatedAt: character.UpdatedAt,
+ }
+}
diff --git a/server/model/app/response/conversation.go b/server/model/app/response/conversation.go
new file mode 100644
index 0000000..858f5d9
--- /dev/null
+++ b/server/model/app/response/conversation.go
@@ -0,0 +1,122 @@
+package response
+
+import (
+ "encoding/json"
+ "time"
+
+ "git.echol.cn/loser/st/server/model/app"
+)
+
+// ConversationResponse 对话响应
+type ConversationResponse struct {
+ ID uint `json:"id"`
+ CharacterID uint `json:"characterId"`
+ Title string `json:"title"`
+ PresetID *uint `json:"presetId"`
+ AIProvider string `json:"aiProvider"`
+ Model string `json:"model"`
+ Settings map[string]interface{} `json:"settings,omitempty"`
+ MessageCount int `json:"messageCount"`
+ TokenCount int `json:"tokenCount"`
+ CreatedAt time.Time `json:"createdAt"`
+ UpdatedAt time.Time `json:"updatedAt"`
+
+ // 关联数据
+ Character *CharacterResponse `json:"character,omitempty"`
+}
+
+// ConversationListItemResponse 对话列表项响应(轻量级)
+type ConversationListItemResponse struct {
+ ID uint `json:"id"`
+ CharacterID uint `json:"characterId"`
+ Title string `json:"title"`
+ MessageCount int `json:"messageCount"`
+ TokenCount int `json:"tokenCount"`
+ CreatedAt time.Time `json:"createdAt"`
+ UpdatedAt time.Time `json:"updatedAt"`
+ Character *CharacterSimpleResponse `json:"character,omitempty"`
+}
+
+// MessageResponse 消息响应
+type MessageResponse struct {
+ ID uint `json:"id"`
+ ConversationID uint `json:"conversationId"`
+ Role string `json:"role"`
+ Content string `json:"content"`
+ TokenCount int `json:"tokenCount"`
+ CreatedAt time.Time `json:"createdAt"`
+}
+
+// ConversationListResponse 对话列表响应
+type ConversationListResponse struct {
+ List []ConversationListItemResponse `json:"list"`
+ Total int64 `json:"total"`
+ Page int `json:"page"`
+ PageSize int `json:"pageSize"`
+}
+
+// MessageListResponse 消息列表响应
+type MessageListResponse struct {
+ List []MessageResponse `json:"list"`
+ Total int64 `json:"total"`
+ Page int `json:"page"`
+ PageSize int `json:"pageSize"`
+}
+
+// ToConversationResponse 转换为对话响应结构
+func ToConversationResponse(conv *app.Conversation) ConversationResponse {
+ // 解析 settings JSON
+ var settings map[string]interface{}
+ if len(conv.Settings) > 0 {
+ // 尝试解析 JSON,如果失败则返回 nil
+ if err := json.Unmarshal(conv.Settings, &settings); err != nil {
+ settings = nil
+ }
+ }
+
+ return ConversationResponse{
+ ID: conv.ID,
+ CharacterID: conv.CharacterID,
+ Title: conv.Title,
+ PresetID: conv.PresetID,
+ AIProvider: conv.AIProvider,
+ Model: conv.Model,
+ Settings: settings,
+ MessageCount: conv.MessageCount,
+ TokenCount: conv.TokenCount,
+ CreatedAt: conv.CreatedAt,
+ UpdatedAt: conv.UpdatedAt,
+ }
+}
+
+// ToMessageResponse 转换为消息响应结构
+func ToMessageResponse(msg *app.Message) MessageResponse {
+ return MessageResponse{
+ ID: msg.ID,
+ ConversationID: msg.ConversationID,
+ Role: msg.Role,
+ Content: msg.Content,
+ TokenCount: msg.TokenCount,
+ CreatedAt: msg.CreatedAt,
+ }
+}
+
+// ToConversationListItemResponse 转换为对话列表项响应结构(轻量级)
+func ToConversationListItemResponse(conv *app.Conversation, character *app.AICharacter) ConversationListItemResponse {
+ resp := ConversationListItemResponse{
+ ID: conv.ID,
+ CharacterID: conv.CharacterID,
+ Title: conv.Title,
+ MessageCount: conv.MessageCount,
+ TokenCount: conv.TokenCount,
+ CreatedAt: conv.CreatedAt,
+ UpdatedAt: conv.UpdatedAt,
+ }
+
+ if character != nil {
+ simpleChar := ToCharacterSimpleResponse(character)
+ resp.Character = &simpleChar
+ }
+
+ return resp
+}
diff --git a/server/model/app/response/preset.go b/server/model/app/response/preset.go
new file mode 100644
index 0000000..99700db
--- /dev/null
+++ b/server/model/app/response/preset.go
@@ -0,0 +1,78 @@
+package response
+
+import (
+ "encoding/json"
+ "time"
+
+ "git.echol.cn/loser/st/server/model/app"
+)
+
+// PresetResponse 预设响应
+type PresetResponse struct {
+ ID uint `json:"id"`
+ UserID uint `json:"userId"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ IsPublic bool `json:"isPublic"`
+ IsDefault bool `json:"isDefault"`
+ Temperature float64 `json:"temperature"`
+ TopP float64 `json:"topP"`
+ TopK int `json:"topK"`
+ FrequencyPenalty float64 `json:"frequencyPenalty"`
+ PresencePenalty float64 `json:"presencePenalty"`
+ MaxTokens int `json:"maxTokens"`
+ RepetitionPenalty float64 `json:"repetitionPenalty"`
+ MinP float64 `json:"minP"`
+ TopA float64 `json:"topA"`
+ SystemPrompt string `json:"systemPrompt"`
+ StopSequences []string `json:"stopSequences"`
+ Extensions map[string]interface{} `json:"extensions"`
+ UseCount int `json:"useCount"`
+ CreatedAt time.Time `json:"createdAt"`
+ UpdatedAt time.Time `json:"updatedAt"`
+}
+
+// PresetListResponse 预设列表响应
+type PresetListResponse struct {
+ List []PresetResponse `json:"list"`
+ Total int64 `json:"total"`
+ Page int `json:"page"`
+ PageSize int `json:"pageSize"`
+}
+
+// ToPresetResponse 转换为预设响应结构
+func ToPresetResponse(preset *app.AIPreset) PresetResponse {
+ var stopSequences []string
+ if len(preset.StopSequences) > 0 {
+ json.Unmarshal(preset.StopSequences, &stopSequences)
+ }
+
+ var extensions map[string]interface{}
+ if len(preset.Extensions) > 0 {
+ json.Unmarshal(preset.Extensions, &extensions)
+ }
+
+ return PresetResponse{
+ ID: preset.ID,
+ UserID: preset.UserID,
+ Name: preset.Name,
+ Description: preset.Description,
+ IsPublic: preset.IsPublic,
+ IsDefault: preset.IsDefault,
+ Temperature: preset.Temperature,
+ TopP: preset.TopP,
+ TopK: preset.TopK,
+ FrequencyPenalty: preset.FrequencyPenalty,
+ PresencePenalty: preset.PresencePenalty,
+ MaxTokens: preset.MaxTokens,
+ RepetitionPenalty: preset.RepetitionPenalty,
+ MinP: preset.MinP,
+ TopA: preset.TopA,
+ SystemPrompt: preset.SystemPrompt,
+ StopSequences: stopSequences,
+ Extensions: extensions,
+ UseCount: preset.UseCount,
+ CreatedAt: preset.CreatedAt,
+ UpdatedAt: preset.UpdatedAt,
+ }
+}
diff --git a/server/model/common/basetypes.go b/server/model/common/basetypes.go
new file mode 100644
index 0000000..1a133d5
--- /dev/null
+++ b/server/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/server/model/common/clearDB.go b/server/model/common/clearDB.go
new file mode 100644
index 0000000..e7fc757
--- /dev/null
+++ b/server/model/common/clearDB.go
@@ -0,0 +1,7 @@
+package common
+
+type ClearDB struct {
+ TableName string
+ CompareField string
+ Interval string
+}
diff --git a/server/model/common/common.go b/server/model/common/common.go
new file mode 100644
index 0000000..d9c6595
--- /dev/null
+++ b/server/model/common/common.go
@@ -0,0 +1,21 @@
+package common
+
+import (
+ "github.com/gin-gonic/gin"
+)
+
+// GetAppUserID 从上下文获取前台用户 ID
+func GetAppUserID(c *gin.Context) uint {
+ if userID, exists := c.Get("appUserId"); exists {
+ return userID.(uint)
+ }
+ return 0
+}
+
+// GetAppUsername 从上下文获取前台用户名
+func GetAppUsername(c *gin.Context) string {
+ if username, exists := c.Get("appUsername"); exists {
+ return username.(string)
+ }
+ return ""
+}
diff --git a/server/model/common/request/common.go b/server/model/common/request/common.go
new file mode 100644
index 0000000..b07611d
--- /dev/null
+++ b/server/model/common/request/common.go
@@ -0,0 +1,48 @@
+package request
+
+import (
+ "gorm.io/gorm"
+)
+
+// PageInfo Paging common input parameter structure
+type PageInfo struct {
+ Page int `json:"page" form:"page,default=1"` // 页码
+ PageSize int `json:"pageSize" form:"pageSize,default=20"` // 每页大小
+ 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/server/model/common/response/common.go b/server/model/common/response/common.go
new file mode 100644
index 0000000..7461096
--- /dev/null
+++ b/server/model/common/response/common.go
@@ -0,0 +1,8 @@
+package response
+
+type PageResult struct {
+ List interface{} `json:"list"`
+ Total int64 `json:"total"`
+ Page int `json:"page"`
+ PageSize int `json:"pageSize"`
+}
diff --git a/server/model/common/response/response.go b/server/model/common/response/response.go
new file mode 100644
index 0000000..f0e0e53
--- /dev/null
+++ b/server/model/common/response/response.go
@@ -0,0 +1,62 @@
+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/server/model/example/exa_attachment_category.go b/server/model/example/exa_attachment_category.go
new file mode 100644
index 0000000..0af352e
--- /dev/null
+++ b/server/model/example/exa_attachment_category.go
@@ -0,0 +1,16 @@
+package example
+
+import (
+ "git.echol.cn/loser/st/server/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/server/model/example/exa_breakpoint_continue.go b/server/model/example/exa_breakpoint_continue.go
new file mode 100644
index 0000000..e541ee7
--- /dev/null
+++ b/server/model/example/exa_breakpoint_continue.go
@@ -0,0 +1,24 @@
+package example
+
+import (
+ "git.echol.cn/loser/st/server/global"
+)
+
+// file struct, 文件结构体
+type ExaFile struct {
+ global.GVA_MODEL
+ FileName string
+ FileMd5 string
+ FilePath string
+ ExaFileChunk []ExaFileChunk
+ ChunkTotal int
+ IsFinish bool
+}
+
+// file chunk struct, 切片结构体
+type ExaFileChunk struct {
+ global.GVA_MODEL
+ ExaFileID uint
+ FileChunkNumber int
+ FileChunkPath string
+}
diff --git a/server/model/example/exa_customer.go b/server/model/example/exa_customer.go
new file mode 100644
index 0000000..eff8904
--- /dev/null
+++ b/server/model/example/exa_customer.go
@@ -0,0 +1,15 @@
+package example
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system"
+)
+
+type ExaCustomer struct {
+ global.GVA_MODEL
+ CustomerName string `json:"customerName" form:"customerName" gorm:"comment:客户名"` // 客户名
+ CustomerPhoneData string `json:"customerPhoneData" form:"customerPhoneData" gorm:"comment:客户手机号"` // 客户手机号
+ SysUserID uint `json:"sysUserId" form:"sysUserId" gorm:"comment:管理ID"` // 管理ID
+ SysUserAuthorityID uint `json:"sysUserAuthorityID" form:"sysUserAuthorityID" gorm:"comment:管理角色ID"` // 管理角色ID
+ SysUser system.SysUser `json:"sysUser" form:"sysUser" gorm:"comment:管理详情"` // 管理详情
+}
diff --git a/server/model/example/exa_file_upload_download.go b/server/model/example/exa_file_upload_download.go
new file mode 100644
index 0000000..9f09fd7
--- /dev/null
+++ b/server/model/example/exa_file_upload_download.go
@@ -0,0 +1,18 @@
+package example
+
+import (
+ "git.echol.cn/loser/st/server/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/server/model/example/request/exa_file_upload_and_downloads.go b/server/model/example/request/exa_file_upload_and_downloads.go
new file mode 100644
index 0000000..20dd62b
--- /dev/null
+++ b/server/model/example/request/exa_file_upload_and_downloads.go
@@ -0,0 +1,10 @@
+package request
+
+import (
+ "git.echol.cn/loser/st/server/model/common/request"
+)
+
+type ExaAttachmentCategorySearch struct {
+ ClassId int `json:"classId" form:"classId"`
+ request.PageInfo
+}
diff --git a/server/model/example/response/exa_breakpoint_continue.go b/server/model/example/response/exa_breakpoint_continue.go
new file mode 100644
index 0000000..0a17ddd
--- /dev/null
+++ b/server/model/example/response/exa_breakpoint_continue.go
@@ -0,0 +1,11 @@
+package response
+
+import "git.echol.cn/loser/st/server/model/example"
+
+type FilePathResponse struct {
+ FilePath string `json:"filePath"`
+}
+
+type FileResponse struct {
+ File example.ExaFile `json:"file"`
+}
diff --git a/server/model/example/response/exa_customer.go b/server/model/example/response/exa_customer.go
new file mode 100644
index 0000000..040bbf4
--- /dev/null
+++ b/server/model/example/response/exa_customer.go
@@ -0,0 +1,7 @@
+package response
+
+import "git.echol.cn/loser/st/server/model/example"
+
+type ExaCustomerResponse struct {
+ Customer example.ExaCustomer `json:"customer"`
+}
diff --git a/server/model/example/response/exa_file_upload_download.go b/server/model/example/response/exa_file_upload_download.go
new file mode 100644
index 0000000..9153521
--- /dev/null
+++ b/server/model/example/response/exa_file_upload_download.go
@@ -0,0 +1,7 @@
+package response
+
+import "git.echol.cn/loser/st/server/model/example"
+
+type ExaFileResponse struct {
+ File example.ExaFileUploadAndDownload `json:"file"`
+}
diff --git a/server/model/system/request/jwt.go b/server/model/system/request/jwt.go
new file mode 100644
index 0000000..1e1615d
--- /dev/null
+++ b/server/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/server/model/system/request/sys_api.go b/server/model/system/request/sys_api.go
new file mode 100644
index 0000000..9f87cd0
--- /dev/null
+++ b/server/model/system/request/sys_api.go
@@ -0,0 +1,14 @@
+package request
+
+import (
+ "git.echol.cn/loser/st/server/model/common/request"
+ "git.echol.cn/loser/st/server/model/system"
+)
+
+// api分页条件查询及排序结构体
+type SearchApiParams struct {
+ system.SysApi
+ request.PageInfo
+ OrderKey string `json:"orderKey"` // 排序
+ Desc bool `json:"desc"` // 排序方式:升序false(默认)|降序true
+}
diff --git a/server/model/system/request/sys_api_token.go b/server/model/system/request/sys_api_token.go
new file mode 100644
index 0000000..16d955c
--- /dev/null
+++ b/server/model/system/request/sys_api_token.go
@@ -0,0 +1,12 @@
+package request
+
+import (
+ "git.echol.cn/loser/st/server/model/common/request"
+ "git.echol.cn/loser/st/server/model/system"
+)
+
+type SysApiTokenSearch struct {
+ system.SysApiToken
+ request.PageInfo
+ Status *bool `json:"status" form:"status"`
+}
diff --git a/server/model/system/request/sys_authority_btn.go b/server/model/system/request/sys_authority_btn.go
new file mode 100644
index 0000000..98493ff
--- /dev/null
+++ b/server/model/system/request/sys_authority_btn.go
@@ -0,0 +1,7 @@
+package request
+
+type SysAuthorityBtnReq struct {
+ MenuID uint `json:"menuID"`
+ AuthorityId uint `json:"authorityId"`
+ Selected []uint `json:"selected"`
+}
diff --git a/server/model/system/request/sys_auto_code.go b/server/model/system/request/sys_auto_code.go
new file mode 100644
index 0000000..fe5d2fe
--- /dev/null
+++ b/server/model/system/request/sys_auto_code.go
@@ -0,0 +1,292 @@
+package request
+
+import (
+ "encoding/json"
+ "fmt"
+ "go/token"
+ "strings"
+
+ "git.echol.cn/loser/st/server/global"
+ model "git.echol.cn/loser/st/server/model/system"
+ "github.com/pkg/errors"
+)
+
+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.Fields[i].FieldSearchType != "BETWEEN" && r.Fields[i].FieldSearchType != "NOT BETWEEN" {
+ 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 InitDictionary struct {
+ PlugName string `json:"plugName"`
+ Dictionaries []uint `json:"dictionaries"`
+}
+
+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/server/model/system/request/sys_auto_code_mcp.go b/server/model/system/request/sys_auto_code_mcp.go
new file mode 100644
index 0000000..a52ec7c
--- /dev/null
+++ b/server/model/system/request/sys_auto_code_mcp.go
@@ -0,0 +1,16 @@
+package request
+
+type AutoMcpTool struct {
+ Name string `json:"name" form:"name" binding:"required"`
+ Description string `json:"description" form:"description" binding:"required"`
+ Params []struct {
+ Name string `json:"name" form:"name" binding:"required"`
+ Description string `json:"description" form:"description" binding:"required"`
+ Type string `json:"type" form:"type" binding:"required"` // string, number, boolean, object, array
+ Required bool `json:"required" form:"required"`
+ Default string `json:"default" form:"default"`
+ } `json:"params" form:"params"`
+ Response []struct {
+ Type string `json:"type" form:"type" binding:"required"` // text, image
+ } `json:"response" form:"response"`
+}
diff --git a/server/model/system/request/sys_auto_code_package.go b/server/model/system/request/sys_auto_code_package.go
new file mode 100644
index 0000000..e5fb49e
--- /dev/null
+++ b/server/model/system/request/sys_auto_code_package.go
@@ -0,0 +1,31 @@
+package request
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ model "git.echol.cn/loser/st/server/model/system"
+)
+
+type SysAutoCodePackageCreate struct {
+ Desc string `json:"desc" example:"描述"`
+ Label string `json:"label" example:"展示名"`
+ Template string `json:"template" example:"模版"`
+ PackageName string `json:"packageName" example:"包名"`
+ Module string `json:"-" example:"模块"`
+}
+
+func (r *SysAutoCodePackageCreate) AutoCode() AutoCode {
+ return AutoCode{
+ Package: r.PackageName,
+ Module: global.GVA_CONFIG.AutoCode.Module,
+ }
+}
+
+func (r *SysAutoCodePackageCreate) Create() model.SysAutoCodePackage {
+ return model.SysAutoCodePackage{
+ Desc: r.Desc,
+ Label: r.Label,
+ Template: r.Template,
+ PackageName: r.PackageName,
+ Module: global.GVA_CONFIG.AutoCode.Module,
+ }
+}
diff --git a/server/model/system/request/sys_auto_history.go b/server/model/system/request/sys_auto_history.go
new file mode 100644
index 0000000..7e358df
--- /dev/null
+++ b/server/model/system/request/sys_auto_history.go
@@ -0,0 +1,57 @@
+package request
+
+import (
+ common "git.echol.cn/loser/st/server/model/common/request"
+ model "git.echol.cn/loser/st/server/model/system"
+)
+
+type SysAutoHistoryCreate struct {
+ Table string // 表名
+ Package string // 模块名/插件名
+ Request string // 前端传入的结构化信息
+ StructName string // 结构体名称
+ BusinessDB string // 业务库
+ Description string // Struct中文名称
+ Injections map[string]string // 注入路径
+ Templates map[string]string // 模板信息
+ ApiIDs []uint // api表注册内容
+ MenuID uint // 菜单ID
+ ExportTemplateID uint // 导出模板ID
+}
+
+func (r *SysAutoHistoryCreate) Create() model.SysAutoCodeHistory {
+ entity := model.SysAutoCodeHistory{
+ Package: r.Package,
+ Request: r.Request,
+ Table: r.Table,
+ StructName: r.StructName,
+ 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/server/model/system/request/sys_casbin.go b/server/model/system/request/sys_casbin.go
new file mode 100644
index 0000000..3ca4212
--- /dev/null
+++ b/server/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/server/model/system/request/sys_dictionary.go b/server/model/system/request/sys_dictionary.go
new file mode 100644
index 0000000..5a84796
--- /dev/null
+++ b/server/model/system/request/sys_dictionary.go
@@ -0,0 +1,9 @@
+package request
+
+type SysDictionarySearch struct {
+ Name string `json:"name" form:"name" gorm:"column:name;comment:字典名(中)"` // 字典名(中)
+}
+
+type ImportSysDictionaryRequest struct {
+ Json string `json:"json" binding:"required"` // JSON字符串
+}
diff --git a/server/model/system/request/sys_dictionary_detail.go b/server/model/system/request/sys_dictionary_detail.go
new file mode 100644
index 0000000..cc8c166
--- /dev/null
+++ b/server/model/system/request/sys_dictionary_detail.go
@@ -0,0 +1,43 @@
+package request
+
+import (
+ "git.echol.cn/loser/st/server/model/common/request"
+ "git.echol.cn/loser/st/server/model/system"
+)
+
+type SysDictionaryDetailSearch struct {
+ system.SysDictionaryDetail
+ request.PageInfo
+ ParentID *uint `json:"parentID" form:"parentID"` // 父级字典详情ID,用于查询指定父级下的子项
+ Level *int `json:"level" form:"level"` // 层级深度,用于查询指定层级的数据
+}
+
+// CreateSysDictionaryDetailRequest 创建字典详情请求
+type CreateSysDictionaryDetailRequest struct {
+ Label string `json:"label" form:"label" binding:"required"` // 展示值
+ Value string `json:"value" form:"value" binding:"required"` // 字典值
+ Extend string `json:"extend" form:"extend"` // 扩展值
+ Status *bool `json:"status" form:"status"` // 启用状态
+ Sort int `json:"sort" form:"sort"` // 排序标记
+ SysDictionaryID int `json:"sysDictionaryID" form:"sysDictionaryID" binding:"required"` // 关联标记
+ ParentID *uint `json:"parentID" form:"parentID"` // 父级字典详情ID
+}
+
+// UpdateSysDictionaryDetailRequest 更新字典详情请求
+type UpdateSysDictionaryDetailRequest struct {
+ ID uint `json:"ID" form:"ID" binding:"required"` // 主键ID
+ Label string `json:"label" form:"label" binding:"required"` // 展示值
+ Value string `json:"value" form:"value" binding:"required"` // 字典值
+ Extend string `json:"extend" form:"extend"` // 扩展值
+ Status *bool `json:"status" form:"status"` // 启用状态
+ Sort int `json:"sort" form:"sort"` // 排序标记
+ SysDictionaryID int `json:"sysDictionaryID" form:"sysDictionaryID" binding:"required"` // 关联标记
+ ParentID *uint `json:"parentID" form:"parentID"` // 父级字典详情ID
+}
+
+// GetDictionaryDetailsByParentRequest 根据父级ID获取字典详情请求
+type GetDictionaryDetailsByParentRequest struct {
+ SysDictionaryID int `json:"sysDictionaryID" form:"sysDictionaryID" binding:"required"` // 字典ID
+ ParentID *uint `json:"parentID" form:"parentID"` // 父级字典详情ID,为空时获取顶级
+ IncludeChildren bool `json:"includeChildren" form:"includeChildren"` // 是否包含子级数据
+}
diff --git a/server/model/system/request/sys_error.go b/server/model/system/request/sys_error.go
new file mode 100644
index 0000000..7e9dc8a
--- /dev/null
+++ b/server/model/system/request/sys_error.go
@@ -0,0 +1,14 @@
+package request
+
+import (
+ "time"
+
+ "git.echol.cn/loser/st/server/model/common/request"
+)
+
+type SysErrorSearch struct {
+ CreatedAtRange []time.Time `json:"createdAtRange" form:"createdAtRange[]"`
+ Form *string `json:"form" form:"form"`
+ Info *string `json:"info" form:"info"`
+ request.PageInfo
+}
diff --git a/server/model/system/request/sys_export_template.go b/server/model/system/request/sys_export_template.go
new file mode 100644
index 0000000..e2ff01a
--- /dev/null
+++ b/server/model/system/request/sys_export_template.go
@@ -0,0 +1,15 @@
+package request
+
+import (
+ "time"
+
+ "git.echol.cn/loser/st/server/model/common/request"
+ "git.echol.cn/loser/st/server/model/system"
+)
+
+type SysExportTemplateSearch struct {
+ system.SysExportTemplate
+ StartCreatedAt *time.Time `json:"startCreatedAt" form:"startCreatedAt"`
+ EndCreatedAt *time.Time `json:"endCreatedAt" form:"endCreatedAt"`
+ request.PageInfo
+}
diff --git a/server/model/system/request/sys_init.go b/server/model/system/request/sys_init.go
new file mode 100644
index 0000000..7194128
--- /dev/null
+++ b/server/model/system/request/sys_init.go
@@ -0,0 +1,125 @@
+package request
+
+import (
+ "fmt"
+ "os"
+
+ "git.echol.cn/loser/st/server/config"
+)
+
+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/server/model/system/request/sys_login_log.go b/server/model/system/request/sys_login_log.go
new file mode 100644
index 0000000..3f5d5cd
--- /dev/null
+++ b/server/model/system/request/sys_login_log.go
@@ -0,0 +1,11 @@
+package request
+
+import (
+ "git.echol.cn/loser/st/server/model/common/request"
+ "git.echol.cn/loser/st/server/model/system"
+)
+
+type SysLoginLogSearch struct {
+ system.SysLoginLog
+ request.PageInfo
+}
diff --git a/server/model/system/request/sys_menu.go b/server/model/system/request/sys_menu.go
new file mode 100644
index 0000000..2b6fa1e
--- /dev/null
+++ b/server/model/system/request/sys_menu.go
@@ -0,0 +1,27 @@
+package request
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/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/server/model/system/request/sys_operation_record.go b/server/model/system/request/sys_operation_record.go
new file mode 100644
index 0000000..fa9d611
--- /dev/null
+++ b/server/model/system/request/sys_operation_record.go
@@ -0,0 +1,11 @@
+package request
+
+import (
+ "git.echol.cn/loser/st/server/model/common/request"
+ "git.echol.cn/loser/st/server/model/system"
+)
+
+type SysOperationRecordSearch struct {
+ system.SysOperationRecord
+ request.PageInfo
+}
diff --git a/server/model/system/request/sys_params.go b/server/model/system/request/sys_params.go
new file mode 100644
index 0000000..8cb2e64
--- /dev/null
+++ b/server/model/system/request/sys_params.go
@@ -0,0 +1,15 @@
+package request
+
+import (
+ "time"
+
+ "git.echol.cn/loser/st/server/model/common/request"
+)
+
+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/server/model/system/request/sys_skills.go b/server/model/system/request/sys_skills.go
new file mode 100644
index 0000000..953fbd6
--- /dev/null
+++ b/server/model/system/request/sys_skills.go
@@ -0,0 +1,64 @@
+package request
+
+import "git.echol.cn/loser/st/server/model/system"
+
+type SkillToolRequest struct {
+ Tool string `json:"tool"`
+}
+
+type SkillDetailRequest struct {
+ Tool string `json:"tool"`
+ Skill string `json:"skill"`
+}
+
+type SkillSaveRequest struct {
+ Tool string `json:"tool"`
+ Skill string `json:"skill"`
+ Meta system.SkillMeta `json:"meta"`
+ Markdown string `json:"markdown"`
+ SyncTools []string `json:"syncTools"`
+}
+
+type SkillScriptCreateRequest struct {
+ Tool string `json:"tool"`
+ Skill string `json:"skill"`
+ FileName string `json:"fileName"`
+ ScriptType string `json:"scriptType"`
+}
+
+type SkillResourceCreateRequest struct {
+ Tool string `json:"tool"`
+ Skill string `json:"skill"`
+ FileName string `json:"fileName"`
+}
+
+type SkillReferenceCreateRequest struct {
+ Tool string `json:"tool"`
+ Skill string `json:"skill"`
+ FileName string `json:"fileName"`
+}
+
+type SkillTemplateCreateRequest struct {
+ Tool string `json:"tool"`
+ Skill string `json:"skill"`
+ FileName string `json:"fileName"`
+}
+
+type SkillFileRequest struct {
+ Tool string `json:"tool"`
+ Skill string `json:"skill"`
+ FileName string `json:"fileName"`
+}
+
+type SkillFileSaveRequest struct {
+ Tool string `json:"tool"`
+ Skill string `json:"skill"`
+ FileName string `json:"fileName"`
+ Content string `json:"content"`
+}
+
+type SkillGlobalConstraintSaveRequest struct {
+ Tool string `json:"tool"`
+ Content string `json:"content"`
+ SyncTools []string `json:"syncTools"`
+}
diff --git a/server/model/system/request/sys_user.go b/server/model/system/request/sys_user.go
new file mode 100644
index 0000000..a7e6544
--- /dev/null
+++ b/server/model/system/request/sys_user.go
@@ -0,0 +1,69 @@
+package request
+
+import (
+ common "git.echol.cn/loser/st/server/model/common/request"
+ "git.echol.cn/loser/st/server/model/system"
+)
+
+// Register User register structure
+type Register struct {
+ Username string `json:"userName" example:"用户名"`
+ Password string `json:"passWord" example:"密码"`
+ NickName string `json:"nickName" example:"昵称"`
+ HeaderImg string `json:"headerImg" example:"头像链接"`
+ AuthorityId uint `json:"authorityId" swaggertype:"string" example:"int 角色id"`
+ Enable int `json:"enable" swaggertype:"string" example:"int 是否启用"`
+ AuthorityIds []uint `json:"authorityIds" swaggertype:"string" example:"[]uint 角色id"`
+ Phone string `json:"phone" example:"电话号码"`
+ Email string `json:"email" example:"电子邮箱"`
+}
+
+// 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"` // 新密码
+}
+
+type ResetPassword struct {
+ ID uint `json:"ID" form:"ID"`
+ Password string `json:"password" form:"password" gorm:"comment:用户登录密码"` // 用户登录密码
+}
+
+// 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:用户头像"` // 用户头像
+ 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/server/model/system/request/sys_version.go b/server/model/system/request/sys_version.go
new file mode 100644
index 0000000..052a904
--- /dev/null
+++ b/server/model/system/request/sys_version.go
@@ -0,0 +1,41 @@
+package request
+
+import (
+ "time"
+
+ "git.echol.cn/loser/st/server/model/common/request"
+ "git.echol.cn/loser/st/server/model/system"
+)
+
+type SysVersionSearch struct {
+ CreatedAtRange []time.Time `json:"createdAtRange" form:"createdAtRange[]"`
+ VersionName *string `json:"versionName" form:"versionName"`
+ VersionCode *string `json:"versionCode" form:"versionCode"`
+ request.PageInfo
+}
+
+// ExportVersionRequest 导出版本请求结构体
+type ExportVersionRequest struct {
+ VersionName string `json:"versionName" binding:"required"` // 版本名称
+ VersionCode string `json:"versionCode" binding:"required"` // 版本号
+ Description string `json:"description"` // 版本描述
+ MenuIds []uint `json:"menuIds"` // 选中的菜单ID列表
+ ApiIds []uint `json:"apiIds"` // 选中的API ID列表
+ DictIds []uint `json:"dictIds"` // 选中的字典ID列表
+}
+
+// ImportVersionRequest 导入版本请求结构体
+type ImportVersionRequest struct {
+ VersionInfo VersionInfo `json:"version" binding:"required"` // 版本信息
+ ExportMenu []system.SysBaseMenu `json:"menus"` // 菜单数据,直接复用SysBaseMenu
+ ExportApi []system.SysApi `json:"apis"` // API数据,直接复用SysApi
+ ExportDictionary []system.SysDictionary `json:"dictionaries"` // 字典数据,直接复用SysDictionary
+}
+
+// VersionInfo 版本信息结构体
+type VersionInfo struct {
+ Name string `json:"name" binding:"required"` // 版本名称
+ Code string `json:"code" binding:"required"` // 版本号
+ Description string `json:"description"` // 版本描述
+ ExportTime string `json:"exportTime"` // 导出时间
+}
diff --git a/server/model/system/response/sys_api.go b/server/model/system/response/sys_api.go
new file mode 100644
index 0000000..ec1ed63
--- /dev/null
+++ b/server/model/system/response/sys_api.go
@@ -0,0 +1,18 @@
+package response
+
+import (
+ "git.echol.cn/loser/st/server/model/system"
+)
+
+type SysAPIResponse struct {
+ Api system.SysApi `json:"api"`
+}
+
+type SysAPIListResponse struct {
+ Apis []system.SysApi `json:"apis"`
+}
+
+type SysSyncApis struct {
+ NewApis []system.SysApi `json:"newApis"`
+ DeleteApis []system.SysApi `json:"deleteApis"`
+}
diff --git a/server/model/system/response/sys_authority.go b/server/model/system/response/sys_authority.go
new file mode 100644
index 0000000..d5704c9
--- /dev/null
+++ b/server/model/system/response/sys_authority.go
@@ -0,0 +1,12 @@
+package response
+
+import "git.echol.cn/loser/st/server/model/system"
+
+type SysAuthorityResponse struct {
+ Authority system.SysAuthority `json:"authority"`
+}
+
+type SysAuthorityCopyResponse struct {
+ Authority system.SysAuthority `json:"authority"`
+ OldAuthorityId uint `json:"oldAuthorityId"` // 旧角色ID
+}
diff --git a/server/model/system/response/sys_authority_btn.go b/server/model/system/response/sys_authority_btn.go
new file mode 100644
index 0000000..2f772cf
--- /dev/null
+++ b/server/model/system/response/sys_authority_btn.go
@@ -0,0 +1,5 @@
+package response
+
+type SysAuthorityBtnRes struct {
+ Selected []uint `json:"selected"`
+}
diff --git a/server/model/system/response/sys_auto_code.go b/server/model/system/response/sys_auto_code.go
new file mode 100644
index 0000000..ec1b552
--- /dev/null
+++ b/server/model/system/response/sys_auto_code.go
@@ -0,0 +1,27 @@
+package response
+
+import "git.echol.cn/loser/st/server/model/system"
+
+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"`
+}
+
+type PluginInfo struct {
+ PluginName string `json:"pluginName"`
+ PluginType string `json:"pluginType"` // web, server, full
+ Apis []system.SysApi `json:"apis"`
+ Menus []system.SysBaseMenu `json:"menus"`
+ Dictionaries []system.SysDictionary `json:"dictionaries"`
+}
diff --git a/server/model/system/response/sys_captcha.go b/server/model/system/response/sys_captcha.go
new file mode 100644
index 0000000..0c3995a
--- /dev/null
+++ b/server/model/system/response/sys_captcha.go
@@ -0,0 +1,8 @@
+package response
+
+type SysCaptchaResponse struct {
+ CaptchaId string `json:"captchaId"`
+ PicPath string `json:"picPath"`
+ CaptchaLength int `json:"captchaLength"`
+ OpenCaptcha bool `json:"openCaptcha"`
+}
diff --git a/server/model/system/response/sys_casbin.go b/server/model/system/response/sys_casbin.go
new file mode 100644
index 0000000..34e5d71
--- /dev/null
+++ b/server/model/system/response/sys_casbin.go
@@ -0,0 +1,9 @@
+package response
+
+import (
+ "git.echol.cn/loser/st/server/model/system/request"
+)
+
+type PolicyPathResponse struct {
+ Paths []request.CasbinInfo `json:"paths"`
+}
diff --git a/server/model/system/response/sys_menu.go b/server/model/system/response/sys_menu.go
new file mode 100644
index 0000000..9ca3fdd
--- /dev/null
+++ b/server/model/system/response/sys_menu.go
@@ -0,0 +1,15 @@
+package response
+
+import "git.echol.cn/loser/st/server/model/system"
+
+type SysMenusResponse struct {
+ Menus []system.SysMenu `json:"menus"`
+}
+
+type SysBaseMenusResponse struct {
+ Menus []system.SysBaseMenu `json:"menus"`
+}
+
+type SysBaseMenuResponse struct {
+ Menu system.SysBaseMenu `json:"menu"`
+}
diff --git a/server/model/system/response/sys_system.go b/server/model/system/response/sys_system.go
new file mode 100644
index 0000000..a5abe44
--- /dev/null
+++ b/server/model/system/response/sys_system.go
@@ -0,0 +1,7 @@
+package response
+
+import "git.echol.cn/loser/st/server/config"
+
+type SysConfigResponse struct {
+ Config config.Server `json:"config"`
+}
diff --git a/server/model/system/response/sys_user.go b/server/model/system/response/sys_user.go
new file mode 100644
index 0000000..9b7b4d9
--- /dev/null
+++ b/server/model/system/response/sys_user.go
@@ -0,0 +1,15 @@
+package response
+
+import (
+ "git.echol.cn/loser/st/server/model/system"
+)
+
+type SysUserResponse struct {
+ User system.SysUser `json:"user"`
+}
+
+type LoginResponse struct {
+ User system.SysUser `json:"user"`
+ Token string `json:"token"`
+ ExpiresAt int64 `json:"expiresAt"`
+}
diff --git a/server/model/system/response/sys_version.go b/server/model/system/response/sys_version.go
new file mode 100644
index 0000000..cdaa058
--- /dev/null
+++ b/server/model/system/response/sys_version.go
@@ -0,0 +1,14 @@
+package response
+
+import (
+ "git.echol.cn/loser/st/server/model/system"
+ "git.echol.cn/loser/st/server/model/system/request"
+)
+
+// ExportVersionResponse 导出版本响应结构体
+type ExportVersionResponse struct {
+ Version request.VersionInfo `json:"version"` // 版本信息
+ Menus []system.SysBaseMenu `json:"menus"` // 菜单数据,直接复用SysBaseMenu
+ Apis []system.SysApi `json:"apis"` // API数据,直接复用SysApi
+ Dictionaries []system.SysDictionary `json:"dictionaries"` // 字典数据,直接复用SysDictionary
+}
diff --git a/server/model/system/sys_api.go b/server/model/system/sys_api.go
new file mode 100644
index 0000000..f960951
--- /dev/null
+++ b/server/model/system/sys_api.go
@@ -0,0 +1,28 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/global"
+)
+
+type SysApi struct {
+ global.GVA_MODEL
+ Path string `json:"path" gorm:"comment:api路径"` // api路径
+ Description string `json:"description" gorm:"comment:api中文描述"` // api中文描述
+ ApiGroup string `json:"apiGroup" gorm:"comment:api组"` // api组
+ Method string `json:"method" gorm:"default:POST;comment:方法"` // 方法:创建POST(默认)|查看GET|更新PUT|删除DELETE
+}
+
+func (SysApi) TableName() string {
+ return "sys_apis"
+}
+
+type SysIgnoreApi struct {
+ global.GVA_MODEL
+ Path string `json:"path" gorm:"comment:api路径"` // api路径
+ Method string `json:"method" gorm:"default:POST;comment:方法"` // 方法:创建POST(默认)|查看GET|更新PUT|删除DELETE
+ Flag bool `json:"flag" gorm:"-"` // 是否忽略
+}
+
+func (SysIgnoreApi) TableName() string {
+ return "sys_ignore_apis"
+}
diff --git a/server/model/system/sys_api_token.go b/server/model/system/sys_api_token.go
new file mode 100644
index 0000000..c062c3d
--- /dev/null
+++ b/server/model/system/sys_api_token.go
@@ -0,0 +1,18 @@
+package system
+
+import (
+ "time"
+
+ "git.echol.cn/loser/st/server/global"
+)
+
+type SysApiToken struct {
+ global.GVA_MODEL
+ UserID uint `json:"userId" gorm:"comment:用户ID"`
+ User SysUser `json:"user" gorm:"foreignKey:UserID;"`
+ AuthorityID uint `json:"authorityId" gorm:"comment:角色ID"`
+ Token string `json:"token" gorm:"type:text;comment:Token"`
+ Status bool `json:"status" gorm:"default:true;comment:状态"` // true有效 false无效
+ ExpiresAt time.Time `json:"expiresAt" gorm:"comment:过期时间"`
+ Remark string `json:"remark" gorm:"comment:备注"`
+}
diff --git a/server/model/system/sys_authority.go b/server/model/system/sys_authority.go
new file mode 100644
index 0000000..01c5efa
--- /dev/null
+++ b/server/model/system/sys_authority.go
@@ -0,0 +1,23 @@
+package system
+
+import (
+ "time"
+)
+
+type SysAuthority struct {
+ CreatedAt time.Time // 创建时间
+ UpdatedAt time.Time // 更新时间
+ DeletedAt *time.Time `sql:"index"`
+ AuthorityId uint `json:"authorityId" gorm:"not null;unique;primary_key;comment:角色ID;size:90"` // 角色ID
+ AuthorityName string `json:"authorityName" gorm:"comment:角色名"` // 角色名
+ ParentId *uint `json:"parentId" gorm:"comment:父角色ID"` // 父角色ID
+ DataAuthorityId []*SysAuthority `json:"dataAuthorityId" gorm:"many2many:sys_data_authority_id;"`
+ Children []SysAuthority `json:"children" gorm:"-"`
+ SysBaseMenus []SysBaseMenu `json:"menus" gorm:"many2many:sys_authority_menus;"`
+ Users []SysUser `json:"-" gorm:"many2many:sys_user_authority;"`
+ DefaultRouter string `json:"defaultRouter" gorm:"comment:默认菜单;default:dashboard"` // 默认菜单(默认dashboard)
+}
+
+func (SysAuthority) TableName() string {
+ return "sys_authorities"
+}
diff --git a/server/model/system/sys_authority_btn.go b/server/model/system/sys_authority_btn.go
new file mode 100644
index 0000000..e005984
--- /dev/null
+++ b/server/model/system/sys_authority_btn.go
@@ -0,0 +1,8 @@
+package system
+
+type SysAuthorityBtn struct {
+ AuthorityId uint `gorm:"comment:角色ID"`
+ SysMenuID uint `gorm:"comment:菜单ID"`
+ SysBaseMenuBtnID uint `gorm:"comment:菜单按钮ID"`
+ SysBaseMenuBtn SysBaseMenuBtn ` gorm:"comment:按钮详情"`
+}
diff --git a/server/model/system/sys_authority_menu.go b/server/model/system/sys_authority_menu.go
new file mode 100644
index 0000000..4467a7e
--- /dev/null
+++ b/server/model/system/sys_authority_menu.go
@@ -0,0 +1,19 @@
+package system
+
+type SysMenu struct {
+ SysBaseMenu
+ MenuId uint `json:"menuId" gorm:"comment:菜单ID"`
+ AuthorityId uint `json:"-" gorm:"comment:角色ID"`
+ Children []SysMenu `json:"children" gorm:"-"`
+ Parameters []SysBaseMenuParameter `json:"parameters" gorm:"foreignKey:SysBaseMenuID;references:MenuId"`
+ Btns map[string]uint `json:"btns" gorm:"-"`
+}
+
+type SysAuthorityMenu struct {
+ MenuId string `json:"menuId" gorm:"comment:菜单ID;column:sys_base_menu_id"`
+ AuthorityId string `json:"-" gorm:"comment:角色ID;column:sys_authority_authority_id"`
+}
+
+func (s SysAuthorityMenu) TableName() string {
+ return "sys_authority_menus"
+}
diff --git a/server/model/system/sys_auto_code_history.go b/server/model/system/sys_auto_code_history.go
new file mode 100644
index 0000000..7eaefa5
--- /dev/null
+++ b/server/model/system/sys_auto_code_history.go
@@ -0,0 +1,69 @@
+package system
+
+import (
+ "os"
+ "path"
+ "path/filepath"
+ "strings"
+
+ "git.echol.cn/loser/st/server/global"
+ "gorm.io/gorm"
+)
+
+// 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/server/model/system/sys_auto_code_package.go b/server/model/system/sys_auto_code_package.go
new file mode 100644
index 0000000..734f9a2
--- /dev/null
+++ b/server/model/system/sys_auto_code_package.go
@@ -0,0 +1,18 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/global"
+)
+
+type SysAutoCodePackage struct {
+ global.GVA_MODEL
+ Desc string `json:"desc" gorm:"comment:描述"`
+ Label string `json:"label" gorm:"comment:展示名"`
+ Template string `json:"template" gorm:"comment:模版"`
+ PackageName string `json:"packageName" gorm:"comment:包名"`
+ Module string `json:"-" example:"模块"`
+}
+
+func (s *SysAutoCodePackage) TableName() string {
+ return "sys_auto_code_packages"
+}
diff --git a/server/model/system/sys_base_menu.go b/server/model/system/sys_base_menu.go
new file mode 100644
index 0000000..620cb90
--- /dev/null
+++ b/server/model/system/sys_base_menu.go
@@ -0,0 +1,43 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/global"
+)
+
+type SysBaseMenu struct {
+ global.GVA_MODEL
+ MenuLevel uint `json:"-"`
+ ParentId uint `json:"parentId" gorm:"comment:父菜单ID"` // 父菜单ID
+ Path string `json:"path" gorm:"comment:路由path"` // 路由path
+ Name string `json:"name" gorm:"comment:路由name"` // 路由name
+ Hidden bool `json:"hidden" gorm:"comment:是否在列表隐藏"` // 是否在列表隐藏
+ Component string `json:"component" gorm:"comment:对应前端文件路径"` // 对应前端文件路径
+ Sort int `json:"sort" gorm:"comment:排序标记"` // 排序标记
+ Meta `json:"meta" gorm:"embedded"` // 附加属性
+ 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
+ TransitionType string `json:"transitionType" gorm:"comment:路由切换动画"` // 路由切换动画
+}
+
+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/server/model/system/sys_dictionary.go b/server/model/system/sys_dictionary.go
new file mode 100644
index 0000000..e19e0de
--- /dev/null
+++ b/server/model/system/sys_dictionary.go
@@ -0,0 +1,22 @@
+// 自动生成模板SysDictionary
+package system
+
+import (
+ "git.echol.cn/loser/st/server/global"
+)
+
+// 如果含有time.Time 请自行import time包
+type SysDictionary struct {
+ global.GVA_MODEL
+ Name string `json:"name" form:"name" gorm:"column:name;comment:字典名(中)"` // 字典名(中)
+ Type string `json:"type" form:"type" gorm:"column:type;comment:字典名(英)"` // 字典名(英)
+ Status *bool `json:"status" form:"status" gorm:"column:status;comment:状态"` // 状态
+ Desc string `json:"desc" form:"desc" gorm:"column:desc;comment:描述"` // 描述
+ ParentID *uint `json:"parentID" form:"parentID" gorm:"column:parent_id;comment:父级字典ID"` // 父级字典ID
+ Children []SysDictionary `json:"children" gorm:"foreignKey:ParentID"` // 子字典
+ SysDictionaryDetails []SysDictionaryDetail `json:"sysDictionaryDetails" form:"sysDictionaryDetails"`
+}
+
+func (SysDictionary) TableName() string {
+ return "sys_dictionaries"
+}
diff --git a/server/model/system/sys_dictionary_detail.go b/server/model/system/sys_dictionary_detail.go
new file mode 100644
index 0000000..8935d57
--- /dev/null
+++ b/server/model/system/sys_dictionary_detail.go
@@ -0,0 +1,26 @@
+// 自动生成模板SysDictionaryDetail
+package system
+
+import (
+ "git.echol.cn/loser/st/server/global"
+)
+
+// 如果含有time.Time 请自行import time包
+type SysDictionaryDetail struct {
+ global.GVA_MODEL
+ Label string `json:"label" form:"label" gorm:"column:label;comment:展示值"` // 展示值
+ Value string `json:"value" form:"value" gorm:"column:value;comment:字典值"` // 字典值
+ Extend string `json:"extend" form:"extend" gorm:"column:extend;comment:扩展值"` // 扩展值
+ Status *bool `json:"status" form:"status" gorm:"column:status;comment:启用状态"` // 启用状态
+ Sort int `json:"sort" form:"sort" gorm:"column:sort;comment:排序标记"` // 排序标记
+ SysDictionaryID int `json:"sysDictionaryID" form:"sysDictionaryID" gorm:"column:sys_dictionary_id;comment:关联标记"` // 关联标记
+ ParentID *uint `json:"parentID" form:"parentID" gorm:"column:parent_id;comment:父级字典详情ID"` // 父级字典详情ID
+ Children []SysDictionaryDetail `json:"children" gorm:"foreignKey:ParentID"` // 子字典详情
+ Level int `json:"level" form:"level" gorm:"column:level;comment:层级深度"` // 层级深度,从0开始
+ Path string `json:"path" form:"path" gorm:"column:path;comment:层级路径"` // 层级路径,如 "1,2,3"
+ Disabled bool `json:"disabled" gorm:"-"` // 禁用状态,根据status字段动态计算
+}
+
+func (SysDictionaryDetail) TableName() string {
+ return "sys_dictionary_details"
+}
diff --git a/server/model/system/sys_error.go b/server/model/system/sys_error.go
new file mode 100644
index 0000000..cd8cebd
--- /dev/null
+++ b/server/model/system/sys_error.go
@@ -0,0 +1,21 @@
+// 自动生成模板SysError
+package system
+
+import (
+ "git.echol.cn/loser/st/server/global"
+)
+
+// 错误日志 结构体 SysError
+type SysError struct {
+ global.GVA_MODEL
+ Form *string `json:"form" form:"form" gorm:"comment:错误来源;column:form;type:text;" binding:"required"` //错误来源
+ Info *string `json:"info" form:"info" gorm:"comment:错误内容;column:info;type:text;"` //错误内容
+ Level string `json:"level" form:"level" gorm:"comment:日志等级;column:level;"`
+ Solution *string `json:"solution" form:"solution" gorm:"comment:解决方案;column:solution;type:text"` //解决方案
+ Status string `json:"status" form:"status" gorm:"comment:处理状态;column:status;type:varchar(20);default:未处理;"` //处理状态:未处理/处理中/处理完成
+}
+
+// TableName 错误日志 SysError自定义表名 sys_error
+func (SysError) TableName() string {
+ return "sys_error"
+}
diff --git a/server/model/system/sys_export_template.go b/server/model/system/sys_export_template.go
new file mode 100644
index 0000000..9484f95
--- /dev/null
+++ b/server/model/system/sys_export_template.go
@@ -0,0 +1,46 @@
+// 自动生成模板SysExportTemplate
+package system
+
+import (
+ "git.echol.cn/loser/st/server/global"
+)
+
+// 导出模板 结构体 SysExportTemplate
+type SysExportTemplate struct {
+ global.GVA_MODEL
+ DBName string `json:"dbName" form:"dbName" gorm:"column:db_name;comment:数据库名称;"` //数据库名称
+ Name string `json:"name" form:"name" gorm:"column:name;comment:模板名称;"` //模板名称
+ TableName string `json:"tableName" form:"tableName" gorm:"column:table_name;comment:表名称;"` //表名称
+ TemplateID string `json:"templateID" form:"templateID" gorm:"column:template_id;comment:模板标识;"` //模板标识
+ TemplateInfo string `json:"templateInfo" form:"templateInfo" gorm:"column:template_info;type:text;"` //模板信息
+ SQL string `json:"sql" form:"sql" gorm:"column:sql;type:text;comment:自定义导出SQL;"` //自定义导出SQL
+ ImportSQL string `json:"importSql" form:"importSql" gorm:"column:import_sql;type:text;comment:自定义导入SQL;"` //自定义导入SQL
+ 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/server/model/system/sys_jwt_blacklist.go b/server/model/system/sys_jwt_blacklist.go
new file mode 100644
index 0000000..c1c4ff8
--- /dev/null
+++ b/server/model/system/sys_jwt_blacklist.go
@@ -0,0 +1,10 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/global"
+)
+
+type JwtBlacklist struct {
+ global.GVA_MODEL
+ Jwt string `gorm:"type:text;comment:jwt"`
+}
diff --git a/server/model/system/sys_login_log.go b/server/model/system/sys_login_log.go
new file mode 100644
index 0000000..c40cb7c
--- /dev/null
+++ b/server/model/system/sys_login_log.go
@@ -0,0 +1,16 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/global"
+)
+
+type SysLoginLog struct {
+ global.GVA_MODEL
+ Username string `json:"username" gorm:"column:username;comment:用户名"`
+ Ip string `json:"ip" gorm:"column:ip;comment:请求ip"`
+ Status bool `json:"status" gorm:"column:status;comment:登录状态"`
+ ErrorMessage string `json:"errorMessage" gorm:"column:error_message;comment:错误信息"`
+ Agent string `json:"agent" gorm:"column:agent;comment:代理"`
+ UserID uint `json:"userId" gorm:"column:user_id;comment:用户id"`
+ User SysUser `json:"user" gorm:"foreignKey:UserID"`
+}
diff --git a/server/model/system/sys_menu_btn.go b/server/model/system/sys_menu_btn.go
new file mode 100644
index 0000000..5f9ecbc
--- /dev/null
+++ b/server/model/system/sys_menu_btn.go
@@ -0,0 +1,10 @@
+package system
+
+import "git.echol.cn/loser/st/server/global"
+
+type SysBaseMenuBtn struct {
+ global.GVA_MODEL
+ Name string `json:"name" gorm:"comment:按钮关键key"`
+ Desc string `json:"desc" gorm:"按钮备注"`
+ SysBaseMenuID uint `json:"sysBaseMenuID" gorm:"comment:菜单ID"`
+}
diff --git a/server/model/system/sys_operation_record.go b/server/model/system/sys_operation_record.go
new file mode 100644
index 0000000..e3b50ca
--- /dev/null
+++ b/server/model/system/sys_operation_record.go
@@ -0,0 +1,24 @@
+// 自动生成模板SysOperationRecord
+package system
+
+import (
+ "time"
+
+ "git.echol.cn/loser/st/server/global"
+)
+
+// 如果含有time.Time 请自行import time包
+type SysOperationRecord struct {
+ global.GVA_MODEL
+ Ip string `json:"ip" form:"ip" gorm:"column:ip;comment:请求ip"` // 请求ip
+ Method string `json:"method" form:"method" gorm:"column:method;comment:请求方法"` // 请求方法
+ Path string `json:"path" form:"path" gorm:"column:path;comment:请求路径"` // 请求路径
+ Status int `json:"status" form:"status" gorm:"column:status;comment:请求状态"` // 请求状态
+ Latency time.Duration `json:"latency" form:"latency" gorm:"column:latency;comment:延迟" swaggertype:"string"` // 延迟
+ Agent string `json:"agent" form:"agent" gorm:"type:text;column:agent;comment:代理"` // 代理
+ ErrorMessage string `json:"error_message" form:"error_message" gorm:"column:error_message;comment:错误信息"` // 错误信息
+ Body string `json:"body" form:"body" gorm:"type:text;column:body;comment:请求Body"` // 请求Body
+ Resp string `json:"resp" form:"resp" gorm:"type:text;column:resp;comment:响应Body"` // 响应Body
+ UserID int `json:"user_id" form:"user_id" gorm:"column:user_id;comment:用户id"` // 用户id
+ User SysUser `json:"user"`
+}
diff --git a/server/model/system/sys_params.go b/server/model/system/sys_params.go
new file mode 100644
index 0000000..7e9511d
--- /dev/null
+++ b/server/model/system/sys_params.go
@@ -0,0 +1,20 @@
+// 自动生成模板SysParams
+package system
+
+import (
+ "git.echol.cn/loser/st/server/global"
+)
+
+// 参数 结构体 SysParams
+type SysParams struct {
+ global.GVA_MODEL
+ Name string `json:"name" form:"name" gorm:"column:name;comment:参数名称;" binding:"required"` //参数名称
+ Key string `json:"key" form:"key" gorm:"column:key;comment:参数键;" binding:"required"` //参数键
+ Value string `json:"value" form:"value" gorm:"column:value;comment:参数值;" binding:"required"` //参数值
+ Desc string `json:"desc" form:"desc" gorm:"column:desc;comment:参数说明;"` //参数说明
+}
+
+// TableName 参数 SysParams自定义表名 sys_params
+func (SysParams) TableName() string {
+ return "sys_params"
+}
diff --git a/server/model/system/sys_skills.go b/server/model/system/sys_skills.go
new file mode 100644
index 0000000..e7013f6
--- /dev/null
+++ b/server/model/system/sys_skills.go
@@ -0,0 +1,25 @@
+package system
+
+type SkillMeta struct {
+ Name string `json:"name" yaml:"name"`
+ Description string `json:"description" yaml:"description"`
+ AllowedTools string `json:"allowedTools" yaml:"allowed-tools,omitempty"`
+ Context string `json:"context" yaml:"context,omitempty"`
+ Agent string `json:"agent" yaml:"agent,omitempty"`
+}
+
+type SkillDetail struct {
+ Tool string `json:"tool"`
+ Skill string `json:"skill"`
+ Meta SkillMeta `json:"meta"`
+ Markdown string `json:"markdown"`
+ Scripts []string `json:"scripts"`
+ Resources []string `json:"resources"`
+ References []string `json:"references"`
+ Templates []string `json:"templates"`
+}
+
+type SkillTool struct {
+ Key string `json:"key"`
+ Label string `json:"label"`
+}
diff --git a/server/model/system/sys_system.go b/server/model/system/sys_system.go
new file mode 100644
index 0000000..22daad6
--- /dev/null
+++ b/server/model/system/sys_system.go
@@ -0,0 +1,10 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/config"
+)
+
+// 配置文件结构体
+type System struct {
+ Config config.Server `json:"config"`
+}
diff --git a/server/model/system/sys_user.go b/server/model/system/sys_user.go
new file mode 100644
index 0000000..cffdf65
--- /dev/null
+++ b/server/model/system/sys_user.go
@@ -0,0 +1,62 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/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/server/model/system/sys_user_authority.go b/server/model/system/sys_user_authority.go
new file mode 100644
index 0000000..1aa83cb
--- /dev/null
+++ b/server/model/system/sys_user_authority.go
@@ -0,0 +1,11 @@
+package system
+
+// SysUserAuthority 是 sysUser 和 sysAuthority 的连接表
+type SysUserAuthority struct {
+ SysUserId uint `gorm:"column:sys_user_id"`
+ SysAuthorityAuthorityId uint `gorm:"column:sys_authority_authority_id"`
+}
+
+func (s *SysUserAuthority) TableName() string {
+ return "sys_user_authority"
+}
diff --git a/server/model/system/sys_version.go b/server/model/system/sys_version.go
new file mode 100644
index 0000000..ab6d398
--- /dev/null
+++ b/server/model/system/sys_version.go
@@ -0,0 +1,20 @@
+// 自动生成模板SysVersion
+package system
+
+import (
+ "git.echol.cn/loser/st/server/global"
+)
+
+// 版本管理 结构体 SysVersion
+type SysVersion struct {
+ global.GVA_MODEL
+ VersionName *string `json:"versionName" form:"versionName" gorm:"comment:版本名称;column:version_name;size:255;" binding:"required"` //版本名称
+ VersionCode *string `json:"versionCode" form:"versionCode" gorm:"comment:版本号;column:version_code;size:100;" binding:"required"` //版本号
+ Description *string `json:"description" form:"description" gorm:"comment:版本描述;column:description;size:500;"` //版本描述
+ VersionData *string `json:"versionData" form:"versionData" gorm:"comment:版本数据JSON;column:version_data;type:text;"` //版本数据
+}
+
+// TableName 版本管理 SysVersion自定义表名 sys_versions
+func (SysVersion) TableName() string {
+ return "sys_versions"
+}
diff --git a/server/plugin/email/README.MD b/server/plugin/email/README.MD
new file mode 100644
index 0000000..685cdd6
--- /dev/null
+++ b/server/plugin/email/README.MD
@@ -0,0 +1,78 @@
+## 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,
+ global.GVA_CONFIG.Email.IsLoginAuth,
+ ))
+
+ 同样也可以再传入时写死
+
+ PluginInit(PrivateGroup, email.CreateEmailPlug(
+ "a@qq.com",
+ "b@qq.com",
+ "smtp.qq.com",
+ "global.GVA_CONFIG.Email.Secret",
+ "登录密钥",
+ 465,
+ true,
+ 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
+ IsLoginAuth bool // 是否LoginAuth 是否使用LoginAuth认证方式(适用于IBM、微软邮箱服务器等)
+ }
+#### 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/server/plugin/email/api/enter.go b/server/plugin/email/api/enter.go
new file mode 100644
index 0000000..353404d
--- /dev/null
+++ b/server/plugin/email/api/enter.go
@@ -0,0 +1,7 @@
+package api
+
+type ApiGroup struct {
+ EmailApi
+}
+
+var ApiGroupApp = new(ApiGroup)
diff --git a/server/plugin/email/api/sys_email.go b/server/plugin/email/api/sys_email.go
new file mode 100644
index 0000000..2befee6
--- /dev/null
+++ b/server/plugin/email/api/sys_email.go
@@ -0,0 +1,53 @@
+package api
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common/response"
+ email_response "git.echol.cn/loser/st/server/plugin/email/model/response"
+ "git.echol.cn/loser/st/server/plugin/email/service"
+ "github.com/gin-gonic/gin"
+ "go.uber.org/zap"
+)
+
+type EmailApi struct{}
+
+// EmailTest
+// @Tags System
+// @Summary 发送测试邮件
+// @Security ApiKeyAuth
+// @Produce application/json
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"发送成功"}"
+// @Router /email/emailTest [post]
+func (s *EmailApi) EmailTest(c *gin.Context) {
+ err := service.ServiceGroupApp.EmailTest()
+ if err != nil {
+ global.GVA_LOG.Error("发送失败!", zap.Error(err))
+ response.FailWithMessage("发送失败", c)
+ return
+ }
+ response.OkWithMessage("发送成功", c)
+}
+
+// SendEmail
+// @Tags System
+// @Summary 发送邮件
+// @Security ApiKeyAuth
+// @Produce application/json
+// @Param data body email_response.Email true "发送邮件必须的参数"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"发送成功"}"
+// @Router /email/sendEmail [post]
+func (s *EmailApi) SendEmail(c *gin.Context) {
+ var email email_response.Email
+ err := c.ShouldBindJSON(&email)
+ if err != nil {
+ response.FailWithMessage(err.Error(), c)
+ return
+ }
+ err = service.ServiceGroupApp.SendEmail(email.To, email.Subject, email.Body)
+ if err != nil {
+ global.GVA_LOG.Error("发送失败!", zap.Error(err))
+ response.FailWithMessage("发送失败", c)
+ return
+ }
+ response.OkWithMessage("发送成功", c)
+}
diff --git a/server/plugin/email/config/email.go b/server/plugin/email/config/email.go
new file mode 100644
index 0000000..412b5a8
--- /dev/null
+++ b/server/plugin/email/config/email.go
@@ -0,0 +1,12 @@
+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
+ IsLoginAuth bool `mapstructure:"is-loginauth" json:"is-loginauth" yaml:"is-loginauth"` // 是否LoginAuth 是否使用LoginAuth认证
+}
diff --git a/server/plugin/email/global/gloabl.go b/server/plugin/email/global/gloabl.go
new file mode 100644
index 0000000..45ec5ec
--- /dev/null
+++ b/server/plugin/email/global/gloabl.go
@@ -0,0 +1,5 @@
+package global
+
+import "git.echol.cn/loser/st/server/plugin/email/config"
+
+var GlobalConfig = new(config.Email)
diff --git a/server/plugin/email/main.go b/server/plugin/email/main.go
new file mode 100644
index 0000000..4e925e1
--- /dev/null
+++ b/server/plugin/email/main.go
@@ -0,0 +1,29 @@
+package email
+
+import (
+ "git.echol.cn/loser/st/server/plugin/email/global"
+ "git.echol.cn/loser/st/server/plugin/email/router"
+ "github.com/gin-gonic/gin"
+)
+
+type emailPlugin struct{}
+
+func CreateEmailPlug(To, From, Host, Secret, Nickname string, Port int, IsSSL bool, IsLoginAuth 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
+ global.GlobalConfig.IsLoginAuth = IsLoginAuth
+ return &emailPlugin{}
+}
+
+func (*emailPlugin) Register(group *gin.RouterGroup) {
+ router.RouterGroupApp.InitEmailRouter(group)
+}
+
+func (*emailPlugin) RouterPath() string {
+ return "email"
+}
diff --git a/server/plugin/email/model/response/email.go b/server/plugin/email/model/response/email.go
new file mode 100644
index 0000000..ed25475
--- /dev/null
+++ b/server/plugin/email/model/response/email.go
@@ -0,0 +1,7 @@
+package response
+
+type Email struct {
+ To string `json:"to"` // 邮件发送给谁
+ Subject string `json:"subject"` // 邮件标题
+ Body string `json:"body"` // 邮件内容
+}
diff --git a/server/plugin/email/router/enter.go b/server/plugin/email/router/enter.go
new file mode 100644
index 0000000..e081a54
--- /dev/null
+++ b/server/plugin/email/router/enter.go
@@ -0,0 +1,7 @@
+package router
+
+type RouterGroup struct {
+ EmailRouter
+}
+
+var RouterGroupApp = new(RouterGroup)
diff --git a/server/plugin/email/router/sys_email.go b/server/plugin/email/router/sys_email.go
new file mode 100644
index 0000000..3d1abeb
--- /dev/null
+++ b/server/plugin/email/router/sys_email.go
@@ -0,0 +1,19 @@
+package router
+
+import (
+ "git.echol.cn/loser/st/server/middleware"
+ "git.echol.cn/loser/st/server/plugin/email/api"
+ "github.com/gin-gonic/gin"
+)
+
+type EmailRouter struct{}
+
+func (s *EmailRouter) InitEmailRouter(Router *gin.RouterGroup) {
+ emailRouter := Router.Use(middleware.OperationRecord())
+ EmailApi := api.ApiGroupApp.EmailApi.EmailTest
+ SendEmail := api.ApiGroupApp.EmailApi.SendEmail
+ {
+ emailRouter.POST("emailTest", EmailApi) // 发送测试邮件
+ emailRouter.POST("sendEmail", SendEmail) // 发送邮件
+ }
+}
diff --git a/server/plugin/email/service/enter.go b/server/plugin/email/service/enter.go
new file mode 100644
index 0000000..e96e267
--- /dev/null
+++ b/server/plugin/email/service/enter.go
@@ -0,0 +1,7 @@
+package service
+
+type ServiceGroup struct {
+ EmailService
+}
+
+var ServiceGroupApp = new(ServiceGroup)
diff --git a/server/plugin/email/service/sys_email.go b/server/plugin/email/service/sys_email.go
new file mode 100644
index 0000000..059db38
--- /dev/null
+++ b/server/plugin/email/service/sys_email.go
@@ -0,0 +1,32 @@
+package service
+
+import (
+ "git.echol.cn/loser/st/server/plugin/email/utils"
+)
+
+type EmailService struct{}
+
+//@author: [maplepie](https://github.com/maplepie)
+//@function: EmailTest
+//@description: 发送邮件测试
+//@return: err error
+
+func (e *EmailService) EmailTest() (err error) {
+ subject := "test"
+ body := "test"
+ err = utils.EmailTest(subject, body)
+ return err
+}
+
+//@author: [maplepie](https://github.com/maplepie)
+//@function: EmailTest
+//@description: 发送邮件测试
+//@return: err error
+//@params to string 收件人
+//@params subject string 标题(主题)
+//@params body string 邮件内容
+
+func (e *EmailService) SendEmail(to, subject, body string) (err error) {
+ err = utils.Email(to, subject, body)
+ return err
+}
diff --git a/server/plugin/email/utils/email.go b/server/plugin/email/utils/email.go
new file mode 100644
index 0000000..288e56c
--- /dev/null
+++ b/server/plugin/email/utils/email.go
@@ -0,0 +1,122 @@
+package utils
+
+import (
+ "crypto/tls"
+ "fmt"
+ "net/smtp"
+ "strings"
+
+ "git.echol.cn/loser/st/server/plugin/email/global"
+
+ "github.com/jordan-wright/email"
+)
+
+//@author: [maplepie](https://github.com/maplepie)
+//@function: Email
+//@description: Email发送方法
+//@param: subject string, body string
+//@return: error
+
+func Email(To, subject string, body string) error {
+ to := strings.Split(To, ",")
+ return send(to, subject, body)
+}
+
+//@author: [SliverHorn](https://github.com/SliverHorn)
+//@function: ErrorToEmail
+//@description: 给email中间件错误发送邮件到指定邮箱
+//@param: subject string, body string
+//@return: error
+
+func ErrorToEmail(subject string, body string) error {
+ to := strings.Split(global.GlobalConfig.To, ",")
+ if to[len(to)-1] == "" { // 判断切片的最后一个元素是否为空,为空则移除
+ to = to[:len(to)-1]
+ }
+ return send(to, subject, body)
+}
+
+//@author: [maplepie](https://github.com/maplepie)
+//@function: EmailTest
+//@description: Email测试方法
+//@param: subject string, body string
+//@return: error
+
+func EmailTest(subject string, body string) error {
+ to := []string{global.GlobalConfig.To}
+ return send(to, subject, body)
+}
+
+//@author: [maplepie](https://github.com/maplepie)
+//@function: send
+//@description: Email发送方法
+//@param: subject string, body string
+//@return: error
+
+func send(to []string, subject string, body string) error {
+ from := global.GlobalConfig.From
+ nickname := global.GlobalConfig.Nickname
+ secret := global.GlobalConfig.Secret
+ host := global.GlobalConfig.Host
+ port := global.GlobalConfig.Port
+ isSSL := global.GlobalConfig.IsSSL
+ isLoginAuth := global.GlobalConfig.IsLoginAuth
+
+ var auth smtp.Auth
+ if isLoginAuth {
+ auth = LoginAuth(from, secret)
+ } else {
+ 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
+}
+
+// LoginAuth 用于IBM、微软邮箱服务器的LOGIN认证方式
+type loginAuth struct {
+ username, password string
+}
+
+func LoginAuth(username, password string) smtp.Auth {
+ return &loginAuth{username, password}
+}
+
+func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
+ return "LOGIN", []byte{}, nil
+}
+
+func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
+ if more {
+ switch string(fromServer) {
+ case "Username:":
+ return []byte(a.username), nil
+ case "Password:":
+ return []byte(a.password), nil
+ default:
+ // 邮箱服务器可能发送的其他提示信息
+ prompt := strings.ToLower(string(fromServer))
+ if strings.Contains(prompt, "username") || strings.Contains(prompt, "user") {
+ return []byte(a.username), nil
+ }
+ if strings.Contains(prompt, "password") || strings.Contains(prompt, "pass") {
+ return []byte(a.password), nil
+ }
+ }
+ }
+ return nil, nil
+}
diff --git a/server/plugin/plugin-tool/utils/check.go b/server/plugin/plugin-tool/utils/check.go
new file mode 100644
index 0000000..36ce274
--- /dev/null
+++ b/server/plugin/plugin-tool/utils/check.go
@@ -0,0 +1,138 @@
+package utils
+
+import (
+ "path/filepath"
+ "runtime"
+ "strings"
+ "sync"
+
+ "github.com/pkg/errors"
+ "go.uber.org/zap"
+ "gorm.io/gorm"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system"
+)
+
+var (
+ ApiMap = make(map[string][]system.SysApi)
+ MenuMap = make(map[string][]system.SysBaseMenu)
+ DictMap = make(map[string][]system.SysDictionary)
+ rw sync.Mutex
+)
+
+func getPluginName() string {
+ _, file, _, ok := runtime.Caller(2)
+ pluginName := ""
+ if ok {
+ file = filepath.ToSlash(file)
+ const key = "server/plugin/"
+ if idx := strings.Index(file, key); idx != -1 {
+ remain := file[idx+len(key):]
+ parts := strings.Split(remain, "/")
+ if len(parts) > 0 {
+ pluginName = parts[0]
+ }
+ }
+ }
+ return pluginName
+}
+
+func RegisterApis(apis ...system.SysApi) {
+ name := getPluginName()
+ if name != "" {
+ rw.Lock()
+ ApiMap[name] = apis
+ rw.Unlock()
+ }
+
+ err := global.GVA_DB.Transaction(func(tx *gorm.DB) error {
+ for _, api := range apis {
+ err := tx.Model(system.SysApi{}).Where("path = ? AND method = ? AND api_group = ? ", api.Path, api.Method, api.ApiGroup).FirstOrCreate(&api).Error
+ if err != nil {
+ zap.L().Error("注册API失败", zap.Error(err), zap.String("api", api.Path), zap.String("method", api.Method), zap.String("apiGroup", api.ApiGroup))
+ return err
+ }
+ }
+ return nil
+ })
+ if err != nil {
+ zap.L().Error("注册API失败", zap.Error(err))
+ }
+}
+
+func RegisterMenus(menus ...system.SysBaseMenu) {
+ name := getPluginName()
+ if name != "" {
+ rw.Lock()
+ MenuMap[name] = menus
+ rw.Unlock()
+ }
+
+ parentMenu := menus[0]
+ otherMenus := menus[1:]
+ err := global.GVA_DB.Transaction(func(tx *gorm.DB) error {
+ err := tx.Model(system.SysBaseMenu{}).Where("name = ? ", parentMenu.Name).FirstOrCreate(&parentMenu).Error
+ if err != nil {
+ zap.L().Error("注册菜单失败", zap.Error(err))
+ return errors.Wrap(err, "注册菜单失败")
+ }
+ pid := parentMenu.ID
+ for i := range otherMenus {
+ otherMenus[i].ParentId = pid
+ err = tx.Model(system.SysBaseMenu{}).Where("name = ? ", otherMenus[i].Name).FirstOrCreate(&otherMenus[i]).Error
+ if err != nil {
+ zap.L().Error("注册菜单失败", zap.Error(err))
+ return errors.Wrap(err, "注册菜单失败")
+ }
+ }
+ return nil
+ })
+ if err != nil {
+ zap.L().Error("注册菜单失败", zap.Error(err))
+ }
+
+}
+
+func RegisterDictionaries(dictionaries ...system.SysDictionary) {
+ name := getPluginName()
+ if name != "" {
+ rw.Lock()
+ DictMap[name] = dictionaries
+ rw.Unlock()
+ }
+
+ err := global.GVA_DB.Transaction(func(tx *gorm.DB) error {
+ for _, dict := range dictionaries {
+ details := dict.SysDictionaryDetails
+ dict.SysDictionaryDetails = nil
+ err := tx.Model(system.SysDictionary{}).Where("type = ?", dict.Type).FirstOrCreate(&dict).Error
+ if err != nil {
+ zap.L().Error("注册字典失败", zap.Error(err), zap.String("type", dict.Type))
+ return err
+ }
+ for _, detail := range details {
+ detail.SysDictionaryID = int(dict.ID)
+ err = tx.Model(system.SysDictionaryDetail{}).Where("sys_dictionary_id = ? AND value = ?", dict.ID, detail.Value).FirstOrCreate(&detail).Error
+ if err != nil {
+ zap.L().Error("注册字典详情失败", zap.Error(err), zap.String("value", detail.Value))
+ return err
+ }
+ }
+ }
+ return nil
+ })
+ if err != nil {
+ zap.L().Error("注册字典失败", zap.Error(err))
+ }
+}
+
+func Pointer[T any](in T) *T {
+ return &in
+}
+
+func GetPluginData(pluginName string) ([]system.SysApi, []system.SysBaseMenu, []system.SysDictionary) {
+ rw.Lock()
+ defer rw.Unlock()
+ return ApiMap[pluginName], MenuMap[pluginName], DictMap[pluginName]
+}
diff --git a/server/plugin/register.go b/server/plugin/register.go
new file mode 100644
index 0000000..b0736c3
--- /dev/null
+++ b/server/plugin/register.go
@@ -0,0 +1 @@
+package plugin
diff --git a/server/resource/function/api.go.tpl b/server/resource/function/api.go.tpl
new file mode 100644
index 0000000..35a1cd5
--- /dev/null
+++ b/server/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/server/resource/function/api.js.tpl b/server/resource/function/api.js.tpl
new file mode 100644
index 0000000..a07b102
--- /dev/null
+++ b/server/resource/function/api.js.tpl
@@ -0,0 +1,32 @@
+{{if .IsPlugin}}
+// {{.FuncName}} {{.FuncDesc}}
+// @Tags {{.StructName}}
+// @Summary {{.FuncDesc}}
+// @Accept application/json
+// @Produce application/json
+// @Success 200 {object} response.Response{data=object,msg=string} "获取成功"
+// @Router /{{.Abbreviation}}/{{.Router}} [{{.Method}}]
+export const {{.Router}} = () => {
+ return service({
+ url: '/{{.Abbreviation}}/{{.Router}}',
+ method: '{{.Method}}'
+ })
+}
+
+{{- else -}}
+
+// {{.FuncName}} {{.FuncDesc}}
+// @Tags {{.StructName}}
+// @Summary {{.FuncDesc}}
+// @Accept application/json
+// @Produce application/json
+// @Success 200 {object} response.Response{data=object,msg=string} "成功"
+// @Router /{{.Abbreviation}}/{{.Router}} [{{.Method}}]
+export const {{.Router}} = () => {
+ return service({
+ url: '/{{.Abbreviation}}/{{.Router}}',
+ method: '{{.Method}}'
+ })
+}
+
+{{- end -}}
diff --git a/server/resource/function/server.go.tpl b/server/resource/function/server.go.tpl
new file mode 100644
index 0000000..7327604
--- /dev/null
+++ b/server/resource/function/server.go.tpl
@@ -0,0 +1,25 @@
+{{- $db := "" }}
+{{- if eq .BusinessDB "" }}
+ {{- $db = "global.GVA_DB" }}
+{{- else}}
+ {{- $db = printf "global.MustGetGlobalDBByDBName(\"%s\")" .BusinessDB }}
+{{- end}}
+{{if .IsPlugin}}
+
+// {{.FuncName}} {{.FuncDesc}}
+// Author [yourname](https://github.com/yourname)
+func (s *{{.Abbreviation}}) {{.FuncName}}(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/server/resource/mcp/tools.tpl b/server/resource/mcp/tools.tpl
new file mode 100644
index 0000000..49bfa20
--- /dev/null
+++ b/server/resource/mcp/tools.tpl
@@ -0,0 +1,56 @@
+package mcpTool
+
+import (
+ "context"
+ "github.com/mark3labs/mcp-go/mcp"
+)
+
+func init() {
+ RegisterTool(&{{.Name | title}}{})
+}
+
+type {{.Name | title}} struct {
+}
+
+// {{.Description}}
+func (t *{{.Name | title}}) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ // TODO: 实现工具逻辑
+ // 参数示例:
+ // {{- range .Params}}
+ // {{.Name}} := request.GetArguments()["{{.Name}}"]
+ // {{- end}}
+ return &mcp.CallToolResult{
+ Content: []mcp.Content{
+ {{- range .Response}}
+ mcp.{{.Type | title}}Content{
+ Type: "{{.Type}}",
+ // TODO: 填充{{.Type}}内容
+ },
+ {{- end}}
+ },
+ }, nil
+}
+
+func (t *{{.Name | title}}) New() mcp.Tool {
+ return mcp.NewTool("{{.Name}}",
+ mcp.WithDescription("{{.Description}}"),
+ {{- range .Params}}
+ mcp.With{{.Type | title}}("{{.Name}}",
+ {{- if .Required}}mcp.Required(),{{end}}
+ mcp.Description("{{.Description}}"),
+ {{- if .Default}}
+ {{- if eq .Type "string"}}
+ mcp.DefaultString("{{.Default}}"),
+ {{- else if eq .Type "number"}}
+ mcp.DefaultNumber({{.Default}}),
+ {{- else if eq .Type "boolean"}}
+ mcp.DefaultBoolean({{if or (eq .Default "true") (eq .Default "True")}}true{{else}}false{{end}}),
+ {{- else if eq .Type "array"}}
+ // 注意:数组默认值需要在后端代码中预处理为正确的格式
+ // mcp.DefaultArray({{.Default}}),
+ {{- end}}
+ {{- end}}
+ ),
+ {{- end}}
+ )
+}
diff --git a/server/resource/package/readme.txt.tpl b/server/resource/package/readme.txt.tpl
new file mode 100644
index 0000000..a737810
--- /dev/null
+++ b/server/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/st/server"
+
+希望大家给个star多多鼓励
diff --git a/server/resource/package/server/api/api.go.tpl b/server/resource/package/server/api/api.go.tpl
new file mode 100644
index 0000000..528487f
--- /dev/null
+++ b/server/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/server/resource/package/server/api/enter.go.tpl b/server/resource/package/server/api/enter.go.tpl
new file mode 100644
index 0000000..778b314
--- /dev/null
+++ b/server/resource/package/server/api/enter.go.tpl
@@ -0,0 +1,4 @@
+package {{ .Package }}
+
+type ApiGroup struct {
+}
\ No newline at end of file
diff --git a/server/resource/package/server/model/model.go.tpl b/server/resource/package/server/model/model.go.tpl
new file mode 100644
index 0000000..e1603ed
--- /dev/null
+++ b/server/resource/package/server/model/model.go.tpl
@@ -0,0 +1,75 @@
+{{- if .IsAdd}}
+// 在结构体中新增如下字段
+{{- range .Fields}}
+ {{ GenerateField . }}
+{{- 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}}
+ {{ GenerateField . }}
+{{- 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/server/resource/package/server/model/request/request.go.tpl b/server/resource/package/server/model/request/request.go.tpl
new file mode 100644
index 0000000..f8749f3
--- /dev/null
+++ b/server/resource/package/server/model/request/request.go.tpl
@@ -0,0 +1,39 @@
+{{- if .IsAdd}}
+// 在结构体中新增如下字段
+{{- range .Fields}}
+ {{- if ne .FieldSearchType ""}}
+ {{ GenerateSearchField . }}
+ {{- 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 }}
+ CreatedAtRange []time.Time `json:"createdAtRange" form:"createdAtRange[]"`
+{{- end }}
+{{- range .Fields}}
+ {{- if ne .FieldSearchType ""}}
+ {{ GenerateSearchField . }}
+ {{- end}}
+{{- end }}
+ request.PageInfo
+ {{- if .NeedSort}}
+ Sort string `json:"sort" form:"sort"`
+ Order string `json:"order" form:"order"`
+ {{- end}}
+{{- end}}
+}
+{{- end }}
diff --git a/server/resource/package/server/router/enter.go.tpl b/server/resource/package/server/router/enter.go.tpl
new file mode 100644
index 0000000..178aecf
--- /dev/null
+++ b/server/resource/package/server/router/enter.go.tpl
@@ -0,0 +1,4 @@
+package {{ .Package }}
+
+type RouterGroup struct {
+}
\ No newline at end of file
diff --git a/server/resource/package/server/router/router.go.tpl b/server/resource/package/server/router/router.go.tpl
new file mode 100644
index 0000000..cac47ab
--- /dev/null
+++ b/server/resource/package/server/router/router.go.tpl
@@ -0,0 +1,42 @@
+package {{.Package}}
+
+import (
+ {{if .OnlyTemplate}}// {{ end}}"{{.Module}}/middleware"
+ "github.com/gin-gonic/gin"
+)
+
+type {{.StructName}}Router struct {}
+
+// Init{{.StructName}}Router 初始化 {{.Description}} 路由信息
+func (s *{{.StructName}}Router) Init{{.StructName}}Router(Router *gin.RouterGroup,PublicRouter *gin.RouterGroup) {
+ {{- if not .OnlyTemplate}}
+ {{.Abbreviation}}Router := Router.Group("{{.Abbreviation}}").Use(middleware.OperationRecord())
+ {{.Abbreviation}}RouterWithoutRecord := Router.Group("{{.Abbreviation}}")
+ {{- else }}
+ // {{.Abbreviation}}Router := Router.Group("{{.Abbreviation}}").Use(middleware.OperationRecord())
+ // {{.Abbreviation}}RouterWithoutRecord := Router.Group("{{.Abbreviation}}")
+ {{- end}}
+ {{.Abbreviation}}RouterWithoutAuth := PublicRouter.Group("{{.Abbreviation}}")
+ {{- if not .OnlyTemplate}}
+ {
+ {{.Abbreviation}}Router.POST("create{{.StructName}}", {{.Abbreviation}}Api.Create{{.StructName}}) // 新建{{.Description}}
+ {{.Abbreviation}}Router.DELETE("delete{{.StructName}}", {{.Abbreviation}}Api.Delete{{.StructName}}) // 删除{{.Description}}
+ {{.Abbreviation}}Router.DELETE("delete{{.StructName}}ByIds", {{.Abbreviation}}Api.Delete{{.StructName}}ByIds) // 批量删除{{.Description}}
+ {{.Abbreviation}}Router.PUT("update{{.StructName}}", {{.Abbreviation}}Api.Update{{.StructName}}) // 更新{{.Description}}
+ }
+ {
+ {{.Abbreviation}}RouterWithoutRecord.GET("find{{.StructName}}", {{.Abbreviation}}Api.Find{{.StructName}}) // 根据ID获取{{.Description}}
+ {{.Abbreviation}}RouterWithoutRecord.GET("get{{.StructName}}List", {{.Abbreviation}}Api.Get{{.StructName}}List) // 获取{{.Description}}列表
+ }
+ {
+ {{- if .HasDataSource}}
+ {{.Abbreviation}}RouterWithoutAuth.GET("get{{.StructName}}DataSource", {{.Abbreviation}}Api.Get{{.StructName}}DataSource) // 获取{{.Description}}数据源
+ {{- end}}
+ {{.Abbreviation}}RouterWithoutAuth.GET("get{{.StructName}}Public", {{.Abbreviation}}Api.Get{{.StructName}}Public) // {{.Description}}开放接口
+ }
+ {{- else}}
+ {
+ {{.Abbreviation}}RouterWithoutAuth.GET("get{{.StructName}}Public", {{.Abbreviation}}Api.Get{{.StructName}}Public) // {{.Description}}开放接口
+ }
+ {{ end }}
+}
diff --git a/server/resource/package/server/service/enter.go.tpl b/server/resource/package/server/service/enter.go.tpl
new file mode 100644
index 0000000..adf1db0
--- /dev/null
+++ b/server/resource/package/server/service/enter.go.tpl
@@ -0,0 +1,4 @@
+package {{ .Package }}
+
+type ServiceGroup struct {
+}
\ No newline at end of file
diff --git a/server/resource/package/server/service/service.go.tpl b/server/resource/package/server/service/service.go.tpl
new file mode 100644
index 0000000..acd57a3
--- /dev/null
+++ b/server/resource/package/server/service/service.go.tpl
@@ -0,0 +1,213 @@
+{{- $db := "" }}
+{{- if eq .BusinessDB "" }}
+ {{- $db = "global.GVA_DB" }}
+{{- else}}
+ {{- $db = printf "global.MustGetGlobalDBByDBName(\"%s\")" .BusinessDB }}
+{{- end}}
+
+{{- if .IsAdd}}
+
+// Get{{.StructName}}InfoList 新增搜索语句
+ {{ GenerateSearchConditions .Fields }}
+// 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 len(info.CreatedAtRange) == 2 {
+ db = db.Where("created_at BETWEEN ? AND ?", info.CreatedAtRange[0], info.CreatedAtRange[1])
+ }
+{{- end }}
+ {{ GenerateSearchConditions .Fields }}
+ err = db.Count(&total).Error
+ if err!=nil {
+ return
+ }
+ {{- if .NeedSort}}
+ var OrderStr string
+ orderMap := make(map[string]bool)
+ {{- if .GvaModel }}
+ orderMap["id"] = true
+ orderMap["created_at"] = true
+ {{- end }}
+ {{- 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 = "global.GVA_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/server/resource/package/web/api/api.js.tpl b/server/resource/package/web/api/api.js.tpl
new file mode 100644
index 0000000..a41ef6f
--- /dev/null
+++ b/server/resource/package/web/api/api.js.tpl
@@ -0,0 +1,130 @@
+import service from '@/utils/request'
+
+{{- if not .OnlyTemplate}}
+// @Tags {{.StructName}}
+// @Summary 创建{{.Description}}
+// @Security ApiKeyAuth
+// @Accept application/json
+// @Produce application/json
+// @Param data body model.{{.StructName}} true "创建{{.Description}}"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}"
+// @Router /{{.Abbreviation}}/create{{.StructName}} [post]
+export const create{{.StructName}} = (data) => {
+ return service({
+ url: '/{{.Abbreviation}}/create{{.StructName}}',
+ method: 'post',
+ data
+ })
+}
+
+// @Tags {{.StructName}}
+// @Summary 删除{{.Description}}
+// @Security ApiKeyAuth
+// @Accept application/json
+// @Produce application/json
+// @Param data body model.{{.StructName}} true "删除{{.Description}}"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}"
+// @Router /{{.Abbreviation}}/delete{{.StructName}} [delete]
+export const delete{{.StructName}} = (params) => {
+ return service({
+ url: '/{{.Abbreviation}}/delete{{.StructName}}',
+ method: 'delete',
+ params
+ })
+}
+
+// @Tags {{.StructName}}
+// @Summary 批量删除{{.Description}}
+// @Security ApiKeyAuth
+// @Accept application/json
+// @Produce application/json
+// @Param data body request.IdsReq true "批量删除{{.Description}}"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}"
+// @Router /{{.Abbreviation}}/delete{{.StructName}} [delete]
+export const delete{{.StructName}}ByIds = (params) => {
+ return service({
+ url: '/{{.Abbreviation}}/delete{{.StructName}}ByIds',
+ method: 'delete',
+ params
+ })
+}
+
+// @Tags {{.StructName}}
+// @Summary 更新{{.Description}}
+// @Security ApiKeyAuth
+// @Accept application/json
+// @Produce application/json
+// @Param data body model.{{.StructName}} true "更新{{.Description}}"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}"
+// @Router /{{.Abbreviation}}/update{{.StructName}} [put]
+export const update{{.StructName}} = (data) => {
+ return service({
+ url: '/{{.Abbreviation}}/update{{.StructName}}',
+ method: 'put',
+ data
+ })
+}
+
+// @Tags {{.StructName}}
+// @Summary 用id查询{{.Description}}
+// @Security ApiKeyAuth
+// @Accept application/json
+// @Produce application/json
+// @Param data query model.{{.StructName}} true "用id查询{{.Description}}"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}"
+// @Router /{{.Abbreviation}}/find{{.StructName}} [get]
+export const find{{.StructName}} = (params) => {
+ return service({
+ url: '/{{.Abbreviation}}/find{{.StructName}}',
+ method: 'get',
+ params
+ })
+}
+
+// @Tags {{.StructName}}
+// @Summary 分页获取{{.Description}}列表
+// @Security ApiKeyAuth
+// @Accept application/json
+// @Produce application/json
+// @Param data query request.PageInfo true "分页获取{{.Description}}列表"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
+// @Router /{{.Abbreviation}}/get{{.StructName}}List [get]
+export const get{{.StructName}}List = (params) => {
+ return service({
+ url: '/{{.Abbreviation}}/get{{.StructName}}List',
+ method: 'get',
+ params
+ })
+}
+
+{{- if .HasDataSource}}
+// @Tags {{.StructName}}
+// @Summary 获取数据源
+// @Security ApiKeyAuth
+// @Accept application/json
+// @Produce application/json
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}"
+// @Router /{{.Abbreviation}}/find{{.StructName}}DataSource [get]
+export const get{{.StructName}}DataSource = () => {
+ return service({
+ url: '/{{.Abbreviation}}/get{{.StructName}}DataSource',
+ method: 'get',
+ })
+}
+{{- end}}
+
+{{- end}}
+
+// @Tags {{.StructName}}
+// @Summary 不需要鉴权的{{.Description}}接口
+// @Accept application/json
+// @Produce application/json
+// @Param data query {{.Package}}Req.{{.StructName}}Search true "分页获取{{.Description}}列表"
+// @Success 200 {object} response.Response{data=object,msg=string} "获取成功"
+// @Router /{{.Abbreviation}}/get{{.StructName}}Public [get]
+export const get{{.StructName}}Public = () => {
+ return service({
+ url: '/{{.Abbreviation}}/get{{.StructName}}Public',
+ method: 'get',
+ })
+}
diff --git a/server/resource/package/web/view/form.vue.tpl b/server/resource/package/web/view/form.vue.tpl
new file mode 100644
index 0000000..28c1f02
--- /dev/null
+++ b/server/resource/package/web/view/form.vue.tpl
@@ -0,0 +1,274 @@
+{{- if .IsAdd }}
+// 新增表单中增加如下代码
+{{- range .Fields}}
+ {{- if .Form}}
+ {{ GenerateFormItem . }}
+ {{- 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}}
+ {{ GenerateDefaultFormValue . }}
+ {{- 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 }}
+
+
+
+
+ {{- if .IsTree }}
+
+
+
+ {{- end }}
+ {{- range .Fields}}
+ {{- if .Form }}
+ {{ GenerateFormItem . }}
+ {{- end }}
+ {{- end }}
+
+ 保存
+ 返回
+
+
+
+
+
+
+
+
+
+{{- else }}
+
+form
+
+
+
+{{- end }}
+{{- end }}
diff --git a/server/resource/package/web/view/table.vue.tpl b/server/resource/package/web/view/table.vue.tpl
new file mode 100644
index 0000000..b2662da
--- /dev/null
+++ b/server/resource/package/web/view/table.vue.tpl
@@ -0,0 +1,694 @@
+{{- $global := . }}
+{{- $templateID := printf "%s_%s" .Package .StructName }}
+{{- if .IsAdd }}
+
+// 请在搜索条件中增加如下代码
+{{- range .Fields}}
+ {{- if .FieldSearchType}}
+{{ GenerateSearchFormItem .}}
+ {{ end }}
+{{ end }}
+
+
+// 表格增加如下列代码
+
+{{- range .Fields}}
+ {{- if .Table}}
+ {{ GenerateTableColumn . }}
+ {{- end }}
+{{- end }}
+
+// 新增表单中增加如下代码
+{{- range .Fields}}
+ {{- if .Form}}
+ {{ GenerateFormItem . }}
+ {{- end }}
+{{- end }}
+
+// 查看抽屉中增加如下代码
+
+{{- range .Fields}}
+ {{- if .Desc }}
+ {{ GenerateDescriptionItem . }}
+ {{- 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}}
+ {{ GenerateDefaultFormValue . }}
+ {{- 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}}
+
+
+ {{- if not .IsTree }}
+
+
+ {{- if .GvaModel }}
+
+
+
+ 创建日期
+
+
+
+
+
+
+
+
+ {{ end -}}
+ {{- range .Fields}} {{- if .FieldSearchType}} {{- if not .FieldSearchHide }}
+ {{ GenerateSearchFormItem .}}
+ {{ end }}{{ end }}{{ end }}
+
+
+
+ {{- range .Fields}} {{- if .FieldSearchType}} {{- if .FieldSearchHide }}
+ {{ GenerateSearchFormItem .}}
+ {{ end }}{{ end }}{{ end }}
+
+
+
+ 查询
+ 重置
+ 展开
+ 收起
+
+
+
+ {{- end }}
+
+
+ 新增
+ 删除
+ {{ if .HasExcel -}}
+
+
+
+ {{- end }}
+
+
+
+ {{ if .GvaModel }}
+
+ {{ "{{ formatDate(scope.row.CreatedAt) }}" }}
+
+ {{ end }}
+ {{- range .Fields}}
+ {{- if .Table}}
+ {{ GenerateTableColumn . }}
+ {{- end }}
+ {{- end }}
+
+
+ {{- if .IsTree }}
+ 新增子节点
+ {{- end }}
+ 查看
+ 编辑
+ 删除
+
+
+
+
+
+
+
+
+
{{"{{"}}type==='create'?'新增':'编辑'{{"}}"}}
+
+ 确 定
+ 取 消
+
+
+
+
+
+ {{- if .IsTree }}
+
+
+
+ {{- end }}
+ {{- range .Fields}}
+ {{- if .Form}}
+ {{ GenerateFormItem . }}
+ {{- end }}
+ {{- end }}
+
+
+
+
+
+ {{- if .IsTree }}
+
+
+
+ {{- end }}
+ {{- range .Fields}}
+ {{- if .Desc }}
+ {{ GenerateDescriptionItem . }}
+ {{- end }}
+ {{- end }}
+
+
+
+
+
+
+
+
+
+{{- else}}
+
+form
+
+
+
+{{- end }}
+
+{{- end }}
diff --git a/server/resource/plugin/server/api/api.go.tpl b/server/resource/plugin/server/api/api.go.tpl
new file mode 100644
index 0000000..e69ae82
--- /dev/null
+++ b/server/resource/plugin/server/api/api.go.tpl
@@ -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/server/resource/plugin/server/api/enter.go.tpl b/server/resource/plugin/server/api/enter.go.tpl
new file mode 100644
index 0000000..989fb35
--- /dev/null
+++ b/server/resource/plugin/server/api/enter.go.tpl
@@ -0,0 +1,6 @@
+package api
+
+var Api = new(api)
+
+type api struct {
+}
diff --git a/server/resource/plugin/server/config/config.go.tpl b/server/resource/plugin/server/config/config.go.tpl
new file mode 100644
index 0000000..809bc99
--- /dev/null
+++ b/server/resource/plugin/server/config/config.go.tpl
@@ -0,0 +1,4 @@
+package config
+
+type Config struct {
+}
diff --git a/server/resource/plugin/server/gen/gen.go.tpl b/server/resource/plugin/server/gen/gen.go.tpl
new file mode 100644
index 0000000..5639d4a
--- /dev/null
+++ b/server/resource/plugin/server/gen/gen.go.tpl
@@ -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/server/resource/plugin/server/initialize/api.go.tpl b/server/resource/plugin/server/initialize/api.go.tpl
new file mode 100644
index 0000000..dfbea23
--- /dev/null
+++ b/server/resource/plugin/server/initialize/api.go.tpl
@@ -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/server/resource/plugin/server/initialize/dictionary.go.tpl b/server/resource/plugin/server/initialize/dictionary.go.tpl
new file mode 100644
index 0000000..e61b42c
--- /dev/null
+++ b/server/resource/plugin/server/initialize/dictionary.go.tpl
@@ -0,0 +1,12 @@
+package initialize
+
+import (
+ "context"
+ model "{{.Module}}/model/system"
+ "{{.Module}}/plugin/plugin-tool/utils"
+)
+
+func Dictionary(ctx context.Context) {
+ entities := []model.SysDictionary{}
+ utils.RegisterDictionaries(entities...)
+}
diff --git a/server/resource/plugin/server/initialize/gorm.go.tpl b/server/resource/plugin/server/initialize/gorm.go.tpl
new file mode 100644
index 0000000..52c8183
--- /dev/null
+++ b/server/resource/plugin/server/initialize/gorm.go.tpl
@@ -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/server/resource/plugin/server/initialize/menu.go.tpl b/server/resource/plugin/server/initialize/menu.go.tpl
new file mode 100644
index 0000000..8774f35
--- /dev/null
+++ b/server/resource/plugin/server/initialize/menu.go.tpl
@@ -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/server/resource/plugin/server/initialize/router.go.tpl b/server/resource/plugin/server/initialize/router.go.tpl
new file mode 100644
index 0000000..fbf03a3
--- /dev/null
+++ b/server/resource/plugin/server/initialize/router.go.tpl
@@ -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/server/resource/plugin/server/initialize/viper.go.tpl b/server/resource/plugin/server/initialize/viper.go.tpl
new file mode 100644
index 0000000..e759ad6
--- /dev/null
+++ b/server/resource/plugin/server/initialize/viper.go.tpl
@@ -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/server/resource/plugin/server/model/model.go.tpl b/server/resource/plugin/server/model/model.go.tpl
new file mode 100644
index 0000000..283841c
--- /dev/null
+++ b/server/resource/plugin/server/model/model.go.tpl
@@ -0,0 +1,76 @@
+{{- if .IsAdd}}
+// 在结构体中新增如下字段
+{{- range .Fields}}
+ {{ GenerateField . }}
+{{- 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}}
+ {{ GenerateField . }}
+{{- 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/server/resource/plugin/server/model/request/request.go.tpl b/server/resource/plugin/server/model/request/request.go.tpl
new file mode 100644
index 0000000..60cf677
--- /dev/null
+++ b/server/resource/plugin/server/model/request/request.go.tpl
@@ -0,0 +1,38 @@
+{{- if .IsAdd}}
+// 在结构体中新增如下字段
+{{- range .Fields}}
+ {{- if ne .FieldSearchType ""}}
+ {{ GenerateSearchField . }}
+ {{- 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 }}
+ CreatedAtRange []time.Time `json:"createdAtRange" form:"createdAtRange[]"`
+{{- end }}
+{{- range .Fields}}
+ {{- if ne .FieldSearchType ""}}
+ {{ GenerateSearchField . }}
+ {{- end}}
+{{- end }}
+ request.PageInfo
+ {{- if .NeedSort}}
+ Sort string `json:"sort" form:"sort"`
+ Order string `json:"order" form:"order"`
+ {{- end}}
+{{- end }}
+}
+{{- end }}
diff --git a/server/resource/plugin/server/plugin.go.tpl b/server/resource/plugin/server/plugin.go.tpl
new file mode 100644
index 0000000..43ed056
--- /dev/null
+++ b/server/resource/plugin/server/plugin.go.tpl
@@ -0,0 +1,33 @@
+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{}
+
+func init() {
+ interfaces.Register(Plugin)
+}
+
+
+// 如果需要配置文件,请到config.Config中填充配置结构,且到下方发放中填入其在config.yaml中的key并添加如下方法
+// initialize.Viper()
+// 安装插件时候自动注册的api数据请到下方法.Api方法中实现并添加如下方法
+// initialize.Api(ctx)
+// 安装插件时候自动注册的api数据请到下方法.Menu方法中实现并添加如下方法
+// initialize.Menu(ctx)
+// 安装插件时候自动注册的api数据请到下方法.Dictionary方法中实现并添加如下方法
+// initialize.Dictionary(ctx)
+func (p *plugin) Register(group *gin.Engine) {
+ ctx := context.Background()
+ initialize.Gorm(ctx)
+ initialize.Router(group)
+}
diff --git a/server/resource/plugin/server/plugin/plugin.go.tpl b/server/resource/plugin/server/plugin/plugin.go.tpl
new file mode 100644
index 0000000..7e25e07
--- /dev/null
+++ b/server/resource/plugin/server/plugin/plugin.go.tpl
@@ -0,0 +1,5 @@
+package plugin
+
+import "{{.Module}}/plugin/{{ .Package }}/config"
+
+var Config config.Config
diff --git a/server/resource/plugin/server/router/enter.go.tpl b/server/resource/plugin/server/router/enter.go.tpl
new file mode 100644
index 0000000..78517b3
--- /dev/null
+++ b/server/resource/plugin/server/router/enter.go.tpl
@@ -0,0 +1,6 @@
+package router
+
+var Router = new(router)
+
+type router struct {
+}
diff --git a/server/resource/plugin/server/router/router.go.tpl b/server/resource/plugin/server/router/router.go.tpl
new file mode 100644
index 0000000..34bf4d8
--- /dev/null
+++ b/server/resource/plugin/server/router/router.go.tpl
@@ -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/server/resource/plugin/server/service/enter.go.tpl b/server/resource/plugin/server/service/enter.go.tpl
new file mode 100644
index 0000000..034facb
--- /dev/null
+++ b/server/resource/plugin/server/service/enter.go.tpl
@@ -0,0 +1,7 @@
+package service
+
+var Service = new(service)
+
+type service struct {
+}
+
diff --git a/server/resource/plugin/server/service/service.go.tpl b/server/resource/plugin/server/service/service.go.tpl
new file mode 100644
index 0000000..9743602
--- /dev/null
+++ b/server/resource/plugin/server/service/service.go.tpl
@@ -0,0 +1,211 @@
+{{- $db := "" }}
+{{- if eq .BusinessDB "" }}
+ {{- $db = "global.GVA_DB" }}
+{{- else}}
+ {{- $db = printf "global.MustGetGlobalDBByDBName(\"%s\")" .BusinessDB }}
+{{- end}}
+
+{{- if .IsAdd}}
+
+// Get{{.StructName}}InfoList 新增搜索语句
+
+ {{ GenerateSearchConditions .Fields }}
+
+// 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)
+{{$db}}.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 len(info.CreatedAtRange) == 2 {
+ db = db.Where("created_at BETWEEN ? AND ?", info.CreatedAtRange[0], info.CreatedAtRange[1])
+ }
+{{- end }}
+ {{ GenerateSearchConditions .Fields }}
+ err = db.Count(&total).Error
+ if err!=nil {
+ return
+ }
+ {{- if .NeedSort}}
+ var OrderStr string
+ orderMap := make(map[string]bool)
+ {{- if .GvaModel }}
+ orderMap["id"] = true
+ orderMap["created_at"] = true
+ {{- end }}
+ {{- 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)
+ {{$db}}.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/server/resource/plugin/web/api/api.js.tpl b/server/resource/plugin/web/api/api.js.tpl
new file mode 100644
index 0000000..0462fde
--- /dev/null
+++ b/server/resource/plugin/web/api/api.js.tpl
@@ -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/server/resource/plugin/web/form/form.vue.tpl b/server/resource/plugin/web/form/form.vue.tpl
new file mode 100644
index 0000000..7d3406a
--- /dev/null
+++ b/server/resource/plugin/web/form/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 }}
+
+form
+
+
+
+{{- end }}
+{{- end }}
diff --git a/server/resource/plugin/web/view/view.vue.tpl b/server/resource/plugin/web/view/view.vue.tpl
new file mode 100644
index 0000000..98b557a
--- /dev/null
+++ b/server/resource/plugin/web/view/view.vue.tpl
@@ -0,0 +1,689 @@
+{{- $global := . }}
+{{- $templateID := printf "%s_%s" .Package .StructName }}
+{{- if .IsAdd }}
+// 请在搜索条件中增加如下代码
+{{- range .Fields}}
+ {{- if .FieldSearchType}}
+{{ GenerateSearchFormItem .}}
+ {{ end }}
+{{ end }}
+
+
+// 表格增加如下列代码
+
+{{- range .Fields}}
+ {{- if .Table}}
+ {{ GenerateTableColumn . }}
+ {{- end }}
+{{- end }}
+
+// 新增表单中增加如下代码
+{{- range .Fields}}
+ {{- if .Form}}
+ {{ GenerateFormItem . }}
+ {{- end }}
+{{- end }}
+
+// 查看抽屉中增加如下代码
+
+{{- range .Fields}}
+ {{- if .Desc }}
+ {{ GenerateDescriptionItem . }}
+ {{- 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}}
+ {{ GenerateDefaultFormValue . }}
+ {{- 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}}
+
+
+ {{- if not .IsTree }}
+
+
+ {{- if .GvaModel }}
+
+
+
+ 创建日期
+
+
+
+
+
+
+
+ {{ end -}}
+ {{- range .Fields}} {{- if .FieldSearchType}} {{- if not .FieldSearchHide }}
+ {{ GenerateSearchFormItem .}}
+ {{ end }}{{ end }}{{ end }}
+
+
+ {{- range .Fields}} {{- if .FieldSearchType}} {{- if .FieldSearchHide }}
+ {{ GenerateSearchFormItem .}}
+ {{ end }}{{ end }}{{ end }}
+
+
+
+ 查询
+ 重置
+ 展开
+ 收起
+
+
+
+ {{- end }}
+
+
+ 新增
+ 删除
+ {{ if .HasExcel -}}
+
+
+
+ {{- end }}
+
+
+
+ {{ if .GvaModel }}
+
+ {{ "{{ formatDate(scope.row.CreatedAt) }}" }}
+
+ {{ end }}
+ {{- range .Fields}}
+ {{- if .Table}}
+ {{ GenerateTableColumn . }}
+ {{- end }}
+ {{- end }}
+
+
+ {{- if .IsTree }}
+ 新增子节点
+ {{- end }}
+ 查看
+ 编辑
+ 删除
+
+
+
+
+
+
+
+
+
{{"{{"}}type==='create'?'新增':'编辑'{{"}}"}}
+
+ 确 定
+ 取 消
+
+
+
+
+
+ {{- if .IsTree }}
+
+
+
+ {{- end }}
+ {{- range .Fields}}
+ {{- if .Form}}
+ {{ GenerateFormItem . }}
+ {{- end }}
+ {{- end }}
+
+
+
+
+
+ {{- if .IsTree }}
+
+
+
+ {{- end }}
+ {{- range .Fields}}
+ {{- if .Desc }}
+ {{ GenerateDescriptionItem . }}
+ {{- end }}
+ {{- end }}
+
+
+
+
+
+
+
+
+
+{{- else}}
+
+form
+
+
+
+{{- end }}
+
+{{- end }}
diff --git a/server/router/app/ai_config.go b/server/router/app/ai_config.go
new file mode 100644
index 0000000..7fd5f8d
--- /dev/null
+++ b/server/router/app/ai_config.go
@@ -0,0 +1,26 @@
+package app
+
+import (
+ v1 "git.echol.cn/loser/st/server/api/v1"
+ "git.echol.cn/loser/st/server/middleware"
+ "github.com/gin-gonic/gin"
+)
+
+type AIConfigRouter struct{}
+
+// InitAIConfigRouter 初始化AI配置路由
+func (r *AIConfigRouter) InitAIConfigRouter(Router *gin.RouterGroup) {
+ aiConfigRouter := Router.Group("ai-config").Use(middleware.AppJWTAuth())
+ aiConfigApi := v1.ApiGroupApp.AppApiGroup.AIConfigApi
+
+ {
+ aiConfigRouter.POST("", aiConfigApi.CreateAIConfig) // 创建AI配置
+ aiConfigRouter.GET("", aiConfigApi.GetAIConfigList) // 获取AI配置列表
+ aiConfigRouter.PUT(":id", aiConfigApi.UpdateAIConfig) // 更新AI配置
+ aiConfigRouter.DELETE(":id", aiConfigApi.DeleteAIConfig) // 删除AI配置
+ aiConfigRouter.POST(":id/test", aiConfigApi.TestAIConfigByID) // 通过ID测试AI配置
+ aiConfigRouter.GET(":id/models", aiConfigApi.GetModelsByID) // 通过ID获取模型列表
+ aiConfigRouter.POST("models", aiConfigApi.GetModels) // 获取模型列表
+ aiConfigRouter.POST("test", aiConfigApi.TestAIConfig) // 测试AI配置(用于新建时)
+ }
+}
diff --git a/server/router/app/auth.go b/server/router/app/auth.go
new file mode 100644
index 0000000..3bb8b09
--- /dev/null
+++ b/server/router/app/auth.go
@@ -0,0 +1,36 @@
+package app
+
+import (
+ v1 "git.echol.cn/loser/st/server/api/v1"
+ "git.echol.cn/loser/st/server/middleware"
+ "github.com/gin-gonic/gin"
+)
+
+type AuthRouter struct{}
+
+// InitAuthRouter 初始化前台用户认证路由
+func (r *AuthRouter) InitAuthRouter(Router *gin.RouterGroup) {
+ authRouter := Router.Group("auth")
+ authApi := v1.ApiGroupApp.AppApiGroup.AuthApi
+
+ {
+ // 公开路由(无需认证)
+ authRouter.POST("register", authApi.Register) // 注册
+ authRouter.POST("login", authApi.Login) // 登录
+ authRouter.POST("refresh", authApi.RefreshToken) // 刷新Token
+ }
+
+ // 需要认证的路由
+ authRouterAuth := Router.Group("auth").Use(middleware.AppJWTAuth())
+ {
+ authRouterAuth.POST("logout", authApi.Logout) // 登出
+ authRouterAuth.GET("userinfo", authApi.GetUserInfo) // 获取用户信息
+ }
+
+ // 用户相关路由
+ userRouter := Router.Group("user").Use(middleware.AppJWTAuth())
+ {
+ userRouter.PUT("profile", authApi.UpdateProfile) // 更新用户信息
+ userRouter.POST("change-password", authApi.ChangePassword) // 修改密码
+ }
+}
diff --git a/server/router/app/character.go b/server/router/app/character.go
new file mode 100644
index 0000000..1589a00
--- /dev/null
+++ b/server/router/app/character.go
@@ -0,0 +1,25 @@
+package app
+
+import (
+ v1 "git.echol.cn/loser/st/server/api/v1"
+ "git.echol.cn/loser/st/server/middleware"
+ "github.com/gin-gonic/gin"
+)
+
+type CharacterRouter struct{}
+
+// InitCharacterRouter 初始化角色卡路由
+func (r *CharacterRouter) InitCharacterRouter(Router *gin.RouterGroup) {
+ characterRouter := Router.Group("character").Use(middleware.AppJWTAuth())
+ characterApi := v1.ApiGroupApp.AppApiGroup.CharacterApi
+
+ {
+ characterRouter.POST("", characterApi.CreateCharacter) // 创建角色卡
+ characterRouter.GET("", characterApi.GetCharacterList) // 获取角色卡列表
+ characterRouter.GET(":id", characterApi.GetCharacterByID) // 获取角色卡详情
+ characterRouter.PUT(":id", characterApi.UpdateCharacter) // 更新角色卡
+ characterRouter.DELETE(":id", characterApi.DeleteCharacter) // 删除角色卡
+ characterRouter.POST("upload", characterApi.UploadCharacter) // 上传角色卡文件
+ characterRouter.GET(":id/export", characterApi.ExportCharacter) // 导出角色卡
+ }
+}
diff --git a/server/router/app/conversation.go b/server/router/app/conversation.go
new file mode 100644
index 0000000..1d02520
--- /dev/null
+++ b/server/router/app/conversation.go
@@ -0,0 +1,25 @@
+package app
+
+import (
+ v1 "git.echol.cn/loser/st/server/api/v1"
+ "git.echol.cn/loser/st/server/middleware"
+ "github.com/gin-gonic/gin"
+)
+
+type ConversationRouter struct{}
+
+// InitConversationRouter 初始化对话路由
+func (r *ConversationRouter) InitConversationRouter(Router *gin.RouterGroup) {
+ conversationRouter := Router.Group("conversation").Use(middleware.AppJWTAuth())
+ conversationApi := v1.ApiGroupApp.AppApiGroup.ConversationApi
+
+ {
+ conversationRouter.POST("", conversationApi.CreateConversation) // 创建对话
+ conversationRouter.GET("", conversationApi.GetConversationList) // 获取对话列表
+ conversationRouter.GET(":id", conversationApi.GetConversationByID) // 获取对话详情
+ conversationRouter.PUT(":id/settings", conversationApi.UpdateConversationSettings) // 更新对话设置
+ conversationRouter.DELETE(":id", conversationApi.DeleteConversation) // 删除对话
+ conversationRouter.GET(":id/messages", conversationApi.GetMessageList) // 获取消息列表
+ conversationRouter.POST(":id/message", conversationApi.SendMessage) // 发送消息
+ }
+}
diff --git a/server/router/app/enter.go b/server/router/app/enter.go
new file mode 100644
index 0000000..fa158fc
--- /dev/null
+++ b/server/router/app/enter.go
@@ -0,0 +1,10 @@
+package app
+
+type RouterGroup struct {
+ AuthRouter
+ CharacterRouter
+ ConversationRouter
+ AIConfigRouter
+ PresetRouter
+ UploadRouter
+}
diff --git a/server/router/app/preset.go b/server/router/app/preset.go
new file mode 100644
index 0000000..15fb378
--- /dev/null
+++ b/server/router/app/preset.go
@@ -0,0 +1,24 @@
+package app
+
+import (
+ v1 "git.echol.cn/loser/st/server/api/v1"
+ "github.com/gin-gonic/gin"
+)
+
+type PresetRouter struct{}
+
+// InitPresetRouter 初始化预设路由
+func (r *PresetRouter) InitPresetRouter(Router *gin.RouterGroup) {
+ presetRouter := Router.Group("preset")
+ presetApi := v1.ApiGroupApp.AppApiGroup.PresetApi
+ {
+ presetRouter.POST("", presetApi.CreatePreset) // 创建预设
+ presetRouter.GET("", presetApi.GetPresetList) // 获取预设列表
+ presetRouter.GET("/:id", presetApi.GetPresetByID) // 获取预设详情
+ presetRouter.PUT("/:id", presetApi.UpdatePreset) // 更新预设
+ presetRouter.DELETE("/:id", presetApi.DeletePreset) // 删除预设
+ presetRouter.POST("/:id/default", presetApi.SetDefaultPreset) // 设置默认预设
+ presetRouter.POST("/import", presetApi.ImportPreset) // 导入预设
+ presetRouter.GET("/:id/export", presetApi.ExportPreset) // 导出预设
+ }
+}
diff --git a/server/router/app/upload.go b/server/router/app/upload.go
new file mode 100644
index 0000000..1e57b8d
--- /dev/null
+++ b/server/router/app/upload.go
@@ -0,0 +1,17 @@
+package app
+
+import (
+ v1 "git.echol.cn/loser/st/server/api/v1"
+ "github.com/gin-gonic/gin"
+)
+
+type UploadRouter struct{}
+
+// InitUploadRouter 初始化上传路由
+func (r *UploadRouter) InitUploadRouter(Router *gin.RouterGroup) {
+ uploadRouter := Router.Group("upload")
+ uploadApi := v1.ApiGroupApp.AppApiGroup.UploadApi
+ {
+ uploadRouter.POST("/image", uploadApi.UploadImage) // 上传图片
+ }
+}
diff --git a/server/router/enter.go b/server/router/enter.go
new file mode 100644
index 0000000..0ffad75
--- /dev/null
+++ b/server/router/enter.go
@@ -0,0 +1,15 @@
+package router
+
+import (
+ "git.echol.cn/loser/st/server/router/app"
+ "git.echol.cn/loser/st/server/router/example"
+ "git.echol.cn/loser/st/server/router/system"
+)
+
+var RouterGroupApp = new(RouterGroup)
+
+type RouterGroup struct {
+ System system.RouterGroup
+ Example example.RouterGroup
+ App app.RouterGroup
+}
diff --git a/server/router/example/enter.go b/server/router/example/enter.go
new file mode 100644
index 0000000..9308d66
--- /dev/null
+++ b/server/router/example/enter.go
@@ -0,0 +1,17 @@
+package example
+
+import (
+ api "git.echol.cn/loser/st/server/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/server/router/example/exa_attachment_category.go b/server/router/example/exa_attachment_category.go
new file mode 100644
index 0000000..4900292
--- /dev/null
+++ b/server/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/server/router/example/exa_customer.go b/server/router/example/exa_customer.go
new file mode 100644
index 0000000..b0160b7
--- /dev/null
+++ b/server/router/example/exa_customer.go
@@ -0,0 +1,22 @@
+package example
+
+import (
+ "git.echol.cn/loser/st/server/middleware"
+ "github.com/gin-gonic/gin"
+)
+
+type CustomerRouter struct{}
+
+func (e *CustomerRouter) InitCustomerRouter(Router *gin.RouterGroup) {
+ customerRouter := Router.Group("customer").Use(middleware.OperationRecord())
+ customerRouterWithoutRecord := Router.Group("customer")
+ {
+ customerRouter.POST("customer", exaCustomerApi.CreateExaCustomer) // 创建客户
+ customerRouter.PUT("customer", exaCustomerApi.UpdateExaCustomer) // 更新客户
+ customerRouter.DELETE("customer", exaCustomerApi.DeleteExaCustomer) // 删除客户
+ }
+ {
+ customerRouterWithoutRecord.GET("customer", exaCustomerApi.GetExaCustomer) // 获取单一客户信息
+ customerRouterWithoutRecord.GET("customerList", exaCustomerApi.GetExaCustomerList) // 获取客户列表
+ }
+}
diff --git a/server/router/example/exa_file_upload_and_download.go b/server/router/example/exa_file_upload_and_download.go
new file mode 100644
index 0000000..84f6ecd
--- /dev/null
+++ b/server/router/example/exa_file_upload_and_download.go
@@ -0,0 +1,22 @@
+package example
+
+import (
+ "github.com/gin-gonic/gin"
+)
+
+type FileUploadAndDownloadRouter struct{}
+
+func (e *FileUploadAndDownloadRouter) InitFileUploadAndDownloadRouter(Router *gin.RouterGroup) {
+ fileUploadAndDownloadRouter := Router.Group("fileUploadAndDownload")
+ {
+ fileUploadAndDownloadRouter.POST("upload", exaFileUploadAndDownloadApi.UploadFile) // 上传文件
+ fileUploadAndDownloadRouter.POST("getFileList", exaFileUploadAndDownloadApi.GetFileList) // 获取上传文件列表
+ fileUploadAndDownloadRouter.POST("deleteFile", exaFileUploadAndDownloadApi.DeleteFile) // 删除指定文件
+ fileUploadAndDownloadRouter.POST("editFileName", exaFileUploadAndDownloadApi.EditFileName) // 编辑文件名或者备注
+ fileUploadAndDownloadRouter.POST("breakpointContinue", exaFileUploadAndDownloadApi.BreakpointContinue) // 断点续传
+ fileUploadAndDownloadRouter.GET("findFile", exaFileUploadAndDownloadApi.FindFile) // 查询当前文件成功的切片
+ fileUploadAndDownloadRouter.POST("breakpointContinueFinish", exaFileUploadAndDownloadApi.BreakpointContinueFinish) // 切片传输完成
+ fileUploadAndDownloadRouter.POST("removeChunk", exaFileUploadAndDownloadApi.RemoveChunk) // 删除切片
+ fileUploadAndDownloadRouter.POST("importURL", exaFileUploadAndDownloadApi.ImportURL) // 导入URL
+ }
+}
diff --git a/server/router/system/enter.go b/server/router/system/enter.go
new file mode 100644
index 0000000..2810131
--- /dev/null
+++ b/server/router/system/enter.go
@@ -0,0 +1,52 @@
+package system
+
+import api "git.echol.cn/loser/st/server/api/v1"
+
+type RouterGroup struct {
+ ApiRouter
+ JwtRouter
+ SysRouter
+ BaseRouter
+ InitRouter
+ MenuRouter
+ UserRouter
+ CasbinRouter
+ AutoCodeRouter
+ AuthorityRouter
+ DictionaryRouter
+ OperationRecordRouter
+ DictionaryDetailRouter
+ AuthorityBtnRouter
+ SysExportTemplateRouter
+ SysParamsRouter
+ SysVersionRouter
+ SysErrorRouter
+ LoginLogRouter
+ ApiTokenRouter
+ SkillsRouter
+}
+
+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
+ sysVersionApi = api.ApiGroupApp.SystemApiGroup.SysVersionApi
+ sysErrorApi = api.ApiGroupApp.SystemApiGroup.SysErrorApi
+ skillsApi = api.ApiGroupApp.SystemApiGroup.SkillsApi
+)
diff --git a/server/router/system/sys_api.go b/server/router/system/sys_api.go
new file mode 100644
index 0000000..908e590
--- /dev/null
+++ b/server/router/system/sys_api.go
@@ -0,0 +1,33 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/middleware"
+ "github.com/gin-gonic/gin"
+)
+
+type ApiRouter struct{}
+
+func (s *ApiRouter) InitApiRouter(Router *gin.RouterGroup, RouterPub *gin.RouterGroup) {
+ apiRouter := Router.Group("api").Use(middleware.OperationRecord())
+ apiRouterWithoutRecord := Router.Group("api")
+
+ apiPublicRouterWithoutRecord := RouterPub.Group("api")
+ {
+ apiRouter.GET("getApiGroups", apiRouterApi.GetApiGroups) // 获取路由组
+ apiRouter.GET("syncApi", apiRouterApi.SyncApi) // 同步Api
+ apiRouter.POST("ignoreApi", apiRouterApi.IgnoreApi) // 忽略Api
+ apiRouter.POST("enterSyncApi", apiRouterApi.EnterSyncApi) // 确认同步Api
+ apiRouter.POST("createApi", apiRouterApi.CreateApi) // 创建Api
+ apiRouter.POST("deleteApi", apiRouterApi.DeleteApi) // 删除Api
+ apiRouter.POST("getApiById", apiRouterApi.GetApiById) // 获取单条Api消息
+ apiRouter.POST("updateApi", apiRouterApi.UpdateApi) // 更新api
+ apiRouter.DELETE("deleteApisByIds", apiRouterApi.DeleteApisByIds) // 删除选中api
+ }
+ {
+ apiRouterWithoutRecord.POST("getAllApis", apiRouterApi.GetAllApis) // 获取所有api
+ apiRouterWithoutRecord.POST("getApiList", apiRouterApi.GetApiList) // 获取Api列表
+ }
+ {
+ apiPublicRouterWithoutRecord.GET("freshCasbin", apiRouterApi.FreshCasbin) // 刷新casbin权限
+ }
+}
diff --git a/server/router/system/sys_api_token.go b/server/router/system/sys_api_token.go
new file mode 100644
index 0000000..4244f0c
--- /dev/null
+++ b/server/router/system/sys_api_token.go
@@ -0,0 +1,19 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/api/v1"
+ "git.echol.cn/loser/st/server/middleware"
+ "github.com/gin-gonic/gin"
+)
+
+type ApiTokenRouter struct{}
+
+func (s *ApiTokenRouter) InitApiTokenRouter(Router *gin.RouterGroup) {
+ apiTokenRouter := Router.Group("sysApiToken").Use(middleware.OperationRecord())
+ apiTokenApi := v1.ApiGroupApp.SystemApiGroup.ApiTokenApi
+ {
+ apiTokenRouter.POST("createApiToken", apiTokenApi.CreateApiToken) // 签发Token
+ apiTokenRouter.POST("getApiTokenList", apiTokenApi.GetApiTokenList) // 获取列表
+ apiTokenRouter.POST("deleteApiToken", apiTokenApi.DeleteApiToken) // 作废Token
+ }
+}
diff --git a/server/router/system/sys_authority.go b/server/router/system/sys_authority.go
new file mode 100644
index 0000000..884be08
--- /dev/null
+++ b/server/router/system/sys_authority.go
@@ -0,0 +1,23 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/middleware"
+ "github.com/gin-gonic/gin"
+)
+
+type AuthorityRouter struct{}
+
+func (s *AuthorityRouter) InitAuthorityRouter(Router *gin.RouterGroup) {
+ authorityRouter := Router.Group("authority").Use(middleware.OperationRecord())
+ authorityRouterWithoutRecord := Router.Group("authority")
+ {
+ authorityRouter.POST("createAuthority", authorityApi.CreateAuthority) // 创建角色
+ authorityRouter.POST("deleteAuthority", authorityApi.DeleteAuthority) // 删除角色
+ authorityRouter.PUT("updateAuthority", authorityApi.UpdateAuthority) // 更新角色
+ authorityRouter.POST("copyAuthority", authorityApi.CopyAuthority) // 拷贝角色
+ authorityRouter.POST("setDataAuthority", authorityApi.SetDataAuthority) // 设置角色资源权限
+ }
+ {
+ authorityRouterWithoutRecord.POST("getAuthorityList", authorityApi.GetAuthorityList) // 获取角色列表
+ }
+}
diff --git a/server/router/system/sys_authority_btn.go b/server/router/system/sys_authority_btn.go
new file mode 100644
index 0000000..370db85
--- /dev/null
+++ b/server/router/system/sys_authority_btn.go
@@ -0,0 +1,19 @@
+package system
+
+import (
+ "github.com/gin-gonic/gin"
+)
+
+type AuthorityBtnRouter struct{}
+
+var AuthorityBtnRouterApp = new(AuthorityBtnRouter)
+
+func (s *AuthorityBtnRouter) InitAuthorityBtnRouterRouter(Router *gin.RouterGroup) {
+ // authorityRouter := Router.Group("authorityBtn").Use(middleware.OperationRecord())
+ authorityRouterWithoutRecord := Router.Group("authorityBtn")
+ {
+ authorityRouterWithoutRecord.POST("getAuthorityBtn", authorityBtnApi.GetAuthorityBtn)
+ authorityRouterWithoutRecord.POST("setAuthorityBtn", authorityBtnApi.SetAuthorityBtn)
+ authorityRouterWithoutRecord.POST("canRemoveAuthorityBtn", authorityBtnApi.CanRemoveAuthorityBtn)
+ }
+}
diff --git a/server/router/system/sys_auto_code.go b/server/router/system/sys_auto_code.go
new file mode 100644
index 0000000..0f19dd9
--- /dev/null
+++ b/server/router/system/sys_auto_code.go
@@ -0,0 +1,47 @@
+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("mcp", autoCodeTemplateApi.MCP) // 自动创建Mcp Tool模板
+ autoCodeRouter.POST("mcpList", autoCodeTemplateApi.MCPList) // 获取MCP ToolList
+ autoCodeRouter.POST("mcpTest", autoCodeTemplateApi.MCPTest) // MCP 工具测试
+ }
+ {
+ 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) // 自动安装插件
+ autoCodeRouter.POST("removePlugin", autoCodePluginApi.Remove) // 自动删除插件
+ autoCodeRouter.GET("getPluginList", autoCodePluginApi.GetPluginList) // 获取插件列表
+ }
+ {
+ publicAutoCodeRouter.POST("llmAuto", autoCodeApi.LLMAuto)
+ publicAutoCodeRouter.POST("initMenu", autoCodePluginApi.InitMenu) // 同步插件菜单
+ publicAutoCodeRouter.POST("initAPI", autoCodePluginApi.InitAPI) // 同步插件API
+ publicAutoCodeRouter.POST("initDictionary", autoCodePluginApi.InitDictionary) // 同步插件字典
+ }
+}
diff --git a/server/router/system/sys_auto_code_history.go b/server/router/system/sys_auto_code_history.go
new file mode 100644
index 0000000..42a2bef
--- /dev/null
+++ b/server/router/system/sys_auto_code_history.go
@@ -0,0 +1,17 @@
+package system
+
+import (
+ "github.com/gin-gonic/gin"
+)
+
+type AutoCodeHistoryRouter struct{}
+
+func (s *AutoCodeRouter) InitAutoCodeHistoryRouter(Router *gin.RouterGroup) {
+ autoCodeHistoryRouter := Router.Group("autoCode")
+ {
+ autoCodeHistoryRouter.POST("getMeta", autocodeHistoryApi.First) // 根据id获取meta信息
+ autoCodeHistoryRouter.POST("rollback", autocodeHistoryApi.RollBack) // 回滚
+ autoCodeHistoryRouter.POST("delSysHistory", autocodeHistoryApi.Delete) // 删除回滚记录
+ autoCodeHistoryRouter.POST("getSysHistory", autocodeHistoryApi.GetList) // 获取回滚记录分页
+ }
+}
diff --git a/server/router/system/sys_base.go b/server/router/system/sys_base.go
new file mode 100644
index 0000000..7d959bb
--- /dev/null
+++ b/server/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/server/router/system/sys_casbin.go b/server/router/system/sys_casbin.go
new file mode 100644
index 0000000..e3cb665
--- /dev/null
+++ b/server/router/system/sys_casbin.go
@@ -0,0 +1,19 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/middleware"
+ "github.com/gin-gonic/gin"
+)
+
+type CasbinRouter struct{}
+
+func (s *CasbinRouter) InitCasbinRouter(Router *gin.RouterGroup) {
+ casbinRouter := Router.Group("casbin").Use(middleware.OperationRecord())
+ casbinRouterWithoutRecord := Router.Group("casbin")
+ {
+ casbinRouter.POST("updateCasbin", casbinApi.UpdateCasbin)
+ }
+ {
+ casbinRouterWithoutRecord.POST("getPolicyPathByAuthorityId", casbinApi.GetPolicyPathByAuthorityId)
+ }
+}
diff --git a/server/router/system/sys_dictionary.go b/server/router/system/sys_dictionary.go
new file mode 100644
index 0000000..01b7ec2
--- /dev/null
+++ b/server/router/system/sys_dictionary.go
@@ -0,0 +1,24 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/middleware"
+ "github.com/gin-gonic/gin"
+)
+
+type DictionaryRouter struct{}
+
+func (s *DictionaryRouter) InitSysDictionaryRouter(Router *gin.RouterGroup) {
+ sysDictionaryRouter := Router.Group("sysDictionary").Use(middleware.OperationRecord())
+ sysDictionaryRouterWithoutRecord := Router.Group("sysDictionary")
+ {
+ sysDictionaryRouter.POST("createSysDictionary", dictionaryApi.CreateSysDictionary) // 新建SysDictionary
+ sysDictionaryRouter.DELETE("deleteSysDictionary", dictionaryApi.DeleteSysDictionary) // 删除SysDictionary
+ sysDictionaryRouter.PUT("updateSysDictionary", dictionaryApi.UpdateSysDictionary) // 更新SysDictionary
+ sysDictionaryRouter.POST("importSysDictionary", dictionaryApi.ImportSysDictionary) // 导入SysDictionary
+ sysDictionaryRouter.GET("exportSysDictionary", dictionaryApi.ExportSysDictionary) // 导出SysDictionary
+ }
+ {
+ sysDictionaryRouterWithoutRecord.GET("findSysDictionary", dictionaryApi.FindSysDictionary) // 根据ID获取SysDictionary
+ sysDictionaryRouterWithoutRecord.GET("getSysDictionaryList", dictionaryApi.GetSysDictionaryList) // 获取SysDictionary列表
+ }
+}
diff --git a/server/router/system/sys_dictionary_detail.go b/server/router/system/sys_dictionary_detail.go
new file mode 100644
index 0000000..af1a32b
--- /dev/null
+++ b/server/router/system/sys_dictionary_detail.go
@@ -0,0 +1,26 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/middleware"
+ "github.com/gin-gonic/gin"
+)
+
+type DictionaryDetailRouter struct{}
+
+func (s *DictionaryDetailRouter) InitSysDictionaryDetailRouter(Router *gin.RouterGroup) {
+ dictionaryDetailRouter := Router.Group("sysDictionaryDetail").Use(middleware.OperationRecord())
+ dictionaryDetailRouterWithoutRecord := Router.Group("sysDictionaryDetail")
+ {
+ dictionaryDetailRouter.POST("createSysDictionaryDetail", dictionaryDetailApi.CreateSysDictionaryDetail) // 新建SysDictionaryDetail
+ dictionaryDetailRouter.DELETE("deleteSysDictionaryDetail", dictionaryDetailApi.DeleteSysDictionaryDetail) // 删除SysDictionaryDetail
+ dictionaryDetailRouter.PUT("updateSysDictionaryDetail", dictionaryDetailApi.UpdateSysDictionaryDetail) // 更新SysDictionaryDetail
+ }
+ {
+ dictionaryDetailRouterWithoutRecord.GET("findSysDictionaryDetail", dictionaryDetailApi.FindSysDictionaryDetail) // 根据ID获取SysDictionaryDetail
+ dictionaryDetailRouterWithoutRecord.GET("getSysDictionaryDetailList", dictionaryDetailApi.GetSysDictionaryDetailList) // 获取SysDictionaryDetail列表
+ dictionaryDetailRouterWithoutRecord.GET("getDictionaryTreeList", dictionaryDetailApi.GetDictionaryTreeList) // 获取字典详情树形结构
+ dictionaryDetailRouterWithoutRecord.GET("getDictionaryTreeListByType", dictionaryDetailApi.GetDictionaryTreeListByType) // 根据字典类型获取字典详情树形结构
+ dictionaryDetailRouterWithoutRecord.GET("getDictionaryDetailsByParent", dictionaryDetailApi.GetDictionaryDetailsByParent) // 根据父级ID获取字典详情
+ dictionaryDetailRouterWithoutRecord.GET("getDictionaryPath", dictionaryDetailApi.GetDictionaryPath) // 获取字典详情的完整路径
+ }
+}
diff --git a/server/router/system/sys_error.go b/server/router/system/sys_error.go
new file mode 100644
index 0000000..168f390
--- /dev/null
+++ b/server/router/system/sys_error.go
@@ -0,0 +1,28 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/middleware"
+ "github.com/gin-gonic/gin"
+)
+
+type SysErrorRouter struct{}
+
+// InitSysErrorRouter 初始化 错误日志 路由信息
+func (s *SysErrorRouter) InitSysErrorRouter(Router *gin.RouterGroup, PublicRouter *gin.RouterGroup) {
+ sysErrorRouter := Router.Group("sysError").Use(middleware.OperationRecord())
+ sysErrorRouterWithoutRecord := Router.Group("sysError")
+ sysErrorRouterWithoutAuth := PublicRouter.Group("sysError")
+ {
+ sysErrorRouter.DELETE("deleteSysError", sysErrorApi.DeleteSysError) // 删除错误日志
+ sysErrorRouter.DELETE("deleteSysErrorByIds", sysErrorApi.DeleteSysErrorByIds) // 批量删除错误日志
+ sysErrorRouter.PUT("updateSysError", sysErrorApi.UpdateSysError) // 更新错误日志
+ sysErrorRouter.GET("getSysErrorSolution", sysErrorApi.GetSysErrorSolution) // 触发错误日志处理
+ }
+ {
+ sysErrorRouterWithoutRecord.GET("findSysError", sysErrorApi.FindSysError) // 根据ID获取错误日志
+ sysErrorRouterWithoutRecord.GET("getSysErrorList", sysErrorApi.GetSysErrorList) // 获取错误日志列表
+ }
+ {
+ sysErrorRouterWithoutAuth.POST("createSysError", sysErrorApi.CreateSysError) // 新建错误日志
+ }
+}
diff --git a/server/router/system/sys_export_template.go b/server/router/system/sys_export_template.go
new file mode 100644
index 0000000..3f54a85
--- /dev/null
+++ b/server/router/system/sys_export_template.go
@@ -0,0 +1,35 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/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) // 导出表格模板
+ sysExportTemplateRouterWithoutRecord.GET("previewSQL", exportTemplateApi.PreviewSQL) // 预览SQL
+ }
+ {
+ sysExportTemplateRouterWithoutAuth.GET("exportExcelByToken", exportTemplateApi.ExportExcelByToken) // 通过token导出表格
+ sysExportTemplateRouterWithoutAuth.GET("exportTemplateByToken", exportTemplateApi.ExportTemplateByToken) // 通过token导出模板
+ }
+}
diff --git a/server/router/system/sys_initdb.go b/server/router/system/sys_initdb.go
new file mode 100644
index 0000000..3a6de50
--- /dev/null
+++ b/server/router/system/sys_initdb.go
@@ -0,0 +1,15 @@
+package system
+
+import (
+ "github.com/gin-gonic/gin"
+)
+
+type InitRouter struct{}
+
+func (s *InitRouter) InitInitRouter(Router *gin.RouterGroup) {
+ initRouter := Router.Group("init")
+ {
+ initRouter.POST("initdb", dbApi.InitDB) // 初始化数据库
+ initRouter.POST("checkdb", dbApi.CheckDB) // 检测是否需要初始化数据库
+ }
+}
diff --git a/server/router/system/sys_jwt.go b/server/router/system/sys_jwt.go
new file mode 100644
index 0000000..4716031
--- /dev/null
+++ b/server/router/system/sys_jwt.go
@@ -0,0 +1,14 @@
+package system
+
+import (
+ "github.com/gin-gonic/gin"
+)
+
+type JwtRouter struct{}
+
+func (s *JwtRouter) InitJwtRouter(Router *gin.RouterGroup) {
+ jwtRouter := Router.Group("jwt")
+ {
+ jwtRouter.POST("jsonInBlacklist", jwtApi.JsonInBlacklist) // jwt加入黑名单
+ }
+}
diff --git a/server/router/system/sys_login_log.go b/server/router/system/sys_login_log.go
new file mode 100644
index 0000000..c7d5a8c
--- /dev/null
+++ b/server/router/system/sys_login_log.go
@@ -0,0 +1,23 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/api/v1"
+ "git.echol.cn/loser/st/server/middleware"
+ "github.com/gin-gonic/gin"
+)
+
+type LoginLogRouter struct{}
+
+func (s *LoginLogRouter) InitLoginLogRouter(Router *gin.RouterGroup) {
+ loginLogRouter := Router.Group("sysLoginLog").Use(middleware.OperationRecord())
+ loginLogRouterWithoutRecord := Router.Group("sysLoginLog")
+ sysLoginLogApi := v1.ApiGroupApp.SystemApiGroup.LoginLogApi
+ {
+ loginLogRouter.DELETE("deleteLoginLog", sysLoginLogApi.DeleteLoginLog) // 删除登录日志
+ loginLogRouter.DELETE("deleteLoginLogByIds", sysLoginLogApi.DeleteLoginLogByIds) // 批量删除登录日志
+ }
+ {
+ loginLogRouterWithoutRecord.GET("findLoginLog", sysLoginLogApi.FindLoginLog) // 根据ID获取登录日志(详情)
+ loginLogRouterWithoutRecord.GET("getLoginLogList", sysLoginLogApi.GetLoginLogList) // 获取登录日志列表
+ }
+}
diff --git a/server/router/system/sys_menu.go b/server/router/system/sys_menu.go
new file mode 100644
index 0000000..5b4779e
--- /dev/null
+++ b/server/router/system/sys_menu.go
@@ -0,0 +1,27 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/middleware"
+ "github.com/gin-gonic/gin"
+)
+
+type MenuRouter struct{}
+
+func (s *MenuRouter) InitMenuRouter(Router *gin.RouterGroup) (R gin.IRoutes) {
+ menuRouter := Router.Group("menu").Use(middleware.OperationRecord())
+ menuRouterWithoutRecord := Router.Group("menu")
+ {
+ menuRouter.POST("addBaseMenu", authorityMenuApi.AddBaseMenu) // 新增菜单
+ menuRouter.POST("addMenuAuthority", authorityMenuApi.AddMenuAuthority) // 增加menu和角色关联关系
+ menuRouter.POST("deleteBaseMenu", authorityMenuApi.DeleteBaseMenu) // 删除菜单
+ menuRouter.POST("updateBaseMenu", authorityMenuApi.UpdateBaseMenu) // 更新菜单
+ }
+ {
+ menuRouterWithoutRecord.POST("getMenu", authorityMenuApi.GetMenu) // 获取菜单树
+ menuRouterWithoutRecord.POST("getMenuList", authorityMenuApi.GetMenuList) // 分页获取基础menu列表
+ menuRouterWithoutRecord.POST("getBaseMenuTree", authorityMenuApi.GetBaseMenuTree) // 获取用户动态路由
+ menuRouterWithoutRecord.POST("getMenuAuthority", authorityMenuApi.GetMenuAuthority) // 获取指定角色menu
+ menuRouterWithoutRecord.POST("getBaseMenuById", authorityMenuApi.GetBaseMenuById) // 根据id获取菜单
+ }
+ return menuRouter
+}
diff --git a/server/router/system/sys_operation_record.go b/server/router/system/sys_operation_record.go
new file mode 100644
index 0000000..d158d5e
--- /dev/null
+++ b/server/router/system/sys_operation_record.go
@@ -0,0 +1,18 @@
+package system
+
+import (
+ "github.com/gin-gonic/gin"
+)
+
+type OperationRecordRouter struct{}
+
+func (s *OperationRecordRouter) InitSysOperationRecordRouter(Router *gin.RouterGroup) {
+ operationRecordRouter := Router.Group("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/server/router/system/sys_params.go b/server/router/system/sys_params.go
new file mode 100644
index 0000000..215a975
--- /dev/null
+++ b/server/router/system/sys_params.go
@@ -0,0 +1,25 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/middleware"
+ "github.com/gin-gonic/gin"
+)
+
+type SysParamsRouter struct{}
+
+// InitSysParamsRouter 初始化 参数 路由信息
+func (s *SysParamsRouter) InitSysParamsRouter(Router *gin.RouterGroup, PublicRouter *gin.RouterGroup) {
+ sysParamsRouter := Router.Group("sysParams").Use(middleware.OperationRecord())
+ sysParamsRouterWithoutRecord := Router.Group("sysParams")
+ {
+ sysParamsRouter.POST("createSysParams", sysParamsApi.CreateSysParams) // 新建参数
+ sysParamsRouter.DELETE("deleteSysParams", sysParamsApi.DeleteSysParams) // 删除参数
+ sysParamsRouter.DELETE("deleteSysParamsByIds", sysParamsApi.DeleteSysParamsByIds) // 批量删除参数
+ sysParamsRouter.PUT("updateSysParams", sysParamsApi.UpdateSysParams) // 更新参数
+ }
+ {
+ sysParamsRouterWithoutRecord.GET("findSysParams", sysParamsApi.FindSysParams) // 根据ID获取参数
+ sysParamsRouterWithoutRecord.GET("getSysParamsList", sysParamsApi.GetSysParamsList) // 获取参数列表
+ sysParamsRouterWithoutRecord.GET("getSysParam", sysParamsApi.GetSysParam) // 根据Key获取参数
+ }
+}
diff --git a/server/router/system/sys_skills.go b/server/router/system/sys_skills.go
new file mode 100644
index 0000000..9529e66
--- /dev/null
+++ b/server/router/system/sys_skills.go
@@ -0,0 +1,29 @@
+package system
+
+import "github.com/gin-gonic/gin"
+
+type SkillsRouter struct{}
+
+func (s *SkillsRouter) InitSkillsRouter(Router *gin.RouterGroup) {
+ skillsRouter := Router.Group("skills")
+ {
+ skillsRouter.GET("getTools", skillsApi.GetTools)
+ skillsRouter.POST("getSkillList", skillsApi.GetSkillList)
+ skillsRouter.POST("getSkillDetail", skillsApi.GetSkillDetail)
+ skillsRouter.POST("saveSkill", skillsApi.SaveSkill)
+ skillsRouter.POST("createScript", skillsApi.CreateScript)
+ skillsRouter.POST("getScript", skillsApi.GetScript)
+ skillsRouter.POST("saveScript", skillsApi.SaveScript)
+ skillsRouter.POST("createResource", skillsApi.CreateResource)
+ skillsRouter.POST("getResource", skillsApi.GetResource)
+ skillsRouter.POST("saveResource", skillsApi.SaveResource)
+ skillsRouter.POST("createReference", skillsApi.CreateReference)
+ skillsRouter.POST("getReference", skillsApi.GetReference)
+ skillsRouter.POST("saveReference", skillsApi.SaveReference)
+ skillsRouter.POST("createTemplate", skillsApi.CreateTemplate)
+ skillsRouter.POST("getTemplate", skillsApi.GetTemplate)
+ skillsRouter.POST("saveTemplate", skillsApi.SaveTemplate)
+ skillsRouter.POST("getGlobalConstraint", skillsApi.GetGlobalConstraint)
+ skillsRouter.POST("saveGlobalConstraint", skillsApi.SaveGlobalConstraint)
+ }
+}
diff --git a/server/router/system/sys_system.go b/server/router/system/sys_system.go
new file mode 100644
index 0000000..f85a356
--- /dev/null
+++ b/server/router/system/sys_system.go
@@ -0,0 +1,22 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/middleware"
+ "github.com/gin-gonic/gin"
+)
+
+type SysRouter struct{}
+
+func (s *SysRouter) InitSystemRouter(Router *gin.RouterGroup) {
+ sysRouter := Router.Group("system").Use(middleware.OperationRecord())
+ sysRouterWithoutRecord := Router.Group("system")
+
+ {
+ sysRouter.POST("setSystemConfig", systemApi.SetSystemConfig) // 设置配置文件内容
+ sysRouter.POST("reloadSystem", systemApi.ReloadSystem) // 重启服务
+ }
+ {
+ sysRouterWithoutRecord.POST("getSystemConfig", systemApi.GetSystemConfig) // 获取配置文件内容
+ sysRouterWithoutRecord.POST("getServerInfo", systemApi.GetServerInfo) // 获取服务器信息
+ }
+}
diff --git a/server/router/system/sys_user.go b/server/router/system/sys_user.go
new file mode 100644
index 0000000..a7dda19
--- /dev/null
+++ b/server/router/system/sys_user.go
@@ -0,0 +1,28 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/middleware"
+ "github.com/gin-gonic/gin"
+)
+
+type UserRouter struct{}
+
+func (s *UserRouter) InitUserRouter(Router *gin.RouterGroup) {
+ userRouter := Router.Group("user").Use(middleware.OperationRecord())
+ userRouterWithoutRecord := Router.Group("user")
+ {
+ userRouter.POST("admin_register", baseApi.Register) // 管理员注册账号
+ userRouter.POST("changePassword", baseApi.ChangePassword) // 用户修改密码
+ userRouter.POST("setUserAuthority", baseApi.SetUserAuthority) // 设置用户权限
+ userRouter.DELETE("deleteUser", baseApi.DeleteUser) // 删除用户
+ userRouter.PUT("setUserInfo", baseApi.SetUserInfo) // 设置用户信息
+ userRouter.PUT("setSelfInfo", baseApi.SetSelfInfo) // 设置自身信息
+ userRouter.POST("setUserAuthorities", baseApi.SetUserAuthorities) // 设置用户权限组
+ userRouter.POST("resetPassword", baseApi.ResetPassword) // 重置用户密码
+ userRouter.PUT("setSelfSetting", baseApi.SetSelfSetting) // 用户界面配置
+ }
+ {
+ userRouterWithoutRecord.POST("getUserList", baseApi.GetUserList) // 分页获取用户列表
+ userRouterWithoutRecord.GET("getUserInfo", baseApi.GetUserInfo) // 获取自身信息
+ }
+}
diff --git a/server/router/system/sys_version.go b/server/router/system/sys_version.go
new file mode 100644
index 0000000..57cae64
--- /dev/null
+++ b/server/router/system/sys_version.go
@@ -0,0 +1,25 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/middleware"
+ "github.com/gin-gonic/gin"
+)
+
+type SysVersionRouter struct{}
+
+// InitSysVersionRouter 初始化 版本管理 路由信息
+func (s *SysVersionRouter) InitSysVersionRouter(Router *gin.RouterGroup) {
+ sysVersionRouter := Router.Group("sysVersion").Use(middleware.OperationRecord())
+ sysVersionRouterWithoutRecord := Router.Group("sysVersion")
+ {
+ sysVersionRouter.DELETE("deleteSysVersion", sysVersionApi.DeleteSysVersion) // 删除版本管理
+ sysVersionRouter.DELETE("deleteSysVersionByIds", sysVersionApi.DeleteSysVersionByIds) // 批量删除版本管理
+ sysVersionRouter.POST("exportVersion", sysVersionApi.ExportVersion) // 导出版本数据
+ sysVersionRouter.POST("importVersion", sysVersionApi.ImportVersion) // 导入版本数据
+ }
+ {
+ sysVersionRouterWithoutRecord.GET("findSysVersion", sysVersionApi.FindSysVersion) // 根据ID获取版本管理
+ sysVersionRouterWithoutRecord.GET("getSysVersionList", sysVersionApi.GetSysVersionList) // 获取版本管理列表
+ sysVersionRouterWithoutRecord.GET("downloadVersionJson", sysVersionApi.DownloadVersionJson) // 下载版本JSON数据
+ }
+}
diff --git a/server/service/app/ai_config.go b/server/service/app/ai_config.go
new file mode 100644
index 0000000..660d5dd
--- /dev/null
+++ b/server/service/app/ai_config.go
@@ -0,0 +1,316 @@
+package app
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "time"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/app"
+ "git.echol.cn/loser/st/server/model/app/request"
+ "git.echol.cn/loser/st/server/model/app/response"
+ "gorm.io/datatypes"
+ "gorm.io/gorm"
+)
+
+type AIConfigService struct{}
+
+// CreateAIConfig 创建AI配置
+func (s *AIConfigService) CreateAIConfig(req *request.CreateAIConfigRequest) (*response.AIConfigResponse, error) {
+ // 序列化 JSON 字段
+ settingsJSON, _ := json.Marshal(req.Settings)
+
+ config := app.AIConfig{
+ Name: req.Name,
+ Provider: req.Provider,
+ BaseURL: req.BaseURL,
+ APIKey: req.APIKey,
+ DefaultModel: req.DefaultModel,
+ Settings: datatypes.JSON(settingsJSON),
+ Models: datatypes.JSON("[]"),
+ IsActive: true,
+ IsDefault: false,
+ }
+
+ err := global.GVA_DB.Create(&config).Error
+ if err != nil {
+ return nil, err
+ }
+
+ resp := response.ToAIConfigResponse(&config)
+ return &resp, nil
+}
+
+// GetAIConfigList 获取AI配置列表
+func (s *AIConfigService) GetAIConfigList() (*response.AIConfigListResponse, error) {
+ var configs []app.AIConfig
+ var total int64
+
+ db := global.GVA_DB.Model(&app.AIConfig{})
+
+ err := db.Count(&total).Error
+ if err != nil {
+ return nil, err
+ }
+
+ err = db.Order("is_default DESC, created_at DESC").Find(&configs).Error
+ if err != nil {
+ return nil, err
+ }
+
+ list := make([]response.AIConfigResponse, len(configs))
+ for i, config := range configs {
+ list[i] = response.ToAIConfigResponse(&config)
+ }
+
+ return &response.AIConfigListResponse{
+ List: list,
+ Total: total,
+ }, nil
+}
+
+// UpdateAIConfig 更新AI配置
+func (s *AIConfigService) UpdateAIConfig(id uint, req *request.UpdateAIConfigRequest) error {
+ var config app.AIConfig
+
+ err := global.GVA_DB.First(&config, id).Error
+ if err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return errors.New("配置不存在")
+ }
+ return err
+ }
+
+ updates := map[string]interface{}{}
+
+ if req.Name != "" {
+ updates["name"] = req.Name
+ }
+ if req.BaseURL != "" {
+ updates["base_url"] = req.BaseURL
+ }
+ // 只有当 API Key 不是脱敏格式时才更新
+ // 脱敏格式: xxxx****xxxx
+ if req.APIKey != "" && !isMaskedAPIKey(req.APIKey) {
+ updates["api_key"] = req.APIKey
+ }
+ if req.DefaultModel != "" {
+ updates["default_model"] = req.DefaultModel
+ }
+ if req.Settings != nil {
+ settingsJSON, _ := json.Marshal(req.Settings)
+ updates["settings"] = datatypes.JSON(settingsJSON)
+ }
+ if req.IsActive != nil {
+ updates["is_active"] = *req.IsActive
+ }
+ if req.IsDefault != nil && *req.IsDefault {
+ // 如果设置为默认,先取消其他配置的默认状态
+ global.GVA_DB.Model(&app.AIConfig{}).Where("id != ?", id).Update("is_default", false)
+ updates["is_default"] = true
+ }
+
+ return global.GVA_DB.Model(&config).Updates(updates).Error
+}
+
+// DeleteAIConfig 删除AI配置
+func (s *AIConfigService) DeleteAIConfig(id uint) error {
+ result := global.GVA_DB.Delete(&app.AIConfig{}, id)
+ if result.Error != nil {
+ return result.Error
+ }
+ if result.RowsAffected == 0 {
+ return errors.New("配置不存在")
+ }
+ return nil
+}
+
+// GetModels 获取可用模型列表
+func (s *AIConfigService) GetModels(req *request.GetModelsRequest) (*response.GetModelsResponse, error) {
+ client := &http.Client{Timeout: 20 * time.Second}
+
+ // 构建请求
+ modelsURL := req.BaseURL + "/models"
+ httpReq, err := http.NewRequest("GET", modelsURL, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ // 设置认证头
+ if req.Provider == "openai" || req.Provider == "custom" {
+ httpReq.Header.Set("Authorization", "Bearer "+req.APIKey)
+ } else if req.Provider == "anthropic" {
+ httpReq.Header.Set("x-api-key", req.APIKey)
+ httpReq.Header.Set("anthropic-version", "2023-06-01")
+ }
+
+ // 发送请求
+ resp, err := client.Do(httpReq)
+ if err != nil {
+ return nil, fmt.Errorf("请求失败: %v", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ return nil, fmt.Errorf("API返回错误 %d: %s", resp.StatusCode, string(body))
+ }
+
+ // 解析响应
+ var result struct {
+ Data []struct {
+ ID string `json:"id"`
+ Object string `json:"object"`
+ OwnedBy string `json:"owned_by"`
+ } `json:"data"`
+ }
+
+ err = json.NewDecoder(resp.Body).Decode(&result)
+ if err != nil {
+ return nil, fmt.Errorf("解析响应失败: %v", err)
+ }
+
+ // 转换为模型信息
+ models := make([]response.ModelInfo, 0, len(result.Data))
+ for _, model := range result.Data {
+ models = append(models, response.ModelInfo{
+ ID: model.ID,
+ Name: model.ID,
+ OwnedBy: model.OwnedBy,
+ })
+ }
+
+ return &response.GetModelsResponse{Models: models}, nil
+}
+
+// GetModelsByID 通过配置ID获取模型列表(使用数据库中的完整API Key)
+func (s *AIConfigService) GetModelsByID(id uint) (*response.GetModelsResponse, error) {
+ var config app.AIConfig
+ err := global.GVA_DB.First(&config, id).Error
+ if err != nil {
+ return nil, errors.New("配置不存在")
+ }
+
+ // 使用数据库中的完整 API Key
+ req := &request.GetModelsRequest{
+ Provider: config.Provider,
+ BaseURL: config.BaseURL,
+ APIKey: config.APIKey,
+ }
+
+ return s.GetModels(req)
+}
+
+// TestAIConfig 测试AI配置
+func (s *AIConfigService) TestAIConfig(req *request.TestAIConfigRequest) (*response.TestAIConfigResponse, error) {
+ startTime := time.Now()
+ client := &http.Client{Timeout: 60 * time.Second}
+
+ // 构建测试请求
+ var requestBody map[string]interface{}
+ var endpoint string
+
+ if req.Provider == "openai" || req.Provider == "custom" {
+ endpoint = req.BaseURL + "/chat/completions"
+ model := req.Model
+ if model == "" {
+ model = "gpt-3.5-turbo"
+ }
+ requestBody = map[string]interface{}{
+ "model": model,
+ "messages": []map[string]string{
+ {"role": "user", "content": "Hello"},
+ },
+ "max_tokens": 10,
+ }
+ } else if req.Provider == "anthropic" {
+ endpoint = req.BaseURL + "/messages"
+ model := req.Model
+ if model == "" {
+ model = "claude-3-haiku-20240307"
+ }
+ requestBody = map[string]interface{}{
+ "model": model,
+ "messages": []map[string]string{
+ {"role": "user", "content": "Hello"},
+ },
+ "max_tokens": 10,
+ }
+ }
+
+ bodyBytes, _ := json.Marshal(requestBody)
+ httpReq, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(bodyBytes))
+ if err != nil {
+ return &response.TestAIConfigResponse{
+ Success: false,
+ Message: fmt.Sprintf("创建请求失败: %v", err),
+ }, nil
+ }
+
+ // 设置请求头
+ httpReq.Header.Set("Content-Type", "application/json")
+ if req.Provider == "openai" || req.Provider == "custom" {
+ httpReq.Header.Set("Authorization", "Bearer "+req.APIKey)
+ } else if req.Provider == "anthropic" {
+ httpReq.Header.Set("x-api-key", req.APIKey)
+ httpReq.Header.Set("anthropic-version", "2023-06-01")
+ }
+
+ // 发送请求
+ resp, err := client.Do(httpReq)
+ latency := time.Since(startTime).Milliseconds()
+
+ if err != nil {
+ return &response.TestAIConfigResponse{
+ Success: false,
+ Message: fmt.Sprintf("连接失败: %v", err),
+ Latency: latency,
+ }, nil
+ }
+ defer resp.Body.Close()
+
+ body, _ := io.ReadAll(resp.Body)
+
+ if resp.StatusCode != http.StatusOK {
+ return &response.TestAIConfigResponse{
+ Success: false,
+ Message: fmt.Sprintf("API返回错误 %d: %s", resp.StatusCode, string(body)),
+ Latency: latency,
+ }, nil
+ }
+
+ return &response.TestAIConfigResponse{
+ Success: true,
+ Message: "连接成功,AI响应正常",
+ Latency: latency,
+ }, nil
+}
+
+// TestAIConfigByID 通过ID测试AI配置(使用数据库中的完整API Key)
+func (s *AIConfigService) TestAIConfigByID(id uint) (*response.TestAIConfigResponse, error) {
+ var config app.AIConfig
+ err := global.GVA_DB.First(&config, id).Error
+ if err != nil {
+ return nil, errors.New("配置不存在")
+ }
+
+ // 使用数据库中的完整 API Key 进行测试
+ req := &request.TestAIConfigRequest{
+ Provider: config.Provider,
+ BaseURL: config.BaseURL,
+ APIKey: config.APIKey, // 使用完整的 API Key,而不是脱敏后的
+ Model: config.DefaultModel,
+ }
+
+ return s.TestAIConfig(req)
+}
+
+// isMaskedAPIKey 检查是否是脱敏的 API Key
+func isMaskedAPIKey(apiKey string) bool {
+ // 脱敏格式: xxxx****xxxx 或 ****
+ return len(apiKey) > 0 && (apiKey == "****" || (len(apiKey) > 8 && apiKey[4:len(apiKey)-4] == "****"))
+}
diff --git a/server/service/app/auth.go b/server/service/app/auth.go
new file mode 100644
index 0000000..8118401
--- /dev/null
+++ b/server/service/app/auth.go
@@ -0,0 +1,254 @@
+package app
+
+import (
+ "errors"
+ "time"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/app"
+ "git.echol.cn/loser/st/server/model/app/request"
+ "git.echol.cn/loser/st/server/model/app/response"
+ "git.echol.cn/loser/st/server/utils"
+ "github.com/google/uuid"
+ "golang.org/x/crypto/bcrypt"
+ "gorm.io/gorm"
+)
+
+type AuthService struct{}
+
+// Register 用户注册
+func (s *AuthService) Register(req *request.RegisterRequest) error {
+ // 检查用户名是否已存在
+ var count int64
+ err := global.GVA_DB.Model(&app.AppUser{}).Where("username = ?", req.Username).Count(&count).Error
+ if err != nil {
+ return err
+ }
+ if count > 0 {
+ return errors.New("用户名已存在")
+ }
+
+ // 检查邮箱是否已存在
+ if req.Email != "" {
+ err = global.GVA_DB.Model(&app.AppUser{}).Where("email = ?", req.Email).Count(&count).Error
+ if err != nil {
+ return err
+ }
+ if count > 0 {
+ return errors.New("邮箱已被使用")
+ }
+ }
+
+ // 密码加密
+ hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
+ if err != nil {
+ return errors.New("密码加密失败")
+ }
+
+ // 创建用户
+ user := app.AppUser{
+ UUID: uuid.New().String(),
+ Username: req.Username,
+ Password: string(hashedPassword),
+ NickName: req.NickName,
+ Email: req.Email,
+ Phone: req.Phone,
+ Status: "active",
+ Enable: true,
+ }
+
+ if user.NickName == "" {
+ user.NickName = req.Username
+ }
+
+ return global.GVA_DB.Create(&user).Error
+}
+
+// Login 用户登录
+func (s *AuthService) Login(req *request.LoginRequest, ip string) (*response.LoginResponse, error) {
+ // 查询用户
+ var user app.AppUser
+ err := global.GVA_DB.Where("username = ?", req.Username).First(&user).Error
+ if err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return nil, errors.New("用户名或密码错误")
+ }
+ return nil, err
+ }
+
+ // 检查用户状态
+ if !user.Enable {
+ return nil, errors.New("账户已被禁用")
+ }
+ if user.Status != "active" {
+ return nil, errors.New("账户状态异常")
+ }
+
+ // 验证密码
+ err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password))
+ if err != nil {
+ return nil, errors.New("用户名或密码错误")
+ }
+
+ // 生成 Token
+ token, expiresAt, err := utils.CreateAppToken(user.ID, user.Username)
+ if err != nil {
+ return nil, errors.New("Token 生成失败")
+ }
+
+ // 生成刷新 Token
+ refreshToken, refreshExpiresAt, err := utils.CreateAppRefreshToken(user.ID, user.Username)
+ if err != nil {
+ return nil, errors.New("刷新 Token 生成失败")
+ }
+
+ // 更新最后登录信息
+ now := time.Now()
+ global.GVA_DB.Model(&user).Updates(map[string]interface{}{
+ "last_login_at": now,
+ "last_login_ip": ip,
+ })
+
+ // 保存会话信息(可选)
+ session := app.AppUserSession{
+ UserID: user.ID,
+ SessionToken: token,
+ RefreshToken: refreshToken,
+ ExpiresAt: time.Unix(expiresAt, 0),
+ RefreshExpiresAt: func() *time.Time { t := time.Unix(refreshExpiresAt, 0); return &t }(),
+ IPAddress: ip,
+ }
+ global.GVA_DB.Create(&session)
+
+ return &response.LoginResponse{
+ User: response.ToAppUserResponse(&user),
+ Token: token,
+ RefreshToken: refreshToken,
+ ExpiresAt: expiresAt,
+ }, nil
+}
+
+// RefreshToken 刷新 Token
+func (s *AuthService) RefreshToken(req *request.RefreshTokenRequest) (*response.LoginResponse, error) {
+ // 解析刷新 Token
+ claims, err := utils.ParseAppToken(req.RefreshToken)
+ if err != nil {
+ return nil, errors.New("刷新 Token 无效")
+ }
+
+ // 查询用户
+ var user app.AppUser
+ err = global.GVA_DB.Where("id = ?", claims.UserID).First(&user).Error
+ if err != nil {
+ return nil, errors.New("用户不存在")
+ }
+
+ // 检查用户状态
+ if !user.Enable {
+ return nil, errors.New("账户已被禁用")
+ }
+
+ // 生成新的 Token
+ token, expiresAt, err := utils.CreateAppToken(user.ID, user.Username)
+ if err != nil {
+ return nil, errors.New("Token 生成失败")
+ }
+
+ // 生成新的刷新 Token
+ refreshToken, _, err := utils.CreateAppRefreshToken(user.ID, user.Username)
+ if err != nil {
+ return nil, errors.New("刷新 Token 生成失败")
+ }
+
+ return &response.LoginResponse{
+ User: response.ToAppUserResponse(&user),
+ Token: token,
+ RefreshToken: refreshToken,
+ ExpiresAt: expiresAt,
+ }, nil
+}
+
+// Logout 用户登出
+func (s *AuthService) Logout(userID uint, token string) error {
+ // 删除会话记录
+ return global.GVA_DB.Where("user_id = ? AND session_token = ?", userID, token).
+ Delete(&app.AppUserSession{}).Error
+}
+
+// GetUserInfo 获取用户信息
+func (s *AuthService) GetUserInfo(userID uint) (*response.AppUserResponse, error) {
+ var user app.AppUser
+ err := global.GVA_DB.Where("id = ?", userID).First(&user).Error
+ if err != nil {
+ return nil, err
+ }
+
+ resp := response.ToAppUserResponse(&user)
+ return &resp, nil
+}
+
+// UpdateProfile 更新用户信息
+func (s *AuthService) UpdateProfile(userID uint, req *request.UpdateProfileRequest) error {
+ updates := make(map[string]interface{})
+
+ if req.NickName != "" {
+ updates["nick_name"] = req.NickName
+ }
+ if req.Email != "" {
+ // 检查邮箱是否已被其他用户使用
+ var count int64
+ err := global.GVA_DB.Model(&app.AppUser{}).
+ Where("email = ? AND id != ?", req.Email, userID).
+ Count(&count).Error
+ if err != nil {
+ return err
+ }
+ if count > 0 {
+ return errors.New("邮箱已被使用")
+ }
+ updates["email"] = req.Email
+ }
+ if req.Phone != "" {
+ updates["phone"] = req.Phone
+ }
+ if req.Avatar != "" {
+ updates["avatar"] = req.Avatar
+ }
+ if req.Preferences != "" {
+ updates["preferences"] = req.Preferences
+ }
+ if req.AISettings != "" {
+ updates["ai_settings"] = req.AISettings
+ }
+
+ if len(updates) == 0 {
+ return nil
+ }
+
+ return global.GVA_DB.Model(&app.AppUser{}).Where("id = ?", userID).Updates(updates).Error
+}
+
+// ChangePassword 修改密码
+func (s *AuthService) ChangePassword(userID uint, req *request.ChangePasswordRequest) error {
+ // 查询用户
+ var user app.AppUser
+ err := global.GVA_DB.Where("id = ?", userID).First(&user).Error
+ if err != nil {
+ return err
+ }
+
+ // 验证旧密码
+ err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.OldPassword))
+ if err != nil {
+ return errors.New("原密码错误")
+ }
+
+ // 加密新密码
+ hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
+ if err != nil {
+ return errors.New("密码加密失败")
+ }
+
+ // 更新密码
+ return global.GVA_DB.Model(&user).Update("password", string(hashedPassword)).Error
+}
diff --git a/server/service/app/character.go b/server/service/app/character.go
new file mode 100644
index 0000000..9b58e99
--- /dev/null
+++ b/server/service/app/character.go
@@ -0,0 +1,366 @@
+package app
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "mime/multipart"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/app"
+ "git.echol.cn/loser/st/server/model/app/request"
+ "git.echol.cn/loser/st/server/model/app/response"
+ "git.echol.cn/loser/st/server/utils"
+ "gorm.io/datatypes"
+ "gorm.io/gorm"
+)
+
+type CharacterService struct{}
+
+// CreateCharacter 创建角色卡
+func (s *CharacterService) CreateCharacter(userID uint, req *request.CreateCharacterRequest) (*response.CharacterResponse, error) {
+ // 序列化 JSON 字段
+ tagsJSON, _ := json.Marshal(req.Tags)
+ alternateGreetingsJSON, _ := json.Marshal(req.AlternateGreetings)
+ characterBookJSON, _ := json.Marshal(req.CharacterBook)
+ extensionsJSON, _ := json.Marshal(req.Extensions)
+
+ character := app.AICharacter{
+ UserID: userID,
+ Name: req.Name,
+ Avatar: req.Avatar,
+ Creator: req.Creator,
+ Version: req.Version,
+ Description: req.Description,
+ Personality: req.Personality,
+ Scenario: req.Scenario,
+ FirstMes: req.FirstMes,
+ MesExample: req.MesExample,
+ CreatorNotes: req.CreatorNotes,
+ SystemPrompt: req.SystemPrompt,
+ PostHistoryInstructions: req.PostHistoryInstructions,
+ Tags: datatypes.JSON(tagsJSON),
+ AlternateGreetings: datatypes.JSON(alternateGreetingsJSON),
+ CharacterBook: datatypes.JSON(characterBookJSON),
+ Extensions: datatypes.JSON(extensionsJSON),
+ IsPublic: req.IsPublic,
+ Spec: "chara_card_v2",
+ SpecVersion: "2.0",
+ }
+
+ err := global.GVA_DB.Create(&character).Error
+ if err != nil {
+ return nil, err
+ }
+
+ resp := response.ToCharacterResponse(&character)
+ return &resp, nil
+}
+
+// GetCharacterList 获取角色卡列表
+func (s *CharacterService) GetCharacterList(userID uint, req *request.GetCharacterListRequest) (*response.CharacterListResponse, error) {
+ var characters []app.AICharacter
+ var total int64
+
+ db := global.GVA_DB.Model(&app.AICharacter{})
+
+ // 筛选条件
+ if req.IsPublic != nil {
+ if *req.IsPublic {
+ db = db.Where("is_public = ?", true)
+ } else {
+ db = db.Where("user_id = ?", userID)
+ }
+ } else {
+ db = db.Where("user_id = ? OR is_public = ?", userID, true)
+ }
+
+ // 关键词搜索
+ if req.Keyword != "" {
+ db = db.Where("name LIKE ? OR description LIKE ?", "%"+req.Keyword+"%", "%"+req.Keyword+"%")
+ }
+
+ // 标签筛选
+ if req.Tag != "" {
+ db = db.Where("tags @> ?", datatypes.JSON(`["`+req.Tag+`"]`))
+ }
+
+ // 统计总数
+ err := db.Count(&total).Error
+ if err != nil {
+ return nil, err
+ }
+
+ // 分页查询
+ offset := (req.Page - 1) * req.PageSize
+ err = db.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&characters).Error
+ if err != nil {
+ return nil, err
+ }
+
+ // 转换响应
+ list := make([]response.CharacterResponse, len(characters))
+ for i, char := range characters {
+ list[i] = response.ToCharacterResponse(&char)
+ }
+
+ return &response.CharacterListResponse{
+ List: list,
+ Total: total,
+ Page: req.Page,
+ PageSize: req.PageSize,
+ }, nil
+}
+
+// GetCharacterByID 获取角色卡详情
+func (s *CharacterService) GetCharacterByID(userID, characterID uint) (*response.CharacterResponse, error) {
+ var character app.AICharacter
+
+ err := global.GVA_DB.Where("id = ? AND (user_id = ? OR is_public = ?)", characterID, userID, true).
+ First(&character).Error
+ if err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return nil, errors.New("角色卡不存在或无权访问")
+ }
+ return nil, err
+ }
+
+ resp := response.ToCharacterResponse(&character)
+ return &resp, nil
+}
+
+// UpdateCharacter 更新角色卡
+func (s *CharacterService) UpdateCharacter(userID, characterID uint, req *request.UpdateCharacterRequest) error {
+ var character app.AICharacter
+
+ // 检查权限
+ err := global.GVA_DB.Where("id = ? AND user_id = ?", characterID, userID).First(&character).Error
+ if err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return errors.New("角色卡不存在或无权修改")
+ }
+ return err
+ }
+
+ // 更新字段
+ updates := map[string]interface{}{}
+
+ if req.Name != "" {
+ updates["name"] = req.Name
+ }
+ if req.Avatar != "" {
+ updates["avatar"] = req.Avatar
+ }
+ if req.Creator != "" {
+ updates["creator"] = req.Creator
+ }
+ if req.Version != "" {
+ updates["version"] = req.Version
+ }
+ if req.Description != "" {
+ updates["description"] = req.Description
+ }
+ if req.Personality != "" {
+ updates["personality"] = req.Personality
+ }
+ if req.Scenario != "" {
+ updates["scenario"] = req.Scenario
+ }
+ if req.FirstMes != "" {
+ updates["first_mes"] = req.FirstMes
+ }
+ if req.MesExample != "" {
+ updates["mes_example"] = req.MesExample
+ }
+ if req.CreatorNotes != "" {
+ updates["creator_notes"] = req.CreatorNotes
+ }
+ if req.SystemPrompt != "" {
+ updates["system_prompt"] = req.SystemPrompt
+ }
+ if req.PostHistoryInstructions != "" {
+ updates["post_history_instructions"] = req.PostHistoryInstructions
+ }
+
+ if req.Tags != nil {
+ tagsJSON, _ := json.Marshal(req.Tags)
+ updates["tags"] = datatypes.JSON(tagsJSON)
+ }
+ if req.AlternateGreetings != nil {
+ alternateGreetingsJSON, _ := json.Marshal(req.AlternateGreetings)
+ updates["alternate_greetings"] = datatypes.JSON(alternateGreetingsJSON)
+ }
+ if req.CharacterBook != nil {
+ characterBookJSON, _ := json.Marshal(req.CharacterBook)
+ updates["character_book"] = datatypes.JSON(characterBookJSON)
+ }
+ if req.Extensions != nil {
+ extensionsJSON, _ := json.Marshal(req.Extensions)
+ updates["extensions"] = datatypes.JSON(extensionsJSON)
+ }
+
+ updates["is_public"] = req.IsPublic
+
+ return global.GVA_DB.Model(&character).Updates(updates).Error
+}
+
+// DeleteCharacter 删除角色卡
+func (s *CharacterService) DeleteCharacter(userID, characterID uint) error {
+ result := global.GVA_DB.Where("id = ? AND user_id = ?", characterID, userID).Delete(&app.AICharacter{})
+ if result.Error != nil {
+ return result.Error
+ }
+ if result.RowsAffected == 0 {
+ return errors.New("角色卡不存在或无权删除")
+ }
+ return nil
+}
+
+// ImportCharacterFromPNG 从 PNG 文件导入角色卡
+func (s *CharacterService) ImportCharacterFromPNG(userID uint, file *multipart.FileHeader) (*response.CharacterResponse, error) {
+ // 读取文件内容
+ src, err := file.Open()
+ if err != nil {
+ return nil, errors.New("打开文件失败")
+ }
+ defer src.Close()
+
+ // 读取文件数据
+ fileData := make([]byte, file.Size)
+ _, err = src.Read(fileData)
+ if err != nil {
+ return nil, errors.New("读取文件失败")
+ }
+
+ // 提取角色卡数据
+ card, err := utils.ExtractCharacterFromPNG(fileData)
+ if err != nil {
+ return nil, err
+ }
+
+ // 上传 PNG 图片到 OSS(替代 Base64)
+ var uploadService UploadService
+ avatarURL, err := uploadService.UploadImage(file)
+ if err != nil {
+ // 如果上传失败,回退到 Base64(向后兼容)
+ avatarURL = "data:image/png;base64," + base64.StdEncoding.EncodeToString(fileData)
+ }
+
+ // 转换为创建请求
+ req := &request.CreateCharacterRequest{
+ Name: card.Data.Name,
+ Avatar: avatarURL,
+ Creator: card.Data.Creator,
+ Version: card.Data.CharacterVersion,
+ Description: card.Data.Description,
+ Personality: card.Data.Personality,
+ Scenario: card.Data.Scenario,
+ FirstMes: card.Data.FirstMes,
+ MesExample: card.Data.MesExample,
+ CreatorNotes: card.Data.CreatorNotes,
+ SystemPrompt: card.Data.SystemPrompt,
+ PostHistoryInstructions: card.Data.PostHistoryInstructions,
+ Tags: card.Data.Tags,
+ AlternateGreetings: card.Data.AlternateGreetings,
+ CharacterBook: card.Data.CharacterBook,
+ Extensions: card.Data.Extensions,
+ IsPublic: false,
+ }
+
+ return s.CreateCharacter(userID, req)
+}
+
+// ImportCharacterFromJSON 从 JSON 文件导入角色卡
+func (s *CharacterService) ImportCharacterFromJSON(userID uint, file *multipart.FileHeader) (*response.CharacterResponse, error) {
+ // 读取文件内容
+ src, err := file.Open()
+ if err != nil {
+ return nil, errors.New("打开文件失败")
+ }
+ defer src.Close()
+
+ // 读取文件数据
+ fileData := make([]byte, file.Size)
+ _, err = src.Read(fileData)
+ if err != nil {
+ return nil, errors.New("读取文件失败")
+ }
+
+ // 解析 JSON
+ card, err := utils.ParseCharacterCardJSON(fileData)
+ if err != nil {
+ return nil, err
+ }
+
+ // 转换为创建请求
+ req := &request.CreateCharacterRequest{
+ Name: card.Data.Name,
+ Creator: card.Data.Creator,
+ Version: card.Data.CharacterVersion,
+ Description: card.Data.Description,
+ Personality: card.Data.Personality,
+ Scenario: card.Data.Scenario,
+ FirstMes: card.Data.FirstMes,
+ MesExample: card.Data.MesExample,
+ CreatorNotes: card.Data.CreatorNotes,
+ SystemPrompt: card.Data.SystemPrompt,
+ PostHistoryInstructions: card.Data.PostHistoryInstructions,
+ Tags: card.Data.Tags,
+ AlternateGreetings: card.Data.AlternateGreetings,
+ CharacterBook: card.Data.CharacterBook,
+ Extensions: card.Data.Extensions,
+ IsPublic: false,
+ }
+
+ return s.CreateCharacter(userID, req)
+}
+
+// ExportCharacterToJSON 导出角色卡为 JSON
+func (s *CharacterService) ExportCharacterToJSON(userID, characterID uint) (*utils.CharacterCardV2, error) {
+ var character app.AICharacter
+
+ err := global.GVA_DB.Where("id = ? AND (user_id = ? OR is_public = ?)", characterID, userID, true).
+ First(&character).Error
+ if err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return nil, errors.New("角色卡不存在或无权访问")
+ }
+ return nil, err
+ }
+
+ // 解析 JSON 字段
+ var tags []string
+ var alternateGreetings []string
+ var characterBook map[string]interface{}
+ var extensions map[string]interface{}
+
+ json.Unmarshal(character.Tags, &tags)
+ json.Unmarshal(character.AlternateGreetings, &alternateGreetings)
+ json.Unmarshal(character.CharacterBook, &characterBook)
+ json.Unmarshal(character.Extensions, &extensions)
+
+ // 构建 V2 格式
+ card := &utils.CharacterCardV2{
+ Spec: character.Spec,
+ SpecVersion: character.SpecVersion,
+ Data: utils.CharacterCardV2Data{
+ Name: character.Name,
+ Description: character.Description,
+ Personality: character.Personality,
+ Scenario: character.Scenario,
+ FirstMes: character.FirstMes,
+ MesExample: character.MesExample,
+ CreatorNotes: character.CreatorNotes,
+ SystemPrompt: character.SystemPrompt,
+ PostHistoryInstructions: character.PostHistoryInstructions,
+ Tags: tags,
+ Creator: character.Creator,
+ CharacterVersion: character.Version,
+ AlternateGreetings: alternateGreetings,
+ CharacterBook: characterBook,
+ Extensions: extensions,
+ },
+ }
+
+ return card, nil
+}
diff --git a/server/service/app/conversation.go b/server/service/app/conversation.go
new file mode 100644
index 0000000..2ed3d6b
--- /dev/null
+++ b/server/service/app/conversation.go
@@ -0,0 +1,1172 @@
+package app
+
+import (
+ "bufio"
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+ "time"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/app"
+ "git.echol.cn/loser/st/server/model/app/request"
+ "git.echol.cn/loser/st/server/model/app/response"
+ "gorm.io/datatypes"
+ "gorm.io/gorm"
+)
+
+type ConversationService struct{}
+
+// CreateConversation 创建对话
+func (s *ConversationService) CreateConversation(userID uint, req *request.CreateConversationRequest) (*response.ConversationResponse, error) {
+ // 验证角色卡是否存在且有权访问
+ var character app.AICharacter
+ err := global.GVA_DB.Where("id = ? AND (user_id = ? OR is_public = ?)", req.CharacterID, userID, true).
+ First(&character).Error
+ if err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return nil, errors.New("角色卡不存在或无权访问")
+ }
+ return nil, err
+ }
+
+ // 生成对话标题
+ title := req.Title
+ if title == "" {
+ title = "与 " + character.Name + " 的对话"
+ }
+
+ // 获取默认 AI 配置
+ var aiConfig app.AIConfig
+ err = global.GVA_DB.Where("is_active = ?", true).
+ Order("is_default DESC, created_at DESC").
+ First(&aiConfig).Error
+
+ // 设置 AI 配置
+ aiProvider := req.AIProvider
+ model := req.Model
+
+ if err == nil {
+ // 如果找到了默认配置,使用它
+ if aiProvider == "" {
+ aiProvider = aiConfig.Provider
+ }
+ if model == "" {
+ model = aiConfig.DefaultModel
+ }
+ global.GVA_LOG.Info(fmt.Sprintf("创建对话使用 AI 配置: %s (Provider: %s, Model: %s)", aiConfig.Name, aiProvider, model))
+ } else {
+ // 如果没有找到配置,使用默认值
+ if aiProvider == "" {
+ aiProvider = "openai"
+ }
+ if model == "" {
+ model = "gpt-4"
+ }
+ global.GVA_LOG.Warn("未找到默认 AI 配置,使用硬编码默认值")
+ }
+
+ // 创建对话
+ conversation := app.Conversation{
+ UserID: userID,
+ CharacterID: req.CharacterID,
+ Title: title,
+ PresetID: req.PresetID,
+ AIProvider: aiProvider,
+ Model: model,
+ Settings: datatypes.JSON("{}"),
+ }
+
+ err = global.GVA_DB.Create(&conversation).Error
+ if err != nil {
+ return nil, err
+ }
+
+ // 如果角色有开场白,创建开场白消息
+ if character.FirstMes != "" {
+ firstMessage := app.Message{
+ ConversationID: conversation.ID,
+ Role: "assistant",
+ Content: character.FirstMes,
+ TokenCount: len(character.FirstMes) / 4,
+ }
+ err = global.GVA_DB.Create(&firstMessage).Error
+ if err != nil {
+ global.GVA_LOG.Warn(fmt.Sprintf("创建开场白消息失败: %v", err))
+ } else {
+ // 更新对话统计
+ conversation.MessageCount = 1
+ conversation.TokenCount = firstMessage.TokenCount
+ global.GVA_DB.Model(&conversation).Updates(map[string]interface{}{
+ "message_count": 1,
+ "token_count": firstMessage.TokenCount,
+ })
+ }
+ }
+
+ resp := response.ToConversationResponse(&conversation)
+ return &resp, nil
+}
+
+// GetConversationList 获取对话列表
+func (s *ConversationService) GetConversationList(userID uint, req *request.GetConversationListRequest) (*response.ConversationListResponse, error) {
+ var conversations []app.Conversation
+ var total int64
+
+ db := global.GVA_DB.Model(&app.Conversation{}).Where("user_id = ?", userID)
+
+ // 统计总数
+ err := db.Count(&total).Error
+ if err != nil {
+ return nil, err
+ }
+
+ // 分页查询
+ offset := (req.Page - 1) * req.PageSize
+ err = db.Order("updated_at DESC").Offset(offset).Limit(req.PageSize).Find(&conversations).Error
+ if err != nil {
+ return nil, err
+ }
+
+ // 收集所有角色ID
+ characterIDs := make([]uint, 0, len(conversations))
+ for _, conv := range conversations {
+ characterIDs = append(characterIDs, conv.CharacterID)
+ }
+
+ // 批量查询角色信息(只查询必要字段)
+ var characters []app.AICharacter
+ if len(characterIDs) > 0 {
+ err = global.GVA_DB.Select("id, name, avatar, description, created_at, updated_at").
+ Where("id IN ?", characterIDs).
+ Find(&characters).Error
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ // 创建角色ID到角色的映射
+ characterMap := make(map[uint]*app.AICharacter)
+ for i := range characters {
+ characterMap[characters[i].ID] = &characters[i]
+ }
+
+ // 转换响应(使用轻量级结构)
+ list := make([]response.ConversationListItemResponse, len(conversations))
+ for i, conv := range conversations {
+ character := characterMap[conv.CharacterID]
+ list[i] = response.ToConversationListItemResponse(&conv, character)
+ }
+
+ return &response.ConversationListResponse{
+ List: list,
+ Total: total,
+ Page: req.Page,
+ PageSize: req.PageSize,
+ }, nil
+}
+
+// GetConversationByID 获取对话详情
+func (s *ConversationService) GetConversationByID(userID, conversationID uint) (*response.ConversationResponse, error) {
+ var conversation app.Conversation
+
+ err := global.GVA_DB.Where("id = ? AND user_id = ?", conversationID, userID).
+ First(&conversation).Error
+ if err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return nil, errors.New("对话不存在或无权访问")
+ }
+ return nil, err
+ }
+
+ resp := response.ToConversationResponse(&conversation)
+ return &resp, nil
+}
+
+// UpdateConversationSettings 更新对话设置
+func (s *ConversationService) UpdateConversationSettings(userID, conversationID uint, settings map[string]interface{}) error {
+ var conversation app.Conversation
+ err := global.GVA_DB.Where("id = ? AND user_id = ?", conversationID, userID).First(&conversation).Error
+ if err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return errors.New("对话不存在或无权访问")
+ }
+ return err
+ }
+
+ // 序列化设置
+ settingsJSON, err := json.Marshal(settings)
+ if err != nil {
+ return err
+ }
+
+ return global.GVA_DB.Model(&conversation).Update("settings", datatypes.JSON(settingsJSON)).Error
+}
+
+// DeleteConversation 删除对话
+func (s *ConversationService) DeleteConversation(userID, conversationID uint) error {
+ // 开启事务
+ return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
+ // 删除对话的所有消息
+ err := tx.Where("conversation_id = ?", conversationID).Delete(&app.Message{}).Error
+ if err != nil {
+ return err
+ }
+
+ // 删除对话
+ result := tx.Where("id = ? AND user_id = ?", conversationID, userID).Delete(&app.Conversation{})
+ if result.Error != nil {
+ return result.Error
+ }
+ if result.RowsAffected == 0 {
+ return errors.New("对话不存在或无权删除")
+ }
+
+ return nil
+ })
+}
+
+// GetMessageList 获取消息列表
+func (s *ConversationService) GetMessageList(userID, conversationID uint, req *request.GetMessageListRequest) (*response.MessageListResponse, error) {
+ // 验证对话权限
+ var conversation app.Conversation
+ err := global.GVA_DB.Where("id = ? AND user_id = ?", conversationID, userID).
+ First(&conversation).Error
+ if err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return nil, errors.New("对话不存在或无权访问")
+ }
+ return nil, err
+ }
+
+ var messages []app.Message
+ var total int64
+
+ db := global.GVA_DB.Model(&app.Message{}).Where("conversation_id = ?", conversationID)
+
+ // 统计总数
+ err = db.Count(&total).Error
+ if err != nil {
+ return nil, err
+ }
+
+ // 分页查询
+ offset := (req.Page - 1) * req.PageSize
+ err = db.Order("created_at ASC").Offset(offset).Limit(req.PageSize).Find(&messages).Error
+ if err != nil {
+ return nil, err
+ }
+
+ // 转换响应
+ list := make([]response.MessageResponse, len(messages))
+ for i, msg := range messages {
+ list[i] = response.ToMessageResponse(&msg)
+ }
+
+ return &response.MessageListResponse{
+ List: list,
+ Total: total,
+ Page: req.Page,
+ PageSize: req.PageSize,
+ }, nil
+}
+
+// SendMessage 发送消息并获取 AI 回复
+func (s *ConversationService) SendMessage(userID, conversationID uint, req *request.SendMessageRequest) (*response.MessageResponse, error) {
+ // 验证对话权限
+ var conversation app.Conversation
+ err := global.GVA_DB.Where("id = ? AND user_id = ?", conversationID, userID).
+ First(&conversation).Error
+ if err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return nil, errors.New("对话不存在或无权访问")
+ }
+ return nil, err
+ }
+
+ // 获取角色卡信息
+ var character app.AICharacter
+ err = global.GVA_DB.Where("id = ?", conversation.CharacterID).First(&character).Error
+ if err != nil {
+ return nil, errors.New("角色卡不存在")
+ }
+
+ // 保存用户消息
+ userMessage := app.Message{
+ ConversationID: conversationID,
+ Role: "user",
+ Content: req.Content,
+ TokenCount: len(req.Content) / 4, // 简单估算
+ }
+
+ err = global.GVA_DB.Create(&userMessage).Error
+ if err != nil {
+ return nil, err
+ }
+
+ // 获取对话历史(最近10条)
+ var messages []app.Message
+ err = global.GVA_DB.Where("conversation_id = ?", conversationID).
+ Order("created_at DESC").
+ Limit(10).
+ Find(&messages).Error
+ if err != nil {
+ return nil, err
+ }
+
+ // 反转消息顺序(从旧到新)
+ for i, j := 0, len(messages)-1; i < j; i, j = i+1, j-1 {
+ messages[i], messages[j] = messages[j], messages[i]
+ }
+
+ // 调用 AI 服务获取回复
+ aiResponse, err := s.callAIService(conversation, character, messages)
+ if err != nil {
+ return nil, err
+ }
+
+ // 保存 AI 回复
+ assistantMessage := app.Message{
+ ConversationID: conversationID,
+ Role: "assistant",
+ Content: aiResponse,
+ TokenCount: len(aiResponse) / 4, // 简单估算
+ }
+
+ err = global.GVA_DB.Create(&assistantMessage).Error
+ if err != nil {
+ return nil, err
+ }
+
+ // 更新对话统计
+ err = global.GVA_DB.Model(&conversation).Updates(map[string]interface{}{
+ "message_count": gorm.Expr("message_count + ?", 2),
+ "token_count": gorm.Expr("token_count + ?", userMessage.TokenCount+assistantMessage.TokenCount),
+ }).Error
+ if err != nil {
+ return nil, err
+ }
+
+ resp := response.ToMessageResponse(&assistantMessage)
+ return &resp, nil
+}
+
+// callAIService 调用 AI 服务
+func (s *ConversationService) callAIService(conversation app.Conversation, character app.AICharacter, messages []app.Message) (string, error) {
+ // 获取 AI 配置
+ var aiConfig app.AIConfig
+ var err error
+
+ // 1. 尝试从对话设置中获取指定的 AI 配置 ID
+ var configID uint
+ if len(conversation.Settings) > 0 {
+ var settings map[string]interface{}
+ if err := json.Unmarshal(conversation.Settings, &settings); err == nil {
+ if id, ok := settings["aiConfigId"].(float64); ok {
+ configID = uint(id)
+ }
+ }
+ }
+
+ if configID > 0 {
+ // 使用用户指定的 AI 配置
+ global.GVA_LOG.Info(fmt.Sprintf("使用用户指定的 AI 配置 ID: %d", configID))
+ err = global.GVA_DB.Where("id = ? AND is_active = ?", configID, true).First(&aiConfig).Error
+ if err != nil {
+ global.GVA_LOG.Error(fmt.Sprintf("未找到指定的 AI 配置 ID: %d, 错误: %v", configID, err))
+ }
+ }
+
+ if err != nil || configID == 0 {
+ // 使用默认 AI 配置
+ global.GVA_LOG.Info("尝试使用默认 AI 配置")
+ err = global.GVA_DB.Where("is_active = ?", true).
+ Order("is_default DESC, created_at DESC").
+ First(&aiConfig).Error
+ if err != nil {
+ global.GVA_LOG.Error(fmt.Sprintf("未找到默认 AI 配置, 错误: %v", err))
+ }
+ }
+
+ if err != nil {
+ return "", errors.New("未找到可用的 AI 配置,请在管理后台添加并激活 AI 配置")
+ }
+
+ global.GVA_LOG.Info(fmt.Sprintf("使用 AI 配置: %s (Provider: %s, Model: %s)", aiConfig.Name, aiConfig.Provider, aiConfig.DefaultModel))
+
+ // 2. 尝试从对话设置中获取预设 ID 并加载预设参数
+ var preset *app.AIPreset
+ var presetID uint
+ if len(conversation.Settings) > 0 {
+ var settings map[string]interface{}
+ if err := json.Unmarshal(conversation.Settings, &settings); err == nil {
+ if id, ok := settings["presetId"].(float64); ok {
+ presetID = uint(id)
+ }
+ }
+ }
+
+ // 加载预设
+ if presetID > 0 {
+ var loadedPreset app.AIPreset
+ if err := global.GVA_DB.First(&loadedPreset, presetID).Error; err == nil {
+ preset = &loadedPreset
+ global.GVA_LOG.Info(fmt.Sprintf("使用预设: %s (Temperature: %.2f, TopP: %.2f)", preset.Name, preset.Temperature, preset.TopP))
+
+ // 增加预设使用次数
+ global.GVA_DB.Model(&preset).Update("use_count", gorm.Expr("use_count + ?", 1))
+ } else {
+ global.GVA_LOG.Warn(fmt.Sprintf("未找到预设 ID: %d, 使用默认参数", presetID))
+ }
+ }
+
+ // 构建系统提示词(如果预设有系统提示词,则追加到角色卡提示词后)
+ systemPrompt := s.buildSystemPrompt(character)
+ if preset != nil && preset.SystemPrompt != "" {
+ systemPrompt = systemPrompt + "\n\n" + preset.SystemPrompt
+ global.GVA_LOG.Info("已追加预设的系统提示词")
+ }
+
+ // 构建消息列表
+ apiMessages := s.buildAPIMessages(messages, systemPrompt)
+
+ // 打印发送给AI的完整内容
+ global.GVA_LOG.Info("========== 发送给AI的完整内容 ==========")
+ global.GVA_LOG.Info(fmt.Sprintf("系统提示词: %s", systemPrompt))
+ global.GVA_LOG.Info("消息列表:")
+ for i, msg := range apiMessages {
+ global.GVA_LOG.Info(fmt.Sprintf(" [%d] Role: %s, Content: %s", i, msg["role"], msg["content"]))
+ }
+ global.GVA_LOG.Info("==========================================")
+
+ // 确定使用的模型:如果用户在设置中指定了AI配置,则使用该配置的默认模型
+ // 否则使用对话创建时的模型(向后兼容)
+ model := aiConfig.DefaultModel
+ if model == "" {
+ // 如果AI配置没有默认模型,才使用对话表中的模型
+ model = conversation.Model
+ }
+ if model == "" {
+ // 最后的兜底
+ model = "gpt-4"
+ }
+
+ global.GVA_LOG.Info(fmt.Sprintf("使用模型: %s (来源: AI配置 %s)", model, aiConfig.Name))
+
+ // 根据提供商调用不同的 API
+ var aiResponse string
+
+ switch aiConfig.Provider {
+ case "openai", "custom":
+ aiResponse, err = s.callOpenAIAPI(&aiConfig, model, apiMessages, preset)
+ case "anthropic":
+ aiResponse, err = s.callAnthropicAPI(&aiConfig, model, apiMessages, systemPrompt, preset)
+ default:
+ return "", fmt.Errorf("不支持的 AI 提供商: %s", aiConfig.Provider)
+ }
+
+ // 打印AI返回的完整内容
+ if err != nil {
+ global.GVA_LOG.Error(fmt.Sprintf("========== AI返回错误 ==========\n%v\n==========================================", err))
+ return "", err
+ }
+ global.GVA_LOG.Info(fmt.Sprintf("========== AI返回的完整内容 ==========\n%s\n==========================================", aiResponse))
+
+ return aiResponse, nil
+}
+
+// buildSystemPrompt 构建系统提示词
+func (s *ConversationService) buildSystemPrompt(character app.AICharacter) string {
+ prompt := fmt.Sprintf("你是 %s。", character.Name)
+
+ if character.Description != "" {
+ prompt += fmt.Sprintf("\n\n描述:%s", character.Description)
+ }
+
+ if character.Personality != "" {
+ prompt += fmt.Sprintf("\n\n性格:%s", character.Personality)
+ }
+
+ if character.Scenario != "" {
+ prompt += fmt.Sprintf("\n\n场景:%s", character.Scenario)
+ }
+
+ if character.FirstMes != "" {
+ prompt += fmt.Sprintf("\n\n开场白:%s", character.FirstMes)
+ }
+
+ if character.MesExample != "" {
+ prompt += fmt.Sprintf("\n\n对话示例:\n%s", character.MesExample)
+ }
+
+ if character.SystemPrompt != "" {
+ prompt += fmt.Sprintf("\n\n系统提示:%s", character.SystemPrompt)
+ }
+
+ // 处理世界书 (Character Book)
+ if len(character.CharacterBook) > 0 {
+ var characterBook map[string]interface{}
+ if err := json.Unmarshal(character.CharacterBook, &characterBook); err == nil {
+ if entries, ok := characterBook["entries"].([]interface{}); ok && len(entries) > 0 {
+ prompt += "\n\n世界设定:"
+ for _, entry := range entries {
+ if entryMap, ok := entry.(map[string]interface{}); ok {
+ // 默认启用,除非明确设置为false
+ enabled := true
+ if enabledVal, ok := entryMap["enabled"].(bool); ok {
+ enabled = enabledVal
+ }
+ if !enabled {
+ continue
+ }
+ // 添加世界书条目内容
+ if content, ok := entryMap["content"].(string); ok && content != "" {
+ prompt += fmt.Sprintf("\n- %s", content)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ prompt += "\n\n请根据以上设定进行角色扮演,保持角色的性格和说话方式。"
+
+ // 应用MVU变量替换
+ prompt = s.applyMacroVariables(prompt, character)
+
+ return prompt
+}
+
+// applyMacroVariables 应用宏变量替换 (MVU功能)
+func (s *ConversationService) applyMacroVariables(text string, character app.AICharacter) string {
+ // 获取当前时间
+ now := time.Now()
+
+ // 基础变量
+ replacements := map[string]string{
+ "{{char}}": character.Name,
+ "{{user}}": "用户", // 可以从用户信息中获取
+ "{{time}}": now.Format("15:04"),
+ "{{date}}": now.Format("2006-01-02"),
+ "{{datetime}}": now.Format("2006-01-02 15:04:05"),
+ "{{weekday}}": s.getWeekdayInChinese(now.Weekday()),
+ "{{idle_duration}}": "0分钟",
+ }
+
+ // 执行替换
+ result := text
+ for macro, value := range replacements {
+ result = strings.ReplaceAll(result, macro, value)
+ }
+
+ return result
+}
+
+// getWeekdayInChinese 获取中文星期
+func (s *ConversationService) getWeekdayInChinese(weekday time.Weekday) string {
+ weekdays := map[time.Weekday]string{
+ time.Sunday: "星期日",
+ time.Monday: "星期一",
+ time.Tuesday: "星期二",
+ time.Wednesday: "星期三",
+ time.Thursday: "星期四",
+ time.Friday: "星期五",
+ time.Saturday: "星期六",
+ }
+ return weekdays[weekday]
+}
+
+// SendMessageStream 流式发送消息并获取 AI 回复
+func (s *ConversationService) SendMessageStream(userID, conversationID uint, req *request.SendMessageRequest, streamChan chan string, doneChan chan bool) error {
+ defer close(streamChan)
+ defer close(doneChan)
+
+ // 验证对话权限
+ var conversation app.Conversation
+ err := global.GVA_DB.Where("id = ? AND user_id = ?", conversationID, userID).
+ First(&conversation).Error
+ if err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return errors.New("对话不存在或无权访问")
+ }
+ return err
+ }
+
+ // 获取角色卡信息
+ var character app.AICharacter
+ err = global.GVA_DB.Where("id = ?", conversation.CharacterID).First(&character).Error
+ if err != nil {
+ return errors.New("角色卡不存在")
+ }
+
+ // 保存用户消息
+ userMessage := app.Message{
+ ConversationID: conversationID,
+ Role: "user",
+ Content: req.Content,
+ TokenCount: len(req.Content) / 4,
+ }
+
+ err = global.GVA_DB.Create(&userMessage).Error
+ if err != nil {
+ return err
+ }
+
+ // 获取对话历史(最近10条)
+ var messages []app.Message
+ err = global.GVA_DB.Where("conversation_id = ?", conversationID).
+ Order("created_at DESC").
+ Limit(10).
+ Find(&messages).Error
+ if err != nil {
+ return err
+ }
+
+ // 反转消息顺序(从旧到新)
+ for i, j := 0, len(messages)-1; i < j; i, j = i+1, j-1 {
+ messages[i], messages[j] = messages[j], messages[i]
+ }
+
+ // 获取 AI 配置
+ var aiConfig app.AIConfig
+ var configID uint
+ if len(conversation.Settings) > 0 {
+ var settings map[string]interface{}
+ if err := json.Unmarshal(conversation.Settings, &settings); err == nil {
+ if id, ok := settings["aiConfigId"].(float64); ok {
+ configID = uint(id)
+ }
+ }
+ }
+
+ if configID > 0 {
+ err = global.GVA_DB.Where("id = ? AND is_active = ?", configID, true).First(&aiConfig).Error
+ }
+
+ if err != nil || configID == 0 {
+ err = global.GVA_DB.Where("is_active = ?", true).
+ Order("is_default DESC, created_at DESC").
+ First(&aiConfig).Error
+ }
+
+ if err != nil {
+ return errors.New("未找到可用的 AI 配置")
+ }
+
+ // 构建系统提示词和消息列表
+ systemPrompt := s.buildSystemPrompt(character)
+ apiMessages := s.buildAPIMessages(messages, systemPrompt)
+
+ // 打印发送给AI的完整内容(流式传输)
+ global.GVA_LOG.Info("========== [流式传输] 发送给AI的完整内容 ==========")
+ global.GVA_LOG.Info(fmt.Sprintf("系统提示词: %s", systemPrompt))
+ global.GVA_LOG.Info("消息列表:")
+ for i, msg := range apiMessages {
+ global.GVA_LOG.Info(fmt.Sprintf(" [%d] Role: %s, Content: %s", i, msg["role"], msg["content"]))
+ }
+ global.GVA_LOG.Info("==========================================")
+
+ // 确定使用的模型
+ model := aiConfig.DefaultModel
+ if model == "" {
+ model = conversation.Model
+ }
+ if model == "" {
+ model = "gpt-4"
+ }
+
+ global.GVA_LOG.Info(fmt.Sprintf("[流式传输] 使用模型: %s (Provider: %s)", model, aiConfig.Provider))
+
+ // 调用流式 API
+ var fullContent string
+ switch aiConfig.Provider {
+ case "openai", "custom":
+ fullContent, err = s.callOpenAIAPIStream(&aiConfig, model, apiMessages, streamChan)
+ case "anthropic":
+ fullContent, err = s.callAnthropicAPIStream(&aiConfig, model, apiMessages, systemPrompt, streamChan)
+ default:
+ return fmt.Errorf("不支持的 AI 提供商: %s", aiConfig.Provider)
+ }
+
+ if err != nil {
+ global.GVA_LOG.Error(fmt.Sprintf("========== [流式传输] AI返回错误 ==========\n%v\n==========================================", err))
+ return err
+ }
+
+ // 打印AI返回的完整内容
+ global.GVA_LOG.Info(fmt.Sprintf("========== [流式传输] AI返回的完整内容 ==========\n%s\n==========================================", fullContent))
+
+ // 保存 AI 回复
+ assistantMessage := app.Message{
+ ConversationID: conversationID,
+ Role: "assistant",
+ Content: fullContent,
+ TokenCount: len(fullContent) / 4,
+ }
+
+ err = global.GVA_DB.Create(&assistantMessage).Error
+ if err != nil {
+ return err
+ }
+
+ // 更新对话统计
+ err = global.GVA_DB.Model(&conversation).Updates(map[string]interface{}{
+ "message_count": gorm.Expr("message_count + ?", 2),
+ "token_count": gorm.Expr("token_count + ?", userMessage.TokenCount+assistantMessage.TokenCount),
+ }).Error
+
+ doneChan <- true
+ return err
+}
+
+// callOpenAIAPIStream 调用 OpenAI API 流式传输
+func (s *ConversationService) callOpenAIAPIStream(config *app.AIConfig, model string, messages []map[string]string, streamChan chan string) (string, error) {
+ client := &http.Client{Timeout: 120 * time.Second}
+
+ if model == "" {
+ model = config.DefaultModel
+ }
+ if model == "" {
+ model = "gpt-4"
+ }
+
+ // 构建请求体,启用流式传输
+ requestBody := map[string]interface{}{
+ "model": model,
+ "messages": messages,
+ "temperature": 0.7,
+ "max_tokens": 2000,
+ "stream": true,
+ }
+
+ bodyBytes, err := json.Marshal(requestBody)
+ if err != nil {
+ return "", fmt.Errorf("序列化请求失败: %v", err)
+ }
+
+ endpoint := config.BaseURL + "/chat/completions"
+ req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(bodyBytes))
+ if err != nil {
+ return "", fmt.Errorf("创建请求失败: %v", err)
+ }
+
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Authorization", "Bearer "+config.APIKey)
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return "", fmt.Errorf("请求失败: %v", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ return "", fmt.Errorf("API 返回错误 %d: %s", resp.StatusCode, string(body))
+ }
+
+ // 读取流式响应
+ var fullContent strings.Builder
+ reader := bufio.NewReader(resp.Body)
+
+ for {
+ line, err := reader.ReadString('\n')
+ if err != nil {
+ if err == io.EOF {
+ break
+ }
+ return "", fmt.Errorf("读取流失败: %v", err)
+ }
+
+ line = strings.TrimSpace(line)
+ if line == "" || line == "data: [DONE]" {
+ continue
+ }
+
+ if strings.HasPrefix(line, "data: ") {
+ data := strings.TrimPrefix(line, "data: ")
+
+ var streamResp struct {
+ Choices []struct {
+ Delta struct {
+ Content string `json:"content"`
+ } `json:"delta"`
+ } `json:"choices"`
+ }
+
+ if err := json.Unmarshal([]byte(data), &streamResp); err != nil {
+ continue
+ }
+
+ if len(streamResp.Choices) > 0 {
+ content := streamResp.Choices[0].Delta.Content
+ if content != "" {
+ fullContent.WriteString(content)
+ streamChan <- content
+ }
+ }
+ }
+ }
+
+ return fullContent.String(), nil
+}
+
+// callAnthropicAPIStream 调用 Anthropic API 流式传输
+func (s *ConversationService) callAnthropicAPIStream(config *app.AIConfig, model string, messages []map[string]string, systemPrompt string, streamChan chan string) (string, error) {
+ client := &http.Client{Timeout: 120 * time.Second}
+
+ if model == "" {
+ model = config.DefaultModel
+ }
+ if model == "" {
+ model = "claude-3-sonnet-20240229"
+ }
+
+ // Anthropic API 不支持 system role
+ apiMessages := make([]map[string]string, 0)
+ for _, msg := range messages {
+ if msg["role"] != "system" {
+ apiMessages = append(apiMessages, msg)
+ }
+ }
+
+ requestBody := map[string]interface{}{
+ "model": model,
+ "messages": apiMessages,
+ "system": systemPrompt,
+ "max_tokens": 2000,
+ "stream": true,
+ }
+
+ bodyBytes, err := json.Marshal(requestBody)
+ if err != nil {
+ return "", fmt.Errorf("序列化请求失败: %v", err)
+ }
+
+ endpoint := config.BaseURL + "/messages"
+ req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(bodyBytes))
+ if err != nil {
+ return "", fmt.Errorf("创建请求失败: %v", err)
+ }
+
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("x-api-key", config.APIKey)
+ req.Header.Set("anthropic-version", "2023-06-01")
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return "", fmt.Errorf("请求失败: %v", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ return "", fmt.Errorf("API 返回错误 %d: %s", resp.StatusCode, string(body))
+ }
+
+ // 读取流式响应
+ var fullContent strings.Builder
+ reader := bufio.NewReader(resp.Body)
+
+ for {
+ line, err := reader.ReadString('\n')
+ if err != nil {
+ if err == io.EOF {
+ break
+ }
+ return "", fmt.Errorf("读取流失败: %v", err)
+ }
+
+ line = strings.TrimSpace(line)
+ if line == "" {
+ continue
+ }
+
+ if strings.HasPrefix(line, "data: ") {
+ data := strings.TrimPrefix(line, "data: ")
+
+ var streamResp struct {
+ Type string `json:"type"`
+ Delta struct {
+ Type string `json:"type"`
+ Text string `json:"text"`
+ } `json:"delta"`
+ }
+
+ if err := json.Unmarshal([]byte(data), &streamResp); err != nil {
+ continue
+ }
+
+ if streamResp.Type == "content_block_delta" && streamResp.Delta.Text != "" {
+ fullContent.WriteString(streamResp.Delta.Text)
+ streamChan <- streamResp.Delta.Text
+ }
+ }
+ }
+
+ return fullContent.String(), nil
+}
+
+func (s *ConversationService) buildAPIMessages(messages []app.Message, systemPrompt string) []map[string]string {
+ apiMessages := make([]map[string]string, 0, len(messages)+1)
+
+ // 添加系统消息(OpenAI 格式)
+ apiMessages = append(apiMessages, map[string]string{
+ "role": "system",
+ "content": systemPrompt,
+ })
+
+ // 添加历史消息
+ for _, msg := range messages {
+ if msg.Role == "system" {
+ continue // 跳过已有的系统消息
+ }
+ apiMessages = append(apiMessages, map[string]string{
+ "role": msg.Role,
+ "content": msg.Content,
+ })
+ }
+
+ return apiMessages
+}
+
+// callOpenAIAPI 调用 OpenAI API
+func (s *ConversationService) callOpenAIAPI(config *app.AIConfig, model string, messages []map[string]string, preset *app.AIPreset) (string, error) {
+ client := &http.Client{Timeout: 120 * time.Second}
+
+ // 使用配置的模型或默认模型
+ if model == "" {
+ model = config.DefaultModel
+ }
+ if model == "" {
+ model = "gpt-4"
+ }
+
+ // 应用预设参数(如果有预设)
+ temperature := 0.7
+ maxTokens := 2000
+ var topP *float64
+ var frequencyPenalty *float64
+ var presencePenalty *float64
+ var stopSequences []string
+
+ if preset != nil {
+ temperature = preset.Temperature
+ maxTokens = preset.MaxTokens
+ if preset.TopP > 0 {
+ topP = &preset.TopP
+ }
+ if preset.FrequencyPenalty != 0 {
+ frequencyPenalty = &preset.FrequencyPenalty
+ }
+ if preset.PresencePenalty != 0 {
+ presencePenalty = &preset.PresencePenalty
+ }
+ // 解析停止序列
+ if len(preset.StopSequences) > 0 {
+ json.Unmarshal(preset.StopSequences, &stopSequences)
+ }
+ global.GVA_LOG.Info(fmt.Sprintf("应用预设参数: Temperature=%.2f, MaxTokens=%d, TopP=%.2f", temperature, maxTokens, preset.TopP))
+ }
+
+ // 构建请求体
+ requestBody := map[string]interface{}{
+ "model": model,
+ "messages": messages,
+ "temperature": temperature,
+ "max_tokens": maxTokens,
+ }
+
+ // 添加可选参数
+ if topP != nil {
+ requestBody["top_p"] = *topP
+ }
+ if frequencyPenalty != nil {
+ requestBody["frequency_penalty"] = *frequencyPenalty
+ }
+ if presencePenalty != nil {
+ requestBody["presence_penalty"] = *presencePenalty
+ }
+ if len(stopSequences) > 0 {
+ requestBody["stop"] = stopSequences
+ }
+
+ bodyBytes, err := json.Marshal(requestBody)
+ if err != nil {
+ return "", fmt.Errorf("序列化请求失败: %v", err)
+ }
+
+ // 创建请求
+ endpoint := config.BaseURL + "/chat/completions"
+ req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(bodyBytes))
+ if err != nil {
+ return "", fmt.Errorf("创建请求失败: %v", err)
+ }
+
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Authorization", "Bearer "+config.APIKey)
+
+ // 发送请求
+ resp, err := client.Do(req)
+ if err != nil {
+ return "", fmt.Errorf("请求失败: %v", err)
+ }
+ defer resp.Body.Close()
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return "", fmt.Errorf("读取响应失败: %v", err)
+ }
+
+ if resp.StatusCode != http.StatusOK {
+ return "", fmt.Errorf("API 返回错误 %d: %s", resp.StatusCode, string(body))
+ }
+
+ // 解析响应
+ var result struct {
+ Choices []struct {
+ Message struct {
+ Content string `json:"content"`
+ } `json:"message"`
+ } `json:"choices"`
+ Error *struct {
+ Message string `json:"message"`
+ } `json:"error"`
+ }
+
+ err = json.Unmarshal(body, &result)
+ if err != nil {
+ return "", fmt.Errorf("解析响应失败: %v", err)
+ }
+
+ if result.Error != nil {
+ return "", fmt.Errorf("API 错误: %s", result.Error.Message)
+ }
+
+ if len(result.Choices) == 0 {
+ return "", errors.New("API 未返回任何回复")
+ }
+
+ return result.Choices[0].Message.Content, nil
+}
+
+// callAnthropicAPI 调用 Anthropic API
+func (s *ConversationService) callAnthropicAPI(config *app.AIConfig, model string, messages []map[string]string, systemPrompt string, preset *app.AIPreset) (string, error) {
+ client := &http.Client{Timeout: 120 * time.Second}
+
+ // 使用配置的模型或默认模型
+ if model == "" {
+ model = config.DefaultModel
+ }
+ if model == "" {
+ model = "claude-3-sonnet-20240229"
+ }
+
+ // Anthropic API 不支持 system role,需要单独传递
+ apiMessages := make([]map[string]string, 0)
+ for _, msg := range messages {
+ if msg["role"] != "system" {
+ apiMessages = append(apiMessages, msg)
+ }
+ }
+
+ // 应用预设参数(如果有预设)
+ maxTokens := 2000
+ var temperature *float64
+ var topP *float64
+ var stopSequences []string
+
+ if preset != nil {
+ maxTokens = preset.MaxTokens
+ if preset.Temperature > 0 {
+ temperature = &preset.Temperature
+ }
+ if preset.TopP > 0 {
+ topP = &preset.TopP
+ }
+ // 解析停止序列
+ if len(preset.StopSequences) > 0 {
+ json.Unmarshal(preset.StopSequences, &stopSequences)
+ }
+ global.GVA_LOG.Info(fmt.Sprintf("应用预设参数: Temperature=%.2f, MaxTokens=%d, TopP=%.2f", preset.Temperature, maxTokens, preset.TopP))
+ }
+
+ // 构建请求体
+ requestBody := map[string]interface{}{
+ "model": model,
+ "messages": apiMessages,
+ "system": systemPrompt,
+ "max_tokens": maxTokens,
+ }
+
+ // 添加可选参数
+ if temperature != nil {
+ requestBody["temperature"] = *temperature
+ }
+ if topP != nil {
+ requestBody["top_p"] = *topP
+ }
+ if len(stopSequences) > 0 {
+ requestBody["stop_sequences"] = stopSequences
+ }
+
+ bodyBytes, err := json.Marshal(requestBody)
+ if err != nil {
+ return "", fmt.Errorf("序列化请求失败: %v", err)
+ }
+
+ // 创建请求
+ endpoint := config.BaseURL + "/messages"
+ req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(bodyBytes))
+ if err != nil {
+ return "", fmt.Errorf("创建请求失败: %v", err)
+ }
+
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("x-api-key", config.APIKey)
+ req.Header.Set("anthropic-version", "2023-06-01")
+
+ // 发送请求
+ resp, err := client.Do(req)
+ if err != nil {
+ return "", fmt.Errorf("请求失败: %v", err)
+ }
+ defer resp.Body.Close()
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return "", fmt.Errorf("读取响应失败: %v", err)
+ }
+
+ if resp.StatusCode != http.StatusOK {
+ return "", fmt.Errorf("API 返回错误 %d: %s", resp.StatusCode, string(body))
+ }
+
+ // 解析响应
+ var result struct {
+ Content []struct {
+ Text string `json:"text"`
+ } `json:"content"`
+ Error *struct {
+ Message string `json:"message"`
+ } `json:"error"`
+ }
+
+ err = json.Unmarshal(body, &result)
+ if err != nil {
+ return "", fmt.Errorf("解析响应失败: %v", err)
+ }
+
+ if result.Error != nil {
+ return "", fmt.Errorf("API 错误: %s", result.Error.Message)
+ }
+
+ if len(result.Content) == 0 {
+ return "", errors.New("API 未返回任何回复")
+ }
+
+ return result.Content[0].Text, nil
+}
diff --git a/server/service/app/enter.go b/server/service/app/enter.go
new file mode 100644
index 0000000..e475060
--- /dev/null
+++ b/server/service/app/enter.go
@@ -0,0 +1,10 @@
+package app
+
+type AppServiceGroup struct {
+ AuthService
+ CharacterService
+ ConversationService
+ AIConfigService
+ PresetService
+ UploadService
+}
diff --git a/server/service/app/preset.go b/server/service/app/preset.go
new file mode 100644
index 0000000..057158b
--- /dev/null
+++ b/server/service/app/preset.go
@@ -0,0 +1,353 @@
+package app
+
+import (
+ "encoding/json"
+ "errors"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/app"
+ "git.echol.cn/loser/st/server/model/app/request"
+ "go.uber.org/zap"
+ "gorm.io/datatypes"
+ "gorm.io/gorm"
+)
+
+type PresetService struct{}
+
+// CreatePreset 创建预设
+func (s *PresetService) CreatePreset(userID uint, req *request.CreatePresetRequest) (*app.AIPreset, error) {
+ // 序列化 StopSequences
+ var stopSequencesJSON datatypes.JSON
+ if len(req.StopSequences) > 0 {
+ data, err := json.Marshal(req.StopSequences)
+ if err != nil {
+ global.GVA_LOG.Error("序列化 StopSequences 失败", zap.Error(err))
+ return nil, err
+ }
+ stopSequencesJSON = data
+ }
+
+ // 序列化 Extensions
+ var extensionsJSON datatypes.JSON
+ if len(req.Extensions) > 0 {
+ data, err := json.Marshal(req.Extensions)
+ if err != nil {
+ global.GVA_LOG.Error("序列化 Extensions 失败", zap.Error(err))
+ return nil, err
+ }
+ extensionsJSON = data
+ }
+
+ preset := &app.AIPreset{
+ UserID: userID,
+ Name: req.Name,
+ Description: req.Description,
+ IsPublic: req.IsPublic,
+ Temperature: req.Temperature,
+ TopP: req.TopP,
+ TopK: req.TopK,
+ FrequencyPenalty: req.FrequencyPenalty,
+ PresencePenalty: req.PresencePenalty,
+ MaxTokens: req.MaxTokens,
+ RepetitionPenalty: req.RepetitionPenalty,
+ MinP: req.MinP,
+ TopA: req.TopA,
+ SystemPrompt: req.SystemPrompt,
+ StopSequences: stopSequencesJSON,
+ Extensions: extensionsJSON,
+ }
+
+ if err := global.GVA_DB.Create(preset).Error; err != nil {
+ global.GVA_LOG.Error("创建预设失败", zap.Error(err))
+ return nil, err
+ }
+
+ return preset, nil
+}
+
+// GetPresetList 获取预设列表
+func (s *PresetService) GetPresetList(userID uint, req *request.GetPresetListRequest) ([]app.AIPreset, int64, error) {
+ var presets []app.AIPreset
+ var total int64
+
+ db := global.GVA_DB.Model(&app.AIPreset{})
+
+ // 权限过滤:只能看到自己的预设或公开的预设
+ db = db.Where("user_id = ? OR is_public = ?", userID, true)
+
+ // 关键词搜索
+ if req.Keyword != "" {
+ db = db.Where("name LIKE ? OR description LIKE ?", "%"+req.Keyword+"%", "%"+req.Keyword+"%")
+ }
+
+ // 公开/私有过滤
+ if req.IsPublic != nil {
+ db = db.Where("is_public = ?", *req.IsPublic)
+ }
+
+ // 获取总数
+ if err := db.Count(&total).Error; err != nil {
+ global.GVA_LOG.Error("获取预设总数失败", zap.Error(err))
+ return nil, 0, err
+ }
+
+ // 分页查询
+ offset := (req.Page - 1) * req.PageSize
+ if err := db.Order("is_default DESC, updated_at DESC").
+ Offset(offset).
+ Limit(req.PageSize).
+ Find(&presets).Error; err != nil {
+ global.GVA_LOG.Error("获取预设列表失败", zap.Error(err))
+ return nil, 0, err
+ }
+
+ return presets, total, nil
+}
+
+// GetPresetByID 根据ID获取预设
+func (s *PresetService) GetPresetByID(userID uint, id uint) (*app.AIPreset, error) {
+ var preset app.AIPreset
+
+ // 权限检查:只能访问自己的预设或公开的预设
+ if err := global.GVA_DB.Where("id = ? AND (user_id = ? OR is_public = ?)", id, userID, true).
+ First(&preset).Error; err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return nil, errors.New("预设不存在或无权访问")
+ }
+ global.GVA_LOG.Error("获取预设失败", zap.Error(err))
+ return nil, err
+ }
+
+ return &preset, nil
+}
+
+// UpdatePreset 更新预设
+func (s *PresetService) UpdatePreset(userID uint, id uint, req *request.UpdatePresetRequest) error {
+ var preset app.AIPreset
+
+ // 权限检查:只能更新自己的预设
+ if err := global.GVA_DB.Where("id = ? AND user_id = ?", id, userID).First(&preset).Error; err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return errors.New("预设不存在或无权修改")
+ }
+ global.GVA_LOG.Error("查询预设失败", zap.Error(err))
+ return err
+ }
+
+ // 构建更新数据
+ updates := make(map[string]interface{})
+
+ if req.Name != "" {
+ updates["name"] = req.Name
+ }
+ if req.Description != "" {
+ updates["description"] = req.Description
+ }
+ if req.IsPublic != nil {
+ updates["is_public"] = *req.IsPublic
+ }
+ if req.Temperature != nil {
+ updates["temperature"] = *req.Temperature
+ }
+ if req.TopP != nil {
+ updates["top_p"] = *req.TopP
+ }
+ if req.TopK != nil {
+ updates["top_k"] = *req.TopK
+ }
+ if req.FrequencyPenalty != nil {
+ updates["frequency_penalty"] = *req.FrequencyPenalty
+ }
+ if req.PresencePenalty != nil {
+ updates["presence_penalty"] = *req.PresencePenalty
+ }
+ if req.MaxTokens != nil {
+ updates["max_tokens"] = *req.MaxTokens
+ }
+ if req.RepetitionPenalty != nil {
+ updates["repetition_penalty"] = *req.RepetitionPenalty
+ }
+ if req.MinP != nil {
+ updates["min_p"] = *req.MinP
+ }
+ if req.TopA != nil {
+ updates["top_a"] = *req.TopA
+ }
+ if req.SystemPrompt != nil {
+ updates["system_prompt"] = *req.SystemPrompt
+ }
+
+ // 更新 StopSequences
+ if req.StopSequences != nil {
+ data, err := json.Marshal(req.StopSequences)
+ if err != nil {
+ global.GVA_LOG.Error("序列化 StopSequences 失败", zap.Error(err))
+ return err
+ }
+ updates["stop_sequences"] = datatypes.JSON(data)
+ }
+
+ // 更新 Extensions
+ if req.Extensions != nil {
+ data, err := json.Marshal(req.Extensions)
+ if err != nil {
+ global.GVA_LOG.Error("序列化 Extensions 失败", zap.Error(err))
+ return err
+ }
+ updates["extensions"] = datatypes.JSON(data)
+ }
+
+ if err := global.GVA_DB.Model(&preset).Updates(updates).Error; err != nil {
+ global.GVA_LOG.Error("更新预设失败", zap.Error(err))
+ return err
+ }
+
+ return nil
+}
+
+// DeletePreset 删除预设
+func (s *PresetService) DeletePreset(userID uint, id uint) error {
+ // 权限检查:只能删除自己的预设
+ result := global.GVA_DB.Where("id = ? AND user_id = ?", id, userID).Delete(&app.AIPreset{})
+ if result.Error != nil {
+ global.GVA_LOG.Error("删除预设失败", zap.Error(result.Error))
+ return result.Error
+ }
+
+ if result.RowsAffected == 0 {
+ return errors.New("预设不存在或无权删除")
+ }
+
+ return nil
+}
+
+// SetDefaultPreset 设置默认预设
+func (s *PresetService) SetDefaultPreset(userID uint, id uint) error {
+ return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
+ // 检查预设是否存在且属于当前用户
+ var preset app.AIPreset
+ if err := tx.Where("id = ? AND user_id = ?", id, userID).First(&preset).Error; err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return errors.New("预设不存在或无权访问")
+ }
+ return err
+ }
+
+ // 取消当前用户的所有默认预设
+ if err := tx.Model(&app.AIPreset{}).
+ Where("user_id = ? AND is_default = ?", userID, true).
+ Update("is_default", false).Error; err != nil {
+ return err
+ }
+
+ // 设置新的默认预设
+ if err := tx.Model(&preset).Update("is_default", true).Error; err != nil {
+ return err
+ }
+
+ return nil
+ })
+}
+
+// ImportPresetFromJSON 从JSON导入预设
+func (s *PresetService) ImportPresetFromJSON(userID uint, jsonData []byte, filename string) (*app.AIPreset, error) {
+ // 尝试解析为 SillyTavern 格式
+ var stPreset struct {
+ Temperature float64 `json:"temperature"`
+ TopP float64 `json:"top_p"`
+ TopK int `json:"top_k"`
+ FrequencyPenalty float64 `json:"frequency_penalty"`
+ PresencePenalty float64 `json:"presence_penalty"`
+ MaxTokens int `json:"openai_max_tokens"`
+ RepetitionPenalty float64 `json:"repetition_penalty"`
+ MinP float64 `json:"min_p"`
+ TopA float64 `json:"top_a"`
+ StopSequences []string `json:"stop_sequences"`
+ Prompts []map[string]interface{} `json:"prompts"`
+ PromptOrder []map[string]interface{} `json:"prompt_order"`
+ }
+
+ if err := json.Unmarshal(jsonData, &stPreset); err != nil {
+ global.GVA_LOG.Error("解析预设JSON失败", zap.Error(err))
+ return nil, errors.New("无效的预设格式")
+ }
+
+ // 从文件名提取预设名称(去掉 .json 后缀)
+ name := filename
+ if len(name) > 5 && name[len(name)-5:] == ".json" {
+ name = name[:len(name)-5]
+ }
+
+ // 构建 extensions 对象,包含 prompts 和 prompt_order
+ extensions := map[string]interface{}{
+ "prompts": stPreset.Prompts,
+ "prompt_order": stPreset.PromptOrder,
+ }
+
+ // 转换为创建请求
+ req := &request.CreatePresetRequest{
+ Name: name,
+ Description: "从 SillyTavern 导入",
+ Temperature: stPreset.Temperature,
+ TopP: stPreset.TopP,
+ TopK: stPreset.TopK,
+ FrequencyPenalty: stPreset.FrequencyPenalty,
+ PresencePenalty: stPreset.PresencePenalty,
+ MaxTokens: stPreset.MaxTokens,
+ RepetitionPenalty: stPreset.RepetitionPenalty,
+ MinP: stPreset.MinP,
+ TopA: stPreset.TopA,
+ SystemPrompt: "",
+ StopSequences: stPreset.StopSequences,
+ Extensions: extensions,
+ }
+
+ return s.CreatePreset(userID, req)
+}
+
+// ExportPresetToJSON 导出预设为JSON
+func (s *PresetService) ExportPresetToJSON(userID uint, id uint) ([]byte, error) {
+ preset, err := s.GetPresetByID(userID, id)
+ if err != nil {
+ return nil, err
+ }
+
+ // 解析 StopSequences
+ var stopSequences []string
+ if len(preset.StopSequences) > 0 {
+ json.Unmarshal(preset.StopSequences, &stopSequences)
+ }
+
+ // 解析 Extensions
+ var extensions map[string]interface{}
+ if len(preset.Extensions) > 0 {
+ json.Unmarshal(preset.Extensions, &extensions)
+ }
+
+ // 转换为 SillyTavern 格式
+ stPreset := map[string]interface{}{
+ "name": preset.Name,
+ "description": preset.Description,
+ "temperature": preset.Temperature,
+ "top_p": preset.TopP,
+ "top_k": preset.TopK,
+ "frequency_penalty": preset.FrequencyPenalty,
+ "presence_penalty": preset.PresencePenalty,
+ "max_tokens": preset.MaxTokens,
+ "repetition_penalty": preset.RepetitionPenalty,
+ "min_p": preset.MinP,
+ "top_a": preset.TopA,
+ "system_prompt": preset.SystemPrompt,
+ "stop_sequences": stopSequences,
+ "extensions": extensions,
+ }
+
+ return json.MarshalIndent(stPreset, "", " ")
+}
+
+// IncrementUseCount 增加使用次数
+func (s *PresetService) IncrementUseCount(id uint) error {
+ return global.GVA_DB.Model(&app.AIPreset{}).
+ Where("id = ?", id).
+ Update("use_count", gorm.Expr("use_count + ?", 1)).Error
+}
diff --git a/server/service/app/upload.go b/server/service/app/upload.go
new file mode 100644
index 0000000..2e2741b
--- /dev/null
+++ b/server/service/app/upload.go
@@ -0,0 +1,49 @@
+package app
+
+import (
+ "errors"
+ "mime/multipart"
+ "strings"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/utils/upload"
+ "go.uber.org/zap"
+)
+
+type UploadService struct{}
+
+// UploadImage 上传图片到 OSS
+// 返回图片的访问 URL
+func (s *UploadService) UploadImage(header *multipart.FileHeader) (string, error) {
+ // 验证文件类型
+ if !isImageFile(header.Filename) {
+ return "", errors.New("只支持图片格式:jpg, jpeg, png, gif, webp")
+ }
+
+ // 验证文件大小(限制 10MB)
+ if header.Size > 10*1024*1024 {
+ return "", errors.New("图片大小不能超过 10MB")
+ }
+
+ // 使用 OSS 上传
+ oss := upload.NewOss()
+ filePath, _, uploadErr := oss.UploadFile(header)
+ if uploadErr != nil {
+ global.GVA_LOG.Error("图片上传失败", zap.Error(uploadErr))
+ return "", errors.New("图片上传失败")
+ }
+
+ return filePath, nil
+}
+
+// isImageFile 检查是否为图片文件
+func isImageFile(filename string) bool {
+ ext := strings.ToLower(filename[strings.LastIndex(filename, ".")+1:])
+ imageExts := []string{"jpg", "jpeg", "png", "gif", "webp"}
+ for _, validExt := range imageExts {
+ if ext == validExt {
+ return true
+ }
+ }
+ return false
+}
diff --git a/server/service/enter.go b/server/service/enter.go
new file mode 100644
index 0000000..fd8640b
--- /dev/null
+++ b/server/service/enter.go
@@ -0,0 +1,15 @@
+package service
+
+import (
+ "git.echol.cn/loser/st/server/service/app"
+ "git.echol.cn/loser/st/server/service/example"
+ "git.echol.cn/loser/st/server/service/system"
+)
+
+var ServiceGroupApp = new(ServiceGroup)
+
+type ServiceGroup struct {
+ SystemServiceGroup system.ServiceGroup
+ ExampleServiceGroup example.ServiceGroup
+ AppServiceGroup app.AppServiceGroup
+}
diff --git a/server/service/example/enter.go b/server/service/example/enter.go
new file mode 100644
index 0000000..f7198da
--- /dev/null
+++ b/server/service/example/enter.go
@@ -0,0 +1,7 @@
+package example
+
+type ServiceGroup struct {
+ CustomerService
+ FileUploadAndDownloadService
+ AttachmentCategoryService
+}
diff --git a/server/service/example/exa_attachment_category.go b/server/service/example/exa_attachment_category.go
new file mode 100644
index 0000000..7e5c1de
--- /dev/null
+++ b/server/service/example/exa_attachment_category.go
@@ -0,0 +1,67 @@
+package example
+
+import (
+ "errors"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/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/server/service/example/exa_breakpoint_continue.go b/server/service/example/exa_breakpoint_continue.go
new file mode 100644
index 0000000..048bdef
--- /dev/null
+++ b/server/service/example/exa_breakpoint_continue.go
@@ -0,0 +1,71 @@
+package example
+
+import (
+ "errors"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/example"
+ "gorm.io/gorm"
+)
+
+type FileUploadAndDownloadService struct{}
+
+var FileUploadAndDownloadServiceApp = new(FileUploadAndDownloadService)
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@function: FindOrCreateFile
+//@description: 上传文件时检测当前文件属性,如果没有文件则创建,有则返回文件的当前切片
+//@param: fileMd5 string, fileName string, chunkTotal int
+//@return: file model.ExaFile, err error
+
+func (e *FileUploadAndDownloadService) FindOrCreateFile(fileMd5 string, fileName string, chunkTotal int) (file example.ExaFile, err error) {
+ var cfile example.ExaFile
+ cfile.FileMd5 = fileMd5
+ cfile.FileName = fileName
+ cfile.ChunkTotal = chunkTotal
+
+ if errors.Is(global.GVA_DB.Where("file_md5 = ? AND file_name = ? AND is_finish = ?", fileMd5, fileName, 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/server/service/example/exa_customer.go b/server/service/example/exa_customer.go
new file mode 100644
index 0000000..3e24325
--- /dev/null
+++ b/server/service/example/exa_customer.go
@@ -0,0 +1,87 @@
+package example
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common/request"
+ "git.echol.cn/loser/st/server/model/example"
+ "git.echol.cn/loser/st/server/model/system"
+ systemService "git.echol.cn/loser/st/server/service/system"
+)
+
+type CustomerService struct{}
+
+var CustomerServiceApp = new(CustomerService)
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@function: CreateExaCustomer
+//@description: 创建客户
+//@param: e model.ExaCustomer
+//@return: err error
+
+func (exa *CustomerService) CreateExaCustomer(e example.ExaCustomer) (err error) {
+ err = global.GVA_DB.Create(&e).Error
+ return err
+}
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@function: DeleteFileChunk
+//@description: 删除客户
+//@param: e model.ExaCustomer
+//@return: err error
+
+func (exa *CustomerService) DeleteExaCustomer(e example.ExaCustomer) (err error) {
+ err = global.GVA_DB.Delete(&e).Error
+ return err
+}
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@function: UpdateExaCustomer
+//@description: 更新客户
+//@param: e *model.ExaCustomer
+//@return: err error
+
+func (exa *CustomerService) UpdateExaCustomer(e *example.ExaCustomer) (err error) {
+ err = global.GVA_DB.Save(e).Error
+ return err
+}
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@function: GetExaCustomer
+//@description: 获取客户信息
+//@param: id uint
+//@return: customer model.ExaCustomer, err error
+
+func (exa *CustomerService) GetExaCustomer(id uint) (customer example.ExaCustomer, err error) {
+ err = global.GVA_DB.Where("id = ?", id).First(&customer).Error
+ return
+}
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@function: GetCustomerInfoList
+//@description: 分页获取客户列表
+//@param: sysUserAuthorityID string, info request.PageInfo
+//@return: list interface{}, total int64, err error
+
+func (exa *CustomerService) GetCustomerInfoList(sysUserAuthorityID uint, info request.PageInfo) (list interface{}, total int64, err error) {
+ limit := info.PageSize
+ offset := info.PageSize * (info.Page - 1)
+ db := global.GVA_DB.Model(&example.ExaCustomer{})
+ var a system.SysAuthority
+ a.AuthorityId = sysUserAuthorityID
+ auth, err := systemService.AuthorityServiceApp.GetAuthorityInfo(a)
+ if err != nil {
+ return
+ }
+ var dataId []uint
+ for _, v := range auth.DataAuthorityId {
+ dataId = append(dataId, v.AuthorityId)
+ }
+ var CustomerList []example.ExaCustomer
+ err = db.Where("sys_user_authority_id in ?", dataId).Count(&total).Error
+ if err != nil {
+ return CustomerList, total, err
+ } else {
+ err = db.Limit(limit).Offset(offset).Preload("SysUser").Where("sys_user_authority_id in ?", dataId).Find(&CustomerList).Error
+ }
+ return CustomerList, total, err
+}
diff --git a/server/service/example/exa_file_upload_download.go b/server/service/example/exa_file_upload_download.go
new file mode 100644
index 0000000..2ca37d6
--- /dev/null
+++ b/server/service/example/exa_file_upload_download.go
@@ -0,0 +1,130 @@
+package example
+
+import (
+ "errors"
+ "mime/multipart"
+ "strings"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/example"
+ "git.echol.cn/loser/st/server/model/example/request"
+ "git.echol.cn/loser/st/server/utils/upload"
+ "gorm.io/gorm"
+)
+
+//@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" {
+ // 检查是否已存在相同key的记录
+ var existingFile example.ExaFileUploadAndDownload
+ err = global.GVA_DB.Where(&example.ExaFileUploadAndDownload{Key: key}).First(&existingFile).Error
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return f, e.Upload(f)
+ }
+ return f, err
+ }
+ 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/server/service/system/auto_code_history.go b/server/service/system/auto_code_history.go
new file mode 100644
index 0000000..419d4a9
--- /dev/null
+++ b/server/service/system/auto_code_history.go
@@ -0,0 +1,218 @@
+package system
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "path"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "time"
+
+ "git.echol.cn/loser/st/server/utils/ast"
+ "github.com/pkg/errors"
+
+ "git.echol.cn/loser/st/server/global"
+ common "git.echol.cn/loser/st/server/model/common/request"
+ model "git.echol.cn/loser/st/server/model/system"
+ request "git.echol.cn/loser/st/server/model/system/request"
+ "git.echol.cn/loser/st/server/utils"
+
+ "go.uber.org/zap"
+)
+
+var AutocodeHistory = new(autoCodeHistory)
+
+type autoCodeHistory struct{}
+
+// Create 创建代码生成器历史记录
+// Author [SliverHorn](https://github.com/SliverHorn)
+// Author [songzhibin97](https://github.com/songzhibin97)
+func (s *autoCodeHistory) Create(ctx context.Context, info request.SysAutoHistoryCreate) error {
+ create := info.Create()
+ err := global.GVA_DB.WithContext(ctx).Create(&create).Error
+ if err != nil {
+ return errors.Wrap(err, "创建失败!")
+ }
+ return nil
+}
+
+// First 根据id获取代码生成器历史的数据
+// Author [SliverHorn](https://github.com/SliverHorn)
+// Author [songzhibin97](https://github.com/songzhibin97)
+func (s *autoCodeHistory) First(ctx context.Context, info common.GetById) (string, error) {
+ var meta string
+ err := global.GVA_DB.WithContext(ctx).Model(model.SysAutoCodeHistory{}).Where("id = ?", info.ID).Pluck("request", &meta).Error
+ if err != nil {
+ return "", errors.Wrap(err, "获取失败!")
+ }
+ return meta, nil
+}
+
+// Repeat 检测重复
+// Author [SliverHorn](https://github.com/SliverHorn)
+// Author [songzhibin97](https://github.com/songzhibin97)
+func (s *autoCodeHistory) Repeat(businessDB, structName, 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/server/service/system/auto_code_llm.go b/server/service/system/auto_code_llm.go
new file mode 100644
index 0000000..9bd7570
--- /dev/null
+++ b/server/service/system/auto_code_llm.go
@@ -0,0 +1,52 @@
+package system
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "strings"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common"
+ commonResp "git.echol.cn/loser/st/server/model/common/response"
+ "git.echol.cn/loser/st/server/utils/request"
+ "github.com/goccy/go-json"
+)
+
+// LLMAuto 调用大模型服务,返回生成结果数据
+// 入参为通用 JSONMap,需包含 mode(例如 ai/butler/eye/painter 等)以及业务 prompt/payload
+func (s *AutoCodeService) LLMAuto(ctx context.Context, llm common.JSONMap) (interface{}, error) {
+ if global.GVA_CONFIG.AutoCode.AiPath == "" {
+ return nil, errors.New("请先前往插件市场个人中心获取AiPath并填入config.yaml中")
+ }
+
+ // 构建调用路径:{AiPath} 中的 {FUNC} 由 mode 替换
+ mode := fmt.Sprintf("%v", llm["mode"]) // 统一转字符串,避免 nil 造成路径异常
+ path := strings.ReplaceAll(global.GVA_CONFIG.AutoCode.AiPath, "{FUNC}", mode)
+
+ res, err := request.HttpRequest(
+ path,
+ "POST",
+ nil,
+ nil,
+ llm,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("大模型生成失败: %w", err)
+ }
+ defer res.Body.Close()
+
+ var resStruct commonResp.Response
+ b, err := io.ReadAll(res.Body)
+ if err != nil {
+ return nil, fmt.Errorf("读取大模型响应失败: %w", err)
+ }
+ if err = json.Unmarshal(b, &resStruct); err != nil {
+ return nil, fmt.Errorf("解析大模型响应失败: %w", err)
+ }
+ if resStruct.Code == 7 { // 业务约定:7 表示模型生成失败
+ return nil, fmt.Errorf("大模型生成失败: %s", resStruct.Msg)
+ }
+ return resStruct.Data, nil
+}
diff --git a/server/service/system/auto_code_mcp.go b/server/service/system/auto_code_mcp.go
new file mode 100644
index 0000000..8c94d00
--- /dev/null
+++ b/server/service/system/auto_code_mcp.go
@@ -0,0 +1,46 @@
+package system
+
+import (
+ "context"
+ "os"
+ "path/filepath"
+ "text/template"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system/request"
+ "git.echol.cn/loser/st/server/utils"
+ "git.echol.cn/loser/st/server/utils/autocode"
+)
+
+func (s *autoCodeTemplate) CreateMcp(ctx context.Context, info request.AutoMcpTool) (toolFilePath string, err error) {
+ mcpTemplatePath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "resource", "mcp", "tools.tpl")
+ mcpToolPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "mcp")
+
+ var files *template.Template
+
+ templateName := filepath.Base(mcpTemplatePath)
+
+ files, err = template.New(templateName).Funcs(autocode.GetTemplateFuncMap()).ParseFiles(mcpTemplatePath)
+ if err != nil {
+ return
+ }
+
+ fileName := utils.HumpToUnderscore(info.Name)
+
+ toolFilePath = filepath.Join(mcpToolPath, fileName+".go")
+
+ f, err := os.Create(toolFilePath)
+ if err != nil {
+ return
+ }
+ defer f.Close()
+
+ // 执行模板,将内容写入文件
+ err = files.Execute(f, info)
+ if err != nil {
+ return
+ }
+
+ return
+
+}
diff --git a/server/service/system/auto_code_package.go b/server/service/system/auto_code_package.go
new file mode 100644
index 0000000..ab33d28
--- /dev/null
+++ b/server/service/system/auto_code_package.go
@@ -0,0 +1,743 @@
+package system
+
+import (
+ "context"
+ "fmt"
+ "go/token"
+ "os"
+ "path/filepath"
+ "strings"
+ "text/template"
+
+ "git.echol.cn/loser/st/server/global"
+ common "git.echol.cn/loser/st/server/model/common/request"
+ model "git.echol.cn/loser/st/server/model/system"
+ "git.echol.cn/loser/st/server/model/system/request"
+ "git.echol.cn/loser/st/server/utils"
+ "git.echol.cn/loser/st/server/utils/ast"
+ "git.echol.cn/loser/st/server/utils/autocode"
+ "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.New(filepath.Base(key)).Funcs(autocode.GetTemplateFuncMap()).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
+}
+
+// DeleteByNames
+// @author: [piexlmax](https://github.com/piexlmax)
+// @author: [SliverHorn](https://github.com/SliverHorn)
+func (s *autoCodePackage) DeleteByNames(ctx context.Context, names []string) error {
+ if len(names) == 0 {
+ return nil
+ }
+ err := global.GVA_DB.WithContext(ctx).Where("package_name IN ?", names).Delete(&model.SysAutoCodePackage{}).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,
+ "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())
+ }
+ }
+ }
+
+ var desc string
+ if len(dirNameMap) == 0 {
+ // 完全符合标准结构
+ desc = "系统自动读取" + pluginDir[i].Name() + "插件,使用前请确认是否为v2版本插件"
+ } else {
+ // 缺少某些结构,生成警告描述
+ var missingDirs []string
+ for dirName := range dirNameMap {
+ missingDirs = append(missingDirs, dirName)
+ }
+ desc = fmt.Sprintf("系统自动读取,但是缺少 %s 结构,不建议自动化和mcp使用", strings.Join(missingDirs, "、"))
+ }
+
+ pluginPackage := model.SysAutoCodePackage{
+ PackageName: pluginDir[i].Name(),
+ Template: "plugin",
+ Label: pluginDir[i].Name() + "插件",
+ Desc: desc,
+ 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...)
+ }
+
+ // 处理数据库存在但实体文件不存在的情况 - 删除数据库中对应的数据
+ existingPackageNames := make(map[string]bool)
+ // 收集所有存在的包名
+ for i := 0; i < len(server); i++ {
+ existingPackageNames[server[i].PackageName] = true
+ }
+ for i := 0; i < len(plugin); i++ {
+ existingPackageNames[plugin[i].PackageName] = true
+ }
+
+ // 找出需要删除的数据库记录
+ deleteEntityIDs := []uint{}
+ for i := 0; i < len(entities); i++ {
+ if !existingPackageNames[entities[i].PackageName] {
+ deleteEntityIDs = append(deleteEntityIDs, entities[i].ID)
+ }
+ }
+
+ // 删除数据库中不存在文件的记录
+ if len(deleteEntityIDs) > 0 {
+ err = global.GVA_DB.WithContext(ctx).Delete(&model.SysAutoCodePackage{}, deleteEntityIDs).Error
+ if err != nil {
+ return nil, errors.Wrap(err, "删除不存在的包记录失败!")
+ }
+ // 从返回结果中移除已删除的记录
+ filteredEntities := []model.SysAutoCodePackage{}
+ for i := 0; i < len(entities); i++ {
+ if existingPackageNames[entities[i].PackageName] {
+ filteredEntities = append(filteredEntities, entities[i])
+ }
+ }
+ entities = filteredEntities
+ }
+
+ 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 为预览代码生成器的代码
+ if entries[i].Name() == "mcp" {
+ continue
+ } // preview 为mcp生成器的代码
+ 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 != ".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, "plugin", "register.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 != ".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 != ".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")
+ dictionary := strings.Index(threeDirs[k].Name(), "dictionary")
+ if gen != -1 && api != -1 && menu != -1 && viper != -1 && plugin != -1 && config != -1 && router != -1 && hasGorm != -1 && response != -1 && dictionary != -1 {
+ return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件!", four)
+ }
+ if api != -1 || menu != -1 || viper != -1 || response != -1 || plugin != -1 || config != -1 || dictionary != -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 != ".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 != ".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 != ".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/server/service/system/auto_code_package_test.go b/server/service/system/auto_code_package_test.go
new file mode 100644
index 0000000..2dc459b
--- /dev/null
+++ b/server/service/system/auto_code_package_test.go
@@ -0,0 +1,108 @@
+package system
+
+import (
+ "context"
+ "reflect"
+ "testing"
+
+ model "git.echol.cn/loser/st/server/model/system"
+ "git.echol.cn/loser/st/server/model/system/request"
+)
+
+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
+ isPackage bool
+ }
+ 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",
+ },
+ isPackage: false,
+ },
+ 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, tt.args.isPackage)
+ 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/server/service/system/auto_code_plugin.go b/server/service/system/auto_code_plugin.go
new file mode 100644
index 0000000..e3172d2
--- /dev/null
+++ b/server/service/system/auto_code_plugin.go
@@ -0,0 +1,512 @@
+package system
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ goast "go/ast"
+ "go/parser"
+ "go/printer"
+ "go/token"
+ "io"
+ "mime/multipart"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system"
+ "git.echol.cn/loser/st/server/model/system/request"
+ pluginUtils "git.echol.cn/loser/st/server/plugin/plugin-tool/utils"
+ "git.echol.cn/loser/st/server/utils"
+ ast "git.echol.cn/loser/st/server/utils/ast"
+ "github.com/mholt/archives"
+ cp "github.com/otiai10/copy"
+ "github.com/pkg/errors"
+ "go.uber.org/zap"
+)
+
+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
+ }
+
+ // 将上传的文件内容复制到临时文件
+ // 使用io.Copy的好处:高效处理大文件,自动管理缓冲区,避免内存溢出
+ _, err = io.Copy(out, src)
+ if err != nil {
+ out.Close()
+ return -1, -1, err
+ }
+
+ // 立即关闭文件,确保数据写入磁盘并释放文件句柄
+ // 必须在解压前关闭,否则在Windows系统上会导致文件被占用无法解压
+ err = out.Close()
+ if err != nil {
+ return -1, -1, err
+ }
+
+ paths, err := utils.Unzip(GVAPLUGPINATH+file.Filename, GVAPLUGPINATH)
+ paths = filterFile(paths)
+ var webIndex = -1
+ var serverIndex = -1
+ webPlugin := ""
+ serverPlugin := ""
+ serverPackage := ""
+ serverRootName := ""
+
+ 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` {
+ if len(serverPlugin) == 0 {
+ serverPlugin = filepath.Join(pathArr[0], pathArr[1], pathArr[2], pathArr[3])
+ }
+ if serverRootName == "" && ln > 1 && pathArr[1] != "" {
+ serverRootName = pathArr[1]
+ }
+ if ln > 4 && serverPackage == "" && pathArr[4] != "" {
+ serverPackage = pathArr[4]
+ }
+ }
+ 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 {
+ if serverPackage == "" {
+ serverPackage = serverRootName
+ }
+ err = installation(serverPlugin, global.GVA_CONFIG.AutoCode.Server, global.GVA_CONFIG.AutoCode.Server)
+ if err != nil {
+ return webIndex, serverIndex, err
+ }
+ err = ensurePluginRegisterImport(serverPackage)
+ 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 ensurePluginRegisterImport(packageName string) error {
+ module := strings.TrimSpace(global.GVA_CONFIG.AutoCode.Module)
+ if module == "" {
+ return errors.New("autocode module is empty")
+ }
+ if packageName == "" {
+ return errors.New("plugin package is empty")
+ }
+
+ registerPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "register.go")
+ src, err := os.ReadFile(registerPath)
+ if err != nil {
+ return err
+ }
+ fileSet := token.NewFileSet()
+ astFile, err := parser.ParseFile(fileSet, registerPath, src, parser.ParseComments)
+ if err != nil {
+ return err
+ }
+
+ importPath := fmt.Sprintf("%s/plugin/%s", module, packageName)
+ if ast.CheckImport(astFile, importPath) {
+ return nil
+ }
+
+ importSpec := &goast.ImportSpec{
+ Name: goast.NewIdent("_"),
+ Path: &goast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", importPath)},
+ }
+ var importDecl *goast.GenDecl
+ for _, decl := range astFile.Decls {
+ genDecl, ok := decl.(*goast.GenDecl)
+ if !ok {
+ continue
+ }
+ if genDecl.Tok == token.IMPORT {
+ importDecl = genDecl
+ break
+ }
+ }
+ if importDecl == nil {
+ astFile.Decls = append([]goast.Decl{
+ &goast.GenDecl{
+ Tok: token.IMPORT,
+ Specs: []goast.Spec{importSpec},
+ },
+ }, astFile.Decls...)
+ } else {
+ importDecl.Specs = append(importDecl.Specs, importSpec)
+ }
+
+ var out []byte
+ bf := bytes.NewBuffer(out)
+ printer.Fprint(bf, fileSet, astFile)
+
+ return os.WriteFile(registerPath, bf.Bytes(), 0666)
+}
+
+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 := archives.FilesFromDisk(context.Background(), 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 := archives.CompressedArchive{
+ //Compression: archives.Gz{},
+ Archival: archives.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.Preload("Parameters").Preload("MenuBtn").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
+}
+
+func (s *autoCodePlugin) InitDictionary(dictInfo request.InitDictionary) (err error) {
+ dictPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", dictInfo.PlugName, "initialize", "dictionary.go")
+ src, err := os.ReadFile(dictPath)
+ if err != nil {
+ fmt.Println(err)
+ }
+ fileSet := token.NewFileSet()
+ astFile, err := parser.ParseFile(fileSet, "", src, 0)
+ arrayAst := ast.FindArray(astFile, "model", "SysDictionary")
+ var dictionaries []system.SysDictionary
+ err = global.GVA_DB.Preload("SysDictionaryDetails").Find(&dictionaries, "id in (?)", dictInfo.Dictionaries).Error
+ if err != nil {
+ return err
+ }
+ dictExpr := ast.CreateDictionaryStructAst(dictionaries)
+ arrayAst.Elts = *dictExpr
+
+ var out []byte
+ bf := bytes.NewBuffer(out)
+ printer.Fprint(bf, fileSet, astFile)
+
+ os.WriteFile(dictPath, bf.Bytes(), 0666)
+ return nil
+}
+
+func (s *autoCodePlugin) Remove(pluginName string, pluginType string) (err error) {
+ // 1. 删除前端代码
+ if pluginType == "web" || pluginType == "full" {
+ webDir := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Web, "plugin", pluginName)
+ err = os.RemoveAll(webDir)
+ if err != nil {
+ return errors.Wrap(err, "删除前端插件目录失败")
+ }
+ }
+
+ // 2. 删除后端代码
+ if pluginType == "server" || pluginType == "full" {
+ serverDir := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", pluginName)
+ err = os.RemoveAll(serverDir)
+ if err != nil {
+ return errors.Wrap(err, "删除后端插件目录失败")
+ }
+
+ // 移除注册
+ removePluginRegisterImport(pluginName)
+ }
+
+ // 通过utils 获取 api 菜单 字典
+ apis, menus, dicts := pluginUtils.GetPluginData(pluginName)
+
+ // 3. 删除菜单 (递归删除)
+ if len(menus) > 0 {
+ for _, menu := range menus {
+ var dbMenu system.SysBaseMenu
+ if err := global.GVA_DB.Where("name = ?", menu.Name).First(&dbMenu).Error; err == nil {
+ // 获取该菜单及其所有子菜单的ID
+ var menuIds []int
+ GetMenuIds(dbMenu, &menuIds)
+ // 逆序删除,先删除子菜单
+ for i := len(menuIds) - 1; i >= 0; i-- {
+ err := BaseMenuServiceApp.DeleteBaseMenu(menuIds[i])
+ if err != nil {
+ zap.L().Error("删除菜单失败", zap.Int("id", menuIds[i]), zap.Error(err))
+ }
+ }
+ }
+ }
+ }
+
+ // 4. 删除API
+ if len(apis) > 0 {
+ for _, api := range apis {
+ var dbApi system.SysApi
+ if err := global.GVA_DB.Where("path = ? AND method = ?", api.Path, api.Method).First(&dbApi).Error; err == nil {
+ err := ApiServiceApp.DeleteApi(dbApi)
+ if err != nil {
+ zap.L().Error("删除API失败", zap.String("path", api.Path), zap.Error(err))
+ }
+ }
+ }
+ }
+
+ // 5. 删除字典
+ if len(dicts) > 0 {
+ for _, dict := range dicts {
+ var dbDict system.SysDictionary
+ if err := global.GVA_DB.Where("type = ?", dict.Type).First(&dbDict).Error; err == nil {
+ err := DictionaryServiceApp.DeleteSysDictionary(dbDict)
+ if err != nil {
+ zap.L().Error("删除字典失败", zap.String("type", dict.Type), zap.Error(err))
+ }
+ }
+ }
+ }
+
+ return nil
+}
+
+func GetMenuIds(menu system.SysBaseMenu, ids *[]int) {
+ *ids = append(*ids, int(menu.ID))
+ var children []system.SysBaseMenu
+ global.GVA_DB.Where("parent_id = ?", menu.ID).Find(&children)
+ for _, child := range children {
+ // 先递归收集子菜单
+ GetMenuIds(child, ids)
+ }
+}
+
+func removePluginRegisterImport(packageName string) error {
+ module := strings.TrimSpace(global.GVA_CONFIG.AutoCode.Module)
+ if module == "" {
+ return errors.New("autocode module is empty")
+ }
+ if packageName == "" {
+ return errors.New("plugin package is empty")
+ }
+
+ registerPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "register.go")
+ src, err := os.ReadFile(registerPath)
+ if err != nil {
+ return err
+ }
+ fileSet := token.NewFileSet()
+ astFile, err := parser.ParseFile(fileSet, registerPath, src, parser.ParseComments)
+ if err != nil {
+ return err
+ }
+
+ importPath := fmt.Sprintf("%s/plugin/%s", module, packageName)
+ importLit := fmt.Sprintf("%q", importPath)
+
+ // 移除 import
+ var newDecls []goast.Decl
+ for _, decl := range astFile.Decls {
+ genDecl, ok := decl.(*goast.GenDecl)
+ if !ok {
+ newDecls = append(newDecls, decl)
+ continue
+ }
+ if genDecl.Tok == token.IMPORT {
+ var newSpecs []goast.Spec
+ for _, spec := range genDecl.Specs {
+ importSpec, ok := spec.(*goast.ImportSpec)
+ if !ok {
+ newSpecs = append(newSpecs, spec)
+ continue
+ }
+ if importSpec.Path.Value != importLit {
+ newSpecs = append(newSpecs, spec)
+ }
+ }
+ // 如果还有其他import,保留该 decl
+ if len(newSpecs) > 0 {
+ genDecl.Specs = newSpecs
+ newDecls = append(newDecls, genDecl)
+ }
+ } else {
+ newDecls = append(newDecls, decl)
+ }
+ }
+ astFile.Decls = newDecls
+
+ var out []byte
+ bf := bytes.NewBuffer(out)
+ printer.Fprint(bf, fileSet, astFile)
+
+ return os.WriteFile(registerPath, bf.Bytes(), 0666)
+}
diff --git a/server/service/system/auto_code_template.go b/server/service/system/auto_code_template.go
new file mode 100644
index 0000000..62f2383
--- /dev/null
+++ b/server/service/system/auto_code_template.go
@@ -0,0 +1,454 @@
+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/st/server/utils/autocode"
+
+ "git.echol.cn/loser/st/server/global"
+ model "git.echol.cn/loser/st/server/model/system"
+ "git.echol.cn/loser/st/server/model/system/request"
+ utilsAst "git.echol.cn/loser/st/server/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.New(filepath.Base(key)).Funcs(autocode.GetTemplateFuncMap()).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.New(filepath.Base(tempPath)).Funcs(autocode.GetTemplateFuncMap()).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/server/service/system/auto_code_template_test.go b/server/service/system/auto_code_template_test.go
new file mode 100644
index 0000000..12889a7
--- /dev/null
+++ b/server/service/system/auto_code_template_test.go
@@ -0,0 +1,85 @@
+package system
+
+import (
+ "context"
+ "encoding/json"
+ "reflect"
+ "testing"
+
+ "git.echol.cn/loser/st/server/model/system/request"
+)
+
+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/server/service/system/enter.go b/server/service/system/enter.go
new file mode 100644
index 0000000..6d68bb7
--- /dev/null
+++ b/server/service/system/enter.go
@@ -0,0 +1,29 @@
+package system
+
+type ServiceGroup struct {
+ JwtService
+ ApiService
+ MenuService
+ UserService
+ CasbinService
+ InitDBService
+ AutoCodeService
+ BaseMenuService
+ AuthorityService
+ DictionaryService
+ SystemConfigService
+ OperationRecordService
+ DictionaryDetailService
+ AuthorityBtnService
+ SysExportTemplateService
+ SysParamsService
+ SysVersionService
+ SkillsService
+ AutoCodePlugin autoCodePlugin
+ AutoCodePackage autoCodePackage
+ AutoCodeHistory autoCodeHistory
+ AutoCodeTemplate autoCodeTemplate
+ SysErrorService
+ LoginLogService
+ ApiTokenService
+}
diff --git a/server/service/system/jwt_black_list.go b/server/service/system/jwt_black_list.go
new file mode 100644
index 0000000..4954c0c
--- /dev/null
+++ b/server/service/system/jwt_black_list.go
@@ -0,0 +1,52 @@
+package system
+
+import (
+ "context"
+
+ "go.uber.org/zap"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system"
+)
+
+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: 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
+}
+
+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/server/service/system/sys_api.go b/server/service/system/sys_api.go
new file mode 100644
index 0000000..60ecd7b
--- /dev/null
+++ b/server/service/system/sys_api.go
@@ -0,0 +1,326 @@
+package system
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common/request"
+ "git.echol.cn/loser/st/server/model/system"
+ systemRes "git.echol.cn/loser/st/server/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/server/service/system/sys_api_token.go b/server/service/system/sys_api_token.go
new file mode 100644
index 0000000..6132731
--- /dev/null
+++ b/server/service/system/sys_api_token.go
@@ -0,0 +1,107 @@
+package system
+
+import (
+ "errors"
+ "time"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system"
+ sysReq "git.echol.cn/loser/st/server/model/system/request"
+ "git.echol.cn/loser/st/server/utils"
+ "github.com/golang-jwt/jwt/v5"
+)
+
+type ApiTokenService struct{}
+
+func (apiVersion *ApiTokenService) CreateApiToken(apiToken system.SysApiToken, days int) (string, error) {
+ var user system.SysUser
+ if err := global.GVA_DB.Where("id = ?", apiToken.UserID).First(&user).Error; err != nil {
+ return "", errors.New("用户不存在")
+ }
+
+ hasAuth := false
+ for _, auth := range user.Authorities {
+ if auth.AuthorityId == apiToken.AuthorityID {
+ hasAuth = true
+ break
+ }
+ }
+ if !hasAuth && user.AuthorityId != apiToken.AuthorityID {
+ return "", errors.New("用户不具备该角色权限")
+ }
+
+ j := &utils.JWT{SigningKey: []byte(global.GVA_CONFIG.JWT.SigningKey)} // 唯一不同的部分是过期时间
+
+ expireTime := time.Duration(days) * 24 * time.Hour
+ if days == -1 {
+ expireTime = 100 * 365 * 24 * time.Hour
+ }
+
+ bf, _ := utils.ParseDuration(global.GVA_CONFIG.JWT.BufferTime)
+
+ claims := sysReq.CustomClaims{
+ BaseClaims: sysReq.BaseClaims{
+ UUID: user.UUID,
+ ID: user.ID,
+ Username: user.Username,
+ NickName: user.NickName,
+ AuthorityId: apiToken.AuthorityID,
+ },
+ BufferTime: int64(bf / time.Second), // 缓冲时间
+ RegisteredClaims: jwt.RegisteredClaims{
+ Audience: jwt.ClaimStrings{"GVA"},
+ NotBefore: jwt.NewNumericDate(time.Now().Add(-1000)),
+ ExpiresAt: jwt.NewNumericDate(time.Now().Add(expireTime)),
+ Issuer: global.GVA_CONFIG.JWT.Issuer,
+ },
+ }
+
+ token, err := j.CreateToken(claims)
+ if err != nil {
+ return "", err
+ }
+
+ apiToken.Token = token
+ apiToken.Status = true
+ apiToken.ExpiresAt = time.Now().Add(expireTime)
+ err = global.GVA_DB.Create(&apiToken).Error
+ return token, err
+}
+
+func (apiVersion *ApiTokenService) GetApiTokenList(info sysReq.SysApiTokenSearch) (list []system.SysApiToken, total int64, err error) {
+ limit := info.PageSize
+ offset := info.PageSize * (info.Page - 1)
+ db := global.GVA_DB.Model(&system.SysApiToken{})
+
+ db = db.Preload("User")
+
+ if info.UserID != 0 {
+ db = db.Where("user_id = ?", info.UserID)
+ }
+ if info.Status != nil {
+ db = db.Where("status = ?", *info.Status)
+ }
+
+ err = db.Count(&total).Error
+ if err != nil {
+ return
+ }
+ err = db.Limit(limit).Offset(offset).Order("created_at desc").Find(&list).Error
+ return list, total, err
+}
+
+func (apiVersion *ApiTokenService) DeleteApiToken(id uint) error {
+ var apiToken system.SysApiToken
+ err := global.GVA_DB.First(&apiToken, id).Error
+ if err != nil {
+ return err
+ }
+
+ jwtService := JwtService{}
+ err = jwtService.JsonInBlacklist(system.JwtBlacklist{Jwt: apiToken.Token})
+ if err != nil {
+ return err
+ }
+
+ return global.GVA_DB.Model(&apiToken).Update("status", false).Error
+}
diff --git a/server/service/system/sys_authority.go b/server/service/system/sys_authority.go
new file mode 100644
index 0000000..5da40c4
--- /dev/null
+++ b/server/service/system/sys_authority.go
@@ -0,0 +1,333 @@
+package system
+
+import (
+ "errors"
+ "strconv"
+
+ systemReq "git.echol.cn/loser/st/server/model/system/request"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common/request"
+ "git.echol.cn/loser/st/server/model/system"
+ "git.echol.cn/loser/st/server/model/system/response"
+ "gorm.io/gorm"
+)
+
+var ErrRoleExistence = errors.New("存在相同角色id")
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@function: CreateAuthority
+//@description: 创建一个角色
+//@param: auth model.SysAuthority
+//@return: authority system.SysAuthority, err error
+
+type AuthorityService struct{}
+
+var AuthorityServiceApp = new(AuthorityService)
+
+func (authorityService *AuthorityService) CreateAuthority(auth system.SysAuthority) (authority system.SysAuthority, err error) {
+
+ if err = global.GVA_DB.Where("authority_id = ?", auth.AuthorityId).First(&system.SysAuthority{}).Error; !errors.Is(err, gorm.ErrRecordNotFound) {
+ return auth, ErrRoleExistence
+ }
+
+ e := global.GVA_DB.Transaction(func(tx *gorm.DB) error {
+
+ if err = tx.Create(&auth).Error; err != nil {
+ return err
+ }
+
+ auth.SysBaseMenus = systemReq.DefaultMenu()
+ if err = tx.Model(&auth).Association("SysBaseMenus").Replace(&auth.SysBaseMenus); err != nil {
+ return err
+ }
+ casbinInfos := systemReq.DefaultCasbin()
+ authorityId := strconv.Itoa(int(auth.AuthorityId))
+ rules := [][]string{}
+ for _, v := range casbinInfos {
+ rules = append(rules, []string{authorityId, v.Path, v.Method})
+ }
+ return CasbinServiceApp.AddPolicies(tx, rules)
+ })
+
+ return auth, e
+}
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@function: CopyAuthority
+//@description: 复制一个角色
+//@param: copyInfo response.SysAuthorityCopyResponse
+//@return: authority system.SysAuthority, err error
+
+func (authorityService *AuthorityService) CopyAuthority(adminAuthorityID uint, copyInfo response.SysAuthorityCopyResponse) (authority system.SysAuthority, err error) {
+ var authorityBox system.SysAuthority
+ if !errors.Is(global.GVA_DB.Where("authority_id = ?", copyInfo.Authority.AuthorityId).First(&authorityBox).Error, gorm.ErrRecordNotFound) {
+ return authority, ErrRoleExistence
+ }
+ copyInfo.Authority.Children = []system.SysAuthority{}
+ menus, err := MenuServiceApp.GetMenuAuthority(&request.GetAuthorityId{AuthorityId: copyInfo.OldAuthorityId})
+ if err != nil {
+ return
+ }
+ var baseMenu []system.SysBaseMenu
+ for _, v := range menus {
+ intNum := v.MenuId
+ v.SysBaseMenu.ID = uint(intNum)
+ baseMenu = append(baseMenu, v.SysBaseMenu)
+ }
+ copyInfo.Authority.SysBaseMenus = baseMenu
+ err = global.GVA_DB.Create(©Info.Authority).Error
+ if err != nil {
+ return
+ }
+
+ var btns []system.SysAuthorityBtn
+
+ err = global.GVA_DB.Find(&btns, "authority_id = ?", copyInfo.OldAuthorityId).Error
+ if err != nil {
+ return
+ }
+ if len(btns) > 0 {
+ for i := range btns {
+ btns[i].AuthorityId = copyInfo.Authority.AuthorityId
+ }
+ err = global.GVA_DB.Create(&btns).Error
+
+ if err != nil {
+ return
+ }
+ }
+ paths := CasbinServiceApp.GetPolicyPathByAuthorityId(copyInfo.OldAuthorityId)
+ err = CasbinServiceApp.UpdateCasbin(adminAuthorityID, copyInfo.Authority.AuthorityId, paths)
+ if err != nil {
+ _ = authorityService.DeleteAuthority(©Info.Authority)
+ }
+ return copyInfo.Authority, err
+}
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@function: UpdateAuthority
+//@description: 更改一个角色
+//@param: auth model.SysAuthority
+//@return: authority system.SysAuthority, err error
+
+func (authorityService *AuthorityService) UpdateAuthority(auth system.SysAuthority) (authority system.SysAuthority, err error) {
+ var oldAuthority system.SysAuthority
+ err = global.GVA_DB.Where("authority_id = ?", auth.AuthorityId).First(&oldAuthority).Error
+ if err != nil {
+ global.GVA_LOG.Debug(err.Error())
+ return system.SysAuthority{}, errors.New("查询角色数据失败")
+ }
+ err = global.GVA_DB.Model(&oldAuthority).Updates(&auth).Error
+ return auth, err
+}
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@function: DeleteAuthority
+//@description: 删除角色
+//@param: auth *model.SysAuthority
+//@return: err error
+
+func (authorityService *AuthorityService) DeleteAuthority(auth *system.SysAuthority) error {
+ if errors.Is(global.GVA_DB.Debug().Preload("Users").First(&auth).Error, gorm.ErrRecordNotFound) {
+ return errors.New("该角色不存在")
+ }
+ if len(auth.Users) != 0 {
+ return errors.New("此角色有用户正在使用禁止删除")
+ }
+ if !errors.Is(global.GVA_DB.Where("authority_id = ?", auth.AuthorityId).First(&system.SysUser{}).Error, gorm.ErrRecordNotFound) {
+ return errors.New("此角色有用户正在使用禁止删除")
+ }
+ if !errors.Is(global.GVA_DB.Where("parent_id = ?", auth.AuthorityId).First(&system.SysAuthority{}).Error, gorm.ErrRecordNotFound) {
+ return errors.New("此角色存在子角色不允许删除")
+ }
+
+ return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
+ var err error
+ if err = tx.Preload("SysBaseMenus").Preload("DataAuthorityId").Where("authority_id = ?", auth.AuthorityId).First(auth).Unscoped().Delete(auth).Error; err != nil {
+ return err
+ }
+
+ if len(auth.SysBaseMenus) > 0 {
+ if err = tx.Model(auth).Association("SysBaseMenus").Delete(auth.SysBaseMenus); err != nil {
+ return err
+ }
+ // err = db.Association("SysBaseMenus").Delete(&auth)
+ }
+ if len(auth.DataAuthorityId) > 0 {
+ if err = tx.Model(auth).Association("DataAuthorityId").Delete(auth.DataAuthorityId); err != nil {
+ return err
+ }
+ }
+
+ if err = tx.Delete(&system.SysUserAuthority{}, "sys_authority_authority_id = ?", auth.AuthorityId).Error; err != nil {
+ return err
+ }
+ if err = tx.Where("authority_id = ?", auth.AuthorityId).Delete(&[]system.SysAuthorityBtn{}).Error; err != nil {
+ return err
+ }
+
+ authorityId := strconv.Itoa(int(auth.AuthorityId))
+
+ if err = CasbinServiceApp.RemoveFilteredPolicy(tx, authorityId); err != nil {
+ return err
+ }
+
+ return nil
+ })
+}
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@function: GetAuthorityInfoList
+//@description: 分页获取数据
+//@param: info request.PageInfo
+//@return: list interface{}, total int64, err error
+
+func (authorityService *AuthorityService) GetAuthorityInfoList(authorityID uint) (list []system.SysAuthority, err error) {
+ var authority system.SysAuthority
+ err = global.GVA_DB.Where("authority_id = ?", authorityID).First(&authority).Error
+ if err != nil {
+ return nil, err
+ }
+ var authorities []system.SysAuthority
+ db := global.GVA_DB.Model(&system.SysAuthority{})
+ if global.GVA_CONFIG.System.UseStrictAuth {
+ // 当开启了严格树形结构后
+ if *authority.ParentId == 0 {
+ // 只有顶级角色可以修改自己的权限和以下权限
+ err = db.Preload("DataAuthorityId").Where("authority_id = ?", authorityID).Find(&authorities).Error
+ } else {
+ // 非顶级角色只能修改以下权限
+ err = db.Debug().Preload("DataAuthorityId").Where("parent_id = ?", authorityID).Find(&authorities).Error
+ }
+ } else {
+ err = db.Preload("DataAuthorityId").Where("parent_id = ?", "0").Find(&authorities).Error
+ }
+
+ for k := range authorities {
+ err = authorityService.findChildrenAuthority(&authorities[k])
+ }
+ return authorities, err
+}
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@function: GetAuthorityInfoList
+//@description: 分页获取数据
+//@param: info request.PageInfo
+//@return: list interface{}, total int64, err error
+
+func (authorityService *AuthorityService) GetStructAuthorityList(authorityID uint) (list []uint, err error) {
+ var auth system.SysAuthority
+ _ = global.GVA_DB.First(&auth, "authority_id = ?", authorityID).Error
+ var authorities []system.SysAuthority
+ err = global.GVA_DB.Preload("DataAuthorityId").Where("parent_id = ?", authorityID).Find(&authorities).Error
+ if len(authorities) > 0 {
+ for k := range authorities {
+ list = append(list, authorities[k].AuthorityId)
+ 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
+ if err != nil {
+ return
+ }
+ return *authority.ParentId, nil
+}
diff --git a/server/service/system/sys_authority_btn.go b/server/service/system/sys_authority_btn.go
new file mode 100644
index 0000000..f470d84
--- /dev/null
+++ b/server/service/system/sys_authority_btn.go
@@ -0,0 +1,61 @@
+package system
+
+import (
+ "errors"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system"
+ "git.echol.cn/loser/st/server/model/system/request"
+ "git.echol.cn/loser/st/server/model/system/response"
+ "gorm.io/gorm"
+)
+
+type AuthorityBtnService struct{}
+
+var AuthorityBtnServiceApp = new(AuthorityBtnService)
+
+func (a *AuthorityBtnService) GetAuthorityBtn(req request.SysAuthorityBtnReq) (res response.SysAuthorityBtnRes, err error) {
+ var authorityBtn []system.SysAuthorityBtn
+ err = global.GVA_DB.Find(&authorityBtn, "authority_id = ? and sys_menu_id = ?", req.AuthorityId, req.MenuID).Error
+ if err != nil {
+ return
+ }
+ var selected []uint
+ for _, v := range authorityBtn {
+ selected = append(selected, v.SysBaseMenuBtnID)
+ }
+ res.Selected = selected
+ return res, err
+}
+
+func (a *AuthorityBtnService) SetAuthorityBtn(req request.SysAuthorityBtnReq) (err error) {
+ return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
+ var authorityBtn []system.SysAuthorityBtn
+ err = tx.Delete(&[]system.SysAuthorityBtn{}, "authority_id = ? and sys_menu_id = ?", req.AuthorityId, req.MenuID).Error
+ if err != nil {
+ return err
+ }
+ for _, v := range req.Selected {
+ authorityBtn = append(authorityBtn, system.SysAuthorityBtn{
+ AuthorityId: req.AuthorityId,
+ SysMenuID: req.MenuID,
+ SysBaseMenuBtnID: v,
+ })
+ }
+ if len(authorityBtn) > 0 {
+ err = tx.Create(&authorityBtn).Error
+ }
+ if err != nil {
+ return err
+ }
+ return err
+ })
+}
+
+func (a *AuthorityBtnService) CanRemoveAuthorityBtn(ID string) (err error) {
+ fErr := global.GVA_DB.First(&system.SysAuthorityBtn{}, "sys_base_menu_btn_id = ?", ID).Error
+ if errors.Is(fErr, gorm.ErrRecordNotFound) {
+ return nil
+ }
+ return errors.New("此按钮正在被使用无法删除")
+}
diff --git a/server/service/system/sys_auto_code_interface.go b/server/service/system/sys_auto_code_interface.go
new file mode 100644
index 0000000..c8fc0f6
--- /dev/null
+++ b/server/service/system/sys_auto_code_interface.go
@@ -0,0 +1,55 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system/response"
+)
+
+type AutoCodeService struct{}
+
+type Database interface {
+ GetDB(businessDB string) (data []response.Db, err error)
+ GetTables(businessDB string, dbName string) (data []response.Table, err error)
+ GetColumn(businessDB string, tableName string, dbName string) (data []response.Column, err error)
+}
+
+func (autoCodeService *AutoCodeService) Database(businessDB string) Database {
+
+ if businessDB == "" {
+ switch global.GVA_CONFIG.System.DbType {
+ case "mysql":
+ return AutoCodeMysql
+ case "pgsql":
+ return AutoCodePgsql
+ case "mssql":
+ return AutoCodeMssql
+ case "oracle":
+ return AutoCodeOracle
+ case "sqlite":
+ return AutoCodeSqlite
+ default:
+ return AutoCodeMysql
+ }
+ } else {
+ for _, info := range global.GVA_CONFIG.DBList {
+ if info.AliasName == businessDB {
+ switch info.Type {
+ case "mysql":
+ return AutoCodeMysql
+ case "mssql":
+ return AutoCodeMssql
+ case "pgsql":
+ return AutoCodePgsql
+ case "oracle":
+ return AutoCodeOracle
+ case "sqlite":
+ return AutoCodeSqlite
+ default:
+ return AutoCodeMysql
+ }
+ }
+ }
+ return AutoCodeMysql
+ }
+
+}
diff --git a/server/service/system/sys_auto_code_mssql.go b/server/service/system/sys_auto_code_mssql.go
new file mode 100644
index 0000000..a9cc92c
--- /dev/null
+++ b/server/service/system/sys_auto_code_mssql.go
@@ -0,0 +1,84 @@
+package system
+
+import (
+ "fmt"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system/response"
+)
+
+var AutoCodeMssql = new(autoCodeMssql)
+
+type autoCodeMssql struct{}
+
+// GetDB 获取数据库的所有数据库名
+// Author [piexlmax](https://github.com/piexlmax)
+// Author [SliverHorn](https://github.com/SliverHorn)
+func (s *autoCodeMssql) GetDB(businessDB string) (data []response.Db, err error) {
+ var entities []response.Db
+ sql := "select name AS 'database' from 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/server/service/system/sys_auto_code_mysql.go b/server/service/system/sys_auto_code_mysql.go
new file mode 100644
index 0000000..01af927
--- /dev/null
+++ b/server/service/system/sys_auto_code_mysql.go
@@ -0,0 +1,83 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system/response"
+)
+
+var AutoCodeMysql = new(autoCodeMysql)
+
+type autoCodeMysql struct{}
+
+// GetDB 获取数据库的所有数据库名
+// Author [piexlmax](https://github.com/piexlmax)
+// Author [SliverHorn](https://github.com/SliverHorn)
+func (s *autoCodeMysql) GetDB(businessDB string) (data []response.Db, err error) {
+ var entities []response.Db
+ sql := "SELECT SCHEMA_NAME AS `database` FROM INFORMATION_SCHEMA.SCHEMATA;"
+ if businessDB == "" {
+ err = global.GVA_DB.Raw(sql).Scan(&entities).Error
+ } else {
+ err = global.GVA_DBList[businessDB].Raw(sql).Scan(&entities).Error
+ }
+ return entities, err
+}
+
+// GetTables 获取数据库的所有表名
+// Author [piexlmax](https://github.com/piexlmax)
+// Author [SliverHorn](https://github.com/SliverHorn)
+func (s *autoCodeMysql) GetTables(businessDB string, dbName string) (data []response.Table, err error) {
+ var entities []response.Table
+ sql := `select table_name as table_name from information_schema.tables where table_schema = ?`
+ if businessDB == "" {
+ err = global.GVA_DB.Raw(sql, dbName).Scan(&entities).Error
+ } else {
+ err = global.GVA_DBList[businessDB].Raw(sql, dbName).Scan(&entities).Error
+ }
+
+ return entities, err
+}
+
+// GetColumn 获取指定数据库和指定数据表的所有字段名,类型值等
+// Author [piexlmax](https://github.com/piexlmax)
+// Author [SliverHorn](https://github.com/SliverHorn)
+func (s *autoCodeMysql) GetColumn(businessDB string, tableName string, dbName string) (data []response.Column, err error) {
+ var entities []response.Column
+ sql := `
+ SELECT
+ c.COLUMN_NAME column_name,
+ c.DATA_TYPE data_type,
+ CASE c.DATA_TYPE
+ WHEN 'longtext' THEN c.CHARACTER_MAXIMUM_LENGTH
+ WHEN 'varchar' THEN c.CHARACTER_MAXIMUM_LENGTH
+ WHEN 'double' THEN CONCAT_WS(',', c.NUMERIC_PRECISION, c.NUMERIC_SCALE)
+ WHEN 'decimal' THEN CONCAT_WS(',', c.NUMERIC_PRECISION, c.NUMERIC_SCALE)
+ WHEN 'int' THEN c.NUMERIC_PRECISION
+ WHEN 'bigint' THEN c.NUMERIC_PRECISION
+ ELSE ''
+ END AS data_type_long,
+ c.COLUMN_COMMENT column_comment,
+ CASE WHEN kcu.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END AS primary_key,
+ c.ORDINAL_POSITION
+FROM
+ INFORMATION_SCHEMA.COLUMNS c
+LEFT JOIN
+ INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
+ON
+ c.TABLE_SCHEMA = kcu.TABLE_SCHEMA
+ AND c.TABLE_NAME = kcu.TABLE_NAME
+ AND c.COLUMN_NAME = kcu.COLUMN_NAME
+ AND kcu.CONSTRAINT_NAME = 'PRIMARY'
+WHERE
+ c.TABLE_NAME = ?
+ AND c.TABLE_SCHEMA = ?
+ORDER BY
+ c.ORDINAL_POSITION;`
+ if businessDB == "" {
+ err = global.GVA_DB.Raw(sql, tableName, dbName).Scan(&entities).Error
+ } else {
+ err = global.GVA_DBList[businessDB].Raw(sql, tableName, dbName).Scan(&entities).Error
+ }
+
+ return entities, err
+}
diff --git a/server/service/system/sys_auto_code_oracle.go b/server/service/system/sys_auto_code_oracle.go
new file mode 100644
index 0000000..5f1c055
--- /dev/null
+++ b/server/service/system/sys_auto_code_oracle.go
@@ -0,0 +1,72 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system/response"
+)
+
+var AutoCodeOracle = new(autoCodeOracle)
+
+type autoCodeOracle struct{}
+
+// GetDB 获取数据库的所有数据库名
+// Author [piexlmax](https://github.com/piexlmax)
+// Author [SliverHorn](https://github.com/SliverHorn)
+func (s *autoCodeOracle) GetDB(businessDB string) (data []response.Db, err error) {
+ var entities []response.Db
+ sql := `SELECT lower(username) AS "database" FROM all_users`
+ err = global.GVA_DBList[businessDB].Raw(sql).Scan(&entities).Error
+ return entities, err
+}
+
+// GetTables 获取数据库的所有表名
+// Author [piexlmax](https://github.com/piexlmax)
+// Author [SliverHorn](https://github.com/SliverHorn)
+func (s *autoCodeOracle) GetTables(businessDB string, dbName string) (data []response.Table, err error) {
+ var entities []response.Table
+ sql := `select lower(table_name) as "table_name" from all_tables where lower(owner) = ?`
+
+ err = global.GVA_DBList[businessDB].Raw(sql, dbName).Scan(&entities).Error
+ return entities, err
+}
+
+// GetColumn 获取指定数据库和指定数据表的所有字段名,类型值等
+// Author [piexlmax](https://github.com/piexlmax)
+// Author [SliverHorn](https://github.com/SliverHorn)
+func (s *autoCodeOracle) GetColumn(businessDB string, tableName string, dbName string) (data []response.Column, err error) {
+ var entities []response.Column
+ sql := `
+ SELECT
+ lower(a.COLUMN_NAME) as "column_name",
+ (CASE WHEN a.DATA_TYPE = 'NUMBER' AND a.DATA_SCALE=0 THEN 'int' else lower(a.DATA_TYPE) end) as "data_type",
+ (CASE WHEN a.DATA_TYPE = 'NUMBER' THEN a.DATA_PRECISION else a.DATA_LENGTH end) as "data_type_long",
+ b.COMMENTS as "column_comment",
+ (CASE WHEN pk.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END) as "primary_key",
+ a.COLUMN_ID
+FROM
+ all_tab_columns a
+JOIN
+ all_col_comments b ON a.OWNER = b.OWNER AND a.TABLE_NAME = b.TABLE_NAME AND a.COLUMN_NAME = b.COLUMN_NAME
+LEFT JOIN
+ (
+ SELECT
+ acc.OWNER,
+ acc.TABLE_NAME,
+ acc.COLUMN_NAME
+ FROM
+ all_cons_columns acc
+ JOIN
+ all_constraints ac ON acc.OWNER = ac.OWNER AND acc.CONSTRAINT_NAME = ac.CONSTRAINT_NAME
+ WHERE
+ ac.CONSTRAINT_TYPE = 'P'
+ ) pk ON a.OWNER = pk.OWNER AND a.TABLE_NAME = pk.TABLE_NAME AND a.COLUMN_NAME = pk.COLUMN_NAME
+WHERE
+ lower(a.table_name) = ?
+ AND lower(a.OWNER) = ?
+ORDER BY
+ a.COLUMN_ID
+`
+
+ err = global.GVA_DBList[businessDB].Raw(sql, tableName, dbName).Scan(&entities).Error
+ return entities, err
+}
diff --git a/server/service/system/sys_auto_code_pgsql.go b/server/service/system/sys_auto_code_pgsql.go
new file mode 100644
index 0000000..c605f08
--- /dev/null
+++ b/server/service/system/sys_auto_code_pgsql.go
@@ -0,0 +1,135 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system/response"
+)
+
+var AutoCodePgsql = new(autoCodePgsql)
+
+type autoCodePgsql struct{}
+
+// GetDB 获取数据库的所有数据库名
+// Author [piexlmax](https://github.com/piexlmax)
+// Author [SliverHorn](https://github.com/SliverHorn)
+func (a *autoCodePgsql) GetDB(businessDB string) (data []response.Db, err error) {
+ var entities []response.Db
+ sql := `SELECT datname as database FROM pg_database WHERE datistemplate = false`
+ if businessDB == "" {
+ err = global.GVA_DB.Raw(sql).Scan(&entities).Error
+ } else {
+ err = global.GVA_DBList[businessDB].Raw(sql).Scan(&entities).Error
+ }
+
+ return entities, err
+}
+
+// GetTables 获取数据库的所有表名
+// Author [piexlmax](https://github.com/piexlmax)
+// Author [SliverHorn](https://github.com/SliverHorn)
+func (a *autoCodePgsql) GetTables(businessDB string, dbName string) (data []response.Table, err error) {
+ var entities []response.Table
+ sql := `select table_name as table_name from information_schema.tables where table_catalog = ? and table_schema = ?`
+
+ db := global.GVA_DB
+ if businessDB != "" {
+ db = global.GVA_DBList[businessDB]
+ }
+
+ err = db.Raw(sql, dbName, "public").Scan(&entities).Error
+ return entities, err
+}
+
+// GetColumn 获取指定数据库和指定数据表的所有字段名,类型值等
+// Author [piexlmax](https://github.com/piexlmax)
+// Author [SliverHorn](https://github.com/SliverHorn)
+func (a *autoCodePgsql) GetColumn(businessDB string, tableName string, dbName string) (data []response.Column, err error) {
+ // todo 数据获取不全, 待完善sql
+ sql := `
+SELECT
+ psc.COLUMN_NAME AS COLUMN_NAME,
+ psc.udt_name AS data_type,
+ CASE
+ psc.udt_name
+ WHEN 'text' THEN
+ concat_ws ( '', '', psc.CHARACTER_MAXIMUM_LENGTH )
+ WHEN 'varchar' THEN
+ concat_ws ( '', '', psc.CHARACTER_MAXIMUM_LENGTH )
+ WHEN 'smallint' THEN
+ concat_ws ( ',', psc.NUMERIC_PRECISION, psc.NUMERIC_SCALE )
+ WHEN 'decimal' THEN
+ concat_ws ( ',', psc.NUMERIC_PRECISION, psc.NUMERIC_SCALE )
+ WHEN 'integer' THEN
+ concat_ws ( '', '', psc.NUMERIC_PRECISION )
+ WHEN 'int4' THEN
+ concat_ws ( '', '', psc.NUMERIC_PRECISION )
+ WHEN 'int8' THEN
+ concat_ws ( '', '', psc.NUMERIC_PRECISION )
+ WHEN 'bigint' THEN
+ concat_ws ( '', '', psc.NUMERIC_PRECISION )
+ WHEN 'timestamp' THEN
+ concat_ws ( '', '', psc.datetime_precision )
+ ELSE ''
+ END AS data_type_long,
+ (
+ SELECT
+ pd.description
+ FROM
+ pg_description pd
+ WHERE
+ (pd.objoid,pd.objsubid) in (
+ SELECT pa.attrelid,pa.attnum
+ FROM
+ pg_attribute pa
+ WHERE pa.attrelid = ( SELECT oid FROM pg_class pc WHERE
+ pc.relname = psc.table_name
+ )
+ and attname = psc.column_name
+ )
+ ) AS column_comment,
+ (
+ SELECT
+ COUNT(*)
+ FROM
+ pg_constraint
+ WHERE
+ contype = 'p'
+ AND conrelid = (
+ SELECT
+ oid
+ FROM
+ pg_class
+ WHERE
+ relname = psc.table_name
+ )
+ AND conkey::int[] @> ARRAY[(
+ SELECT
+ attnum::integer
+ FROM
+ pg_attribute
+ WHERE
+ attrelid = conrelid
+ AND attname = psc.column_name
+ )]
+ ) > 0 AS primary_key,
+ psc.ordinal_position
+FROM
+ INFORMATION_SCHEMA.COLUMNS psc
+WHERE
+ table_catalog = ?
+ AND table_schema = 'public'
+ AND TABLE_NAME = ?
+ORDER BY
+ psc.ordinal_position;
+`
+ var entities []response.Column
+ //sql = strings.ReplaceAll(sql, "@table_catalog", dbName)
+ //sql = strings.ReplaceAll(sql, "@table_name", tableName)
+ db := global.GVA_DB
+ if businessDB != "" {
+ db = global.GVA_DBList[businessDB]
+ }
+
+ err = db.Raw(sql, dbName, tableName).Scan(&entities).Error
+ return entities, err
+}
diff --git a/server/service/system/sys_auto_code_sqlite.go b/server/service/system/sys_auto_code_sqlite.go
new file mode 100644
index 0000000..3987230
--- /dev/null
+++ b/server/service/system/sys_auto_code_sqlite.go
@@ -0,0 +1,85 @@
+package system
+
+import (
+ "fmt"
+ "path/filepath"
+ "strings"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system/response"
+)
+
+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/server/service/system/sys_base_menu.go b/server/service/system/sys_base_menu.go
new file mode 100644
index 0000000..034a766
--- /dev/null
+++ b/server/service/system/sys_base_menu.go
@@ -0,0 +1,147 @@
+package system
+
+import (
+ "errors"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system"
+ "gorm.io/gorm"
+)
+
+type BaseMenuService struct{}
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@function: DeleteBaseMenu
+//@description: 删除基础路由
+//@param: id float64
+//@return: err error
+
+var BaseMenuServiceApp = new(BaseMenuService)
+
+func (baseMenuService *BaseMenuService) DeleteBaseMenu(id int) (err error) {
+ err = global.GVA_DB.First(&system.SysBaseMenu{}, "parent_id = ?", id).Error
+ if err == nil {
+ return errors.New("此菜单存在子菜单不可删除")
+ }
+ var menu system.SysBaseMenu
+ err = global.GVA_DB.First(&menu, id).Error
+ if err != nil {
+ return errors.New("记录不存在")
+ }
+ err = global.GVA_DB.First(&system.SysAuthority{}, "default_router = ?", menu.Name).Error
+ if err == nil {
+ return errors.New("此菜单有角色正在作为首页,不可删除")
+ }
+ return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
+
+ err = tx.Delete(&system.SysBaseMenu{}, "id = ?", id).Error
+ if err != nil {
+ return err
+ }
+
+ err = tx.Delete(&system.SysBaseMenuParameter{}, "sys_base_menu_id = ?", id).Error
+ if err != nil {
+ return err
+ }
+
+ err = tx.Delete(&system.SysBaseMenuBtn{}, "sys_base_menu_id = ?", id).Error
+ if err != nil {
+ return err
+ }
+ err = tx.Delete(&system.SysAuthorityBtn{}, "sys_menu_id = ?", id).Error
+ if err != nil {
+ return err
+ }
+
+ err = tx.Delete(&system.SysAuthorityMenu{}, "sys_base_menu_id = ?", id).Error
+ if err != nil {
+ return err
+ }
+ return nil
+ })
+
+}
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@function: UpdateBaseMenu
+//@description: 更新路由
+//@param: menu model.SysBaseMenu
+//@return: err error
+
+func (baseMenuService *BaseMenuService) UpdateBaseMenu(menu system.SysBaseMenu) (err error) {
+ var oldMenu system.SysBaseMenu
+ upDateMap := make(map[string]interface{})
+ upDateMap["keep_alive"] = menu.KeepAlive
+ upDateMap["transition_type"] = menu.TransitionType
+ 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/server/service/system/sys_casbin.go b/server/service/system/sys_casbin.go
new file mode 100644
index 0000000..89148ae
--- /dev/null
+++ b/server/service/system/sys_casbin.go
@@ -0,0 +1,173 @@
+package system
+
+import (
+ "errors"
+ "strconv"
+
+ "gorm.io/gorm"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system/request"
+ "git.echol.cn/loser/st/server/utils"
+ gormadapter "github.com/casbin/gorm-adapter/v3"
+ _ "github.com/go-sql-driver/mysql"
+)
+
+//@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 := utils.GetCasbin()
+ 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 := utils.GetCasbin()
+ 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 := utils.GetCasbin()
+ 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 := utils.GetCasbin()
+ 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 := utils.GetCasbin()
+ err = e.LoadPolicy()
+ return err
+}
diff --git a/server/service/system/sys_dictionary.go b/server/service/system/sys_dictionary.go
new file mode 100644
index 0000000..c52d174
--- /dev/null
+++ b/server/service/system/sys_dictionary.go
@@ -0,0 +1,297 @@
+package system
+
+import (
+ "encoding/json"
+ "errors"
+
+ "git.echol.cn/loser/st/server/model/system/request"
+ "github.com/gin-gonic/gin"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system"
+ "gorm.io/gorm"
+)
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@function: CreateSysDictionary
+//@description: 创建字典数据
+//@param: sysDictionary model.SysDictionary
+//@return: err error
+
+type DictionaryService struct{}
+
+var DictionaryServiceApp = new(DictionaryService)
+
+func (dictionaryService *DictionaryService) CreateSysDictionary(sysDictionary system.SysDictionary) (err error) {
+ if (!errors.Is(global.GVA_DB.First(&system.SysDictionary{}, "type = ?", sysDictionary.Type).Error, gorm.ErrRecordNotFound)) {
+ return errors.New("存在相同的type,不允许创建")
+ }
+ err = global.GVA_DB.Create(&sysDictionary).Error
+ return err
+}
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@function: DeleteSysDictionary
+//@description: 删除字典数据
+//@param: sysDictionary model.SysDictionary
+//@return: err error
+
+func (dictionaryService *DictionaryService) DeleteSysDictionary(sysDictionary system.SysDictionary) (err error) {
+ err = global.GVA_DB.Where("id = ?", sysDictionary.ID).Preload("SysDictionaryDetails").First(&sysDictionary).Error
+ if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
+ return errors.New("请不要搞事")
+ }
+ if err != nil {
+ return err
+ }
+ err = global.GVA_DB.Delete(&sysDictionary).Error
+ if err != nil {
+ return err
+ }
+
+ if sysDictionary.SysDictionaryDetails != nil {
+ return global.GVA_DB.Where("sys_dictionary_id=?", sysDictionary.ID).Delete(sysDictionary.SysDictionaryDetails).Error
+ }
+ return
+}
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@function: UpdateSysDictionary
+//@description: 更新字典数据
+//@param: sysDictionary *model.SysDictionary
+//@return: err error
+
+func (dictionaryService *DictionaryService) UpdateSysDictionary(sysDictionary *system.SysDictionary) (err error) {
+ var dict system.SysDictionary
+ sysDictionaryMap := map[string]interface{}{
+ "Name": sysDictionary.Name,
+ "Type": sysDictionary.Type,
+ "Status": sysDictionary.Status,
+ "Desc": sysDictionary.Desc,
+ "ParentID": sysDictionary.ParentID,
+ }
+ 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,不允许创建")
+ }
+ }
+
+ // 检查是否会形成循环引用
+ if sysDictionary.ParentID != nil && *sysDictionary.ParentID != 0 {
+ if err := dictionaryService.checkCircularReference(sysDictionary.ID, *sysDictionary.ParentID); err != nil {
+ return err
+ }
+ }
+
+ 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 = ? and deleted_at is null", 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(c *gin.Context, req request.SysDictionarySearch) (list interface{}, err error) {
+ var sysDictionarys []system.SysDictionary
+ query := global.GVA_DB.WithContext(c)
+ if req.Name != "" {
+ query = query.Where("name LIKE ? OR type LIKE ?", "%"+req.Name+"%", "%"+req.Name+"%")
+ }
+ // 预加载子字典
+ query = query.Preload("Children")
+ err = query.Find(&sysDictionarys).Error
+ return sysDictionarys, err
+}
+
+// checkCircularReference 检查是否会形成循环引用
+func (dictionaryService *DictionaryService) checkCircularReference(currentID uint, parentID uint) error {
+ if currentID == parentID {
+ return errors.New("不能将字典设置为自己的父级")
+ }
+
+ // 递归检查父级链条
+ var parent system.SysDictionary
+ err := global.GVA_DB.Where("id = ?", parentID).First(&parent).Error
+ if err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return nil // 父级不存在,允许设置
+ }
+ return err
+ }
+
+ // 如果父级还有父级,继续检查
+ if parent.ParentID != nil && *parent.ParentID != 0 {
+ return dictionaryService.checkCircularReference(currentID, *parent.ParentID)
+ }
+
+ return nil
+}
+
+//@author: [pixelMax]
+//@function: ExportSysDictionary
+//@description: 导出字典JSON(包含字典详情)
+//@param: id uint
+//@return: exportData map[string]interface{}, err error
+
+func (dictionaryService *DictionaryService) ExportSysDictionary(id uint) (exportData map[string]interface{}, err error) {
+ var dictionary system.SysDictionary
+ // 查询字典及其所有详情
+ err = global.GVA_DB.Where("id = ?", id).Preload("SysDictionaryDetails", func(db *gorm.DB) *gorm.DB {
+ return db.Order("sort")
+ }).First(&dictionary).Error
+ if err != nil {
+ return nil, err
+ }
+
+ // 清空字典详情中的ID、创建时间、更新时间等字段
+ var cleanDetails []map[string]interface{}
+ for _, detail := range dictionary.SysDictionaryDetails {
+ cleanDetail := map[string]interface{}{
+ "label": detail.Label,
+ "value": detail.Value,
+ "extend": detail.Extend,
+ "status": detail.Status,
+ "sort": detail.Sort,
+ "level": detail.Level,
+ "path": detail.Path,
+ }
+ cleanDetails = append(cleanDetails, cleanDetail)
+ }
+
+ // 构造导出数据
+ exportData = map[string]interface{}{
+ "name": dictionary.Name,
+ "type": dictionary.Type,
+ "status": dictionary.Status,
+ "desc": dictionary.Desc,
+ "sysDictionaryDetails": cleanDetails,
+ }
+
+ return exportData, nil
+}
+
+//@author: [pixelMax]
+//@function: ImportSysDictionary
+//@description: 导入字典JSON(包含字典详情)
+//@param: jsonStr string
+//@return: err error
+
+func (dictionaryService *DictionaryService) ImportSysDictionary(jsonStr string) error {
+ // 直接解析到 SysDictionary 结构体
+ var importData system.SysDictionary
+ if err := json.Unmarshal([]byte(jsonStr), &importData); err != nil {
+ return errors.New("JSON 格式错误: " + err.Error())
+ }
+
+ // 验证必填字段
+ if importData.Name == "" {
+ return errors.New("字典名称不能为空")
+ }
+ if importData.Type == "" {
+ return errors.New("字典类型不能为空")
+ }
+
+ // 检查字典类型是否已存在
+ if !errors.Is(global.GVA_DB.First(&system.SysDictionary{}, "type = ?", importData.Type).Error, gorm.ErrRecordNotFound) {
+ return errors.New("存在相同的type,不允许导入")
+ }
+
+ // 创建字典(清空导入数据的ID和时间戳)
+ dictionary := system.SysDictionary{
+ Name: importData.Name,
+ Type: importData.Type,
+ Status: importData.Status,
+ Desc: importData.Desc,
+ }
+
+ // 开启事务
+ return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
+ // 创建字典
+ if err := tx.Create(&dictionary).Error; err != nil {
+ return err
+ }
+
+ // 处理字典详情
+ if len(importData.SysDictionaryDetails) > 0 {
+ // 创建一个映射来跟踪旧ID到新ID的对应关系
+ idMap := make(map[uint]uint)
+
+ // 第一遍:创建所有详情记录
+ for _, detail := range importData.SysDictionaryDetails {
+ // 验证必填字段
+ if detail.Label == "" || detail.Value == "" {
+ continue
+ }
+
+ // 记录旧ID
+ oldID := detail.ID
+
+ // 创建新的详情记录(ID会被GORM自动设置)
+ detailRecord := system.SysDictionaryDetail{
+ Label: detail.Label,
+ Value: detail.Value,
+ Extend: detail.Extend,
+ Status: detail.Status,
+ Sort: detail.Sort,
+ Level: detail.Level,
+ Path: detail.Path,
+ SysDictionaryID: int(dictionary.ID),
+ }
+
+ // 创建详情记录
+ if err := tx.Create(&detailRecord).Error; err != nil {
+ return err
+ }
+
+ // 记录旧ID到新ID的映射
+ if oldID > 0 {
+ idMap[oldID] = detailRecord.ID
+ }
+ }
+
+ // 第二遍:更新parent_id关系
+ for _, detail := range importData.SysDictionaryDetails {
+ if detail.ParentID != nil && *detail.ParentID > 0 && detail.ID > 0 {
+ if newID, exists := idMap[detail.ID]; exists {
+ if newParentID, parentExists := idMap[*detail.ParentID]; parentExists {
+ if err := tx.Model(&system.SysDictionaryDetail{}).
+ Where("id = ?", newID).
+ Update("parent_id", newParentID).Error; err != nil {
+ return err
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return nil
+ })
+}
diff --git a/server/service/system/sys_dictionary_detail.go b/server/service/system/sys_dictionary_detail.go
new file mode 100644
index 0000000..cc5942a
--- /dev/null
+++ b/server/service/system/sys_dictionary_detail.go
@@ -0,0 +1,392 @@
+package system
+
+import (
+ "fmt"
+ "strconv"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system"
+ "git.echol.cn/loser/st/server/model/system/request"
+)
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@function: CreateSysDictionaryDetail
+//@description: 创建字典详情数据
+//@param: sysDictionaryDetail model.SysDictionaryDetail
+//@return: err error
+
+type DictionaryDetailService struct{}
+
+var DictionaryDetailServiceApp = new(DictionaryDetailService)
+
+func (dictionaryDetailService *DictionaryDetailService) CreateSysDictionaryDetail(sysDictionaryDetail system.SysDictionaryDetail) (err error) {
+ // 计算层级和路径
+ if sysDictionaryDetail.ParentID != nil {
+ var parent system.SysDictionaryDetail
+ err = global.GVA_DB.First(&parent, *sysDictionaryDetail.ParentID).Error
+ if err != nil {
+ return err
+ }
+ sysDictionaryDetail.Level = parent.Level + 1
+ if parent.Path == "" {
+ sysDictionaryDetail.Path = strconv.Itoa(int(parent.ID))
+ } else {
+ sysDictionaryDetail.Path = parent.Path + "," + strconv.Itoa(int(parent.ID))
+ }
+ } else {
+ sysDictionaryDetail.Level = 0
+ sysDictionaryDetail.Path = ""
+ }
+
+ 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) {
+ // 检查是否有子项
+ var count int64
+ err = global.GVA_DB.Model(&system.SysDictionaryDetail{}).Where("parent_id = ?", sysDictionaryDetail.ID).Count(&count).Error
+ if err != nil {
+ return err
+ }
+ if count > 0 {
+ return fmt.Errorf("该字典详情下还有子项,无法删除")
+ }
+
+ 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) {
+ // 如果更新了父级ID,需要重新计算层级和路径
+ if sysDictionaryDetail.ParentID != nil {
+ var parent system.SysDictionaryDetail
+ err = global.GVA_DB.First(&parent, *sysDictionaryDetail.ParentID).Error
+ if err != nil {
+ return err
+ }
+
+ // 检查循环引用
+ if dictionaryDetailService.checkCircularReference(sysDictionaryDetail.ID, *sysDictionaryDetail.ParentID) {
+ return fmt.Errorf("不能将字典详情设置为自己或其子项的父级")
+ }
+
+ sysDictionaryDetail.Level = parent.Level + 1
+ if parent.Path == "" {
+ sysDictionaryDetail.Path = strconv.Itoa(int(parent.ID))
+ } else {
+ sysDictionaryDetail.Path = parent.Path + "," + strconv.Itoa(int(parent.ID))
+ }
+ } else {
+ sysDictionaryDetail.Level = 0
+ sysDictionaryDetail.Path = ""
+ }
+
+ err = global.GVA_DB.Save(sysDictionaryDetail).Error
+ if err != nil {
+ return err
+ }
+
+ // 更新所有子项的层级和路径
+ return dictionaryDetailService.updateChildrenLevelAndPath(sysDictionaryDetail.ID)
+}
+
+// checkCircularReference 检查循环引用
+func (dictionaryDetailService *DictionaryDetailService) checkCircularReference(id, parentID uint) bool {
+ if id == parentID {
+ return true
+ }
+
+ var parent system.SysDictionaryDetail
+ err := global.GVA_DB.First(&parent, parentID).Error
+ if err != nil {
+ return false
+ }
+
+ if parent.ParentID == nil {
+ return false
+ }
+
+ return dictionaryDetailService.checkCircularReference(id, *parent.ParentID)
+}
+
+// updateChildrenLevelAndPath 更新子项的层级和路径
+func (dictionaryDetailService *DictionaryDetailService) updateChildrenLevelAndPath(parentID uint) error {
+ var children []system.SysDictionaryDetail
+ err := global.GVA_DB.Where("parent_id = ?", parentID).Find(&children).Error
+ if err != nil {
+ return err
+ }
+
+ var parent system.SysDictionaryDetail
+ err = global.GVA_DB.First(&parent, parentID).Error
+ if err != nil {
+ return err
+ }
+
+ for _, child := range children {
+ child.Level = parent.Level + 1
+ if parent.Path == "" {
+ child.Path = strconv.Itoa(int(parent.ID))
+ } else {
+ child.Path = parent.Path + "," + strconv.Itoa(int(parent.ID))
+ }
+
+ err = global.GVA_DB.Save(&child).Error
+ if err != nil {
+ return err
+ }
+
+ // 递归更新子项的子项
+ err = dictionaryDetailService.updateChildrenLevelAndPath(child.ID)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+//@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)
+ }
+ if info.ParentID != nil {
+ db = db.Where("parent_id = ?", *info.ParentID)
+ }
+ if info.Level != nil {
+ db = db.Where("level = ?", *info.Level)
+ }
+ err = db.Count(&total).Error
+ if err != nil {
+ return
+ }
+ err = db.Limit(limit).Offset(offset).Order("sort").Order("id").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
+}
+
+// GetDictionaryTreeList 获取字典树形结构列表
+func (dictionaryDetailService *DictionaryDetailService) GetDictionaryTreeList(dictionaryID uint) (list []system.SysDictionaryDetail, err error) {
+ var sysDictionaryDetails []system.SysDictionaryDetail
+ // 只获取顶级项目(parent_id为空)
+ err = global.GVA_DB.Where("sys_dictionary_id = ? AND parent_id IS NULL", dictionaryID).Order("sort").Find(&sysDictionaryDetails).Error
+ if err != nil {
+ return nil, err
+ }
+
+ // 递归加载子项并设置disabled属性
+ for i := range sysDictionaryDetails {
+ // 设置disabled属性:当status为false时,disabled为true
+ if sysDictionaryDetails[i].Status != nil {
+ sysDictionaryDetails[i].Disabled = !*sysDictionaryDetails[i].Status
+ } else {
+ sysDictionaryDetails[i].Disabled = false // 默认不禁用
+ }
+
+ err = dictionaryDetailService.loadChildren(&sysDictionaryDetails[i])
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return sysDictionaryDetails, nil
+}
+
+// loadChildren 递归加载子项
+func (dictionaryDetailService *DictionaryDetailService) loadChildren(detail *system.SysDictionaryDetail) error {
+ var children []system.SysDictionaryDetail
+ err := global.GVA_DB.Where("parent_id = ?", detail.ID).Order("sort").Find(&children).Error
+ if err != nil {
+ return err
+ }
+
+ for i := range children {
+ // 设置disabled属性:当status为false时,disabled为true
+ if children[i].Status != nil {
+ children[i].Disabled = !*children[i].Status
+ } else {
+ children[i].Disabled = false // 默认不禁用
+ }
+
+ err = dictionaryDetailService.loadChildren(&children[i])
+ if err != nil {
+ return err
+ }
+ }
+
+ detail.Children = children
+ return nil
+}
+
+// GetDictionaryDetailsByParent 根据父级ID获取字典详情
+func (dictionaryDetailService *DictionaryDetailService) GetDictionaryDetailsByParent(req request.GetDictionaryDetailsByParentRequest) (list []system.SysDictionaryDetail, err error) {
+ db := global.GVA_DB.Model(&system.SysDictionaryDetail{}).Where("sys_dictionary_id = ?", req.SysDictionaryID)
+
+ if req.ParentID != nil {
+ db = db.Where("parent_id = ?", *req.ParentID)
+ } else {
+ db = db.Where("parent_id IS NULL")
+ }
+
+ err = db.Order("sort").Find(&list).Error
+ if err != nil {
+ return list, err
+ }
+
+ // 设置disabled属性
+ for i := range list {
+ if list[i].Status != nil {
+ list[i].Disabled = !*list[i].Status
+ } else {
+ list[i].Disabled = false // 默认不禁用
+ }
+ }
+
+ // 如果需要包含子级数据,使用递归方式加载所有层级的子项
+ if req.IncludeChildren {
+ for i := range list {
+ err = dictionaryDetailService.loadChildren(&list[i])
+ if err != nil {
+ return list, err
+ }
+ }
+ }
+
+ return list, 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.Find(&sysDictionaryDetails, "type = ?", t).Error
+ return sysDictionaryDetails, err
+}
+
+// GetDictionaryTreeListByType 根据字典类型获取树形结构
+func (dictionaryDetailService *DictionaryDetailService) GetDictionaryTreeListByType(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").
+ Where("sys_dictionaries.type = ? AND sys_dictionary_details.parent_id IS NULL", t).
+ Order("sys_dictionary_details.sort")
+
+ err = db.Find(&sysDictionaryDetails).Error
+ if err != nil {
+ return nil, err
+ }
+
+ // 递归加载子项并设置disabled属性
+ for i := range sysDictionaryDetails {
+ // 设置disabled属性:当status为false时,disabled为true
+ if sysDictionaryDetails[i].Status != nil {
+ sysDictionaryDetails[i].Disabled = !*sysDictionaryDetails[i].Status
+ } else {
+ sysDictionaryDetails[i].Disabled = false // 默认不禁用
+ }
+
+ err = dictionaryDetailService.loadChildren(&sysDictionaryDetails[i])
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return sysDictionaryDetails, nil
+}
+
+// 按照字典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
+}
+
+// GetDictionaryPath 获取字典详情的完整路径
+func (dictionaryDetailService *DictionaryDetailService) GetDictionaryPath(id uint) (path []system.SysDictionaryDetail, err error) {
+ var detail system.SysDictionaryDetail
+ err = global.GVA_DB.First(&detail, id).Error
+ if err != nil {
+ return nil, err
+ }
+
+ path = append(path, detail)
+
+ if detail.ParentID != nil {
+ parentPath, err := dictionaryDetailService.GetDictionaryPath(*detail.ParentID)
+ if err != nil {
+ return nil, err
+ }
+ path = append(parentPath, path...)
+ }
+
+ return path, nil
+}
+
+// GetDictionaryPathByValue 根据值获取字典详情的完整路径
+func (dictionaryDetailService *DictionaryDetailService) GetDictionaryPathByValue(dictionaryID uint, value string) (path []system.SysDictionaryDetail, err error) {
+ detail, err := dictionaryDetailService.GetDictionaryInfoByValue(dictionaryID, value)
+ if err != nil {
+ return nil, err
+ }
+
+ return dictionaryDetailService.GetDictionaryPath(detail.ID)
+}
diff --git a/server/service/system/sys_error.go b/server/service/system/sys_error.go
new file mode 100644
index 0000000..efaf95b
--- /dev/null
+++ b/server/service/system/sys_error.go
@@ -0,0 +1,127 @@
+package system
+
+import (
+ "context"
+ "fmt"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common"
+ "git.echol.cn/loser/st/server/model/system"
+ systemReq "git.echol.cn/loser/st/server/model/system/request"
+)
+
+type SysErrorService struct{}
+
+// CreateSysError 创建错误日志记录
+// Author [yourname](https://github.com/yourname)
+func (sysErrorService *SysErrorService) CreateSysError(ctx context.Context, sysError *system.SysError) (err error) {
+ if global.GVA_DB == nil {
+ return nil
+ }
+ err = global.GVA_DB.Create(sysError).Error
+ return err
+}
+
+// DeleteSysError 删除错误日志记录
+// Author [yourname](https://github.com/yourname)
+func (sysErrorService *SysErrorService) DeleteSysError(ctx context.Context, ID string) (err error) {
+ err = global.GVA_DB.Delete(&system.SysError{}, "id = ?", ID).Error
+ return err
+}
+
+// DeleteSysErrorByIds 批量删除错误日志记录
+// Author [yourname](https://github.com/yourname)
+func (sysErrorService *SysErrorService) DeleteSysErrorByIds(ctx context.Context, IDs []string) (err error) {
+ err = global.GVA_DB.Delete(&[]system.SysError{}, "id in ?", IDs).Error
+ return err
+}
+
+// UpdateSysError 更新错误日志记录
+// Author [yourname](https://github.com/yourname)
+func (sysErrorService *SysErrorService) UpdateSysError(ctx context.Context, sysError system.SysError) (err error) {
+ err = global.GVA_DB.Model(&system.SysError{}).Where("id = ?", sysError.ID).Updates(&sysError).Error
+ return err
+}
+
+// GetSysError 根据ID获取错误日志记录
+// Author [yourname](https://github.com/yourname)
+func (sysErrorService *SysErrorService) GetSysError(ctx context.Context, ID string) (sysError system.SysError, err error) {
+ err = global.GVA_DB.Where("id = ?", ID).First(&sysError).Error
+ return
+}
+
+// GetSysErrorInfoList 分页获取错误日志记录
+// Author [yourname](https://github.com/yourname)
+func (sysErrorService *SysErrorService) GetSysErrorInfoList(ctx context.Context, info systemReq.SysErrorSearch) (list []system.SysError, total int64, err error) {
+ limit := info.PageSize
+ offset := info.PageSize * (info.Page - 1)
+ // 创建db
+ db := global.GVA_DB.Model(&system.SysError{}).Order("created_at desc")
+ var sysErrors []system.SysError
+ // 如果有条件搜索 下方会自动创建搜索语句
+ if len(info.CreatedAtRange) == 2 {
+ db = db.Where("created_at BETWEEN ? AND ?", info.CreatedAtRange[0], info.CreatedAtRange[1])
+ }
+
+ if info.Form != nil && *info.Form != "" {
+ db = db.Where("form = ?", *info.Form)
+ }
+ if info.Info != nil && *info.Info != "" {
+ db = db.Where("info LIKE ?", "%"+*info.Info+"%")
+ }
+ err = db.Count(&total).Error
+ if err != nil {
+ return
+ }
+
+ if limit != 0 {
+ db = db.Limit(limit).Offset(offset)
+ }
+
+ err = db.Find(&sysErrors).Error
+ return sysErrors, total, err
+}
+
+// GetSysErrorSolution 异步处理错误
+// Author [yourname](https://github.com/yourname)
+func (sysErrorService *SysErrorService) GetSysErrorSolution(ctx context.Context, ID string) (err error) {
+ // 立即更新为处理中
+ err = global.GVA_DB.WithContext(ctx).Model(&system.SysError{}).Where("id = ?", ID).Update("status", "处理中").Error
+ if err != nil {
+ return err
+ }
+
+ // 异步协程在一分钟后更新为处理完成
+ go func(id string) {
+ // 查询当前错误信息用于生成方案
+ var se system.SysError
+ _ = global.GVA_DB.Model(&system.SysError{}).Where("id = ?", id).First(&se).Error
+
+ // 构造 LLM 请求参数,使用管家模式(butler)根据错误信息生成解决方案
+ var form, info string
+ if se.Form != nil {
+ form = *se.Form
+ }
+ if se.Info != nil {
+ info = *se.Info
+ }
+
+ llmReq := common.JSONMap{
+ "mode": "solution",
+ "info": info,
+ "form": form,
+ }
+
+ // 调用服务层 LLMAuto,忽略错误但尽量写入方案
+ var solution string
+ if data, err := (&AutoCodeService{}).LLMAuto(context.Background(), llmReq); err == nil {
+ solution = fmt.Sprintf("%v", data.(map[string]interface{})["text"])
+ _ = global.GVA_DB.Model(&system.SysError{}).Where("id = ?", id).Updates(map[string]interface{}{"status": "处理完成", "solution": solution}).Error
+ } else {
+ // 即使生成失败也标记为完成,避免任务卡住
+ _ = global.GVA_DB.Model(&system.SysError{}).Where("id = ?", id).Update("status", "处理失败").Error
+ }
+ }(ID)
+
+ return nil
+}
diff --git a/server/service/system/sys_export_template.go b/server/service/system/sys_export_template.go
new file mode 100644
index 0000000..a39cc5e
--- /dev/null
+++ b/server/service/system/sys_export_template.go
@@ -0,0 +1,724 @@
+package system
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "mime/multipart"
+ "net/url"
+ "strconv"
+ "strings"
+ "time"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common/request"
+ "git.echol.cn/loser/st/server/model/system"
+ systemReq "git.echol.cn/loser/st/server/model/system/request"
+ "git.echol.cn/loser/st/server/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 params = values.Get("params")
+ paramsValues, err := url.ParseQuery(params)
+ if err != nil {
+ return nil, "", fmt.Errorf("解析 params 参数失败: %v", err)
+ }
+ 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)
+ }
+
+ // 如果有自定义SQL,则优先使用自定义SQL
+ if template.SQL != "" {
+ // 将 url.Values 转换为 map[string]interface{} 以支持 GORM 的命名参数
+ sqlParams := make(map[string]interface{})
+ for k, v := range paramsValues {
+ if len(v) > 0 {
+ sqlParams[k] = v[0]
+ }
+ }
+
+ // 执行原生 SQL,支持 @key 命名参数
+ err = db.Raw(template.SQL, sqlParams).Scan(&tableMap).Error
+ if err != nil {
+ return nil, "", err
+ }
+ } else {
+ 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)
+
+ filterDeleted := false
+
+ filterParam := paramsValues.Get("filterDeleted")
+ if filterParam == "true" {
+ filterDeleted = true
+ }
+
+ if filterDeleted {
+ // 自动过滤主表的软删除
+ db = db.Where(fmt.Sprintf("%s.deleted_at IS NULL", template.TableName))
+
+ // 过滤关联表的软删除(如果有)
+ if len(template.JoinTemplate) > 0 {
+ for _, join := range template.JoinTemplate {
+ // 检查关联表是否有deleted_at字段
+ hasDeletedAt := sysExportTemplateService.hasDeletedAtColumn(join.Table)
+ if hasDeletedAt {
+ db = db.Where(fmt.Sprintf("%s.deleted_at IS NULL", join.Table))
+ }
+ }
+ }
+ }
+
+ if len(template.Conditions) > 0 {
+ for _, condition := range template.Conditions {
+ sql := fmt.Sprintf("%s %s ?", condition.Column, condition.Operator)
+ value := paramsValues.Get(condition.From)
+
+ if condition.Operator == "IN" || condition.Operator == "NOT IN" {
+ sql = fmt.Sprintf("%s %s (?)", condition.Column, condition.Operator)
+ }
+
+ if condition.Operator == "BETWEEN" {
+ sql = fmt.Sprintf("%s BETWEEN ? AND ?", condition.Column)
+ startValue := paramsValues.Get("start" + condition.From)
+ endValue := paramsValues.Get("end" + condition.From)
+ if startValue != "" && endValue != "" {
+ db = db.Where(sql, startValue, endValue)
+ }
+ continue
+ }
+
+ if value != "" {
+ if condition.Operator == "LIKE" {
+ value = "%" + value + "%"
+ }
+ db = db.Where(sql, value)
+ }
+ }
+ }
+ // 通过参数传入limit
+ limit := paramsValues.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 := paramsValues.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 := paramsValues.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 {
+ cell := fmt.Sprintf("%s%d", getColumnName(j+1), i+1)
+
+ var sErr error
+ if v, err := strconv.ParseFloat(colCell, 64); err == nil {
+ sErr = f.SetCellValue("Sheet1", cell, v)
+ } else if v, err := strconv.ParseInt(colCell, 10, 64); err == nil {
+ sErr = f.SetCellValue("Sheet1", cell, v)
+ } else {
+ sErr = f.SetCellValue("Sheet1", cell, 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
+}
+
+// PreviewSQL 预览最终生成的 SQL(不执行查询,仅返回 SQL 字符串)
+// Author [piexlmax](https://github.com/piexlmax) & [trae-ai]
+func (sysExportTemplateService *SysExportTemplateService) PreviewSQL(templateID string, values url.Values) (sqlPreview string, err error) {
+ // 解析 params(与导出逻辑保持一致)
+ var params = values.Get("params")
+ paramsValues, _ := url.ParseQuery(params)
+
+ // 加载模板
+ var template system.SysExportTemplate
+ err = global.GVA_DB.Preload("Conditions").Preload("JoinTemplate").First(&template, "template_id = ?", templateID).Error
+ if err != nil {
+ return "", err
+ }
+
+ // 解析模板列
+ var templateInfoMap = make(map[string]string)
+ columns, err := utils.GetJSONKeys(template.TemplateInfo)
+ if err != nil {
+ return "", err
+ }
+ err = json.Unmarshal([]byte(template.TemplateInfo), &templateInfoMap)
+ if err != nil {
+ return "", err
+ }
+ var selectKeyFmt []string
+ for _, key := range columns {
+ selectKeyFmt = append(selectKeyFmt, key)
+ }
+ selects := strings.Join(selectKeyFmt, ", ")
+
+ // 生成 FROM 与 JOIN 片段
+ var sb strings.Builder
+ sb.WriteString("SELECT ")
+ sb.WriteString(selects)
+ sb.WriteString(" FROM ")
+ sb.WriteString(template.TableName)
+
+ if len(template.JoinTemplate) > 0 {
+ for _, join := range template.JoinTemplate {
+ sb.WriteString(" ")
+ sb.WriteString(join.JOINS)
+ sb.WriteString(" ")
+ sb.WriteString(join.Table)
+ sb.WriteString(" ON ")
+ sb.WriteString(join.ON)
+ }
+ }
+
+ // WHERE 条件
+ var wheres []string
+
+ // 软删除过滤
+ filterDeleted := false
+ if paramsValues != nil {
+ filterParam := paramsValues.Get("filterDeleted")
+ if filterParam == "true" {
+ filterDeleted = true
+ }
+ }
+ if filterDeleted {
+ wheres = append(wheres, fmt.Sprintf("%s.deleted_at IS NULL", template.TableName))
+ if len(template.JoinTemplate) > 0 {
+ for _, join := range template.JoinTemplate {
+ if sysExportTemplateService.hasDeletedAtColumn(join.Table) {
+ wheres = append(wheres, fmt.Sprintf("%s.deleted_at IS NULL", join.Table))
+ }
+ }
+ }
+ }
+
+ // 模板条件(保留与 ExportExcel 同步的解析规则)
+ if len(template.Conditions) > 0 {
+ for _, condition := range template.Conditions {
+ op := strings.ToUpper(strings.TrimSpace(condition.Operator))
+ col := strings.TrimSpace(condition.Column)
+
+ // 预览优先展示传入值,没有则展示占位符
+ val := ""
+ if paramsValues != nil {
+ val = paramsValues.Get(condition.From)
+ }
+
+ switch op {
+ case "BETWEEN":
+ startValue := ""
+ endValue := ""
+ if paramsValues != nil {
+ startValue = paramsValues.Get("start" + condition.From)
+ endValue = paramsValues.Get("end" + condition.From)
+ }
+ if startValue != "" && endValue != "" {
+ wheres = append(wheres, fmt.Sprintf("%s BETWEEN '%s' AND '%s'", col, startValue, endValue))
+ } else {
+ wheres = append(wheres, fmt.Sprintf("%s BETWEEN {start%s} AND {end%s}", col, condition.From, condition.From))
+ }
+ case "IN", "NOT IN":
+ if val != "" {
+ // 逗号分隔值做简单展示
+ parts := strings.Split(val, ",")
+ for i := range parts {
+ parts[i] = strings.TrimSpace(parts[i])
+ }
+ wheres = append(wheres, fmt.Sprintf("%s %s ('%s')", col, op, strings.Join(parts, "','")))
+ } else {
+ wheres = append(wheres, fmt.Sprintf("%s %s ({%s})", col, op, condition.From))
+ }
+ case "LIKE":
+ if val != "" {
+ wheres = append(wheres, fmt.Sprintf("%s LIKE '%%%s%%'", col, val))
+ } else {
+ wheres = append(wheres, fmt.Sprintf("%s LIKE {%%%s%%}", col, condition.From))
+ }
+ default:
+ if val != "" {
+ wheres = append(wheres, fmt.Sprintf("%s %s '%s'", col, op, val))
+ } else {
+ wheres = append(wheres, fmt.Sprintf("%s %s {%s}", col, op, condition.From))
+ }
+ }
+ }
+ }
+
+ if len(wheres) > 0 {
+ sb.WriteString(" WHERE ")
+ sb.WriteString(strings.Join(wheres, " AND "))
+ }
+
+ // 排序
+ order := ""
+ if paramsValues != nil {
+ order = paramsValues.Get("order")
+ }
+ if order == "" && template.Order != "" {
+ order = template.Order
+ }
+ if order != "" {
+ sb.WriteString(" ORDER BY ")
+ sb.WriteString(order)
+ }
+
+ // limit/offset(如果传入或默认值为0,则不生成)
+ limitStr := ""
+ offsetStr := ""
+ if paramsValues != nil {
+ limitStr = paramsValues.Get("limit")
+ offsetStr = paramsValues.Get("offset")
+ }
+
+ // 处理模板默认limit(仅当非0时)
+ if limitStr == "" && template.Limit != nil && *template.Limit != 0 {
+ limitStr = strconv.Itoa(*template.Limit)
+ }
+
+ // 解析为数值,用于判断是否生成
+ limitInt := 0
+ offsetInt := 0
+ if limitStr != "" {
+ if v, e := strconv.Atoi(limitStr); e == nil {
+ limitInt = v
+ }
+ }
+ if offsetStr != "" {
+ if v, e := strconv.Atoi(offsetStr); e == nil {
+ offsetInt = v
+ }
+ }
+
+ if limitInt > 0 {
+ sb.WriteString(" LIMIT ")
+ sb.WriteString(strconv.Itoa(limitInt))
+ if offsetInt > 0 {
+ sb.WriteString(" OFFSET ")
+ sb.WriteString(strconv.Itoa(offsetInt))
+ }
+ } else {
+ // 当limit未设置或为0时,仅当offset>0才生成OFFSET
+ if offsetInt > 0 {
+ sb.WriteString(" OFFSET ")
+ sb.WriteString(strconv.Itoa(offsetInt))
+ }
+ }
+
+ return sb.String(), 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
+}
+
+// 辅助函数:检查表是否有deleted_at列
+func (s *SysExportTemplateService) hasDeletedAtColumn(tableName string) bool {
+ var count int64
+ global.GVA_DB.Raw("SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND COLUMN_NAME = 'deleted_at'", tableName).Count(&count)
+ return count > 0
+}
+
+// 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
+ }
+ if len(rows) < 2 {
+ return errors.New("Excel data is not enough.\nIt should contain title row and data")
+ }
+
+ var templateInfoMap = make(map[string]string)
+ err = json.Unmarshal([]byte(template.TemplateInfo), &templateInfoMap)
+ if err != nil {
+ return err
+ }
+
+ db := global.GVA_DB
+ if template.DBName != "" {
+ db = global.MustGetGlobalDBByDBName(template.DBName)
+ }
+
+ items, err := sysExportTemplateService.parseExcelToMap(rows, templateInfoMap)
+ if err != nil {
+ return err
+ }
+
+ return db.Transaction(func(tx *gorm.DB) error {
+ if template.ImportSQL != "" {
+ return sysExportTemplateService.importBySQL(tx, template.ImportSQL, items)
+ }
+ return sysExportTemplateService.importByGORM(tx, template.TableName, items)
+ })
+}
+
+func (sysExportTemplateService *SysExportTemplateService) parseExcelToMap(rows [][]string, templateInfoMap map[string]string) ([]map[string]interface{}, error) {
+ var titleKeyMap = make(map[string]string)
+ for key, title := range templateInfoMap {
+ titleKeyMap[title] = key
+ }
+
+ excelTitle := rows[0]
+ for i, str := range excelTitle {
+ excelTitle[i] = strings.TrimSpace(str)
+ }
+ 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 {
+ if ii >= len(excelTitle) {
+ continue
+ }
+ if _, ok := titleKeyMap[excelTitle[ii]]; !ok {
+ continue // excel中多余的标题,在模板信息中没有对应的字段,因此key为空,必须跳过
+ }
+ key := titleKeyMap[excelTitle[ii]]
+ item[key] = value
+ }
+ items = append(items, item)
+ }
+ return items, nil
+}
+
+func (sysExportTemplateService *SysExportTemplateService) importBySQL(tx *gorm.DB, sql string, items []map[string]interface{}) error {
+ for _, item := range items {
+ if err := tx.Exec(sql, item).Error; err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (sysExportTemplateService *SysExportTemplateService) importByGORM(tx *gorm.DB, tableName string, items []map[string]interface{}) error {
+ needCreated := tx.Migrator().HasColumn(tableName, "created_at")
+ needUpdated := tx.Migrator().HasColumn(tableName, "updated_at")
+
+ for _, item := range items {
+ if item["created_at"] == nil && needCreated {
+ item["created_at"] = time.Now()
+ }
+ if item["updated_at"] == nil && needUpdated {
+ item["updated_at"] = time.Now()
+ }
+ }
+ return tx.Table(tableName).CreateInBatches(&items, 1000).Error
+}
+
+func getColumnName(n int) string {
+ columnName := ""
+ for n > 0 {
+ n--
+ columnName = string(rune('A'+n%26)) + columnName
+ n /= 26
+ }
+ return columnName
+}
diff --git a/server/service/system/sys_initdb.go b/server/service/system/sys_initdb.go
new file mode 100644
index 0000000..4cf07b0
--- /dev/null
+++ b/server/service/system/sys_initdb.go
@@ -0,0 +1,190 @@
+package system
+
+import (
+ "context"
+ "database/sql"
+ "errors"
+ "fmt"
+ "sort"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system/request"
+ "gorm.io/gorm"
+)
+
+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 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/server/service/system/sys_initdb_mssql.go b/server/service/system/sys_initdb_mssql.go
new file mode 100644
index 0000000..b5b75ca
--- /dev/null
+++ b/server/service/system/sys_initdb_mssql.go
@@ -0,0 +1,93 @@
+package system
+
+import (
+ "context"
+ "errors"
+ "path/filepath"
+
+ "git.echol.cn/loser/st/server/config"
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system/request"
+ "git.echol.cn/loser/st/server/utils"
+ "github.com/google/uuid"
+ "github.com/gookit/color"
+ "gorm.io/driver/sqlserver"
+ "gorm.io/gorm"
+)
+
+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 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/server/service/system/sys_initdb_mysql.go b/server/service/system/sys_initdb_mysql.go
new file mode 100644
index 0000000..b620324
--- /dev/null
+++ b/server/service/system/sys_initdb_mysql.go
@@ -0,0 +1,97 @@
+package system
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "path/filepath"
+
+ "git.echol.cn/loser/st/server/config"
+ "github.com/gookit/color"
+
+ "git.echol.cn/loser/st/server/utils"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/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 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/server/service/system/sys_initdb_pgsql.go b/server/service/system/sys_initdb_pgsql.go
new file mode 100644
index 0000000..d7039a4
--- /dev/null
+++ b/server/service/system/sys_initdb_pgsql.go
@@ -0,0 +1,101 @@
+package system
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "path/filepath"
+
+ "git.echol.cn/loser/st/server/config"
+ "github.com/gookit/color"
+
+ "git.echol.cn/loser/st/server/utils"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/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 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/server/service/system/sys_initdb_sqlite.go b/server/service/system/sys_initdb_sqlite.go
new file mode 100644
index 0000000..b70c907
--- /dev/null
+++ b/server/service/system/sys_initdb_sqlite.go
@@ -0,0 +1,89 @@
+package system
+
+import (
+ "context"
+ "errors"
+ "path/filepath"
+
+ "github.com/glebarez/sqlite"
+ "github.com/google/uuid"
+ "github.com/gookit/color"
+ "gorm.io/gorm"
+
+ "git.echol.cn/loser/st/server/config"
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system/request"
+ "git.echol.cn/loser/st/server/utils"
+)
+
+type SqliteInitHandler struct{}
+
+func NewSqliteInitHandler() *SqliteInitHandler {
+ return &SqliteInitHandler{}
+}
+
+// WriteConfig mysql回写配置
+func (h SqliteInitHandler) WriteConfig(ctx context.Context) error {
+ c, ok := ctx.Value("config").(config.Sqlite)
+ if !ok {
+ return errors.New("sqlite config invalid")
+ }
+ global.GVA_CONFIG.System.DbType = "sqlite"
+ global.GVA_CONFIG.Sqlite = c
+ global.GVA_CONFIG.JWT.SigningKey = uuid.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 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/server/service/system/sys_login_log.go b/server/service/system/sys_login_log.go
new file mode 100644
index 0000000..c71e5ff
--- /dev/null
+++ b/server/service/system/sys_login_log.go
@@ -0,0 +1,53 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common/request"
+ "git.echol.cn/loser/st/server/model/system"
+ systemReq "git.echol.cn/loser/st/server/model/system/request"
+)
+
+type LoginLogService struct{}
+
+var LoginLogServiceApp = new(LoginLogService)
+
+func (loginLogService *LoginLogService) CreateLoginLog(loginLog system.SysLoginLog) (err error) {
+ err = global.GVA_DB.Create(&loginLog).Error
+ return err
+}
+
+func (loginLogService *LoginLogService) DeleteLoginLogByIds(ids request.IdsReq) (err error) {
+ err = global.GVA_DB.Delete(&[]system.SysLoginLog{}, "id in (?)", ids.Ids).Error
+ return err
+}
+
+func (loginLogService *LoginLogService) DeleteLoginLog(loginLog system.SysLoginLog) (err error) {
+ err = global.GVA_DB.Delete(&loginLog).Error
+ return err
+}
+
+func (loginLogService *LoginLogService) GetLoginLog(id uint) (loginLog system.SysLoginLog, err error) {
+ err = global.GVA_DB.Where("id = ?", id).First(&loginLog).Error
+ return
+}
+
+func (loginLogService *LoginLogService) GetLoginLogInfoList(info systemReq.SysLoginLogSearch) (list interface{}, total int64, err error) {
+ limit := info.PageSize
+ offset := info.PageSize * (info.Page - 1)
+ // 创建db
+ db := global.GVA_DB.Model(&system.SysLoginLog{})
+ var loginLogs []system.SysLoginLog
+ // 如果有条件搜索 下方会自动创建搜索语句
+ if info.Username != "" {
+ db = db.Where("username LIKE ?", "%"+info.Username+"%")
+ }
+ if info.Status != false {
+ db = db.Where("status = ?", info.Status)
+ }
+ err = db.Count(&total).Error
+ if err != nil {
+ return
+ }
+ err = db.Limit(limit).Offset(offset).Order("id desc").Preload("User").Find(&loginLogs).Error
+ return loginLogs, total, err
+}
diff --git a/server/service/system/sys_menu.go b/server/service/system/sys_menu.go
new file mode 100644
index 0000000..7c84af5
--- /dev/null
+++ b/server/service/system/sys_menu.go
@@ -0,0 +1,332 @@
+package system
+
+import (
+ "errors"
+ "strconv"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common/request"
+ "git.echol.cn/loser/st/server/model/system"
+ "gorm.io/gorm"
+)
+
+//@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 {
+ return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
+ // 检查name是否重复
+ if !errors.Is(tx.Where("name = ?", menu.Name).First(&system.SysBaseMenu{}).Error, gorm.ErrRecordNotFound) {
+ return errors.New("存在重复name,请修改name")
+ }
+
+ if menu.ParentId != 0 {
+ // 检查父菜单是否存在
+ var parentMenu system.SysBaseMenu
+ if err := tx.First(&parentMenu, menu.ParentId).Error; err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return errors.New("父菜单不存在")
+ }
+ return err
+ }
+
+ // 检查父菜单下现有子菜单数量
+ var existingChildrenCount int64
+ err := tx.Model(&system.SysBaseMenu{}).Where("parent_id = ?", menu.ParentId).Count(&existingChildrenCount).Error
+ if err != nil {
+ return err
+ }
+
+ // 如果父菜单原本是叶子菜单(没有子菜单),现在要变成枝干菜单,需要清空其权限分配
+ if existingChildrenCount == 0 {
+ // 检查父菜单是否被其他角色设置为首页
+ var defaultRouterCount int64
+ err := tx.Model(&system.SysAuthority{}).Where("default_router = ?", parentMenu.Name).Count(&defaultRouterCount).Error
+ if err != nil {
+ return err
+ }
+ if defaultRouterCount > 0 {
+ return errors.New("父菜单已被其他角色的首页占用,请先释放父菜单的首页权限")
+ }
+
+ // 清空父菜单的所有权限分配
+ err = tx.Where("sys_base_menu_id = ?", menu.ParentId).Delete(&system.SysAuthorityMenu{}).Error
+ if err != nil {
+ return err
+ }
+ }
+ }
+
+ // 创建菜单
+ return tx.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/server/service/system/sys_operation_record.go b/server/service/system/sys_operation_record.go
new file mode 100644
index 0000000..20b37a5
--- /dev/null
+++ b/server/service/system/sys_operation_record.go
@@ -0,0 +1,83 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/common/request"
+ "git.echol.cn/loser/st/server/model/system"
+ systemReq "git.echol.cn/loser/st/server/model/system/request"
+)
+
+//@author: [granty1](https://github.com/granty1)
+//@function: CreateSysOperationRecord
+//@description: 创建记录
+//@param: sysOperationRecord model.SysOperationRecord
+//@return: err error
+
+type OperationRecordService struct{}
+
+var OperationRecordServiceApp = new(OperationRecordService)
+
+//@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/server/service/system/sys_params.go b/server/service/system/sys_params.go
new file mode 100644
index 0000000..953f004
--- /dev/null
+++ b/server/service/system/sys_params.go
@@ -0,0 +1,82 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system"
+ systemReq "git.echol.cn/loser/st/server/model/system/request"
+)
+
+type SysParamsService struct{}
+
+// CreateSysParams 创建参数记录
+// Author [Mr.奇淼](https://github.com/pixelmaxQm)
+func (sysParamsService *SysParamsService) CreateSysParams(sysParams *system.SysParams) (err error) {
+ err = global.GVA_DB.Create(sysParams).Error
+ return err
+}
+
+// DeleteSysParams 删除参数记录
+// Author [Mr.奇淼](https://github.com/pixelmaxQm)
+func (sysParamsService *SysParamsService) DeleteSysParams(ID string) (err error) {
+ err = global.GVA_DB.Delete(&system.SysParams{}, "id = ?", ID).Error
+ return err
+}
+
+// DeleteSysParamsByIds 批量删除参数记录
+// Author [Mr.奇淼](https://github.com/pixelmaxQm)
+func (sysParamsService *SysParamsService) DeleteSysParamsByIds(IDs []string) (err error) {
+ err = global.GVA_DB.Delete(&[]system.SysParams{}, "id in ?", IDs).Error
+ return err
+}
+
+// UpdateSysParams 更新参数记录
+// Author [Mr.奇淼](https://github.com/pixelmaxQm)
+func (sysParamsService *SysParamsService) UpdateSysParams(sysParams system.SysParams) (err error) {
+ err = global.GVA_DB.Model(&system.SysParams{}).Where("id = ?", sysParams.ID).Updates(&sysParams).Error
+ return err
+}
+
+// GetSysParams 根据ID获取参数记录
+// Author [Mr.奇淼](https://github.com/pixelmaxQm)
+func (sysParamsService *SysParamsService) GetSysParams(ID string) (sysParams system.SysParams, err error) {
+ err = global.GVA_DB.Where("id = ?", ID).First(&sysParams).Error
+ return
+}
+
+// GetSysParamsInfoList 分页获取参数记录
+// Author [Mr.奇淼](https://github.com/pixelmaxQm)
+func (sysParamsService *SysParamsService) GetSysParamsInfoList(info systemReq.SysParamsSearch) (list []system.SysParams, total int64, err error) {
+ limit := info.PageSize
+ offset := info.PageSize * (info.Page - 1)
+ // 创建db
+ db := global.GVA_DB.Model(&system.SysParams{})
+ var sysParamss []system.SysParams
+ // 如果有条件搜索 下方会自动创建搜索语句
+ if info.StartCreatedAt != nil && info.EndCreatedAt != nil {
+ db = db.Where("created_at BETWEEN ? AND ?", info.StartCreatedAt, info.EndCreatedAt)
+ }
+ if info.Name != "" {
+ db = db.Where("name LIKE ?", "%"+info.Name+"%")
+ }
+ if info.Key != "" {
+ db = db.Where("key LIKE ?", "%"+info.Key+"%")
+ }
+ err = db.Count(&total).Error
+ if err != nil {
+ return
+ }
+
+ if limit != 0 {
+ db = db.Limit(limit).Offset(offset)
+ }
+
+ err = db.Find(&sysParamss).Error
+ return sysParamss, total, err
+}
+
+// GetSysParam 根据key获取参数value
+// Author [Mr.奇淼](https://github.com/pixelmaxQm)
+func (sysParamsService *SysParamsService) GetSysParam(key string) (param system.SysParams, err error) {
+ err = global.GVA_DB.Where(system.SysParams{Key: key}).First(¶m).Error
+ return
+}
diff --git a/server/service/system/sys_skills.go b/server/service/system/sys_skills.go
new file mode 100644
index 0000000..673f957
--- /dev/null
+++ b/server/service/system/sys_skills.go
@@ -0,0 +1,549 @@
+package system
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system"
+ "git.echol.cn/loser/st/server/model/system/request"
+ "gopkg.in/yaml.v3"
+)
+
+const (
+ skillFileName = "SKILL.md"
+ globalConstraintFileName = "README.md"
+)
+
+var skillToolOrder = []string{"copilot", "claude", "cursor", "trae", "codex"}
+
+var skillToolDirs = map[string]string{
+ "copilot": ".aone_copilot",
+ "claude": ".claude",
+ "trae": ".trae",
+ "codex": ".codex",
+ "cursor": ".cursor",
+}
+
+var skillToolLabels = map[string]string{
+ "copilot": "Copilot",
+ "claude": "Claude",
+ "trae": "Trae",
+ "codex": "Codex",
+ "cursor": "Cursor",
+}
+
+const defaultSkillMarkdown = "## 技能用途\n请在这里描述技能的目标、适用场景与限制条件。\n\n## 输入\n- 请补充输入格式与示例。\n\n## 输出\n- 请补充输出格式与示例。\n\n## 关键步骤\n1. 第一步\n2. 第二步\n\n## 示例\n在此补充一到两个典型示例。\n"
+
+const defaultResourceMarkdown = "# 资源说明\n请在这里补充资源内容。\n"
+
+const defaultReferenceMarkdown = "# 参考资料\n请在这里补充参考资料内容。\n"
+
+const defaultTemplateMarkdown = "# 模板\n请在这里补充模板内容。\n"
+
+const defaultGlobalConstraintMarkdown = "# 全局约束\n请在这里补充该工具的统一约束与使用规范。\n"
+
+type SkillsService struct{}
+
+func (s *SkillsService) Tools(_ context.Context) ([]system.SkillTool, error) {
+ tools := make([]system.SkillTool, 0, len(skillToolOrder))
+ for _, key := range skillToolOrder {
+ if _, err := s.toolSkillsDir(key); err != nil {
+ return nil, err
+ }
+ tools = append(tools, system.SkillTool{Key: key, Label: skillToolLabels[key]})
+ }
+ return tools, nil
+}
+
+func (s *SkillsService) List(_ context.Context, tool string) ([]string, error) {
+ skillsDir, err := s.toolSkillsDir(tool)
+ if err != nil {
+ return nil, err
+ }
+ entries, err := os.ReadDir(skillsDir)
+ if err != nil {
+ return nil, err
+ }
+ var skills []string
+ for _, entry := range entries {
+ if entry.IsDir() {
+ skills = append(skills, entry.Name())
+ }
+ }
+ sort.Strings(skills)
+ return skills, nil
+}
+
+func (s *SkillsService) Detail(_ context.Context, tool, skill string) (system.SkillDetail, error) {
+ var detail system.SkillDetail
+ if !isSafeName(skill) {
+ return detail, errors.New("技能名称不合法")
+ }
+ detail.Tool = tool
+ detail.Skill = skill
+
+ skillDir, err := s.skillDir(tool, skill)
+ if err != nil {
+ return detail, err
+ }
+
+ skillFilePath := filepath.Join(skillDir, skillFileName)
+ content, err := os.ReadFile(skillFilePath)
+ if err != nil {
+ if !os.IsNotExist(err) {
+ return detail, err
+ }
+ detail.Meta = system.SkillMeta{Name: skill}
+ detail.Markdown = defaultSkillMarkdown
+ } else {
+ meta, body, parseErr := parseSkillContent(string(content))
+ if parseErr != nil {
+ meta = system.SkillMeta{Name: skill}
+ body = string(content)
+ }
+ if meta.Name == "" {
+ meta.Name = skill
+ }
+ detail.Meta = meta
+ detail.Markdown = body
+ }
+
+ detail.Scripts = listFiles(filepath.Join(skillDir, "scripts"))
+ detail.Resources = listFiles(filepath.Join(skillDir, "resources"))
+ detail.References = listFiles(filepath.Join(skillDir, "references"))
+ detail.Templates = listFiles(filepath.Join(skillDir, "templates"))
+ return detail, nil
+}
+
+func (s *SkillsService) Save(_ context.Context, req request.SkillSaveRequest) error {
+ if !isSafeName(req.Skill) {
+ return errors.New("技能名称不合法")
+ }
+ skillDir, err := s.ensureSkillDir(req.Tool, req.Skill)
+ if err != nil {
+ return err
+ }
+ if req.Meta.Name == "" {
+ req.Meta.Name = req.Skill
+ }
+ content, err := buildSkillContent(req.Meta, req.Markdown)
+ if err != nil {
+ return err
+ }
+ if err := os.WriteFile(filepath.Join(skillDir, skillFileName), []byte(content), 0644); err != nil {
+ return err
+ }
+
+ if len(req.SyncTools) > 0 {
+ for _, tool := range req.SyncTools {
+ if tool == req.Tool {
+ continue
+ }
+ targetDir, err := s.ensureSkillDir(tool, req.Skill)
+ if err != nil {
+ return err
+ }
+ if err := copySkillDir(skillDir, targetDir); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+func (s *SkillsService) CreateScript(_ context.Context, req request.SkillScriptCreateRequest) (string, string, error) {
+ if !isSafeName(req.Skill) {
+ return "", "", errors.New("技能名称不合法")
+ }
+ fileName, lang, err := buildScriptFileName(req.FileName, req.ScriptType)
+ if err != nil {
+ return "", "", err
+ }
+ if lang == "" {
+ return "", "", errors.New("脚本类型不支持")
+ }
+ skillDir, err := s.ensureSkillDir(req.Tool, req.Skill)
+ if err != nil {
+ return "", "", err
+ }
+ filePath := filepath.Join(skillDir, "scripts", fileName)
+ if _, err := os.Stat(filePath); err == nil {
+ return "", "", errors.New("脚本已存在")
+ }
+ if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {
+ return "", "", err
+ }
+ content := scriptTemplate(lang)
+ if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
+ return "", "", err
+ }
+ return fileName, content, nil
+}
+
+func (s *SkillsService) GetScript(_ context.Context, req request.SkillFileRequest) (string, error) {
+ return s.readSkillFile(req.Tool, req.Skill, "scripts", req.FileName)
+}
+
+func (s *SkillsService) SaveScript(_ context.Context, req request.SkillFileSaveRequest) error {
+ return s.writeSkillFile(req.Tool, req.Skill, "scripts", req.FileName, req.Content)
+}
+
+func (s *SkillsService) CreateResource(_ context.Context, req request.SkillResourceCreateRequest) (string, string, error) {
+ return s.createMarkdownFile(req.Tool, req.Skill, "resources", req.FileName, defaultResourceMarkdown, "资源")
+}
+
+func (s *SkillsService) GetResource(_ context.Context, req request.SkillFileRequest) (string, error) {
+ return s.readSkillFile(req.Tool, req.Skill, "resources", req.FileName)
+}
+
+func (s *SkillsService) SaveResource(_ context.Context, req request.SkillFileSaveRequest) error {
+ return s.writeSkillFile(req.Tool, req.Skill, "resources", req.FileName, req.Content)
+}
+
+func (s *SkillsService) CreateReference(_ context.Context, req request.SkillReferenceCreateRequest) (string, string, error) {
+ return s.createMarkdownFile(req.Tool, req.Skill, "references", req.FileName, defaultReferenceMarkdown, "参考")
+}
+
+func (s *SkillsService) GetReference(_ context.Context, req request.SkillFileRequest) (string, error) {
+ return s.readSkillFile(req.Tool, req.Skill, "references", req.FileName)
+}
+
+func (s *SkillsService) SaveReference(_ context.Context, req request.SkillFileSaveRequest) error {
+ return s.writeSkillFile(req.Tool, req.Skill, "references", req.FileName, req.Content)
+}
+
+func (s *SkillsService) CreateTemplate(_ context.Context, req request.SkillTemplateCreateRequest) (string, string, error) {
+ return s.createMarkdownFile(req.Tool, req.Skill, "templates", req.FileName, defaultTemplateMarkdown, "模板")
+}
+
+func (s *SkillsService) GetTemplate(_ context.Context, req request.SkillFileRequest) (string, error) {
+ return s.readSkillFile(req.Tool, req.Skill, "templates", req.FileName)
+}
+
+func (s *SkillsService) SaveTemplate(_ context.Context, req request.SkillFileSaveRequest) error {
+ return s.writeSkillFile(req.Tool, req.Skill, "templates", req.FileName, req.Content)
+}
+
+func (s *SkillsService) GetGlobalConstraint(_ context.Context, tool string) (string, bool, error) {
+ skillsDir, err := s.toolSkillsDir(tool)
+ if err != nil {
+ return "", false, err
+ }
+ filePath := filepath.Join(skillsDir, globalConstraintFileName)
+ content, err := os.ReadFile(filePath)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return defaultGlobalConstraintMarkdown, false, nil
+ }
+ return "", false, err
+ }
+ return string(content), true, nil
+}
+
+func (s *SkillsService) SaveGlobalConstraint(_ context.Context, req request.SkillGlobalConstraintSaveRequest) error {
+ if strings.TrimSpace(req.Tool) == "" {
+ return errors.New("工具类型不能为空")
+ }
+ writeConstraint := func(tool, content string) error {
+ skillsDir, err := s.toolSkillsDir(tool)
+ if err != nil {
+ return err
+ }
+ filePath := filepath.Join(skillsDir, globalConstraintFileName)
+ if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {
+ return err
+ }
+ return os.WriteFile(filePath, []byte(content), 0644)
+ }
+ if err := writeConstraint(req.Tool, req.Content); err != nil {
+ return err
+ }
+ if len(req.SyncTools) == 0 {
+ return nil
+ }
+ for _, tool := range req.SyncTools {
+ if tool == "" || tool == req.Tool {
+ continue
+ }
+ if err := writeConstraint(tool, req.Content); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (s *SkillsService) toolSkillsDir(tool string) (string, error) {
+ toolDir, ok := skillToolDirs[tool]
+ if !ok {
+ return "", errors.New("工具类型不支持")
+ }
+ root := strings.TrimSpace(global.GVA_CONFIG.AutoCode.Root)
+ if root == "" {
+ root = "."
+ }
+ skillsDir := filepath.Join(root, toolDir, "skills")
+ if err := os.MkdirAll(skillsDir, os.ModePerm); err != nil {
+ return "", err
+ }
+ return skillsDir, nil
+}
+
+func (s *SkillsService) skillDir(tool, skill string) (string, error) {
+ skillsDir, err := s.toolSkillsDir(tool)
+ if err != nil {
+ return "", err
+ }
+ return filepath.Join(skillsDir, skill), nil
+}
+
+func (s *SkillsService) ensureSkillDir(tool, skill string) (string, error) {
+ if !isSafeName(skill) {
+ return "", errors.New("技能名称不合法")
+ }
+ skillDir, err := s.skillDir(tool, skill)
+ if err != nil {
+ return "", err
+ }
+ if err := os.MkdirAll(skillDir, os.ModePerm); err != nil {
+ return "", err
+ }
+ return skillDir, nil
+}
+
+func (s *SkillsService) createMarkdownFile(tool, skill, subDir, fileName, defaultContent, label string) (string, string, error) {
+ if !isSafeName(skill) {
+ return "", "", errors.New("技能名称不合法")
+ }
+ cleanName, err := buildResourceFileName(fileName)
+ if err != nil {
+ return "", "", err
+ }
+ skillDir, err := s.ensureSkillDir(tool, skill)
+ if err != nil {
+ return "", "", err
+ }
+ filePath := filepath.Join(skillDir, subDir, cleanName)
+ if _, err := os.Stat(filePath); err == nil {
+ if label == "" {
+ label = "文件"
+ }
+ return "", "", fmt.Errorf("%s已存在", label)
+ }
+ if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {
+ return "", "", err
+ }
+ content := defaultContent
+ if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
+ return "", "", err
+ }
+ return cleanName, content, nil
+}
+
+func (s *SkillsService) readSkillFile(tool, skill, subDir, fileName string) (string, error) {
+ if !isSafeName(skill) {
+ return "", errors.New("技能名称不合法")
+ }
+ if !isSafeFileName(fileName) {
+ return "", errors.New("文件名不合法")
+ }
+ skillDir, err := s.skillDir(tool, skill)
+ if err != nil {
+ return "", err
+ }
+ filePath := filepath.Join(skillDir, subDir, fileName)
+ content, err := os.ReadFile(filePath)
+ if err != nil {
+ return "", err
+ }
+ return string(content), nil
+}
+
+func (s *SkillsService) writeSkillFile(tool, skill, subDir, fileName, content string) error {
+ if !isSafeName(skill) {
+ return errors.New("技能名称不合法")
+ }
+ if !isSafeFileName(fileName) {
+ return errors.New("文件名不合法")
+ }
+ skillDir, err := s.ensureSkillDir(tool, skill)
+ if err != nil {
+ return err
+ }
+ filePath := filepath.Join(skillDir, subDir, fileName)
+ if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {
+ return err
+ }
+ return os.WriteFile(filePath, []byte(content), 0644)
+}
+
+func parseSkillContent(content string) (system.SkillMeta, string, error) {
+ clean := strings.TrimPrefix(content, "\ufeff")
+ lines := strings.Split(clean, "\n")
+ if len(lines) == 0 || strings.TrimSpace(lines[0]) != "---" {
+ return system.SkillMeta{}, clean, nil
+ }
+ end := -1
+ for i := 1; i < len(lines); i++ {
+ if strings.TrimSpace(lines[i]) == "---" {
+ end = i
+ break
+ }
+ }
+ if end == -1 {
+ return system.SkillMeta{}, clean, nil
+ }
+ yamlText := strings.Join(lines[1:end], "\n")
+ body := strings.Join(lines[end+1:], "\n")
+ var meta system.SkillMeta
+ if err := yaml.Unmarshal([]byte(yamlText), &meta); err != nil {
+ return system.SkillMeta{}, body, err
+ }
+ return meta, body, nil
+}
+
+func buildSkillContent(meta system.SkillMeta, markdown string) (string, error) {
+ if meta.Name == "" {
+ return "", errors.New("name不能为空")
+ }
+ data, err := yaml.Marshal(meta)
+ if err != nil {
+ return "", err
+ }
+ yamlText := strings.TrimRight(string(data), "\n")
+ body := strings.TrimLeft(markdown, "\n")
+ if body != "" {
+ body = body + "\n"
+ }
+ return fmt.Sprintf("---\n%s\n---\n%s", yamlText, body), nil
+}
+
+func listFiles(dir string) []string {
+ entries, err := os.ReadDir(dir)
+ if err != nil {
+ return []string{}
+ }
+ files := make([]string, 0, len(entries))
+ for _, entry := range entries {
+ if entry.Type().IsRegular() {
+ files = append(files, entry.Name())
+ }
+ }
+ sort.Strings(files)
+ return files
+}
+
+func isSafeName(name string) bool {
+ if strings.TrimSpace(name) == "" {
+ return false
+ }
+ if strings.Contains(name, "..") {
+ return false
+ }
+ if strings.ContainsAny(name, "/\\") {
+ return false
+ }
+ return name == filepath.Base(name)
+}
+
+func isSafeFileName(name string) bool {
+ if strings.TrimSpace(name) == "" {
+ return false
+ }
+ if strings.Contains(name, "..") {
+ return false
+ }
+ if strings.ContainsAny(name, "/\\") {
+ return false
+ }
+ return name == filepath.Base(name)
+}
+
+func buildScriptFileName(fileName, scriptType string) (string, string, error) {
+ clean := strings.TrimSpace(fileName)
+ if clean == "" {
+ return "", "", errors.New("文件名不能为空")
+ }
+ if !isSafeFileName(clean) {
+ return "", "", errors.New("文件名不合法")
+ }
+ base := strings.TrimSuffix(clean, filepath.Ext(clean))
+ if base == "" {
+ return "", "", errors.New("文件名不合法")
+ }
+
+ switch strings.ToLower(scriptType) {
+ case "py", "python":
+ return base + ".py", "python", nil
+ case "js", "javascript", "script":
+ return base + ".js", "javascript", nil
+ case "sh", "shell", "bash":
+ return base + ".sh", "sh", nil
+ default:
+ return "", "", errors.New("脚本类型不支持")
+ }
+}
+
+func buildResourceFileName(fileName string) (string, error) {
+ clean := strings.TrimSpace(fileName)
+ if clean == "" {
+ return "", errors.New("文件名不能为空")
+ }
+ if !isSafeFileName(clean) {
+ return "", errors.New("文件名不合法")
+ }
+ base := strings.TrimSuffix(clean, filepath.Ext(clean))
+ if base == "" {
+ return "", errors.New("文件名不合法")
+ }
+ return base + ".md", nil
+}
+
+func scriptTemplate(lang string) string {
+ switch lang {
+ case "python":
+ return "# -*- coding: utf-8 -*-\n# TODO: 在这里实现脚本逻辑\n"
+ case "javascript":
+ return "// TODO: 在这里实现脚本逻辑\n"
+ case "sh":
+ return "#!/usr/bin/env bash\nset -euo pipefail\n\n# TODO: 在这里实现脚本逻辑\n"
+ default:
+ return ""
+ }
+}
+
+func copySkillDir(src, dst string) error {
+ return filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+ rel, err := filepath.Rel(src, path)
+ if err != nil {
+ return err
+ }
+ if rel == "." {
+ return nil
+ }
+ target := filepath.Join(dst, rel)
+ if d.IsDir() {
+ return os.MkdirAll(target, os.ModePerm)
+ }
+ if !d.Type().IsRegular() {
+ return nil
+ }
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return err
+ }
+ if err := os.MkdirAll(filepath.Dir(target), os.ModePerm); err != nil {
+ return err
+ }
+ return os.WriteFile(target, data, 0644)
+ })
+}
diff --git a/server/service/system/sys_system.go b/server/service/system/sys_system.go
new file mode 100644
index 0000000..5db7fde
--- /dev/null
+++ b/server/service/system/sys_system.go
@@ -0,0 +1,62 @@
+package system
+
+import (
+ "git.echol.cn/loser/st/server/config"
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system"
+ "git.echol.cn/loser/st/server/utils"
+ "go.uber.org/zap"
+)
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@function: GetSystemConfig
+//@description: 读取配置文件
+//@return: conf config.Server, err error
+
+type SystemConfigService struct{}
+
+var SystemConfigServiceApp = new(SystemConfigService)
+
+func (systemConfigService *SystemConfigService) GetSystemConfig() (conf config.Server, err error) {
+ return global.GVA_CONFIG, nil
+}
+
+// @description set system config,
+//@author: [piexlmax](https://github.com/piexlmax)
+//@function: SetSystemConfig
+//@description: 设置配置文件
+//@param: system model.System
+//@return: err error
+
+func (systemConfigService *SystemConfigService) SetSystemConfig(system system.System) (err error) {
+ cs := utils.StructToMap(system.Config)
+ for k, v := range cs {
+ global.GVA_VP.Set(k, v)
+ }
+ err = global.GVA_VP.WriteConfig()
+ return err
+}
+
+//@author: [SliverHorn](https://github.com/SliverHorn)
+//@function: GetServerInfo
+//@description: 获取服务器信息
+//@return: server *utils.Server, err error
+
+func (systemConfigService *SystemConfigService) GetServerInfo() (server *utils.Server, err error) {
+ var s utils.Server
+ s.Os = utils.InitOS()
+ if s.Cpu, err = utils.InitCPU(); err != nil {
+ global.GVA_LOG.Error("func utils.InitCPU() Failed", zap.String("err", err.Error()))
+ return &s, err
+ }
+ if s.Ram, err = utils.InitRAM(); err != nil {
+ global.GVA_LOG.Error("func utils.InitRAM() Failed", zap.String("err", err.Error()))
+ return &s, err
+ }
+ if s.Disk, err = utils.InitDisk(); err != nil {
+ global.GVA_LOG.Error("func utils.InitDisk() Failed", zap.String("err", err.Error()))
+ return &s, err
+ }
+
+ return &s, nil
+}
diff --git a/server/service/system/sys_user.go b/server/service/system/sys_user.go
new file mode 100644
index 0000000..bd79a32
--- /dev/null
+++ b/server/service/system/sys_user.go
@@ -0,0 +1,318 @@
+package system
+
+import (
+ "errors"
+ "fmt"
+ "time"
+
+ "git.echol.cn/loser/st/server/model/common"
+ systemReq "git.echol.cn/loser/st/server/model/system/request"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system"
+ "git.echol.cn/loser/st/server/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: err error
+
+func (userService *UserService) ChangePassword(u *system.SysUser, newPassword string) (err error) {
+ var user system.SysUser
+ err = global.GVA_DB.Select("id, password").Where("id = ?", u.ID).First(&user).Error
+ if err != nil {
+ return err
+ }
+ if ok := utils.BcryptCheck(u.Password, user.Password); !ok {
+ return errors.New("原密码错误")
+ }
+ pwd := utils.BcryptHash(newPassword)
+ err = global.GVA_DB.Model(&user).Update("password", pwd).Error
+ return 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, password string) (err error) {
+ err = global.GVA_DB.Model(&system.SysUser{}).Where("id = ?", ID).Update("password", utils.BcryptHash(password)).Error
+ return err
+}
diff --git a/server/service/system/sys_version.go b/server/service/system/sys_version.go
new file mode 100644
index 0000000..37f2a6b
--- /dev/null
+++ b/server/service/system/sys_version.go
@@ -0,0 +1,231 @@
+package system
+
+import (
+ "context"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system"
+ systemReq "git.echol.cn/loser/st/server/model/system/request"
+ "gorm.io/gorm"
+)
+
+type SysVersionService struct{}
+
+// CreateSysVersion 创建版本管理记录
+// Author [yourname](https://github.com/yourname)
+func (sysVersionService *SysVersionService) CreateSysVersion(ctx context.Context, sysVersion *system.SysVersion) (err error) {
+ err = global.GVA_DB.Create(sysVersion).Error
+ return err
+}
+
+// DeleteSysVersion 删除版本管理记录
+// Author [yourname](https://github.com/yourname)
+func (sysVersionService *SysVersionService) DeleteSysVersion(ctx context.Context, ID string) (err error) {
+ err = global.GVA_DB.Delete(&system.SysVersion{}, "id = ?", ID).Error
+ return err
+}
+
+// DeleteSysVersionByIds 批量删除版本管理记录
+// Author [yourname](https://github.com/yourname)
+func (sysVersionService *SysVersionService) DeleteSysVersionByIds(ctx context.Context, IDs []string) (err error) {
+ err = global.GVA_DB.Where("id in ?", IDs).Delete(&system.SysVersion{}).Error
+ return err
+}
+
+// GetSysVersion 根据ID获取版本管理记录
+// Author [yourname](https://github.com/yourname)
+func (sysVersionService *SysVersionService) GetSysVersion(ctx context.Context, ID string) (sysVersion system.SysVersion, err error) {
+ err = global.GVA_DB.Where("id = ?", ID).First(&sysVersion).Error
+ return
+}
+
+// GetSysVersionInfoList 分页获取版本管理记录
+// Author [yourname](https://github.com/yourname)
+func (sysVersionService *SysVersionService) GetSysVersionInfoList(ctx context.Context, info systemReq.SysVersionSearch) (list []system.SysVersion, total int64, err error) {
+ limit := info.PageSize
+ offset := info.PageSize * (info.Page - 1)
+ // 创建db
+ db := global.GVA_DB.Model(&system.SysVersion{})
+ var sysVersions []system.SysVersion
+ // 如果有条件搜索 下方会自动创建搜索语句
+ if len(info.CreatedAtRange) == 2 {
+ db = db.Where("created_at BETWEEN ? AND ?", info.CreatedAtRange[0], info.CreatedAtRange[1])
+ }
+
+ if info.VersionName != nil && *info.VersionName != "" {
+ db = db.Where("version_name LIKE ?", "%"+*info.VersionName+"%")
+ }
+ if info.VersionCode != nil && *info.VersionCode != "" {
+ db = db.Where("version_code = ?", *info.VersionCode)
+ }
+ err = db.Count(&total).Error
+ if err != nil {
+ return
+ }
+
+ if limit != 0 {
+ db = db.Limit(limit).Offset(offset)
+ }
+
+ err = db.Find(&sysVersions).Error
+ return sysVersions, total, err
+}
+func (sysVersionService *SysVersionService) GetSysVersionPublic(ctx context.Context) {
+ // 此方法为获取数据源定义的数据
+ // 请自行实现
+}
+
+// GetMenusByIds 根据ID列表获取菜单数据
+func (sysVersionService *SysVersionService) GetMenusByIds(ctx context.Context, ids []uint) (menus []system.SysBaseMenu, err error) {
+ err = global.GVA_DB.Where("id in ?", ids).Preload("Parameters").Preload("MenuBtn").Find(&menus).Error
+ return
+}
+
+// GetApisByIds 根据ID列表获取API数据
+func (sysVersionService *SysVersionService) GetApisByIds(ctx context.Context, ids []uint) (apis []system.SysApi, err error) {
+ err = global.GVA_DB.Where("id in ?", ids).Find(&apis).Error
+ return
+}
+
+// GetDictionariesByIds 根据ID列表获取字典数据
+func (sysVersionService *SysVersionService) GetDictionariesByIds(ctx context.Context, ids []uint) (dictionaries []system.SysDictionary, err error) {
+ err = global.GVA_DB.Where("id in ?", ids).Preload("SysDictionaryDetails").Find(&dictionaries).Error
+ return
+}
+
+// ImportMenus 导入菜单数据
+func (sysVersionService *SysVersionService) ImportMenus(ctx context.Context, menus []system.SysBaseMenu) error {
+ return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
+ // 递归创建菜单
+ return sysVersionService.createMenusRecursively(tx, menus, 0)
+ })
+}
+
+// createMenusRecursively 递归创建菜单
+func (sysVersionService *SysVersionService) createMenusRecursively(tx *gorm.DB, menus []system.SysBaseMenu, parentId uint) error {
+ for _, menu := range menus {
+ // 检查菜单是否已存在
+ var existingMenu system.SysBaseMenu
+ if err := tx.Where("name = ? AND path = ?", menu.Name, menu.Path).First(&existingMenu).Error; err == nil {
+ // 菜单已存在,使用现有菜单ID继续处理子菜单
+ if len(menu.Children) > 0 {
+ if err := sysVersionService.createMenusRecursively(tx, menu.Children, existingMenu.ID); err != nil {
+ return err
+ }
+ }
+ continue
+ }
+
+ // 保存参数和按钮数据,稍后处理
+ parameters := menu.Parameters
+ menuBtns := menu.MenuBtn
+ children := menu.Children
+
+ // 创建新菜单(不包含关联数据)
+ newMenu := system.SysBaseMenu{
+ ParentId: parentId,
+ Path: menu.Path,
+ Name: menu.Name,
+ Hidden: menu.Hidden,
+ Component: menu.Component,
+ Sort: menu.Sort,
+ Meta: menu.Meta,
+ }
+
+ if err := tx.Create(&newMenu).Error; err != nil {
+ return err
+ }
+
+ // 创建参数
+ if len(parameters) > 0 {
+ for _, param := range parameters {
+ newParam := system.SysBaseMenuParameter{
+ SysBaseMenuID: newMenu.ID,
+ Type: param.Type,
+ Key: param.Key,
+ Value: param.Value,
+ }
+ if err := tx.Create(&newParam).Error; err != nil {
+ return err
+ }
+ }
+ }
+
+ // 创建菜单按钮
+ if len(menuBtns) > 0 {
+ for _, btn := range menuBtns {
+ newBtn := system.SysBaseMenuBtn{
+ SysBaseMenuID: newMenu.ID,
+ Name: btn.Name,
+ Desc: btn.Desc,
+ }
+ if err := tx.Create(&newBtn).Error; err != nil {
+ return err
+ }
+ }
+ }
+
+ // 递归处理子菜单
+ if len(children) > 0 {
+ if err := sysVersionService.createMenusRecursively(tx, children, newMenu.ID); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+// ImportApis 导入API数据
+func (sysVersionService *SysVersionService) ImportApis(apis []system.SysApi) error {
+ return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
+ for _, api := range apis {
+ // 检查API是否已存在
+ var existingApi system.SysApi
+ if err := tx.Where("path = ? AND method = ?", api.Path, api.Method).First(&existingApi).Error; err == nil {
+ // API已存在,跳过
+ continue
+ }
+
+ // 创建新API
+ newApi := system.SysApi{
+ Path: api.Path,
+ Description: api.Description,
+ ApiGroup: api.ApiGroup,
+ Method: api.Method,
+ }
+
+ if err := tx.Create(&newApi).Error; err != nil {
+ return err
+ }
+ }
+ return nil
+ })
+}
+
+// ImportDictionaries 导入字典数据
+func (sysVersionService *SysVersionService) ImportDictionaries(dictionaries []system.SysDictionary) error {
+ return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
+ for _, dict := range dictionaries {
+ // 检查字典是否已存在
+ var existingDict system.SysDictionary
+ if err := tx.Where("type = ?", dict.Type).First(&existingDict).Error; err == nil {
+ // 字典已存在,跳过
+ continue
+ }
+
+ // 创建新字典
+ newDict := system.SysDictionary{
+ Name: dict.Name,
+ Type: dict.Type,
+ Status: dict.Status,
+ Desc: dict.Desc,
+ SysDictionaryDetails: dict.SysDictionaryDetails,
+ }
+
+ if err := tx.Create(&newDict).Error; err != nil {
+ return err
+ }
+ }
+ return nil
+ })
+}
diff --git a/server/source/example/file_upload_download.go b/server/source/example/file_upload_download.go
new file mode 100644
index 0000000..2a6cb3c
--- /dev/null
+++ b/server/source/example/file_upload_download.go
@@ -0,0 +1,66 @@
+package example
+
+import (
+ "context"
+
+ "git.echol.cn/loser/st/server/model/example"
+ "git.echol.cn/loser/st/server/service/system"
+ "github.com/pkg/errors"
+ "gorm.io/gorm"
+)
+
+const initOrderExaFile = system.InitOrderInternal + 1
+
+type initExaFileMysql struct{}
+
+// auto run
+func init() {
+ system.RegisterInit(initOrderExaFile, &initExaFileMysql{})
+}
+
+func (i *initExaFileMysql) MigrateTable(ctx context.Context) (context.Context, error) {
+ db, ok := ctx.Value("db").(*gorm.DB)
+ if !ok {
+ return ctx, system.ErrMissingDBContext
+ }
+ return ctx, db.AutoMigrate(&example.ExaFileUploadAndDownload{})
+}
+
+func (i *initExaFileMysql) TableCreated(ctx context.Context) bool {
+ db, ok := ctx.Value("db").(*gorm.DB)
+ if !ok {
+ return false
+ }
+ return db.Migrator().HasTable(&example.ExaFileUploadAndDownload{})
+}
+
+func (i *initExaFileMysql) InitializerName() string {
+ return example.ExaFileUploadAndDownload{}.TableName()
+}
+
+func (i *initExaFileMysql) InitializeData(ctx context.Context) (context.Context, error) {
+ db, ok := ctx.Value("db").(*gorm.DB)
+ if !ok {
+ return ctx, system.ErrMissingDBContext
+ }
+ entities := []example.ExaFileUploadAndDownload{
+ {Name: "10.png", Url: "https://qmplusimg.henrongyi.top/gvalogo.png", Tag: "png", Key: "158787308910.png"},
+ {Name: "logo.png", Url: "https://qmplusimg.henrongyi.top/1576554439myAvatar.png", Tag: "png", Key: "1587973709logo.png"},
+ }
+ if err := db.Create(&entities).Error; err != nil {
+ return ctx, errors.Wrap(err, example.ExaFileUploadAndDownload{}.TableName()+"表数据初始化失败!")
+ }
+ return ctx, nil
+}
+
+func (i *initExaFileMysql) DataInserted(ctx context.Context) bool {
+ db, ok := ctx.Value("db").(*gorm.DB)
+ if !ok {
+ return false
+ }
+ lookup := example.ExaFileUploadAndDownload{Name: "logo.png", Key: "1587973709logo.png"}
+ if errors.Is(db.First(&lookup, &lookup).Error, gorm.ErrRecordNotFound) {
+ return false
+ }
+ return true
+}
diff --git a/server/source/system/api.go b/server/source/system/api.go
new file mode 100644
index 0000000..e7e35ab
--- /dev/null
+++ b/server/source/system/api.go
@@ -0,0 +1,264 @@
+package system
+
+import (
+ "context"
+
+ sysModel "git.echol.cn/loser/st/server/model/system"
+ "git.echol.cn/loser/st/server/service/system"
+ "github.com/pkg/errors"
+ "gorm.io/gorm"
+)
+
+type initApi struct{}
+
+const initOrderApi = system.InitOrderSystem + 1
+
+// auto run
+func init() {
+ system.RegisterInit(initOrderApi, &initApi{})
+}
+
+func (i *initApi) InitializerName() string {
+ return sysModel.SysApi{}.TableName()
+}
+
+func (i *initApi) MigrateTable(ctx context.Context) (context.Context, error) {
+ db, ok := ctx.Value("db").(*gorm.DB)
+ if !ok {
+ return ctx, system.ErrMissingDBContext
+ }
+ return ctx, db.AutoMigrate(&sysModel.SysApi{})
+}
+
+func (i *initApi) TableCreated(ctx context.Context) bool {
+ db, ok := ctx.Value("db").(*gorm.DB)
+ if !ok {
+ return false
+ }
+ return db.Migrator().HasTable(&sysModel.SysApi{})
+}
+
+func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) {
+ db, ok := ctx.Value("db").(*gorm.DB)
+ if !ok {
+ return ctx, system.ErrMissingDBContext
+ }
+ entities := []sysModel.SysApi{
+ {ApiGroup: "jwt", Method: "POST", Path: "/jwt/jsonInBlacklist", Description: "jwt加入黑名单(退出,必选)"},
+
+ {ApiGroup: "登录日志", Method: "DELETE", Path: "/sysLoginLog/deleteLoginLog", Description: "删除登录日志"},
+ {ApiGroup: "登录日志", Method: "DELETE", Path: "/sysLoginLog/deleteLoginLogByIds", Description: "批量删除登录日志"},
+ {ApiGroup: "登录日志", Method: "GET", Path: "/sysLoginLog/findLoginLog", Description: "根据ID获取登录日志"},
+ {ApiGroup: "登录日志", Method: "GET", Path: "/sysLoginLog/getLoginLogList", Description: "获取登录日志列表"},
+
+ {ApiGroup: "API Token", Method: "POST", Path: "/sysApiToken/createApiToken", Description: "签发API Token"},
+ {ApiGroup: "API Token", Method: "POST", Path: "/sysApiToken/getApiTokenList", Description: "获取API Token列表"},
+ {ApiGroup: "API Token", Method: "POST", Path: "/sysApiToken/deleteApiToken", Description: "作废API Token"},
+
+ {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: "skills", Method: "GET", Path: "/skills/getTools", Description: "获取技能工具列表"},
+ {ApiGroup: "skills", Method: "POST", Path: "/skills/getSkillList", Description: "获取技能列表"},
+ {ApiGroup: "skills", Method: "POST", Path: "/skills/getSkillDetail", Description: "获取技能详情"},
+ {ApiGroup: "skills", Method: "POST", Path: "/skills/saveSkill", Description: "保存技能定义"},
+ {ApiGroup: "skills", Method: "POST", Path: "/skills/createScript", Description: "创建技能脚本"},
+ {ApiGroup: "skills", Method: "POST", Path: "/skills/getScript", Description: "读取技能脚本"},
+ {ApiGroup: "skills", Method: "POST", Path: "/skills/saveScript", Description: "保存技能脚本"},
+ {ApiGroup: "skills", Method: "POST", Path: "/skills/createResource", Description: "创建技能资源"},
+ {ApiGroup: "skills", Method: "POST", Path: "/skills/getResource", Description: "读取技能资源"},
+ {ApiGroup: "skills", Method: "POST", Path: "/skills/saveResource", Description: "保存技能资源"},
+ {ApiGroup: "skills", Method: "POST", Path: "/skills/createReference", Description: "创建技能参考"},
+ {ApiGroup: "skills", Method: "POST", Path: "/skills/getReference", Description: "读取技能参考"},
+ {ApiGroup: "skills", Method: "POST", Path: "/skills/saveReference", Description: "保存技能参考"},
+ {ApiGroup: "skills", Method: "POST", Path: "/skills/createTemplate", Description: "创建技能模板"},
+ {ApiGroup: "skills", Method: "POST", Path: "/skills/getTemplate", Description: "读取技能模板"},
+ {ApiGroup: "skills", Method: "POST", Path: "/skills/saveTemplate", Description: "保存技能模板"},
+ {ApiGroup: "skills", Method: "POST", Path: "/skills/getGlobalConstraint", Description: "读取全局约束"},
+ {ApiGroup: "skills", Method: "POST", Path: "/skills/saveGlobalConstraint", 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/removePlugin", Description: "卸载插件"},
+ {ApiGroup: "代码生成器", Method: "GET", Path: "/autoCode/getPluginList", Description: "获取已安装插件"},
+ {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/mcp", Description: "自动生成 MCP Tool 模板"},
+ {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/mcpTest", Description: "MCP Tool 测试"},
+ {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/mcpList", Description: "获取 MCP ToolList"},
+
+ {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: "GET", Path: "/sysDictionaryDetail/getDictionaryTreeList", Description: "获取字典数列表"},
+ {ApiGroup: "系统字典详情", Method: "GET", Path: "/sysDictionaryDetail/getDictionaryTreeListByType", Description: "根据分类获取字典数列表"},
+ {ApiGroup: "系统字典详情", Method: "GET", Path: "/sysDictionaryDetail/getDictionaryDetailsByParent", Description: "根据父级ID获取字典详情"},
+ {ApiGroup: "系统字典详情", Method: "GET", Path: "/sysDictionaryDetail/getDictionaryPath", 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: "/sysDictionary/importSysDictionary", Description: "导入字典JSON"},
+ {ApiGroup: "系统字典", Method: "GET", Path: "/sysDictionary/exportSysDictionary", Description: "导出字典JSON"},
+
+ {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: "GET", Path: "/sysExportTemplate/previewSQL", Description: "预览SQL"},
+ {ApiGroup: "导出模板", Method: "POST", Path: "/sysExportTemplate/importExcel", Description: "导入Excel"},
+
+ {ApiGroup: "错误日志", Method: "POST", Path: "/sysError/createSysError", Description: "新建错误日志"},
+ {ApiGroup: "错误日志", Method: "DELETE", Path: "/sysError/deleteSysError", Description: "删除错误日志"},
+ {ApiGroup: "错误日志", Method: "DELETE", Path: "/sysError/deleteSysErrorByIds", Description: "批量删除错误日志"},
+ {ApiGroup: "错误日志", Method: "PUT", Path: "/sysError/updateSysError", Description: "更新错误日志"},
+ {ApiGroup: "错误日志", Method: "GET", Path: "/sysError/findSysError", Description: "根据ID获取错误日志"},
+ {ApiGroup: "错误日志", Method: "GET", Path: "/sysError/getSysErrorList", Description: "获取错误日志列表"},
+ {ApiGroup: "错误日志", Method: "GET", Path: "/sysError/getSysErrorSolution", Description: "触发错误处理(异步)"},
+
+ {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: "删除分类"},
+
+ {ApiGroup: "版本控制", Method: "GET", Path: "/sysVersion/findSysVersion", Description: "获取单一版本"},
+ {ApiGroup: "版本控制", Method: "GET", Path: "/sysVersion/getSysVersionList", Description: "获取版本列表"},
+ {ApiGroup: "版本控制", Method: "GET", Path: "/sysVersion/downloadVersionJson", Description: "下载版本json"},
+ {ApiGroup: "版本控制", Method: "POST", Path: "/sysVersion/exportVersion", Description: "创建版本"},
+ {ApiGroup: "版本控制", Method: "POST", Path: "/sysVersion/importVersion", Description: "同步版本"},
+ {ApiGroup: "版本控制", Method: "DELETE", Path: "/sysVersion/deleteSysVersion", Description: "删除版本"},
+ {ApiGroup: "版本控制", Method: "DELETE", Path: "/sysVersion/deleteSysVersionByIds", 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/server/source/system/api_ignore.go b/server/source/system/api_ignore.go
new file mode 100644
index 0000000..3d1805c
--- /dev/null
+++ b/server/source/system/api_ignore.go
@@ -0,0 +1,78 @@
+package system
+
+import (
+ "context"
+
+ sysModel "git.echol.cn/loser/st/server/model/system"
+ "git.echol.cn/loser/st/server/service/system"
+ "github.com/pkg/errors"
+ "gorm.io/gorm"
+)
+
+type initApiIgnore struct{}
+
+const initOrderApiIgnore = initOrderApi + 1
+
+// auto run
+func init() {
+ system.RegisterInit(initOrderApiIgnore, &initApiIgnore{})
+}
+
+func (i *initApiIgnore) InitializerName() string {
+ return sysModel.SysIgnoreApi{}.TableName()
+}
+
+func (i *initApiIgnore) MigrateTable(ctx context.Context) (context.Context, error) {
+ db, ok := ctx.Value("db").(*gorm.DB)
+ if !ok {
+ return ctx, system.ErrMissingDBContext
+ }
+ return ctx, db.AutoMigrate(&sysModel.SysIgnoreApi{})
+}
+
+func (i *initApiIgnore) TableCreated(ctx context.Context) bool {
+ db, ok := ctx.Value("db").(*gorm.DB)
+ if !ok {
+ return false
+ }
+ return db.Migrator().HasTable(&sysModel.SysIgnoreApi{})
+}
+
+func (i *initApiIgnore) InitializeData(ctx context.Context) (context.Context, error) {
+ db, ok := ctx.Value("db").(*gorm.DB)
+ if !ok {
+ return ctx, system.ErrMissingDBContext
+ }
+ entities := []sysModel.SysIgnoreApi{
+ {Method: "GET", Path: "/swagger/*any"},
+ {Method: "GET", Path: "/api/freshCasbin"},
+ {Method: "GET", Path: "/uploads/file/*filepath"},
+ {Method: "GET", Path: "/health"},
+ {Method: "HEAD", Path: "/uploads/file/*filepath"},
+ {Method: "POST", Path: "/autoCode/llmAuto"},
+ {Method: "POST", Path: "/system/reloadSystem"},
+ {Method: "POST", Path: "/base/login"},
+ {Method: "POST", Path: "/base/captcha"},
+ {Method: "POST", Path: "/init/initdb"},
+ {Method: "POST", Path: "/init/checkdb"},
+ {Method: "GET", Path: "/info/getInfoDataSource"},
+ {Method: "GET", Path: "/info/getInfoPublic"},
+ }
+ if err := db.Create(&entities).Error; err != nil {
+ return ctx, errors.Wrap(err, sysModel.SysIgnoreApi{}.TableName()+"表数据初始化失败!")
+ }
+ next := context.WithValue(ctx, i.InitializerName(), entities)
+ return next, nil
+}
+
+func (i *initApiIgnore) DataInserted(ctx context.Context) bool {
+ db, ok := ctx.Value("db").(*gorm.DB)
+ if !ok {
+ return false
+ }
+ if errors.Is(db.Where("path = ? AND method = ?", "/swagger/*any", "GET").
+ First(&sysModel.SysIgnoreApi{}).Error, gorm.ErrRecordNotFound) {
+ return false
+ }
+ return true
+}
diff --git a/server/source/system/authorities_menus.go b/server/source/system/authorities_menus.go
new file mode 100644
index 0000000..27e47d2
--- /dev/null
+++ b/server/source/system/authorities_menus.go
@@ -0,0 +1,121 @@
+package system
+
+import (
+ "context"
+
+ sysModel "git.echol.cn/loser/st/server/model/system"
+ "git.echol.cn/loser/st/server/service/system"
+ "github.com/pkg/errors"
+ "gorm.io/gorm"
+)
+
+const initOrderMenuAuthority = initOrderMenu + initOrderAuthority
+
+type initMenuAuthority struct{}
+
+// auto run
+func init() {
+ system.RegisterInit(initOrderMenuAuthority, &initMenuAuthority{})
+}
+
+func (i *initMenuAuthority) MigrateTable(ctx context.Context) (context.Context, error) {
+ return ctx, nil // do nothing
+}
+
+func (i *initMenuAuthority) TableCreated(ctx context.Context) bool {
+ return false // always replace
+}
+
+func (i *initMenuAuthority) InitializerName() string {
+ return "sys_menu_authorities"
+}
+
+func (i *initMenuAuthority) InitializeData(ctx context.Context) (next context.Context, err error) {
+ db, ok := ctx.Value("db").(*gorm.DB)
+ if !ok {
+ return ctx, system.ErrMissingDBContext
+ }
+
+ initAuth := &initAuthority{}
+ authorities, ok := ctx.Value(initAuth.InitializerName()).([]sysModel.SysAuthority)
+ if !ok {
+ return ctx, errors.Wrap(system.ErrMissingDependentContext, "创建 [菜单-权限] 关联失败, 未找到权限表初始化数据")
+ }
+
+ allMenus, ok := ctx.Value(new(initMenu).InitializerName()).([]sysModel.SysBaseMenu)
+ if !ok {
+ return next, errors.Wrap(errors.New(""), "创建 [菜单-权限] 关联失败, 未找到菜单表初始化数据")
+ }
+ next = ctx
+
+ // 构建菜单ID映射,方便快速查找
+ menuMap := make(map[uint]sysModel.SysBaseMenu)
+ for _, menu := range allMenus {
+ menuMap[menu.ID] = menu
+ }
+
+ // 为不同角色分配不同权限
+ // 1. 超级管理员角色(888) - 拥有所有菜单权限
+ if err = db.Model(&authorities[0]).Association("SysBaseMenus").Replace(allMenus); err != nil {
+ return next, errors.Wrap(err, "为超级管理员分配菜单失败")
+ }
+
+ // 2. 普通用户角色(8881) - 仅拥有基础功能菜单
+ // 仅选择部分父级菜单及其子菜单
+ var menu8881 []sysModel.SysBaseMenu
+
+ // 添加仪表盘、关于我们和个人信息菜单
+ for _, menu := range allMenus {
+ if menu.ParentId == 0 && (menu.Name == "dashboard" || menu.Name == "about" || menu.Name == "person" || menu.Name == "state") {
+ menu8881 = append(menu8881, menu)
+ }
+ }
+
+ if err = db.Model(&authorities[1]).Association("SysBaseMenus").Replace(menu8881); err != nil {
+ return next, errors.Wrap(err, "为普通用户分配菜单失败")
+ }
+
+ // 3. 测试角色(9528) - 拥有部分菜单权限
+ var menu9528 []sysModel.SysBaseMenu
+
+ // 添加所有父级菜单
+ for _, menu := range allMenus {
+ if menu.ParentId == 0 {
+ menu9528 = append(menu9528, menu)
+ }
+ }
+
+ // 添加部分子菜单 - 系统工具、示例文件等模块的子菜单
+ for _, menu := range allMenus {
+ parentName := ""
+ if menu.ParentId > 0 && menuMap[menu.ParentId].Name != "" {
+ parentName = menuMap[menu.ParentId].Name
+ }
+
+ if menu.ParentId > 0 && (parentName == "systemTools" || parentName == "example") {
+ menu9528 = append(menu9528, menu)
+ }
+ }
+
+ if err = db.Model(&authorities[2]).Association("SysBaseMenus").Replace(menu9528); err != nil {
+ return next, errors.Wrap(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/server/source/system/authority.go b/server/source/system/authority.go
new file mode 100644
index 0000000..95dcffd
--- /dev/null
+++ b/server/source/system/authority.go
@@ -0,0 +1,89 @@
+package system
+
+import (
+ "context"
+
+ sysModel "git.echol.cn/loser/st/server/model/system"
+ "git.echol.cn/loser/st/server/service/system"
+ "git.echol.cn/loser/st/server/utils"
+ "github.com/pkg/errors"
+ "gorm.io/gorm"
+)
+
+const initOrderAuthority = initOrderCasbin + 1
+
+type initAuthority struct{}
+
+// auto run
+func init() {
+ system.RegisterInit(initOrderAuthority, &initAuthority{})
+}
+
+func (i *initAuthority) MigrateTable(ctx context.Context) (context.Context, error) {
+ db, ok := ctx.Value("db").(*gorm.DB)
+ if !ok {
+ return ctx, system.ErrMissingDBContext
+ }
+ return ctx, db.AutoMigrate(&sysModel.SysAuthority{})
+}
+
+func (i *initAuthority) TableCreated(ctx context.Context) bool {
+ db, ok := ctx.Value("db").(*gorm.DB)
+ if !ok {
+ return false
+ }
+ return db.Migrator().HasTable(&sysModel.SysAuthority{})
+}
+
+func (i *initAuthority) InitializerName() string {
+ return sysModel.SysAuthority{}.TableName()
+}
+
+func (i *initAuthority) InitializeData(ctx context.Context) (context.Context, error) {
+ db, ok := ctx.Value("db").(*gorm.DB)
+ if !ok {
+ return ctx, system.ErrMissingDBContext
+ }
+ entities := []sysModel.SysAuthority{
+ {AuthorityId: 888, AuthorityName: "普通用户", ParentId: utils.Pointer[uint](0), DefaultRouter: "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/server/source/system/casbin.go b/server/source/system/casbin.go
new file mode 100644
index 0000000..6fb9446
--- /dev/null
+++ b/server/source/system/casbin.go
@@ -0,0 +1,348 @@
+package system
+
+import (
+ "context"
+
+ "git.echol.cn/loser/st/server/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: "/sysLoginLog/deleteLoginLog", V2: "DELETE"},
+ {Ptype: "p", V0: "888", V1: "/sysLoginLog/deleteLoginLogByIds", V2: "DELETE"},
+ {Ptype: "p", V0: "888", V1: "/sysLoginLog/findLoginLog", V2: "GET"},
+ {Ptype: "p", V0: "888", V1: "/sysLoginLog/getLoginLogList", V2: "GET"},
+
+ {Ptype: "p", V0: "888", V1: "/sysApiToken/createApiToken", V2: "POST"},
+ {Ptype: "p", V0: "888", V1: "/sysApiToken/getApiTokenList", V2: "POST"},
+ {Ptype: "p", V0: "888", V1: "/sysApiToken/deleteApiToken", 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: "/skills/getTools", V2: "GET"},
+ {Ptype: "p", V0: "888", V1: "/skills/getSkillList", V2: "POST"},
+ {Ptype: "p", V0: "888", V1: "/skills/getSkillDetail", V2: "POST"},
+ {Ptype: "p", V0: "888", V1: "/skills/saveSkill", V2: "POST"},
+ {Ptype: "p", V0: "888", V1: "/skills/createScript", V2: "POST"},
+ {Ptype: "p", V0: "888", V1: "/skills/getScript", V2: "POST"},
+ {Ptype: "p", V0: "888", V1: "/skills/saveScript", V2: "POST"},
+ {Ptype: "p", V0: "888", V1: "/skills/createResource", V2: "POST"},
+ {Ptype: "p", V0: "888", V1: "/skills/getResource", V2: "POST"},
+ {Ptype: "p", V0: "888", V1: "/skills/saveResource", V2: "POST"},
+ {Ptype: "p", V0: "888", V1: "/skills/createReference", V2: "POST"},
+ {Ptype: "p", V0: "888", V1: "/skills/getReference", V2: "POST"},
+ {Ptype: "p", V0: "888", V1: "/skills/saveReference", V2: "POST"},
+ {Ptype: "p", V0: "888", V1: "/skills/createTemplate", V2: "POST"},
+ {Ptype: "p", V0: "888", V1: "/skills/getTemplate", V2: "POST"},
+ {Ptype: "p", V0: "888", V1: "/skills/saveTemplate", V2: "POST"},
+ {Ptype: "p", V0: "888", V1: "/skills/getGlobalConstraint", V2: "POST"},
+ {Ptype: "p", V0: "888", V1: "/skills/saveGlobalConstraint", 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/removePlugin", V2: "POST"},
+ {Ptype: "p", V0: "888", V1: "/autoCode/getPluginList", V2: "GET"},
+ {Ptype: "p", V0: "888", V1: "/autoCode/addFunc", V2: "POST"},
+ {Ptype: "p", V0: "888", V1: "/autoCode/mcp", V2: "POST"},
+ {Ptype: "p", V0: "888", V1: "/autoCode/mcpTest", V2: "POST"},
+ {Ptype: "p", V0: "888", V1: "/autoCode/mcpList", 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: "/sysDictionaryDetail/getDictionaryTreeList", V2: "GET"},
+ {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/getDictionaryTreeListByType", V2: "GET"},
+ {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/getDictionaryDetailsByParent", V2: "GET"},
+ {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/getDictionaryPath", V2: "GET"},
+
+ {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: "/sysDictionary/importSysDictionary", V2: "POST"},
+ {Ptype: "p", V0: "888", V1: "/sysDictionary/exportSysDictionary", V2: "GET"},
+
+ {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/previewSQL", V2: "GET"},
+ {Ptype: "p", V0: "888", V1: "/sysExportTemplate/importExcel", V2: "POST"},
+
+ {Ptype: "p", V0: "888", V1: "/sysError/createSysError", V2: "POST"},
+ {Ptype: "p", V0: "888", V1: "/sysError/deleteSysError", V2: "DELETE"},
+ {Ptype: "p", V0: "888", V1: "/sysError/deleteSysErrorByIds", V2: "DELETE"},
+ {Ptype: "p", V0: "888", V1: "/sysError/updateSysError", V2: "PUT"},
+ {Ptype: "p", V0: "888", V1: "/sysError/findSysError", V2: "GET"},
+ {Ptype: "p", V0: "888", V1: "/sysError/getSysErrorList", V2: "GET"},
+ {Ptype: "p", V0: "888", V1: "/sysError/getSysErrorSolution", V2: "GET"},
+
+ {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: "888", V1: "/sysVersion/findSysVersion", V2: "GET"},
+ {Ptype: "p", V0: "888", V1: "/sysVersion/getSysVersionList", V2: "GET"},
+ {Ptype: "p", V0: "888", V1: "/sysVersion/downloadVersionJson", V2: "GET"},
+ {Ptype: "p", V0: "888", V1: "/sysVersion/exportVersion", V2: "POST"},
+ {Ptype: "p", V0: "888", V1: "/sysVersion/importVersion", V2: "POST"},
+ {Ptype: "p", V0: "888", V1: "/sysVersion/deleteSysVersion", V2: "DELETE"},
+ {Ptype: "p", V0: "888", V1: "/sysVersion/deleteSysVersionByIds", V2: "DELETE"},
+
+ {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/server/source/system/dictionary.go b/server/source/system/dictionary.go
new file mode 100644
index 0000000..071e5b1
--- /dev/null
+++ b/server/source/system/dictionary.go
@@ -0,0 +1,72 @@
+package system
+
+import (
+ "context"
+
+ sysModel "git.echol.cn/loser/st/server/model/system"
+ "git.echol.cn/loser/st/server/service/system"
+ "github.com/pkg/errors"
+ "gorm.io/gorm"
+)
+
+const initOrderDict = initOrderCasbin + 1
+
+type initDict struct{}
+
+// auto run
+func init() {
+ system.RegisterInit(initOrderDict, &initDict{})
+}
+
+func (i *initDict) MigrateTable(ctx context.Context) (context.Context, error) {
+ db, ok := ctx.Value("db").(*gorm.DB)
+ if !ok {
+ return ctx, system.ErrMissingDBContext
+ }
+ return ctx, db.AutoMigrate(&sysModel.SysDictionary{})
+}
+
+func (i *initDict) TableCreated(ctx context.Context) bool {
+ db, ok := ctx.Value("db").(*gorm.DB)
+ if !ok {
+ return false
+ }
+ return db.Migrator().HasTable(&sysModel.SysDictionary{})
+}
+
+func (i *initDict) InitializerName() string {
+ return sysModel.SysDictionary{}.TableName()
+}
+
+func (i *initDict) InitializeData(ctx context.Context) (next context.Context, err error) {
+ db, ok := ctx.Value("db").(*gorm.DB)
+ if !ok {
+ return ctx, system.ErrMissingDBContext
+ }
+ True := true
+ entities := []sysModel.SysDictionary{
+ {Name: "性别", Type: "gender", Status: &True, Desc: "性别字典"},
+ {Name: "数据库int类型", Type: "int", Status: &True, Desc: "int类型对应的数据库类型"},
+ {Name: "数据库时间日期类型", Type: "time.Time", Status: &True, Desc: "数据库时间日期类型"},
+ {Name: "数据库浮点型", Type: "float64", Status: &True, Desc: "数据库浮点型"},
+ {Name: "数据库字符串", Type: "string", Status: &True, Desc: "数据库字符串"},
+ {Name: "数据库bool类型", Type: "bool", Status: &True, Desc: "数据库bool类型"},
+ }
+
+ if err = db.Create(&entities).Error; err != nil {
+ return ctx, errors.Wrap(err, sysModel.SysDictionary{}.TableName()+"表数据初始化失败!")
+ }
+ next = context.WithValue(ctx, i.InitializerName(), entities)
+ return next, nil
+}
+
+func (i *initDict) DataInserted(ctx context.Context) bool {
+ db, ok := ctx.Value("db").(*gorm.DB)
+ if !ok {
+ return false
+ }
+ if errors.Is(db.Where("type = ?", "bool").First(&sysModel.SysDictionary{}).Error, gorm.ErrRecordNotFound) { // 判断是否存在数据
+ return false
+ }
+ return true
+}
diff --git a/server/source/system/dictionary_detail.go b/server/source/system/dictionary_detail.go
new file mode 100644
index 0000000..58d130c
--- /dev/null
+++ b/server/source/system/dictionary_detail.go
@@ -0,0 +1,122 @@
+package system
+
+import (
+ "context"
+ "fmt"
+
+ sysModel "git.echol.cn/loser/st/server/model/system"
+ "git.echol.cn/loser/st/server/service/system"
+ "github.com/pkg/errors"
+ "gorm.io/gorm"
+)
+
+const initOrderDictDetail = initOrderDict + 1
+
+type initDictDetail struct{}
+
+// auto run
+func init() {
+ system.RegisterInit(initOrderDictDetail, &initDictDetail{})
+}
+
+func (i *initDictDetail) MigrateTable(ctx context.Context) (context.Context, error) {
+ db, ok := ctx.Value("db").(*gorm.DB)
+ if !ok {
+ return ctx, system.ErrMissingDBContext
+ }
+ return ctx, db.AutoMigrate(&sysModel.SysDictionaryDetail{})
+}
+
+func (i *initDictDetail) TableCreated(ctx context.Context) bool {
+ db, ok := ctx.Value("db").(*gorm.DB)
+ if !ok {
+ return false
+ }
+ return db.Migrator().HasTable(&sysModel.SysDictionaryDetail{})
+}
+
+func (i *initDictDetail) InitializerName() string {
+ return sysModel.SysDictionaryDetail{}.TableName()
+}
+
+func (i *initDictDetail) InitializeData(ctx context.Context) (context.Context, error) {
+ db, ok := ctx.Value("db").(*gorm.DB)
+ if !ok {
+ return ctx, system.ErrMissingDBContext
+ }
+ dicts, ok := ctx.Value(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", Value: "0", Status: &True, Extend: "mysql", Sort: 0},
+ {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", Value: "0", Status: &True, Extend: "mysql", Sort: 0},
+ {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", Value: "0", Status: &True, Extend: "mysql", Sort: 0},
+ {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/server/source/system/excel_template.go b/server/source/system/excel_template.go
new file mode 100644
index 0000000..fac0b13
--- /dev/null
+++ b/server/source/system/excel_template.go
@@ -0,0 +1,76 @@
+package system
+
+import (
+ "context"
+
+ sysModel "git.echol.cn/loser/st/server/model/system"
+ "git.echol.cn/loser/st/server/service/system"
+ "github.com/pkg/errors"
+ "gorm.io/gorm"
+)
+
+type initExcelTemplate struct{}
+
+const initOrderExcelTemplate = initOrderDictDetail + 1
+
+// auto run
+func init() {
+ system.RegisterInit(initOrderExcelTemplate, &initExcelTemplate{})
+}
+
+func (i *initExcelTemplate) InitializerName() string {
+ return "sys_export_templates"
+}
+
+func (i *initExcelTemplate) MigrateTable(ctx context.Context) (context.Context, error) {
+ db, ok := ctx.Value("db").(*gorm.DB)
+ if !ok {
+ return ctx, system.ErrMissingDBContext
+ }
+ return ctx, db.AutoMigrate(&sysModel.SysExportTemplate{})
+}
+
+func (i *initExcelTemplate) TableCreated(ctx context.Context) bool {
+ db, ok := ctx.Value("db").(*gorm.DB)
+ if !ok {
+ return false
+ }
+ return db.Migrator().HasTable(&sysModel.SysExportTemplate{})
+}
+
+func (i *initExcelTemplate) InitializeData(ctx context.Context) (context.Context, error) {
+ db, ok := ctx.Value("db").(*gorm.DB)
+ if !ok {
+ return ctx, system.ErrMissingDBContext
+ }
+
+ entities := []sysModel.SysExportTemplate{
+ {
+ Name: "api",
+ TableName: "sys_apis",
+ TemplateID: "api",
+ TemplateInfo: `{
+"path":"路径",
+"method":"方法(大写)",
+"description":"方法介绍",
+"api_group":"方法分组"
+}`,
+ },
+ }
+ if err := db.Create(&entities).Error; err != nil {
+ return ctx, errors.Wrap(err, "sys_export_templates"+"表数据初始化失败!")
+ }
+ next := context.WithValue(ctx, i.InitializerName(), entities)
+ return next, nil
+}
+
+func (i *initExcelTemplate) DataInserted(ctx context.Context) bool {
+ db, ok := ctx.Value("db").(*gorm.DB)
+ if !ok {
+ return false
+ }
+ if errors.Is(db.First(&sysModel.SysExportTemplate{}).Error, gorm.ErrRecordNotFound) {
+ return false
+ }
+ return true
+}
diff --git a/server/source/system/menu.go b/server/source/system/menu.go
new file mode 100644
index 0000000..b1ccbde
--- /dev/null
+++ b/server/source/system/menu.go
@@ -0,0 +1,138 @@
+package system
+
+import (
+ "context"
+
+ . "git.echol.cn/loser/st/server/model/system"
+ "git.echol.cn/loser/st/server/service/system"
+ "github.com/pkg/errors"
+ "gorm.io/gorm"
+)
+
+const initOrderMenu = initOrderAuthority + 1
+
+type initMenu struct{}
+
+// auto run
+func init() {
+ system.RegisterInit(initOrderMenu, &initMenu{})
+}
+
+func (i *initMenu) InitializerName() string {
+ return SysBaseMenu{}.TableName()
+}
+
+func (i *initMenu) MigrateTable(ctx context.Context) (context.Context, error) {
+ db, ok := ctx.Value("db").(*gorm.DB)
+ if !ok {
+ return ctx, system.ErrMissingDBContext
+ }
+ return ctx, db.AutoMigrate(
+ &SysBaseMenu{},
+ &SysBaseMenuParameter{},
+ &SysBaseMenuBtn{},
+ )
+}
+
+func (i *initMenu) TableCreated(ctx context.Context) bool {
+ db, ok := ctx.Value("db").(*gorm.DB)
+ if !ok {
+ return false
+ }
+ m := db.Migrator()
+ return m.HasTable(&SysBaseMenu{}) &&
+ m.HasTable(&SysBaseMenuParameter{}) &&
+ m.HasTable(&SysBaseMenuBtn{})
+}
+
+func (i *initMenu) InitializeData(ctx context.Context) (next context.Context, err error) {
+ db, ok := ctx.Value("db").(*gorm.DB)
+ if !ok {
+ return ctx, system.ErrMissingDBContext
+ }
+
+ // 定义所有菜单
+ allMenus := []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: 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: 0, Path: "systemTools", Name: "systemTools", Component: "view/systemTools/index.vue", Sort: 5, Meta: Meta{Title: "系统工具", Icon: "tools"}},
+ {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"}},
+ }
+
+ // 先创建父级菜单(ParentId = 0 的菜单)
+ if err = db.Create(&allMenus).Error; err != nil {
+ return ctx, errors.Wrap(err, SysBaseMenu{}.TableName()+"父级菜单初始化失败!")
+ }
+
+ // 建立菜单映射 - 通过Name查找已创建的菜单及其ID
+ menuNameMap := make(map[string]uint)
+ for _, menu := range allMenus {
+ menuNameMap[menu.Name] = menu.ID
+ }
+
+ // 定义子菜单,并设置正确的ParentId
+ childMenus := []SysBaseMenu{
+ // superAdmin子菜单
+ {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "authority", Name: "authority", Component: "view/superAdmin/authority/authority.vue", Sort: 1, Meta: Meta{Title: "角色管理", Icon: "avatar"}},
+ {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "menu", Name: "menu", Component: "view/superAdmin/menu/menu.vue", Sort: 2, Meta: Meta{Title: "菜单管理", Icon: "tickets", KeepAlive: true}},
+ {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "api", Name: "api", Component: "view/superAdmin/api/api.vue", Sort: 3, Meta: Meta{Title: "api管理", Icon: "platform", KeepAlive: true}},
+ {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "user", Name: "user", Component: "view/superAdmin/user/user.vue", Sort: 4, Meta: Meta{Title: "用户管理", Icon: "coordinate"}},
+ {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "dictionary", Name: "dictionary", Component: "view/superAdmin/dictionary/sysDictionary.vue", Sort: 5, Meta: Meta{Title: "字典管理", Icon: "notebook"}},
+ {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "operation", Name: "operation", Component: "view/superAdmin/operation/sysOperationRecord.vue", Sort: 6, Meta: Meta{Title: "操作历史", Icon: "pie-chart"}},
+ {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "sysParams", Name: "sysParams", Component: "view/superAdmin/params/sysParams.vue", Sort: 7, Meta: Meta{Title: "参数管理", Icon: "compass"}},
+
+ // example子菜单
+ {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["example"], Path: "upload", Name: "upload", Component: "view/example/upload/upload.vue", Sort: 5, Meta: Meta{Title: "媒体库(上传下载)", Icon: "upload"}},
+ {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["example"], Path: "breakpoint", Name: "breakpoint", Component: "view/example/breakpoint/breakpoint.vue", Sort: 6, Meta: Meta{Title: "断点续传", Icon: "upload-filled"}},
+ {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["example"], Path: "customer", Name: "customer", Component: "view/example/customer/customer.vue", Sort: 7, Meta: Meta{Title: "客户列表(资源示例)", Icon: "avatar"}},
+
+ // systemTools子菜单
+ {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "autoCode", Name: "autoCode", Component: "view/systemTools/autoCode/index.vue", Sort: 1, Meta: Meta{Title: "代码生成器", Icon: "cpu", KeepAlive: true}},
+ {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "formCreate", Name: "formCreate", Component: "view/systemTools/formCreate/index.vue", Sort: 3, Meta: Meta{Title: "表单生成器", Icon: "magic-stick", KeepAlive: true}},
+ {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "system", Name: "system", Component: "view/systemTools/system/system.vue", Sort: 4, Meta: Meta{Title: "系统配置", Icon: "operation"}},
+ {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "autoCodeAdmin", Name: "autoCodeAdmin", Component: "view/systemTools/autoCodeAdmin/index.vue", Sort: 2, Meta: Meta{Title: "自动化代码管理", Icon: "magic-stick"}},
+ {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "loginLog", Name: "loginLog", Component: "view/systemTools/loginLog/index.vue", Sort: 5, Meta: Meta{Title: "登录日志", Icon: "monitor"}},
+ {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "apiToken", Name: "apiToken", Component: "view/systemTools/apiToken/index.vue", Sort: 6, Meta: Meta{Title: "API Token", Icon: "key"}},
+ {MenuLevel: 1, Hidden: true, ParentId: menuNameMap["systemTools"], Path: "autoCodeEdit/:id", Name: "autoCodeEdit", Component: "view/systemTools/autoCode/index.vue", Sort: 0, Meta: Meta{Title: "自动化代码-${id}", Icon: "magic-stick"}},
+ {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "autoPkg", Name: "autoPkg", Component: "view/systemTools/autoPkg/autoPkg.vue", Sort: 0, Meta: Meta{Title: "模板配置", Icon: "folder"}},
+ {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "exportTemplate", Name: "exportTemplate", Component: "view/systemTools/exportTemplate/exportTemplate.vue", Sort: 5, Meta: Meta{Title: "导出模板", Icon: "reading"}},
+ {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "skills", Name: "skills", Component: "view/systemTools/skills/index.vue", Sort: 6, Meta: Meta{Title: "Skills管理", Icon: "document"}},
+ {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "picture", Name: "picture", Component: "view/systemTools/autoCode/picture.vue", Sort: 6, Meta: Meta{Title: "AI页面绘制", Icon: "picture-filled"}},
+ {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "mcpTool", Name: "mcpTool", Component: "view/systemTools/autoCode/mcp.vue", Sort: 7, Meta: Meta{Title: "Mcp Tools模板", Icon: "magnet"}},
+ {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "mcpTest", Name: "mcpTest", Component: "view/systemTools/autoCode/mcpTest.vue", Sort: 7, Meta: Meta{Title: "Mcp Tools测试", Icon: "partly-cloudy"}},
+ {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "sysVersion", Name: "sysVersion", Component: "view/systemTools/version/version.vue", Sort: 8, Meta: Meta{Title: "版本管理", Icon: "server"}},
+ {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "sysError", Name: "sysError", Component: "view/systemTools/sysError/sysError.vue", Sort: 9, Meta: Meta{Title: "错误日志", Icon: "warn"}},
+
+ {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["plugin"], 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: 1, Hidden: false, ParentId: menuNameMap["plugin"], Path: "installPlugin", Name: "installPlugin", Component: "view/systemTools/installPlugin/index.vue", Sort: 1, Meta: Meta{Title: "插件安装", Icon: "box"}},
+ {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["plugin"], Path: "pubPlug", Name: "pubPlug", Component: "view/systemTools/pubPlug/pubPlug.vue", Sort: 3, Meta: Meta{Title: "打包插件", Icon: "files"}},
+ {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["plugin"], Path: "plugin-email", Name: "plugin-email", Component: "plugin/email/view/index.vue", Sort: 4, Meta: Meta{Title: "邮件插件", Icon: "message"}},
+ {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["plugin"], Path: "anInfo", Name: "anInfo", Component: "plugin/announcement/view/info.vue", Sort: 5, Meta: Meta{Title: "公告管理[示例]", Icon: "scaleToOriginal"}},
+ }
+
+ // 创建子菜单
+ if err = db.Create(&childMenus).Error; err != nil {
+ return ctx, errors.Wrap(err, SysBaseMenu{}.TableName()+"子菜单初始化失败!")
+ }
+
+ // 组合所有菜单作为返回结果
+ allEntities := append(allMenus, childMenus...)
+ next = context.WithValue(ctx, i.InitializerName(), allEntities)
+ 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/server/source/system/user.go b/server/source/system/user.go
new file mode 100644
index 0000000..1563368
--- /dev/null
+++ b/server/source/system/user.go
@@ -0,0 +1,107 @@
+package system
+
+import (
+ "context"
+
+ sysModel "git.echol.cn/loser/st/server/model/system"
+ "git.echol.cn/loser/st/server/service/system"
+ "git.echol.cn/loser/st/server/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/server/task/clearTable.go b/server/task/clearTable.go
new file mode 100644
index 0000000..e138a11
--- /dev/null
+++ b/server/task/clearTable.go
@@ -0,0 +1,52 @@
+package task
+
+import (
+ "errors"
+ "fmt"
+ "time"
+
+ "git.echol.cn/loser/st/server/model/common"
+
+ "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/server/utils/app_jwt.go b/server/utils/app_jwt.go
new file mode 100644
index 0000000..2d73851
--- /dev/null
+++ b/server/utils/app_jwt.go
@@ -0,0 +1,84 @@
+package utils
+
+import (
+ "errors"
+ "time"
+
+ "git.echol.cn/loser/st/server/global"
+ "github.com/golang-jwt/jwt/v5"
+)
+
+const (
+ UserTypeApp = "app" // 前台用户类型标识
+)
+
+// AppJWTClaims 前台用户 JWT Claims
+type AppJWTClaims struct {
+ UserID uint `json:"userId"`
+ Username string `json:"username"`
+ UserType string `json:"userType"` // 用户类型标识
+ jwt.RegisteredClaims
+}
+
+// CreateAppToken 创建前台用户 Token(有效期 7 天)
+func CreateAppToken(userID uint, username string) (tokenString string, expiresAt int64, err error) {
+ // Token 有效期为 7 天
+ expiresTime := time.Now().Add(7 * 24 * time.Hour)
+ expiresAt = expiresTime.Unix()
+
+ claims := AppJWTClaims{
+ UserID: userID,
+ Username: username,
+ UserType: UserTypeApp,
+ RegisteredClaims: jwt.RegisteredClaims{
+ ExpiresAt: jwt.NewNumericDate(expiresTime),
+ IssuedAt: jwt.NewNumericDate(time.Now()),
+ NotBefore: jwt.NewNumericDate(time.Now()),
+ Issuer: global.GVA_CONFIG.JWT.Issuer,
+ },
+ }
+
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
+ tokenString, err = token.SignedString([]byte(global.GVA_CONFIG.JWT.SigningKey))
+ return
+}
+
+// CreateAppRefreshToken 创建前台用户刷新 Token(有效期更长)
+func CreateAppRefreshToken(userID uint, username string) (tokenString string, expiresAt int64, err error) {
+ // 刷新 Token 有效期为 7 天
+ expiresTime := time.Now().Add(7 * 24 * time.Hour)
+ expiresAt = expiresTime.Unix()
+
+ claims := AppJWTClaims{
+ UserID: userID,
+ Username: username,
+ UserType: UserTypeApp,
+ RegisteredClaims: jwt.RegisteredClaims{
+ ExpiresAt: jwt.NewNumericDate(expiresTime),
+ IssuedAt: jwt.NewNumericDate(time.Now()),
+ NotBefore: jwt.NewNumericDate(time.Now()),
+ Issuer: global.GVA_CONFIG.JWT.Issuer,
+ },
+ }
+
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
+ tokenString, err = token.SignedString([]byte(global.GVA_CONFIG.JWT.SigningKey))
+ return
+}
+
+// ParseAppToken 解析前台用户 Token
+func ParseAppToken(tokenString string) (*AppJWTClaims, error) {
+ token, err := jwt.ParseWithClaims(tokenString, &AppJWTClaims{}, func(token *jwt.Token) (interface{}, error) {
+ return []byte(global.GVA_CONFIG.JWT.SigningKey), nil
+ })
+
+ if err != nil {
+ return nil, err
+ }
+
+ if claims, ok := token.Claims.(*AppJWTClaims); ok && token.Valid {
+ return claims, nil
+ }
+
+ return nil, errors.New("invalid token")
+}
diff --git a/server/utils/ast/ast.go b/server/utils/ast/ast.go
new file mode 100644
index 0000000..8e30098
--- /dev/null
+++ b/server/utils/ast/ast.go
@@ -0,0 +1,410 @@
+package ast
+
+import (
+ "fmt"
+ "go/ast"
+ "go/parser"
+ "go/token"
+ "log"
+
+ "git.echol.cn/loser/st/server/model/system"
+)
+
+// AddImport 增加 import 方法
+func AddImport(astNode ast.Node, imp string) {
+ impStr := fmt.Sprintf("\"%s\"", imp)
+ ast.Inspect(astNode, func(node ast.Node) bool {
+ if genDecl, ok := node.(*ast.GenDecl); ok {
+ if genDecl.Tok == token.IMPORT {
+ for i := range genDecl.Specs {
+ if impNode, ok := genDecl.Specs[i].(*ast.ImportSpec); ok {
+ if impNode.Path.Value == impStr {
+ return false
+ }
+ }
+ }
+ genDecl.Specs = append(genDecl.Specs, &ast.ImportSpec{
+ Path: &ast.BasicLit{
+ Kind: token.STRING,
+ Value: impStr,
+ },
+ })
+ }
+ }
+ return true
+ })
+}
+
+// FindFunction 查询特定function方法
+func FindFunction(astNode ast.Node, FunctionName string) *ast.FuncDecl {
+ var funcDeclP *ast.FuncDecl
+ ast.Inspect(astNode, func(node ast.Node) bool {
+ if funcDecl, ok := node.(*ast.FuncDecl); ok {
+ if funcDecl.Name.String() == FunctionName {
+ funcDeclP = funcDecl
+ return false
+ }
+ }
+ return true
+ })
+ return funcDeclP
+}
+
+// FindArray 查询特定数组方法
+func FindArray(astNode ast.Node, identName, selectorExprName string) *ast.CompositeLit {
+ var assignStmt *ast.CompositeLit
+ ast.Inspect(astNode, func(n ast.Node) bool {
+ switch node := n.(type) {
+ case *ast.AssignStmt:
+ for _, expr := range node.Rhs {
+ if exprType, ok := expr.(*ast.CompositeLit); ok {
+ if arrayType, ok := exprType.Type.(*ast.ArrayType); ok {
+ sel, ok1 := arrayType.Elt.(*ast.SelectorExpr)
+ x, ok2 := sel.X.(*ast.Ident)
+ if ok1 && ok2 && x.Name == identName && sel.Sel.Name == selectorExprName {
+ assignStmt = exprType
+ return false
+ }
+ }
+ }
+ }
+ }
+ return true
+ })
+ return assignStmt
+}
+
+func CreateMenuStructAst(menus []system.SysBaseMenu) *[]ast.Expr {
+ var menuElts []ast.Expr
+ for i := range menus {
+ elts := []ast.Expr{ // 结构体的字段
+ &ast.KeyValueExpr{
+ Key: &ast.Ident{Name: "ParentId"},
+ Value: &ast.BasicLit{Kind: token.INT, Value: "0"},
+ },
+ &ast.KeyValueExpr{
+ Key: &ast.Ident{Name: "Path"},
+ Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", menus[i].Path)},
+ },
+ &ast.KeyValueExpr{
+ Key: &ast.Ident{Name: "Name"},
+ Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", menus[i].Name)},
+ },
+ &ast.KeyValueExpr{
+ Key: &ast.Ident{Name: "Hidden"},
+ Value: &ast.Ident{Name: "false"},
+ },
+ &ast.KeyValueExpr{
+ Key: &ast.Ident{Name: "Component"},
+ Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", menus[i].Component)},
+ },
+ &ast.KeyValueExpr{
+ Key: &ast.Ident{Name: "Sort"},
+ Value: &ast.BasicLit{Kind: token.INT, Value: fmt.Sprintf("%d", menus[i].Sort)},
+ },
+ &ast.KeyValueExpr{
+ Key: &ast.Ident{Name: "Meta"},
+ Value: &ast.CompositeLit{
+ Type: &ast.SelectorExpr{
+ X: &ast.Ident{Name: "model"},
+ Sel: &ast.Ident{Name: "Meta"},
+ },
+ Elts: []ast.Expr{
+ &ast.KeyValueExpr{
+ Key: &ast.Ident{Name: "Title"},
+ Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", menus[i].Title)},
+ },
+ &ast.KeyValueExpr{
+ Key: &ast.Ident{Name: "Icon"},
+ Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", menus[i].Icon)},
+ },
+ },
+ },
+ },
+ }
+
+ // 添加菜单参数
+ if len(menus[i].Parameters) > 0 {
+ var paramElts []ast.Expr
+ for _, param := range menus[i].Parameters {
+ paramElts = append(paramElts, &ast.CompositeLit{
+ Type: &ast.SelectorExpr{
+ X: &ast.Ident{Name: "model"},
+ Sel: &ast.Ident{Name: "SysBaseMenuParameter"},
+ },
+ Elts: []ast.Expr{
+ &ast.KeyValueExpr{
+ Key: &ast.Ident{Name: "Type"},
+ Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", param.Type)},
+ },
+ &ast.KeyValueExpr{
+ Key: &ast.Ident{Name: "Key"},
+ Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", param.Key)},
+ },
+ &ast.KeyValueExpr{
+ Key: &ast.Ident{Name: "Value"},
+ Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", param.Value)},
+ },
+ },
+ })
+ }
+ elts = append(elts, &ast.KeyValueExpr{
+ Key: &ast.Ident{Name: "Parameters"},
+ Value: &ast.CompositeLit{
+ Type: &ast.ArrayType{
+ Elt: &ast.SelectorExpr{
+ X: &ast.Ident{Name: "model"},
+ Sel: &ast.Ident{Name: "SysBaseMenuParameter"},
+ },
+ },
+ Elts: paramElts,
+ },
+ })
+ }
+
+ // 添加菜单按钮
+ if len(menus[i].MenuBtn) > 0 {
+ var btnElts []ast.Expr
+ for _, btn := range menus[i].MenuBtn {
+ btnElts = append(btnElts, &ast.CompositeLit{
+ Type: &ast.SelectorExpr{
+ X: &ast.Ident{Name: "model"},
+ Sel: &ast.Ident{Name: "SysBaseMenuBtn"},
+ },
+ Elts: []ast.Expr{
+ &ast.KeyValueExpr{
+ Key: &ast.Ident{Name: "Name"},
+ Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", btn.Name)},
+ },
+ &ast.KeyValueExpr{
+ Key: &ast.Ident{Name: "Desc"},
+ Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", btn.Desc)},
+ },
+ },
+ })
+ }
+ elts = append(elts, &ast.KeyValueExpr{
+ Key: &ast.Ident{Name: "MenuBtn"},
+ Value: &ast.CompositeLit{
+ Type: &ast.ArrayType{
+ Elt: &ast.SelectorExpr{
+ X: &ast.Ident{Name: "model"},
+ Sel: &ast.Ident{Name: "SysBaseMenuBtn"},
+ },
+ },
+ Elts: btnElts,
+ },
+ })
+ }
+
+ menuElts = append(menuElts, &ast.CompositeLit{
+ Type: nil,
+ Elts: elts,
+ })
+ }
+ return &menuElts
+}
+
+func CreateApiStructAst(apis []system.SysApi) *[]ast.Expr {
+ var apiElts []ast.Expr
+ for i := range apis {
+ elts := []ast.Expr{ // 结构体的字段
+ &ast.KeyValueExpr{
+ Key: &ast.Ident{Name: "Path"},
+ Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", apis[i].Path)},
+ },
+ &ast.KeyValueExpr{
+ Key: &ast.Ident{Name: "Description"},
+ Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", apis[i].Description)},
+ },
+ &ast.KeyValueExpr{
+ Key: &ast.Ident{Name: "ApiGroup"},
+ Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", apis[i].ApiGroup)},
+ },
+ &ast.KeyValueExpr{
+ Key: &ast.Ident{Name: "Method"},
+ Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", apis[i].Method)},
+ },
+ }
+ apiElts = append(apiElts, &ast.CompositeLit{
+ Type: nil,
+ Elts: elts,
+ })
+ }
+ return &apiElts
+}
+
+// CheckImport 检查是否存在Import
+func CheckImport(file *ast.File, importPath string) bool {
+ for _, imp := range file.Imports {
+ // Remove quotes around the import path
+ path := imp.Path.Value[1 : len(imp.Path.Value)-1]
+
+ if path == importPath {
+ return true
+ }
+ }
+
+ return false
+}
+
+func clearPosition(astNode ast.Node) {
+ ast.Inspect(astNode, func(n ast.Node) bool {
+ switch node := n.(type) {
+ case *ast.Ident:
+ // 清除位置信息
+ node.NamePos = token.NoPos
+ case *ast.CallExpr:
+ // 清除位置信息
+ node.Lparen = token.NoPos
+ node.Rparen = token.NoPos
+ case *ast.BasicLit:
+ // 清除位置信息
+ node.ValuePos = token.NoPos
+ case *ast.SelectorExpr:
+ // 清除位置信息
+ node.Sel.NamePos = token.NoPos
+ case *ast.BinaryExpr:
+ node.OpPos = token.NoPos
+ case *ast.UnaryExpr:
+ node.OpPos = token.NoPos
+ case *ast.StarExpr:
+ node.Star = token.NoPos
+ }
+ return true
+ })
+}
+
+func CreateStmt(statement string) *ast.ExprStmt {
+ expr, err := parser.ParseExpr(statement)
+ if err != nil {
+ log.Fatal(err)
+ }
+ clearPosition(expr)
+ return &ast.ExprStmt{X: expr}
+}
+
+func IsBlockStmt(node ast.Node) bool {
+ _, ok := node.(*ast.BlockStmt)
+ return ok
+}
+
+func VariableExistsInBlock(block *ast.BlockStmt, varName string) bool {
+ exists := false
+ ast.Inspect(block, func(n ast.Node) bool {
+ switch node := n.(type) {
+ case *ast.AssignStmt:
+ for _, expr := range node.Lhs {
+ if ident, ok := expr.(*ast.Ident); ok && ident.Name == varName {
+ exists = true
+ return false
+ }
+ }
+ }
+ return true
+ })
+ return exists
+}
+
+func CreateDictionaryStructAst(dictionaries []system.SysDictionary) *[]ast.Expr {
+ var dictElts []ast.Expr
+ for i := range dictionaries {
+ statusStr := "true"
+ if dictionaries[i].Status != nil && !*dictionaries[i].Status {
+ statusStr = "false"
+ }
+
+ elts := []ast.Expr{
+ &ast.KeyValueExpr{
+ Key: &ast.Ident{Name: "Name"},
+ Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", dictionaries[i].Name)},
+ },
+ &ast.KeyValueExpr{
+ Key: &ast.Ident{Name: "Type"},
+ Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", dictionaries[i].Type)},
+ },
+ &ast.KeyValueExpr{
+ Key: &ast.Ident{Name: "Status"},
+ Value: &ast.CallExpr{
+ Fun: &ast.SelectorExpr{
+ X: &ast.Ident{Name: "utils"},
+ Sel: &ast.Ident{Name: "Pointer"},
+ },
+ Args: []ast.Expr{
+ &ast.Ident{Name: statusStr},
+ },
+ },
+ },
+ &ast.KeyValueExpr{
+ Key: &ast.Ident{Name: "Desc"},
+ Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", dictionaries[i].Desc)},
+ },
+ }
+
+ if len(dictionaries[i].SysDictionaryDetails) > 0 {
+ var detailElts []ast.Expr
+ for _, detail := range dictionaries[i].SysDictionaryDetails {
+ detailStatusStr := "true"
+ if detail.Status != nil && !*detail.Status {
+ detailStatusStr = "false"
+ }
+
+ detailElts = append(detailElts, &ast.CompositeLit{
+ Type: &ast.SelectorExpr{
+ X: &ast.Ident{Name: "model"},
+ Sel: &ast.Ident{Name: "SysDictionaryDetail"},
+ },
+ Elts: []ast.Expr{
+ &ast.KeyValueExpr{
+ Key: &ast.Ident{Name: "Label"},
+ Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", detail.Label)},
+ },
+ &ast.KeyValueExpr{
+ Key: &ast.Ident{Name: "Value"},
+ Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", detail.Value)},
+ },
+ &ast.KeyValueExpr{
+ Key: &ast.Ident{Name: "Extend"},
+ Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", detail.Extend)},
+ },
+ &ast.KeyValueExpr{
+ Key: &ast.Ident{Name: "Status"},
+ Value: &ast.CallExpr{
+ Fun: &ast.SelectorExpr{
+ X: &ast.Ident{Name: "utils"},
+ Sel: &ast.Ident{Name: "Pointer"},
+ },
+ Args: []ast.Expr{
+ &ast.Ident{Name: detailStatusStr},
+ },
+ },
+ },
+ &ast.KeyValueExpr{
+ Key: &ast.Ident{Name: "Sort"},
+ Value: &ast.BasicLit{Kind: token.INT, Value: fmt.Sprintf("%d", detail.Sort)},
+ },
+ },
+ })
+ }
+ elts = append(elts, &ast.KeyValueExpr{
+ Key: &ast.Ident{Name: "SysDictionaryDetails"},
+ Value: &ast.CompositeLit{
+ Type: &ast.ArrayType{Elt: &ast.SelectorExpr{
+ X: &ast.Ident{Name: "model"},
+ Sel: &ast.Ident{Name: "SysDictionaryDetail"},
+ }},
+ Elts: detailElts,
+ },
+ })
+ }
+
+ dictElts = append(dictElts, &ast.CompositeLit{
+ Type: &ast.SelectorExpr{
+ X: &ast.Ident{Name: "model"},
+ Sel: &ast.Ident{Name: "SysDictionary"},
+ },
+ Elts: elts,
+ })
+ }
+ return &dictElts
+}
diff --git a/server/utils/ast/ast_auto_enter.go b/server/utils/ast/ast_auto_enter.go
new file mode 100644
index 0000000..382f554
--- /dev/null
+++ b/server/utils/ast/ast_auto_enter.go
@@ -0,0 +1,47 @@
+package ast
+
+import (
+ "bytes"
+ "fmt"
+ "go/ast"
+ "go/parser"
+ "go/printer"
+ "go/token"
+ "os"
+)
+
+func ImportForAutoEnter(path string, funcName string, code string) {
+ src, err := os.ReadFile(path)
+ if err != nil {
+ fmt.Println(err)
+ }
+ fileSet := token.NewFileSet()
+ astFile, err := parser.ParseFile(fileSet, "", src, 0)
+ ast.Inspect(astFile, func(node ast.Node) bool {
+ if typeSpec, ok := node.(*ast.TypeSpec); ok {
+ if typeSpec.Name.Name == funcName {
+ if st, ok := typeSpec.Type.(*ast.StructType); ok {
+ for i := range st.Fields.List {
+ if t, ok := st.Fields.List[i].Type.(*ast.Ident); ok {
+ if t.Name == code {
+ return false
+ }
+ }
+ }
+ sn := &ast.Field{
+ Type: &ast.Ident{Name: code},
+ }
+ st.Fields.List = append(st.Fields.List, sn)
+ }
+ }
+ }
+ return true
+ })
+ var out []byte
+ bf := bytes.NewBuffer(out)
+ err = printer.Fprint(bf, fileSet, astFile)
+ if err != nil {
+ return
+ }
+ _ = os.WriteFile(path, bf.Bytes(), 0666)
+}
diff --git a/server/utils/ast/ast_enter.go b/server/utils/ast/ast_enter.go
new file mode 100644
index 0000000..b7fc85a
--- /dev/null
+++ b/server/utils/ast/ast_enter.go
@@ -0,0 +1,182 @@
+package ast
+
+import (
+ "bytes"
+ "go/ast"
+ "go/format"
+ "go/parser"
+ "go/token"
+ "log"
+ "os"
+ "strconv"
+ "strings"
+
+ "golang.org/x/text/cases"
+ "golang.org/x/text/language"
+)
+
+type Visitor struct {
+ ImportCode string
+ StructName string
+ PackageName string
+ GroupName string
+}
+
+func (vi *Visitor) Visit(node ast.Node) ast.Visitor {
+ switch n := node.(type) {
+ case *ast.GenDecl:
+ // 查找有没有import context包
+ // Notice:没有考虑没有import任何包的情况
+ if n.Tok == token.IMPORT && vi.ImportCode != "" {
+ vi.addImport(n)
+ // 不需要再遍历子树
+ return nil
+ }
+ if n.Tok == token.TYPE && vi.StructName != "" && vi.PackageName != "" && vi.GroupName != "" {
+ vi.addStruct(n)
+ return nil
+ }
+ case *ast.FuncDecl:
+ if n.Name.Name == "Routers" {
+ vi.addFuncBodyVar(n)
+ return nil
+ }
+
+ }
+ return vi
+}
+
+func (vi *Visitor) addStruct(genDecl *ast.GenDecl) ast.Visitor {
+ for i := range genDecl.Specs {
+ switch n := genDecl.Specs[i].(type) {
+ case *ast.TypeSpec:
+ if strings.Index(n.Name.Name, "Group") > -1 {
+ switch t := n.Type.(type) {
+ case *ast.StructType:
+ f := &ast.Field{
+ Names: []*ast.Ident{
+ {
+ Name: vi.StructName,
+ Obj: &ast.Object{
+ Kind: ast.Var,
+ Name: vi.StructName,
+ },
+ },
+ },
+ Type: &ast.SelectorExpr{
+ X: &ast.Ident{
+ Name: vi.PackageName,
+ },
+ Sel: &ast.Ident{
+ Name: vi.GroupName,
+ },
+ },
+ }
+ t.Fields.List = append(t.Fields.List, f)
+ }
+ }
+ }
+ }
+ return vi
+}
+
+func (vi *Visitor) addImport(genDecl *ast.GenDecl) ast.Visitor {
+ // 是否已经import
+ hasImported := false
+ for _, v := range genDecl.Specs {
+ importSpec := v.(*ast.ImportSpec)
+ // 如果已经包含
+ if importSpec.Path.Value == strconv.Quote(vi.ImportCode) {
+ hasImported = true
+ }
+ }
+ if !hasImported {
+ genDecl.Specs = append(genDecl.Specs, &ast.ImportSpec{
+ Path: &ast.BasicLit{
+ Kind: token.STRING,
+ Value: strconv.Quote(vi.ImportCode),
+ },
+ })
+ }
+ return vi
+}
+
+func (vi *Visitor) addFuncBodyVar(funDecl *ast.FuncDecl) ast.Visitor {
+ hasVar := false
+ for _, v := range funDecl.Body.List {
+ switch varSpec := v.(type) {
+ case *ast.AssignStmt:
+ for i := range varSpec.Lhs {
+ switch nn := varSpec.Lhs[i].(type) {
+ case *ast.Ident:
+ if nn.Name == vi.PackageName+"Router" {
+ hasVar = true
+ }
+ }
+ }
+ }
+ }
+ if !hasVar {
+ assignStmt := &ast.AssignStmt{
+ Lhs: []ast.Expr{
+ &ast.Ident{
+ Name: vi.PackageName + "Router",
+ Obj: &ast.Object{
+ Kind: ast.Var,
+ Name: vi.PackageName + "Router",
+ },
+ },
+ },
+ Tok: token.DEFINE,
+ Rhs: []ast.Expr{
+ &ast.SelectorExpr{
+ X: &ast.SelectorExpr{
+ X: &ast.Ident{
+ Name: "router",
+ },
+ Sel: &ast.Ident{
+ Name: "RouterGroupApp",
+ },
+ },
+ Sel: &ast.Ident{
+ Name: cases.Title(language.English).String(vi.PackageName),
+ },
+ },
+ },
+ }
+ funDecl.Body.List = append(funDecl.Body.List, funDecl.Body.List[1])
+ index := 1
+ copy(funDecl.Body.List[index+1:], funDecl.Body.List[index:])
+ funDecl.Body.List[index] = assignStmt
+ }
+ return vi
+}
+
+func ImportReference(filepath, importCode, structName, packageName, groupName string) error {
+ fSet := token.NewFileSet()
+ fParser, err := parser.ParseFile(fSet, filepath, nil, parser.ParseComments)
+ if err != nil {
+ return err
+ }
+ importCode = strings.TrimSpace(importCode)
+ v := &Visitor{
+ ImportCode: importCode,
+ StructName: structName,
+ PackageName: packageName,
+ GroupName: groupName,
+ }
+ if importCode == "" {
+ ast.Print(fSet, fParser)
+ }
+
+ ast.Walk(v, fParser)
+
+ var output []byte
+ buffer := bytes.NewBuffer(output)
+ err = format.Node(buffer, fSet, fParser)
+ if err != nil {
+ log.Fatal(err)
+ }
+ // 写回数据
+ return os.WriteFile(filepath, buffer.Bytes(), 0o600)
+}
diff --git a/server/utils/ast/ast_gorm.go b/server/utils/ast/ast_gorm.go
new file mode 100644
index 0000000..b975632
--- /dev/null
+++ b/server/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/st/server/model/%s", pk)
+ src, err := os.ReadFile(path)
+ if err != nil {
+ fmt.Println(err)
+ }
+ fileSet := token.NewFileSet()
+ astFile, err := parser.ParseFile(fileSet, "", src, 0)
+ if err != nil {
+ fmt.Println(err)
+ }
+ AddImport(astFile, modelPk)
+ FuncNode := FindFunction(astFile, funcName)
+ if FuncNode != nil {
+ ast.Print(fileSet, FuncNode)
+ }
+ addDBVar(FuncNode.Body, varName, dbName)
+ addAutoMigrate(FuncNode.Body, varName, pk, model)
+ var out []byte
+ bf := bytes.NewBuffer(out)
+ printer.Fprint(bf, fileSet, astFile)
+
+ os.WriteFile(path, bf.Bytes(), 0666)
+}
+
+// 增加一个 db库变量
+func addDBVar(astBody *ast.BlockStmt, varName, dbName string) {
+ if dbName == "" {
+ return
+ }
+ dbStr := fmt.Sprintf("\"%s\"", dbName)
+
+ for i := range astBody.List {
+ if assignStmt, ok := astBody.List[i].(*ast.AssignStmt); ok {
+ if ident, ok := assignStmt.Lhs[0].(*ast.Ident); ok {
+ if ident.Name == varName {
+ return
+ }
+ }
+ }
+ }
+ assignNode := &ast.AssignStmt{
+ Lhs: []ast.Expr{
+ &ast.Ident{
+ Name: varName,
+ },
+ },
+ Tok: token.DEFINE,
+ Rhs: []ast.Expr{
+ &ast.CallExpr{
+ Fun: &ast.SelectorExpr{
+ X: &ast.Ident{
+ Name: "global",
+ },
+ Sel: &ast.Ident{
+ Name: "GetGlobalDBByDBName",
+ },
+ },
+ Args: []ast.Expr{
+ &ast.BasicLit{
+ Kind: token.STRING,
+ Value: dbStr,
+ },
+ },
+ },
+ },
+ }
+ astBody.List = append([]ast.Stmt{assignNode}, astBody.List...)
+}
+
+// 为db库变量增加 AutoMigrate 方法
+func addAutoMigrate(astBody *ast.BlockStmt, dbname string, pk string, model string) {
+ if dbname == "" {
+ dbname = "db"
+ }
+ flag := true
+ ast.Inspect(astBody, func(node ast.Node) bool {
+ // 首先判断需要加入的方法调用语句是否存在 不存在则直接走到下方逻辑
+ switch n := node.(type) {
+ case *ast.CallExpr:
+ // 判断是否找到了AutoMigrate语句
+ if s, ok := n.Fun.(*ast.SelectorExpr); ok {
+ if x, ok := s.X.(*ast.Ident); ok {
+ if s.Sel.Name == "AutoMigrate" && x.Name == dbname {
+ flag = false
+ if !NeedAppendModel(n, pk, model) {
+ return false
+ }
+ // 判断已经找到了AutoMigrate语句
+ n.Args = append(n.Args, &ast.CompositeLit{
+ Type: &ast.SelectorExpr{
+ X: &ast.Ident{
+ Name: pk,
+ },
+ Sel: &ast.Ident{
+ Name: model,
+ },
+ },
+ })
+ return false
+ }
+ }
+ }
+ }
+ return true
+ //然后判断 pk.model是否存在 如果存在直接跳出 如果不存在 则向已经找到的方法调用语句的node里面push一条
+ })
+
+ if flag {
+ exprStmt := &ast.ExprStmt{
+ X: &ast.CallExpr{
+ Fun: &ast.SelectorExpr{
+ X: &ast.Ident{
+ Name: dbname,
+ },
+ Sel: &ast.Ident{
+ Name: "AutoMigrate",
+ },
+ },
+ Args: []ast.Expr{
+ &ast.CompositeLit{
+ Type: &ast.SelectorExpr{
+ X: &ast.Ident{
+ Name: pk,
+ },
+ Sel: &ast.Ident{
+ Name: model,
+ },
+ },
+ },
+ },
+ }}
+ astBody.List = append(astBody.List, exprStmt)
+ }
+}
+
+// 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/server/utils/ast/ast_init_test.go b/server/utils/ast/ast_init_test.go
new file mode 100644
index 0000000..b75b475
--- /dev/null
+++ b/server/utils/ast/ast_init_test.go
@@ -0,0 +1,12 @@
+package ast
+
+import (
+ "path/filepath"
+
+ "git.echol.cn/loser/st/server/global"
+)
+
+func init() {
+ global.GVA_CONFIG.AutoCode.Root, _ = filepath.Abs("../../../")
+ global.GVA_CONFIG.AutoCode.Server = "server"
+}
diff --git a/server/utils/ast/ast_rollback.go b/server/utils/ast/ast_rollback.go
new file mode 100644
index 0000000..6407c72
--- /dev/null
+++ b/server/utils/ast/ast_rollback.go
@@ -0,0 +1,174 @@
+package ast
+
+import (
+ "bytes"
+ "fmt"
+ "go/ast"
+ "go/parser"
+ "go/printer"
+ "go/token"
+ "os"
+ "path/filepath"
+
+ "git.echol.cn/loser/st/server/global"
+)
+
+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/st/server/model/"+pk+"\"" {
+ gp = gen
+ imI = i
+ return false
+ }
+ }
+ }
+ }
+ return true
+ })
+
+ if imI > -1 {
+ gp.Specs = append(append([]ast.Spec{}, gp.Specs[:imI]...), gp.Specs[imI+1:]...)
+ }
+ }
+
+ var out []byte
+ bf := bytes.NewBuffer(out)
+ printer.Fprint(bf, fileSet, astFile)
+ os.Remove(path)
+ os.WriteFile(path, bf.Bytes(), 0666)
+
+}
+
+func RollRouterBack(pk, model string) {
+
+ // 首先抓到所有的代码块结构 {}
+ // 分析结构中是否存在一个变量叫做 pk+Router
+ // 然后获取到代码块指针 对内部需要回滚的代码进行剔除
+ path := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "router_biz.go")
+ src, err := os.ReadFile(path)
+ if err != nil {
+ fmt.Println(err)
+ }
+ fileSet := token.NewFileSet()
+ astFile, err := parser.ParseFile(fileSet, "", src, 0)
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ var block *ast.BlockStmt
+ var routerStmt *ast.FuncDecl
+
+ ast.Inspect(astFile, func(node ast.Node) bool {
+ if n, ok := node.(*ast.FuncDecl); ok {
+ if n.Name.Name == "initBizRouter" {
+ routerStmt = n
+ }
+ }
+
+ if n, ok := node.(*ast.BlockStmt); ok {
+ ast.Inspect(n, func(bNode ast.Node) bool {
+ if in, ok := bNode.(*ast.Ident); ok {
+ if in.Name == pk+"Router" {
+ block = n
+ return false
+ }
+ }
+ return true
+ })
+ return true
+ }
+ return true
+ })
+ var k int
+ for i := range block.List {
+ if stmtNode, ok := block.List[i].(*ast.ExprStmt); ok {
+ ast.Inspect(stmtNode, func(node ast.Node) bool {
+ if n, ok := node.(*ast.Ident); ok {
+ if n.Name == "Init"+model+"Router" {
+ k = i
+ return false
+ }
+ }
+ return true
+ })
+ }
+ }
+
+ block.List = append(append([]ast.Stmt{}, block.List[:k]...), block.List[k+1:]...)
+
+ if len(block.List) == 1 {
+ // 说明这个块就没任何意义了
+ block.List = nil
+ }
+
+ for i, n := range routerStmt.Body.List {
+ if n, ok := n.(*ast.BlockStmt); ok {
+ if n.List == nil {
+ routerStmt.Body.List = append(append([]ast.Stmt{}, routerStmt.Body.List[:i]...), routerStmt.Body.List[i+1:]...)
+ i--
+ }
+ }
+ }
+
+ var out []byte
+ bf := bytes.NewBuffer(out)
+ printer.Fprint(bf, fileSet, astFile)
+ os.Remove(path)
+ os.WriteFile(path, bf.Bytes(), 0666)
+}
diff --git a/server/utils/ast/ast_router.go b/server/utils/ast/ast_router.go
new file mode 100644
index 0000000..86356b8
--- /dev/null
+++ b/server/utils/ast/ast_router.go
@@ -0,0 +1,135 @@
+package ast
+
+import (
+ "bytes"
+ "fmt"
+ "go/ast"
+ "go/parser"
+ "go/printer"
+ "go/token"
+ "os"
+ "strings"
+)
+
+func AppendNodeToList(stmts []ast.Stmt, stmt ast.Stmt, index int) []ast.Stmt {
+ return append(stmts[:index], append([]ast.Stmt{stmt}, stmts[index:]...)...)
+}
+
+func AddRouterCode(path, funcName, pk, model string) {
+ src, err := os.ReadFile(path)
+ if err != nil {
+ fmt.Println(err)
+ }
+ fileSet := token.NewFileSet()
+ astFile, err := parser.ParseFile(fileSet, "", src, parser.ParseComments)
+
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ FuncNode := FindFunction(astFile, funcName)
+
+ pkName := strings.ToUpper(pk[:1]) + pk[1:]
+ routerName := fmt.Sprintf("%sRouter", pk)
+ modelName := fmt.Sprintf("Init%sRouter", model)
+ var bloctPre *ast.BlockStmt
+ for i := len(FuncNode.Body.List) - 1; i >= 0; i-- {
+ if block, ok := FuncNode.Body.List[i].(*ast.BlockStmt); ok {
+ bloctPre = block
+ }
+ }
+ ast.Print(fileSet, FuncNode)
+ if ok, b := needAppendRouter(FuncNode, pk); ok {
+ routerNode :=
+ &ast.BlockStmt{
+ List: []ast.Stmt{
+ &ast.AssignStmt{
+ Lhs: []ast.Expr{
+ &ast.Ident{Name: routerName},
+ },
+ Tok: token.DEFINE,
+ Rhs: []ast.Expr{
+ &ast.SelectorExpr{
+ X: &ast.SelectorExpr{
+ X: &ast.Ident{Name: "router"},
+ Sel: &ast.Ident{Name: "RouterGroupApp"},
+ },
+ Sel: &ast.Ident{Name: pkName},
+ },
+ },
+ },
+ },
+ }
+
+ FuncNode.Body.List = AppendNodeToList(FuncNode.Body.List, routerNode, len(FuncNode.Body.List)-1)
+ bloctPre = routerNode
+ } else {
+ bloctPre = b
+ }
+
+ if needAppendInit(FuncNode, routerName, modelName) {
+ bloctPre.List = append(bloctPre.List,
+ &ast.ExprStmt{
+ X: &ast.CallExpr{
+ Fun: &ast.SelectorExpr{
+ X: &ast.Ident{Name: routerName},
+ Sel: &ast.Ident{Name: modelName},
+ },
+ Args: []ast.Expr{
+ &ast.Ident{
+ Name: "privateGroup",
+ },
+ &ast.Ident{
+ Name: "publicGroup",
+ },
+ },
+ },
+ })
+ }
+ var out []byte
+ bf := bytes.NewBuffer(out)
+ printer.Fprint(bf, fileSet, astFile)
+ os.WriteFile(path, bf.Bytes(), 0666)
+}
+
+func needAppendRouter(funcNode ast.Node, pk string) (bool, *ast.BlockStmt) {
+ flag := true
+ var block *ast.BlockStmt
+ ast.Inspect(funcNode, func(node ast.Node) bool {
+ switch n := node.(type) {
+ case *ast.BlockStmt:
+ for i := range n.List {
+ if assignNode, ok := n.List[i].(*ast.AssignStmt); ok {
+ if identNode, ok := assignNode.Lhs[0].(*ast.Ident); ok {
+ if identNode.Name == fmt.Sprintf("%sRouter", pk) {
+ flag = false
+ block = n
+ return false
+ }
+ }
+ }
+ }
+
+ }
+ return true
+ })
+ return flag, block
+}
+
+func needAppendInit(funcNode ast.Node, routerName string, modelName string) bool {
+ flag := true
+ ast.Inspect(funcNode, func(node ast.Node) bool {
+ switch n := funcNode.(type) {
+ case *ast.CallExpr:
+ if selectNode, ok := n.Fun.(*ast.SelectorExpr); ok {
+ x, xok := selectNode.X.(*ast.Ident)
+ if xok && x.Name == routerName && selectNode.Sel.Name == modelName {
+ flag = false
+ return false
+ }
+ }
+ }
+ return true
+ })
+ return flag
+}
diff --git a/server/utils/ast/ast_test.go b/server/utils/ast/ast_test.go
new file mode 100644
index 0000000..3c9f65d
--- /dev/null
+++ b/server/utils/ast/ast_test.go
@@ -0,0 +1,33 @@
+package ast
+
+import (
+ "go/ast"
+ "go/parser"
+ "go/printer"
+ "go/token"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "git.echol.cn/loser/st/server/global"
+)
+
+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/server/utils/ast/ast_type.go b/server/utils/ast/ast_type.go
new file mode 100644
index 0000000..43285c9
--- /dev/null
+++ b/server/utils/ast/ast_type.go
@@ -0,0 +1,53 @@
+package ast
+
+type Type string
+
+func (r Type) String() string {
+ return string(r)
+}
+
+func (r Type) Group() string {
+ switch r {
+ case TypePackageApiEnter:
+ return "ApiGroup"
+ case TypePackageRouterEnter:
+ return "RouterGroup"
+ case TypePackageServiceEnter:
+ return "ServiceGroup"
+ case TypePackageApiModuleEnter:
+ return "ApiGroup"
+ case TypePackageRouterModuleEnter:
+ return "RouterGroup"
+ case TypePackageServiceModuleEnter:
+ return "ServiceGroup"
+ case TypePluginApiEnter:
+ return "api"
+ case TypePluginRouterEnter:
+ return "router"
+ case TypePluginServiceEnter:
+ return "service"
+ default:
+ return ""
+ }
+}
+
+const (
+ TypePackageApiEnter = "PackageApiEnter" // server/api/v1/enter.go
+ TypePackageRouterEnter = "PackageRouterEnter" // server/router/enter.go
+ TypePackageServiceEnter = "PackageServiceEnter" // server/service/enter.go
+ TypePackageApiModuleEnter = "PackageApiModuleEnter" // server/api/v1/{package}/enter.go
+ TypePackageRouterModuleEnter = "PackageRouterModuleEnter" // server/router/{package}/enter.go
+ TypePackageServiceModuleEnter = "PackageServiceModuleEnter" // server/service/{package}/enter.go
+ TypePackageInitializeGorm = "PackageInitializeGorm" // server/initialize/gorm_biz.go
+ TypePackageInitializeRouter = "PackageInitializeRouter" // server/initialize/router_biz.go
+ TypePluginGen = "PluginGen" // server/plugin/{package}/gen/main.go
+ TypePluginApiEnter = "PluginApiEnter" // server/plugin/{package}/enter.go
+ TypePluginInitializeV1 = "PluginInitializeV1" // server/initialize/plugin_biz_v1.go
+ TypePluginInitializeV2 = "PluginInitializeV2" // server/plugin/register.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/server/utils/ast/extract_func.go b/server/utils/ast/extract_func.go
new file mode 100644
index 0000000..35260c3
--- /dev/null
+++ b/server/utils/ast/extract_func.go
@@ -0,0 +1,62 @@
+package ast
+
+import (
+ "fmt"
+ "go/ast"
+ "go/parser"
+ "go/token"
+ "os"
+)
+
+// ExtractFuncSourceByPosition 根据文件路径与行号,提取包含该行的整个方法源码
+// 返回:方法名、完整源码、起止行号
+func ExtractFuncSourceByPosition(filePath string, line int) (name string, source string, startLine int, endLine int, err error) {
+ // 读取源文件
+ src, readErr := os.ReadFile(filePath)
+ if readErr != nil {
+ err = fmt.Errorf("read file failed: %w", readErr)
+ return
+ }
+
+ // 解析 AST
+ fset := token.NewFileSet()
+ file, parseErr := parser.ParseFile(fset, filePath, src, parser.ParseComments)
+ if parseErr != nil {
+ err = fmt.Errorf("parse file failed: %w", parseErr)
+ return
+ }
+
+ // 在 AST 中定位包含指定行号的函数声明
+ var target *ast.FuncDecl
+ ast.Inspect(file, func(n ast.Node) bool {
+ fd, ok := n.(*ast.FuncDecl)
+ if !ok {
+ return true
+ }
+ s := fset.Position(fd.Pos()).Line
+ e := fset.Position(fd.End()).Line
+ if line >= s && line <= e {
+ target = fd
+ startLine = s
+ endLine = e
+ return false
+ }
+ return true
+ })
+
+ if target == nil {
+ err = fmt.Errorf("no function encloses line %d in %s", line, filePath)
+ return
+ }
+
+ // 使用字节偏移精确提取源码片段(包含注释与原始格式)
+ start := fset.Position(target.Pos()).Offset
+ end := fset.Position(target.End()).Offset
+ if start < 0 || end > len(src) || start >= end {
+ err = fmt.Errorf("invalid offsets for function: start=%d end=%d len=%d", start, end, len(src))
+ return
+ }
+ source = string(src[start:end])
+ name = target.Name.Name
+ return
+}
diff --git a/server/utils/ast/import.go b/server/utils/ast/import.go
new file mode 100644
index 0000000..5de18a3
--- /dev/null
+++ b/server/utils/ast/import.go
@@ -0,0 +1,94 @@
+package ast
+
+import (
+ "go/ast"
+ "go/token"
+ "io"
+ "strings"
+)
+
+type Import struct {
+ Base
+ ImportPath string // 导包路径
+}
+
+func NewImport(importPath string) *Import {
+ return &Import{ImportPath: importPath}
+}
+
+func (a *Import) Parse(filename string, writer io.Writer) (file *ast.File, err error) {
+ return a.Base.Parse(filename, writer)
+}
+
+func (a *Import) Rollback(file *ast.File) error {
+ if a.ImportPath == "" {
+ return nil
+ }
+ for i := 0; i < len(file.Decls); i++ {
+ v1, o1 := file.Decls[i].(*ast.GenDecl)
+ if o1 {
+ if v1.Tok != token.IMPORT {
+ break
+ }
+ for j := 0; j < len(v1.Specs); j++ {
+ v2, o2 := v1.Specs[j].(*ast.ImportSpec)
+ if o2 && strings.HasSuffix(a.ImportPath, v2.Path.Value) {
+ v1.Specs = append(v1.Specs[:j], v1.Specs[j+1:]...)
+ if len(v1.Specs) == 0 {
+ file.Decls = append(file.Decls[:i], file.Decls[i+1:]...)
+ } // 如果没有import声明,就删除, 如果不删除则会出现import()
+ break
+ }
+ }
+ }
+ }
+ return nil
+}
+
+func (a *Import) Injection(file *ast.File) error {
+ if a.ImportPath == "" {
+ return nil
+ }
+ var has bool
+ for i := 0; i < len(file.Decls); i++ {
+ v1, o1 := file.Decls[i].(*ast.GenDecl)
+ if o1 {
+ if v1.Tok != token.IMPORT {
+ break
+ }
+ for j := 0; j < len(v1.Specs); j++ {
+ v2, o2 := v1.Specs[j].(*ast.ImportSpec)
+ if o2 && strings.HasSuffix(a.ImportPath, v2.Path.Value) {
+ has = true
+ break
+ }
+ }
+ if !has {
+ spec := &ast.ImportSpec{
+ Path: &ast.BasicLit{Kind: token.STRING, Value: a.ImportPath},
+ }
+ v1.Specs = append(v1.Specs, spec)
+ return nil
+ }
+ }
+ }
+ if !has {
+ decls := file.Decls
+ file.Decls = make([]ast.Decl, 0, len(file.Decls)+1)
+ decl := &ast.GenDecl{
+ Tok: token.IMPORT,
+ Specs: []ast.Spec{
+ &ast.ImportSpec{
+ Path: &ast.BasicLit{Kind: token.STRING, Value: a.ImportPath},
+ },
+ },
+ }
+ file.Decls = append(file.Decls, decl)
+ file.Decls = append(file.Decls, decls...)
+ } // 如果没有import声明,就创建一个, 主要要放在第一个
+ return nil
+}
+
+func (a *Import) Format(filename string, writer io.Writer, file *ast.File) error {
+ return a.Base.Format(filename, writer, file)
+}
diff --git a/server/utils/ast/interfaces.go b/server/utils/ast/interfaces.go
new file mode 100644
index 0000000..33ecc47
--- /dev/null
+++ b/server/utils/ast/interfaces.go
@@ -0,0 +1,17 @@
+package ast
+
+import (
+ "go/ast"
+ "io"
+)
+
+type Ast interface {
+ // Parse 解析文件/代码
+ Parse(filename string, writer io.Writer) (file *ast.File, err error)
+ // Rollback 回滚
+ Rollback(file *ast.File) error
+ // Injection 注入
+ Injection(file *ast.File) error
+ // Format 格式化输出
+ Format(filename string, writer io.Writer, file *ast.File) error
+}
diff --git a/server/utils/ast/interfaces_base.go b/server/utils/ast/interfaces_base.go
new file mode 100644
index 0000000..a470a23
--- /dev/null
+++ b/server/utils/ast/interfaces_base.go
@@ -0,0 +1,77 @@
+package ast
+
+import (
+ "go/ast"
+ "go/format"
+ "go/parser"
+ "go/token"
+ "io"
+ "os"
+ "path"
+ "path/filepath"
+ "strings"
+
+ "git.echol.cn/loser/st/server/global"
+ "github.com/pkg/errors"
+)
+
+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/server/utils/ast/package_enter.go b/server/utils/ast/package_enter.go
new file mode 100644
index 0000000..f4b6305
--- /dev/null
+++ b/server/utils/ast/package_enter.go
@@ -0,0 +1,85 @@
+package ast
+
+import (
+ "go/ast"
+ "go/token"
+ "io"
+)
+
+// PackageEnter 模块化入口
+type PackageEnter struct {
+ Base
+ Type Type // 类型
+ Path string // 文件路径
+ ImportPath string // 导包路径
+ StructName string // 结构体名称
+ PackageName string // 包名
+ RelativePath string // 相对路径
+ PackageStructName string // 包结构体名称
+}
+
+func (a *PackageEnter) Parse(filename string, writer io.Writer) (file *ast.File, err error) {
+ if filename == "" {
+ if a.RelativePath == "" {
+ filename = a.Path
+ a.RelativePath = a.Base.RelativePath(a.Path)
+ return a.Base.Parse(filename, writer)
+ }
+ a.Path = a.Base.AbsolutePath(a.RelativePath)
+ filename = a.Path
+ }
+ return a.Base.Parse(filename, writer)
+}
+
+func (a *PackageEnter) Rollback(file *ast.File) error {
+ // 无需回滚
+ return nil
+}
+
+func (a *PackageEnter) Injection(file *ast.File) error {
+ _ = NewImport(a.ImportPath).Injection(file)
+ ast.Inspect(file, func(n ast.Node) bool {
+ genDecl, ok := n.(*ast.GenDecl)
+ if !ok || genDecl.Tok != token.TYPE {
+ return true
+ }
+
+ for _, spec := range genDecl.Specs {
+ typeSpec, specok := spec.(*ast.TypeSpec)
+ if !specok || typeSpec.Name.Name != a.Type.Group() {
+ continue
+ }
+
+ structType, structTypeOK := typeSpec.Type.(*ast.StructType)
+ if !structTypeOK {
+ continue
+ }
+
+ for _, field := range structType.Fields.List {
+ if len(field.Names) == 1 && field.Names[0].Name == a.StructName {
+ return true
+ }
+ }
+
+ field := &ast.Field{
+ Names: []*ast.Ident{{Name: a.StructName}},
+ Type: &ast.SelectorExpr{
+ X: &ast.Ident{Name: a.PackageName},
+ Sel: &ast.Ident{Name: a.PackageStructName},
+ },
+ }
+ structType.Fields.List = append(structType.Fields.List, field)
+ return false
+ }
+
+ return true
+ })
+ return nil
+}
+
+func (a *PackageEnter) Format(filename string, writer io.Writer, file *ast.File) error {
+ if filename == "" {
+ filename = a.Path
+ }
+ return a.Base.Format(filename, writer, file)
+}
diff --git a/server/utils/ast/package_enter_test.go b/server/utils/ast/package_enter_test.go
new file mode 100644
index 0000000..17cfb56
--- /dev/null
+++ b/server/utils/ast/package_enter_test.go
@@ -0,0 +1,155 @@
+package ast
+
+import (
+ "path/filepath"
+ "testing"
+
+ "git.echol.cn/loser/st/server/global"
+)
+
+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/st/server/api/v1/example"`,
+ StructName: "ExampleApiGroup",
+ PackageName: "example",
+ PackageStructName: "ApiGroup",
+ },
+ wantErr: false,
+ },
+ {
+ name: "测试ExampleRouterGroup回滚",
+ fields: fields{
+ Type: TypePackageRouterEnter,
+ Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", "enter.go"),
+ ImportPath: `"git.echol.cn/loser/st/server/router/example"`,
+ StructName: "Example",
+ PackageName: "example",
+ PackageStructName: "RouterGroup",
+ },
+ wantErr: false,
+ },
+ {
+ name: "测试ExampleServiceGroup回滚",
+ fields: fields{
+ Type: TypePackageServiceEnter,
+ Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", "enter.go"),
+ ImportPath: `"git.echol.cn/loser/st/server/service/example"`,
+ StructName: "ExampleServiceGroup",
+ PackageName: "example",
+ PackageStructName: "ServiceGroup",
+ },
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ a := &PackageEnter{
+ Type: tt.fields.Type,
+ Path: tt.fields.Path,
+ ImportPath: tt.fields.ImportPath,
+ StructName: tt.fields.StructName,
+ PackageName: tt.fields.PackageName,
+ PackageStructName: tt.fields.PackageStructName,
+ }
+ file, err := a.Parse(a.Path, nil)
+ if err != nil {
+ t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ a.Rollback(file)
+ err = a.Format(a.Path, nil, file)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
+
+func TestPackageEnter_Injection(t *testing.T) {
+ type fields struct {
+ Type Type
+ Path string
+ ImportPath string
+ StructName string
+ PackageName string
+ PackageStructName string
+ }
+ tests := []struct {
+ name string
+ fields fields
+ wantErr bool
+ }{
+ {
+ name: "测试ExampleApiGroup注入",
+ fields: fields{
+ Type: TypePackageApiEnter,
+ Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", "enter.go"),
+ ImportPath: `"git.echol.cn/loser/st/server/api/v1/example"`,
+ StructName: "ExampleApiGroup",
+ PackageName: "example",
+ PackageStructName: "ApiGroup",
+ },
+ },
+ {
+ name: "测试ExampleRouterGroup注入",
+ fields: fields{
+ Type: TypePackageRouterEnter,
+ Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", "enter.go"),
+ ImportPath: `"git.echol.cn/loser/st/server/router/example"`,
+ StructName: "Example",
+ PackageName: "example",
+ PackageStructName: "RouterGroup",
+ },
+ wantErr: false,
+ },
+ {
+ name: "测试ExampleServiceGroup注入",
+ fields: fields{
+ Type: TypePackageServiceEnter,
+ Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", "enter.go"),
+ ImportPath: `"git.echol.cn/loser/st/server/service/example"`,
+ StructName: "ExampleServiceGroup",
+ PackageName: "example",
+ PackageStructName: "ServiceGroup",
+ },
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ a := &PackageEnter{
+ Type: tt.fields.Type,
+ Path: tt.fields.Path,
+ ImportPath: tt.fields.ImportPath,
+ StructName: tt.fields.StructName,
+ PackageName: tt.fields.PackageName,
+ PackageStructName: tt.fields.PackageStructName,
+ }
+ file, err := a.Parse(a.Path, nil)
+ if err != nil {
+ t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ a.Injection(file)
+ err = a.Format(a.Path, nil, file)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("Format() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
diff --git a/server/utils/ast/package_initialize_gorm.go b/server/utils/ast/package_initialize_gorm.go
new file mode 100644
index 0000000..594f714
--- /dev/null
+++ b/server/utils/ast/package_initialize_gorm.go
@@ -0,0 +1,196 @@
+package ast
+
+import (
+ "fmt"
+ "go/ast"
+ "go/token"
+ "io"
+)
+
+// PackageInitializeGorm 包初始化gorm
+type PackageInitializeGorm struct {
+ Base
+ Type Type // 类型
+ Path string // 文件路径
+ ImportPath string // 导包路径
+ Business string // 业务库 gva => gva, 不要传"gva"
+ StructName string // 结构体名称
+ PackageName string // 包名
+ RelativePath string // 相对路径
+ IsNew bool // 是否使用new关键字 true: new(PackageName.StructName) false: &PackageName.StructName{}
+}
+
+func (a *PackageInitializeGorm) Parse(filename string, writer io.Writer) (file *ast.File, err error) {
+ if filename == "" {
+ if a.RelativePath == "" {
+ filename = a.Path
+ a.RelativePath = a.Base.RelativePath(a.Path)
+ return a.Base.Parse(filename, writer)
+ }
+ a.Path = a.Base.AbsolutePath(a.RelativePath)
+ filename = a.Path
+ }
+ return a.Base.Parse(filename, writer)
+}
+
+func (a *PackageInitializeGorm) Rollback(file *ast.File) error {
+ packageNameNum := 0
+ // 寻找目标结构
+ ast.Inspect(file, func(n ast.Node) bool {
+ // 总调用的db变量根据business来决定
+ varDB := a.Business + "Db"
+
+ if a.Business == "" {
+ varDB = "db"
+ }
+
+ callExpr, ok := n.(*ast.CallExpr)
+ if !ok {
+ return true
+ }
+
+ // 检查是不是 db.AutoMigrate() 方法
+ selExpr, ok := callExpr.Fun.(*ast.SelectorExpr)
+ if !ok || selExpr.Sel.Name != "AutoMigrate" {
+ return true
+ }
+
+ // 检查调用方是不是 db
+ ident, ok := selExpr.X.(*ast.Ident)
+ if !ok || ident.Name != varDB {
+ return true
+ }
+
+ // 删除结构体参数
+ for i := 0; i < len(callExpr.Args); i++ {
+ if com, comok := callExpr.Args[i].(*ast.CompositeLit); comok {
+ if selector, exprok := com.Type.(*ast.SelectorExpr); exprok {
+ if x, identok := selector.X.(*ast.Ident); identok {
+ if x.Name == a.PackageName {
+ packageNameNum++
+ if selector.Sel.Name == a.StructName {
+ callExpr.Args = append(callExpr.Args[:i], callExpr.Args[i+1:]...)
+ i--
+ }
+ }
+ }
+ }
+ }
+ }
+ return true
+ })
+
+ if packageNameNum == 1 {
+ _ = NewImport(a.ImportPath).Rollback(file)
+ }
+ return nil
+}
+
+func (a *PackageInitializeGorm) Injection(file *ast.File) error {
+ _ = NewImport(a.ImportPath).Injection(file)
+ bizModelDecl := FindFunction(file, "bizModel")
+ if bizModelDecl != nil {
+ a.addDbVar(bizModelDecl.Body)
+ }
+ // 寻找目标结构
+ ast.Inspect(file, func(n ast.Node) bool {
+ // 总调用的db变量根据business来决定
+ varDB := a.Business + "Db"
+
+ if a.Business == "" {
+ varDB = "db"
+ }
+
+ callExpr, ok := n.(*ast.CallExpr)
+ if !ok {
+ return true
+ }
+
+ // 检查是不是 db.AutoMigrate() 方法
+ selExpr, ok := callExpr.Fun.(*ast.SelectorExpr)
+ if !ok || selExpr.Sel.Name != "AutoMigrate" {
+ return true
+ }
+
+ // 检查调用方是不是 db
+ ident, ok := selExpr.X.(*ast.Ident)
+ if !ok || ident.Name != varDB {
+ return true
+ }
+
+ // 添加结构体参数
+ callExpr.Args = append(callExpr.Args, &ast.CompositeLit{
+ Type: &ast.SelectorExpr{
+ X: ast.NewIdent(a.PackageName),
+ Sel: ast.NewIdent(a.StructName),
+ },
+ })
+ return true
+ })
+ return nil
+}
+
+func (a *PackageInitializeGorm) Format(filename string, writer io.Writer, file *ast.File) error {
+ if filename == "" {
+ filename = a.Path
+ }
+ return a.Base.Format(filename, writer, file)
+}
+
+// 创建businessDB变量
+func (a *PackageInitializeGorm) addDbVar(astBody *ast.BlockStmt) {
+ for i := range astBody.List {
+ if assignStmt, ok := astBody.List[i].(*ast.AssignStmt); ok {
+ if ident, ok := assignStmt.Lhs[0].(*ast.Ident); ok {
+ if (a.Business == "" && ident.Name == "db") || ident.Name == a.Business+"Db" {
+ return
+ }
+ }
+ }
+ }
+
+ // 添加 businessDb := global.GetGlobalDBByDBName("business") 变量
+ assignNode := &ast.AssignStmt{
+ Lhs: []ast.Expr{
+ &ast.Ident{
+ Name: a.Business + "Db",
+ },
+ },
+ Tok: token.DEFINE,
+ Rhs: []ast.Expr{
+ &ast.CallExpr{
+ Fun: &ast.SelectorExpr{
+ X: &ast.Ident{
+ Name: "global",
+ },
+ Sel: &ast.Ident{
+ Name: "GetGlobalDBByDBName",
+ },
+ },
+ Args: []ast.Expr{
+ &ast.BasicLit{
+ Kind: token.STRING,
+ Value: fmt.Sprintf("\"%s\"", a.Business),
+ },
+ },
+ },
+ },
+ }
+
+ // 添加 businessDb.AutoMigrate() 方法
+ autoMigrateCall := &ast.ExprStmt{
+ X: &ast.CallExpr{
+ Fun: &ast.SelectorExpr{
+ X: &ast.Ident{
+ Name: a.Business + "Db",
+ },
+ Sel: &ast.Ident{
+ Name: "AutoMigrate",
+ },
+ },
+ },
+ }
+
+ returnNode := astBody.List[len(astBody.List)-1]
+ astBody.List = append(astBody.List[:len(astBody.List)-1], assignNode, autoMigrateCall, returnNode)
+}
diff --git a/server/utils/ast/package_initialize_gorm_test.go b/server/utils/ast/package_initialize_gorm_test.go
new file mode 100644
index 0000000..282df53
--- /dev/null
+++ b/server/utils/ast/package_initialize_gorm_test.go
@@ -0,0 +1,172 @@
+package ast
+
+import (
+ "path/filepath"
+ "testing"
+
+ "git.echol.cn/loser/st/server/global"
+)
+
+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/st/server/model/example"`,
+ StructName: "ExaFileUploadAndDownload",
+ PackageName: "example",
+ IsNew: false,
+ },
+ },
+ {
+ name: "测试 &example.ExaCustomer{} 注入",
+ fields: fields{
+ Type: TypePackageInitializeGorm,
+ Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"),
+ ImportPath: `"git.echol.cn/loser/st/server/model/example"`,
+ StructName: "ExaCustomer",
+ PackageName: "example",
+ IsNew: false,
+ },
+ },
+ {
+ name: "测试 new(example.ExaFileUploadAndDownload) 注入",
+ fields: fields{
+ Type: TypePackageInitializeGorm,
+ Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"),
+ ImportPath: `"git.echol.cn/loser/st/server/model/example"`,
+ StructName: "ExaFileUploadAndDownload",
+ PackageName: "example",
+ IsNew: true,
+ },
+ },
+ {
+ name: "测试 new(example.ExaCustomer) 注入",
+ fields: fields{
+ Type: TypePackageInitializeGorm,
+ Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"),
+ ImportPath: `"git.echol.cn/loser/st/server/model/example"`,
+ StructName: "ExaCustomer",
+ PackageName: "example",
+ IsNew: true,
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ a := &PackageInitializeGorm{
+ Type: tt.fields.Type,
+ Path: tt.fields.Path,
+ ImportPath: tt.fields.ImportPath,
+ StructName: tt.fields.StructName,
+ PackageName: tt.fields.PackageName,
+ IsNew: tt.fields.IsNew,
+ }
+ file, err := a.Parse(a.Path, nil)
+ if err != nil {
+ t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ a.Injection(file)
+ err = a.Format(a.Path, nil, file)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
+
+func TestPackageInitializeGorm_Rollback(t *testing.T) {
+ type fields struct {
+ Type Type
+ Path string
+ ImportPath string
+ StructName string
+ PackageName string
+ IsNew bool
+ }
+ tests := []struct {
+ name string
+ fields fields
+ wantErr bool
+ }{
+ {
+ name: "测试 &example.ExaFileUploadAndDownload{} 回滚",
+ fields: fields{
+ Type: TypePackageInitializeGorm,
+ Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"),
+ ImportPath: `"git.echol.cn/loser/st/server/model/example"`,
+ StructName: "ExaFileUploadAndDownload",
+ PackageName: "example",
+ IsNew: false,
+ },
+ },
+ {
+ name: "测试 &example.ExaCustomer{} 回滚",
+ fields: fields{
+ Type: TypePackageInitializeGorm,
+ Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"),
+ ImportPath: `"git.echol.cn/loser/st/server/model/example"`,
+ StructName: "ExaCustomer",
+ PackageName: "example",
+ IsNew: false,
+ },
+ },
+ {
+ name: "测试 new(example.ExaFileUploadAndDownload) 回滚",
+ fields: fields{
+ Type: TypePackageInitializeGorm,
+ Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"),
+ ImportPath: `"git.echol.cn/loser/st/server/model/example"`,
+ StructName: "ExaFileUploadAndDownload",
+ PackageName: "example",
+ IsNew: true,
+ },
+ },
+ {
+ name: "测试 new(example.ExaCustomer) 回滚",
+ fields: fields{
+ Type: TypePackageInitializeGorm,
+ Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"),
+ ImportPath: `"git.echol.cn/loser/st/server/model/example"`,
+ StructName: "ExaCustomer",
+ PackageName: "example",
+ IsNew: true,
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ a := &PackageInitializeGorm{
+ Type: tt.fields.Type,
+ Path: tt.fields.Path,
+ ImportPath: tt.fields.ImportPath,
+ StructName: tt.fields.StructName,
+ PackageName: tt.fields.PackageName,
+ IsNew: tt.fields.IsNew,
+ }
+ file, err := a.Parse(a.Path, nil)
+ if err != nil {
+ t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ a.Rollback(file)
+ err = a.Format(a.Path, nil, file)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
diff --git a/server/utils/ast/package_initialize_router.go b/server/utils/ast/package_initialize_router.go
new file mode 100644
index 0000000..9fe4429
--- /dev/null
+++ b/server/utils/ast/package_initialize_router.go
@@ -0,0 +1,150 @@
+package ast
+
+import (
+ "fmt"
+ "go/ast"
+ "go/token"
+ "io"
+)
+
+// PackageInitializeRouter 包初始化路由
+// ModuleName := PackageName.AppName.GroupName
+// ModuleName.FunctionName(RouterGroupName)
+type PackageInitializeRouter struct {
+ Base
+ Type Type // 类型
+ Path string // 文件路径
+ ImportPath string // 导包路径
+ RelativePath string // 相对路径
+ AppName string // 应用名称
+ GroupName string // 分组名称
+ ModuleName string // 模块名称
+ PackageName string // 包名
+ FunctionName string // 函数名
+ RouterGroupName string // 路由分组名称
+ LeftRouterGroupName string // 左路由分组名称
+ RightRouterGroupName string // 右路由分组名称
+}
+
+func (a *PackageInitializeRouter) Parse(filename string, writer io.Writer) (file *ast.File, err error) {
+ if filename == "" {
+ if a.RelativePath == "" {
+ filename = a.Path
+ a.RelativePath = a.Base.RelativePath(a.Path)
+ return a.Base.Parse(filename, writer)
+ }
+ a.Path = a.Base.AbsolutePath(a.RelativePath)
+ filename = a.Path
+ }
+ return a.Base.Parse(filename, writer)
+}
+
+func (a *PackageInitializeRouter) Rollback(file *ast.File) error {
+ funcDecl := FindFunction(file, "initBizRouter")
+ exprNum := 0
+ for i := range funcDecl.Body.List {
+ if IsBlockStmt(funcDecl.Body.List[i]) {
+ if VariableExistsInBlock(funcDecl.Body.List[i].(*ast.BlockStmt), a.ModuleName) {
+ for ii, stmt := range funcDecl.Body.List[i].(*ast.BlockStmt).List {
+ // 检查语句是否为 *ast.ExprStmt
+ exprStmt, ok := stmt.(*ast.ExprStmt)
+ if !ok {
+ continue
+ }
+ // 检查表达式是否为 *ast.CallExpr
+ callExpr, ok := exprStmt.X.(*ast.CallExpr)
+ if !ok {
+ continue
+ }
+ // 检查是否调用了我们正在寻找的函数
+ selExpr, ok := callExpr.Fun.(*ast.SelectorExpr)
+ if !ok {
+ continue
+ }
+ // 检查调用的函数是否为 systemRouter.InitApiRouter
+ ident, ok := selExpr.X.(*ast.Ident)
+ //只要存在调用则+1
+ if ok && ident.Name == a.ModuleName {
+ exprNum++
+ }
+ //判断是否为目标结构
+ if !ok || ident.Name != a.ModuleName || selExpr.Sel.Name != a.FunctionName {
+ continue
+ }
+ exprNum--
+ // 从语句列表中移除。
+ funcDecl.Body.List[i].(*ast.BlockStmt).List = append(funcDecl.Body.List[i].(*ast.BlockStmt).List[:ii], funcDecl.Body.List[i].(*ast.BlockStmt).List[ii+1:]...)
+ // 如果不再存在任何调用,则删除导入和变量。
+ if exprNum == 0 {
+ funcDecl.Body.List = append(funcDecl.Body.List[:i], funcDecl.Body.List[i+1:]...)
+ }
+ break
+ }
+ break
+ }
+ }
+ }
+
+ return nil
+}
+
+func (a *PackageInitializeRouter) Injection(file *ast.File) error {
+ funcDecl := FindFunction(file, "initBizRouter")
+ hasRouter := false
+ var varBlock *ast.BlockStmt
+ for i := range funcDecl.Body.List {
+ if IsBlockStmt(funcDecl.Body.List[i]) {
+ if VariableExistsInBlock(funcDecl.Body.List[i].(*ast.BlockStmt), a.ModuleName) {
+ hasRouter = true
+ varBlock = funcDecl.Body.List[i].(*ast.BlockStmt)
+ break
+ }
+ }
+ }
+ if !hasRouter {
+ stmt := a.CreateAssignStmt()
+ varBlock = &ast.BlockStmt{
+ List: []ast.Stmt{
+ stmt,
+ },
+ }
+ }
+ routerStmt := CreateStmt(fmt.Sprintf("%s.%s(%s,%s)", a.ModuleName, a.FunctionName, a.LeftRouterGroupName, a.RightRouterGroupName))
+ varBlock.List = append(varBlock.List, routerStmt)
+ if !hasRouter {
+ funcDecl.Body.List = append(funcDecl.Body.List, varBlock)
+ }
+ return nil
+}
+
+func (a *PackageInitializeRouter) Format(filename string, writer io.Writer, file *ast.File) error {
+ if filename == "" {
+ filename = a.Path
+ }
+ return a.Base.Format(filename, writer, file)
+}
+
+func (a *PackageInitializeRouter) CreateAssignStmt() *ast.AssignStmt {
+ //创建左侧变量
+ ident := &ast.Ident{
+ Name: a.ModuleName,
+ }
+
+ //创建右侧的赋值语句
+ selector := &ast.SelectorExpr{
+ X: &ast.SelectorExpr{
+ X: &ast.Ident{Name: a.PackageName},
+ Sel: &ast.Ident{Name: a.AppName},
+ },
+ Sel: &ast.Ident{Name: a.GroupName},
+ }
+
+ // 创建一个组合的赋值语句
+ stmt := &ast.AssignStmt{
+ Lhs: []ast.Expr{ident},
+ Tok: token.DEFINE,
+ Rhs: []ast.Expr{selector},
+ }
+
+ return stmt
+}
diff --git a/server/utils/ast/package_initialize_router_test.go b/server/utils/ast/package_initialize_router_test.go
new file mode 100644
index 0000000..5398615
--- /dev/null
+++ b/server/utils/ast/package_initialize_router_test.go
@@ -0,0 +1,159 @@
+package ast
+
+import (
+ "path/filepath"
+ "testing"
+
+ "git.echol.cn/loser/st/server/global"
+)
+
+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/st/server/router"`,
+ AppName: "RouterGroupApp",
+ GroupName: "Example",
+ ModuleName: "exampleRouter",
+ PackageName: "router",
+ FunctionName: "InitCustomerRouter",
+ RouterGroupName: "privateGroup",
+ },
+ wantErr: false,
+ },
+ {
+ name: "测试 InitFileUploadAndDownloadRouter 注入",
+ fields: fields{
+ Type: TypePackageInitializeRouter,
+ Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "router_biz.go"),
+ ImportPath: `"git.echol.cn/loser/st/server/router"`,
+ AppName: "RouterGroupApp",
+ GroupName: "Example",
+ ModuleName: "exampleRouter",
+ PackageName: "router",
+ FunctionName: "InitFileUploadAndDownloadRouter",
+ RouterGroupName: "privateGroup",
+ },
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ a := &PackageInitializeRouter{
+ Type: tt.fields.Type,
+ Path: tt.fields.Path,
+ ImportPath: tt.fields.ImportPath,
+ AppName: tt.fields.AppName,
+ GroupName: tt.fields.GroupName,
+ ModuleName: tt.fields.ModuleName,
+ PackageName: tt.fields.PackageName,
+ FunctionName: tt.fields.FunctionName,
+ RouterGroupName: tt.fields.RouterGroupName,
+ LeftRouterGroupName: "privateGroup",
+ RightRouterGroupName: "publicGroup",
+ }
+ file, err := a.Parse(a.Path, nil)
+ if err != nil {
+ t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ a.Injection(file)
+ err = a.Format(a.Path, nil, file)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
+
+func TestPackageInitializeRouter_Rollback(t *testing.T) {
+ type fields struct {
+ Type Type
+ Path string
+ ImportPath string
+ AppName string
+ GroupName string
+ ModuleName string
+ PackageName string
+ FunctionName string
+ RouterGroupName string
+ }
+ tests := []struct {
+ name string
+ fields fields
+ wantErr bool
+ }{
+
+ {
+ name: "测试 InitCustomerRouter 回滚",
+ fields: fields{
+ Type: TypePackageInitializeRouter,
+ Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "router_biz.go"),
+ ImportPath: `"git.echol.cn/loser/st/server/router"`,
+ AppName: "RouterGroupApp",
+ GroupName: "Example",
+ ModuleName: "exampleRouter",
+ PackageName: "router",
+ FunctionName: "InitCustomerRouter",
+ RouterGroupName: "privateGroup",
+ },
+ wantErr: false,
+ },
+ {
+ name: "测试 InitFileUploadAndDownloadRouter 回滚",
+ fields: fields{
+ Type: TypePackageInitializeRouter,
+ Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "router_biz.go"),
+ ImportPath: `"git.echol.cn/loser/st/server/router"`,
+ AppName: "RouterGroupApp",
+ GroupName: "Example",
+ ModuleName: "exampleRouter",
+ PackageName: "router",
+ FunctionName: "InitFileUploadAndDownloadRouter",
+ RouterGroupName: "privateGroup",
+ },
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ a := &PackageInitializeRouter{
+ Type: tt.fields.Type,
+ Path: tt.fields.Path,
+ ImportPath: tt.fields.ImportPath,
+ AppName: tt.fields.AppName,
+ GroupName: tt.fields.GroupName,
+ ModuleName: tt.fields.ModuleName,
+ PackageName: tt.fields.PackageName,
+ FunctionName: tt.fields.FunctionName,
+ RouterGroupName: tt.fields.RouterGroupName,
+ }
+ file, err := a.Parse(a.Path, nil)
+ if err != nil {
+ t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ a.Rollback(file)
+ err = a.Format(a.Path, nil, file)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
diff --git a/server/utils/ast/package_module_enter.go b/server/utils/ast/package_module_enter.go
new file mode 100644
index 0000000..881fb3f
--- /dev/null
+++ b/server/utils/ast/package_module_enter.go
@@ -0,0 +1,180 @@
+package ast
+
+import (
+ "go/ast"
+ "go/token"
+ "io"
+)
+
+// PackageModuleEnter 模块化入口
+// ModuleName := PackageName.AppName.GroupName.ServiceName
+type PackageModuleEnter struct {
+ Base
+ Type Type // 类型
+ Path string // 文件路径
+ ImportPath string // 导包路径
+ RelativePath string // 相对路径
+ StructName string // 结构体名称
+ AppName string // 应用名称
+ GroupName string // 分组名称
+ ModuleName string // 模块名称
+ PackageName string // 包名
+ ServiceName string // 服务名称
+}
+
+func (a *PackageModuleEnter) Parse(filename string, writer io.Writer) (file *ast.File, err error) {
+ if filename == "" {
+ if a.RelativePath == "" {
+ filename = a.Path
+ a.RelativePath = a.Base.RelativePath(a.Path)
+ return a.Base.Parse(filename, writer)
+ }
+ a.Path = a.Base.AbsolutePath(a.RelativePath)
+ filename = a.Path
+ }
+ return a.Base.Parse(filename, writer)
+}
+
+func (a *PackageModuleEnter) Rollback(file *ast.File) error {
+ for i := 0; i < len(file.Decls); i++ {
+ v1, o1 := file.Decls[i].(*ast.GenDecl)
+ if o1 {
+ for j := 0; j < len(v1.Specs); j++ {
+ v2, o2 := v1.Specs[j].(*ast.TypeSpec)
+ if o2 {
+ if v2.Name.Name != a.Type.Group() {
+ continue
+ }
+ v3, o3 := v2.Type.(*ast.StructType)
+ if o3 {
+ for k := 0; k < len(v3.Fields.List); k++ {
+ v4, o4 := v3.Fields.List[k].Type.(*ast.Ident)
+ if o4 && v4.Name == a.StructName {
+ v3.Fields.List = append(v3.Fields.List[:k], v3.Fields.List[k+1:]...)
+ }
+ }
+ }
+ continue
+ }
+ if a.Type == TypePackageServiceModuleEnter {
+ continue
+ }
+ v3, o3 := v1.Specs[j].(*ast.ValueSpec)
+ if o3 {
+ if len(v3.Names) == 1 && v3.Names[0].Name == a.ModuleName {
+ v1.Specs = append(v1.Specs[:j], v1.Specs[j+1:]...)
+ }
+ }
+ if v1.Tok == token.VAR && len(v1.Specs) == 0 {
+ _ = NewImport(a.ImportPath).Rollback(file)
+ if i == len(file.Decls) {
+ file.Decls = append(file.Decls[:i-1])
+ break
+ } // 空的var(), 如果不删除则会影响的注入变量, 因为识别不到*ast.ValueSpec
+ file.Decls = append(file.Decls[:i], file.Decls[i+1:]...)
+ }
+ }
+ }
+ }
+ return nil
+}
+
+func (a *PackageModuleEnter) Injection(file *ast.File) error {
+ _ = NewImport(a.ImportPath).Injection(file)
+ var hasValue bool
+ var hasVariables bool
+ for i := 0; i < len(file.Decls); i++ {
+ v1, o1 := file.Decls[i].(*ast.GenDecl)
+ if o1 {
+ if v1.Tok == token.VAR {
+ hasVariables = true
+ }
+ for j := 0; j < len(v1.Specs); j++ {
+ if a.Type == TypePackageServiceModuleEnter {
+ hasValue = true
+ }
+ v2, o2 := v1.Specs[j].(*ast.TypeSpec)
+ if o2 {
+ if v2.Name.Name != a.Type.Group() {
+ continue
+ }
+ v3, o3 := v2.Type.(*ast.StructType)
+ if o3 {
+ var hasStruct bool
+ for k := 0; k < len(v3.Fields.List); k++ {
+ v4, o4 := v3.Fields.List[k].Type.(*ast.Ident)
+ if o4 && v4.Name == a.StructName {
+ hasStruct = true
+ }
+ }
+ if !hasStruct {
+ field := &ast.Field{Type: &ast.Ident{Name: a.StructName}}
+ v3.Fields.List = append(v3.Fields.List, field)
+ }
+ }
+ continue
+ }
+ v3, o3 := v1.Specs[j].(*ast.ValueSpec)
+ if o3 {
+ hasVariables = true
+ if len(v3.Names) == 1 && v3.Names[0].Name == a.ModuleName {
+ hasValue = true
+ }
+ }
+ if v1.Tok == token.VAR && len(v1.Specs) == 0 {
+ hasVariables = false
+ } // 说明是空var()
+ if hasVariables && !hasValue {
+ spec := &ast.ValueSpec{
+ Names: []*ast.Ident{{Name: a.ModuleName}},
+ Values: []ast.Expr{
+ &ast.SelectorExpr{
+ X: &ast.SelectorExpr{
+ X: &ast.SelectorExpr{
+ X: &ast.Ident{Name: a.PackageName},
+ Sel: &ast.Ident{Name: a.AppName},
+ },
+ Sel: &ast.Ident{Name: a.GroupName},
+ },
+ Sel: &ast.Ident{Name: a.ServiceName},
+ },
+ },
+ }
+ v1.Specs = append(v1.Specs, spec)
+ hasValue = true
+ }
+ }
+ }
+ }
+ if !hasValue && !hasVariables {
+ decl := &ast.GenDecl{
+ Tok: token.VAR,
+ Specs: []ast.Spec{
+ &ast.ValueSpec{
+ Names: []*ast.Ident{{Name: a.ModuleName}},
+ Values: []ast.Expr{
+ &ast.SelectorExpr{
+ X: &ast.SelectorExpr{
+ X: &ast.SelectorExpr{
+ X: &ast.Ident{Name: a.PackageName},
+ Sel: &ast.Ident{Name: a.AppName},
+ },
+ Sel: &ast.Ident{Name: a.GroupName},
+ },
+ Sel: &ast.Ident{Name: a.ServiceName},
+ },
+ },
+ },
+ },
+ }
+ file.Decls = append(file.Decls, decl)
+ }
+ return nil
+}
+
+func (a *PackageModuleEnter) Format(filename string, writer io.Writer, file *ast.File) error {
+ if filename == "" {
+ filename = a.Path
+ }
+ return a.Base.Format(filename, writer, file)
+}
diff --git a/server/utils/ast/package_module_enter_test.go b/server/utils/ast/package_module_enter_test.go
new file mode 100644
index 0000000..d04b1f1
--- /dev/null
+++ b/server/utils/ast/package_module_enter_test.go
@@ -0,0 +1,186 @@
+package ast
+
+import (
+ "path/filepath"
+ "testing"
+
+ "git.echol.cn/loser/st/server/global"
+)
+
+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/st/server/api/v1"`,
+ StructName: "FileUploadAndDownloadRouter",
+ AppName: "ApiGroupApp",
+ GroupName: "ExampleApiGroup",
+ ModuleName: "exaFileUploadAndDownloadApi",
+ PackageName: "api",
+ ServiceName: "FileUploadAndDownloadApi",
+ },
+ wantErr: false,
+ },
+ {
+ name: "测试 FileUploadAndDownloadApi 回滚",
+ fields: fields{
+ Type: TypePackageApiModuleEnter,
+ Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", "example", "enter.go"),
+ ImportPath: `"git.echol.cn/loser/st/server/service"`,
+ StructName: "FileUploadAndDownloadApi",
+ AppName: "ServiceGroupApp",
+ GroupName: "ExampleServiceGroup",
+ ModuleName: "fileUploadAndDownloadService",
+ PackageName: "service",
+ ServiceName: "FileUploadAndDownloadService",
+ },
+ wantErr: false,
+ },
+ {
+ name: "测试 FileUploadAndDownloadService 回滚",
+ fields: fields{
+ Type: TypePackageServiceModuleEnter,
+ Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", "example", "enter.go"),
+ ImportPath: ``,
+ StructName: "FileUploadAndDownloadService",
+ AppName: "",
+ GroupName: "",
+ ModuleName: "",
+ PackageName: "",
+ ServiceName: "",
+ },
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ a := &PackageModuleEnter{
+ Type: tt.fields.Type,
+ Path: tt.fields.Path,
+ ImportPath: tt.fields.ImportPath,
+ StructName: tt.fields.StructName,
+ AppName: tt.fields.AppName,
+ GroupName: tt.fields.GroupName,
+ ModuleName: tt.fields.ModuleName,
+ PackageName: tt.fields.PackageName,
+ ServiceName: tt.fields.ServiceName,
+ }
+ file, err := a.Parse(a.Path, nil)
+ if err != nil {
+ t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ a.Rollback(file)
+ err = a.Format(a.Path, nil, file)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
+
+func TestPackageModuleEnter_Injection(t *testing.T) {
+ type fields struct {
+ Type Type
+ Path string
+ ImportPath string
+ StructName string
+ AppName string
+ GroupName string
+ ModuleName string
+ PackageName string
+ ServiceName string
+ }
+ tests := []struct {
+ name string
+ fields fields
+ wantErr bool
+ }{
+ {
+ name: "测试 FileUploadAndDownloadRouter 注入",
+ fields: fields{
+ Type: TypePackageRouterModuleEnter,
+ Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", "example", "enter.go"),
+ ImportPath: `api "git.echol.cn/loser/st/server/api/v1"`,
+ StructName: "FileUploadAndDownloadRouter",
+ AppName: "ApiGroupApp",
+ GroupName: "ExampleApiGroup",
+ ModuleName: "exaFileUploadAndDownloadApi",
+ PackageName: "api",
+ ServiceName: "FileUploadAndDownloadApi",
+ },
+ wantErr: false,
+ },
+ {
+ name: "测试 FileUploadAndDownloadApi 注入",
+ fields: fields{
+ Type: TypePackageApiModuleEnter,
+ Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", "example", "enter.go"),
+ ImportPath: `"git.echol.cn/loser/st/server/service"`,
+ StructName: "FileUploadAndDownloadApi",
+ AppName: "ServiceGroupApp",
+ GroupName: "ExampleServiceGroup",
+ ModuleName: "fileUploadAndDownloadService",
+ PackageName: "service",
+ ServiceName: "FileUploadAndDownloadService",
+ },
+ wantErr: false,
+ },
+ {
+ name: "测试 FileUploadAndDownloadService 注入",
+ fields: fields{
+ Type: TypePackageServiceModuleEnter,
+ Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", "example", "enter.go"),
+ ImportPath: ``,
+ StructName: "FileUploadAndDownloadService",
+ AppName: "",
+ GroupName: "",
+ ModuleName: "",
+ PackageName: "",
+ ServiceName: "",
+ },
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ a := &PackageModuleEnter{
+ Type: tt.fields.Type,
+ Path: tt.fields.Path,
+ ImportPath: tt.fields.ImportPath,
+ StructName: tt.fields.StructName,
+ AppName: tt.fields.AppName,
+ GroupName: tt.fields.GroupName,
+ ModuleName: tt.fields.ModuleName,
+ PackageName: tt.fields.PackageName,
+ ServiceName: tt.fields.ServiceName,
+ }
+ file, err := a.Parse(a.Path, nil)
+ if err != nil {
+ t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ a.Injection(file)
+ err = a.Format(a.Path, nil, file)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
diff --git a/server/utils/ast/plugin_enter.go b/server/utils/ast/plugin_enter.go
new file mode 100644
index 0000000..df5bba4
--- /dev/null
+++ b/server/utils/ast/plugin_enter.go
@@ -0,0 +1,167 @@
+package ast
+
+import (
+ "go/ast"
+ "go/token"
+ "io"
+)
+
+// PluginEnter 插件化入口
+// ModuleName := PackageName.GroupName.ServiceName
+type PluginEnter struct {
+ Base
+ Type Type // 类型
+ Path string // 文件路径
+ ImportPath string // 导包路径
+ RelativePath string // 相对路径
+ StructName string // 结构体名称
+ StructCamelName string // 结构体小驼峰名称
+ ModuleName string // 模块名称
+ GroupName string // 分组名称
+ PackageName string // 包名
+ ServiceName string // 服务名称
+}
+
+func (a *PluginEnter) Parse(filename string, writer io.Writer) (file *ast.File, err error) {
+ if filename == "" {
+ if a.RelativePath == "" {
+ filename = a.Path
+ a.RelativePath = a.Base.RelativePath(a.Path)
+ return a.Base.Parse(filename, writer)
+ }
+ a.Path = a.Base.AbsolutePath(a.RelativePath)
+ filename = a.Path
+ }
+ return a.Base.Parse(filename, writer)
+}
+
+func (a *PluginEnter) Rollback(file *ast.File) error {
+ //回滚结构体内内容
+ var structType *ast.StructType
+ ast.Inspect(file, func(n ast.Node) bool {
+ switch x := n.(type) {
+ case *ast.TypeSpec:
+ if s, ok := x.Type.(*ast.StructType); ok {
+ structType = s
+ for i, field := range x.Type.(*ast.StructType).Fields.List {
+ if len(field.Names) > 0 && field.Names[0].Name == a.StructName {
+ s.Fields.List = append(s.Fields.List[:i], s.Fields.List[i+1:]...)
+ return false
+ }
+ }
+ }
+ }
+ return true
+ })
+
+ if len(structType.Fields.List) == 0 {
+ _ = NewImport(a.ImportPath).Rollback(file)
+ }
+
+ if a.Type == TypePluginServiceEnter {
+ return nil
+ }
+
+ //回滚变量内容
+ ast.Inspect(file, func(n ast.Node) bool {
+ genDecl, ok := n.(*ast.GenDecl)
+ if ok && genDecl.Tok == token.VAR {
+ for i, spec := range genDecl.Specs {
+ valueSpec, vsok := spec.(*ast.ValueSpec)
+ if vsok {
+ for _, name := range valueSpec.Names {
+ if name.Name == a.ModuleName {
+ genDecl.Specs = append(genDecl.Specs[:i], genDecl.Specs[i+1:]...)
+ return false
+ }
+ }
+ }
+ }
+ }
+ return true
+ })
+
+ return nil
+}
+
+func (a *PluginEnter) Injection(file *ast.File) error {
+ _ = NewImport(a.ImportPath).Injection(file)
+
+ has := false
+ hasVar := false
+ var firstStruct *ast.StructType
+ var varSpec *ast.GenDecl
+ //寻找是否存在结构且定位
+ ast.Inspect(file, func(n ast.Node) bool {
+ switch x := n.(type) {
+ case *ast.TypeSpec:
+ if s, ok := x.Type.(*ast.StructType); ok {
+ firstStruct = s
+ for _, field := range x.Type.(*ast.StructType).Fields.List {
+ if len(field.Names) > 0 && field.Names[0].Name == a.StructName {
+ has = true
+ return false
+ }
+ }
+ }
+ }
+ return true
+ })
+
+ if !has {
+ field := &ast.Field{
+ Names: []*ast.Ident{{Name: a.StructName}},
+ Type: &ast.Ident{Name: a.StructCamelName},
+ }
+ firstStruct.Fields.List = append(firstStruct.Fields.List, field)
+ }
+
+ if a.Type == TypePluginServiceEnter {
+ return nil
+ }
+
+ //寻找是否存在变量且定位
+ ast.Inspect(file, func(n ast.Node) bool {
+ genDecl, ok := n.(*ast.GenDecl)
+ if ok && genDecl.Tok == token.VAR {
+ for _, spec := range genDecl.Specs {
+ valueSpec, vsok := spec.(*ast.ValueSpec)
+ if vsok {
+ varSpec = genDecl
+ for _, name := range valueSpec.Names {
+ if name.Name == a.ModuleName {
+ hasVar = true
+ return false
+ }
+ }
+ }
+ }
+ }
+ return true
+ })
+
+ if !hasVar {
+ spec := &ast.ValueSpec{
+ Names: []*ast.Ident{{Name: a.ModuleName}},
+ Values: []ast.Expr{
+ &ast.SelectorExpr{
+ X: &ast.SelectorExpr{
+ X: &ast.Ident{Name: a.PackageName},
+ Sel: &ast.Ident{Name: a.GroupName},
+ },
+ Sel: &ast.Ident{Name: a.ServiceName},
+ },
+ },
+ }
+ varSpec.Specs = append(varSpec.Specs, spec)
+ }
+
+ return nil
+}
+
+func (a *PluginEnter) Format(filename string, writer io.Writer, file *ast.File) error {
+ if filename == "" {
+ filename = a.Path
+ }
+ return a.Base.Format(filename, writer, file)
+}
diff --git a/server/utils/ast/plugin_enter_test.go b/server/utils/ast/plugin_enter_test.go
new file mode 100644
index 0000000..92013f3
--- /dev/null
+++ b/server/utils/ast/plugin_enter_test.go
@@ -0,0 +1,201 @@
+package ast
+
+import (
+ "path/filepath"
+ "testing"
+
+ "git.echol.cn/loser/st/server/global"
+)
+
+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/st/server/plugin/gva/service"`,
+ StructName: "User",
+ StructCamelName: "user",
+ ModuleName: "serviceUser",
+ GroupName: "Service",
+ PackageName: "service",
+ ServiceName: "User",
+ },
+ wantErr: false,
+ },
+ {
+ name: "测试 Gva插件UserRouter 注入",
+ fields: fields{
+ Type: TypePluginRouterEnter,
+ Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "router", "enter.go"),
+ ImportPath: `"git.echol.cn/loser/st/server/plugin/gva/api"`,
+ StructName: "User",
+ StructCamelName: "user",
+ ModuleName: "userApi",
+ GroupName: "Api",
+ PackageName: "api",
+ ServiceName: "User",
+ },
+ wantErr: false,
+ },
+ {
+ name: "测试 Gva插件UserService 注入",
+ fields: fields{
+ Type: TypePluginServiceEnter,
+ Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "service", "enter.go"),
+ ImportPath: "",
+ StructName: "User",
+ StructCamelName: "user",
+ ModuleName: "",
+ GroupName: "",
+ PackageName: "",
+ ServiceName: "",
+ },
+ wantErr: false,
+ },
+ {
+ name: "测试 gva的User 注入",
+ fields: fields{
+ Type: TypePluginServiceEnter,
+ Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "service", "enter.go"),
+ ImportPath: "",
+ StructName: "User",
+ StructCamelName: "user",
+ ModuleName: "",
+ GroupName: "",
+ PackageName: "",
+ ServiceName: "",
+ },
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ a := &PluginEnter{
+ Type: tt.fields.Type,
+ Path: tt.fields.Path,
+ ImportPath: tt.fields.ImportPath,
+ StructName: tt.fields.StructName,
+ StructCamelName: tt.fields.StructCamelName,
+ ModuleName: tt.fields.ModuleName,
+ GroupName: tt.fields.GroupName,
+ PackageName: tt.fields.PackageName,
+ ServiceName: tt.fields.ServiceName,
+ }
+ file, err := a.Parse(a.Path, nil)
+ if err != nil {
+ t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ a.Injection(file)
+ err = a.Format(a.Path, nil, file)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
+
+func TestPluginEnter_Rollback(t *testing.T) {
+ type fields struct {
+ Type Type
+ Path string
+ ImportPath string
+ StructName string
+ StructCamelName string
+ ModuleName string
+ GroupName string
+ PackageName string
+ ServiceName string
+ }
+ tests := []struct {
+ name string
+ fields fields
+ wantErr bool
+ }{
+ {
+ name: "测试 Gva插件UserRouter 回滚",
+ fields: fields{
+ Type: TypePluginRouterEnter,
+ Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "router", "enter.go"),
+ ImportPath: `"git.echol.cn/loser/st/server/plugin/gva/api"`,
+ StructName: "User",
+ StructCamelName: "user",
+ ModuleName: "userApi",
+ GroupName: "Api",
+ PackageName: "api",
+ ServiceName: "User",
+ },
+ wantErr: false,
+ },
+ {
+ name: "测试 Gva插件UserApi 回滚",
+ fields: fields{
+ Type: TypePluginApiEnter,
+ Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "api", "enter.go"),
+ ImportPath: `"git.echol.cn/loser/st/server/plugin/gva/service"`,
+ StructName: "User",
+ StructCamelName: "user",
+ ModuleName: "serviceUser",
+ GroupName: "Service",
+ PackageName: "service",
+ ServiceName: "User",
+ },
+ wantErr: false,
+ },
+ {
+ name: "测试 Gva插件UserService 回滚",
+ fields: fields{
+ Type: TypePluginServiceEnter,
+ Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "service", "enter.go"),
+ ImportPath: "",
+ StructName: "User",
+ StructCamelName: "user",
+ ModuleName: "",
+ GroupName: "",
+ PackageName: "",
+ ServiceName: "",
+ },
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ a := &PluginEnter{
+ Type: tt.fields.Type,
+ Path: tt.fields.Path,
+ ImportPath: tt.fields.ImportPath,
+ StructName: tt.fields.StructName,
+ StructCamelName: tt.fields.StructCamelName,
+ ModuleName: tt.fields.ModuleName,
+ GroupName: tt.fields.GroupName,
+ PackageName: tt.fields.PackageName,
+ ServiceName: tt.fields.ServiceName,
+ }
+ file, err := a.Parse(a.Path, nil)
+ if err != nil {
+ t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ a.Rollback(file)
+ err = a.Format(a.Path, nil, file)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
diff --git a/server/utils/ast/plugin_gen.go b/server/utils/ast/plugin_gen.go
new file mode 100644
index 0000000..ed7d04f
--- /dev/null
+++ b/server/utils/ast/plugin_gen.go
@@ -0,0 +1,189 @@
+package ast
+
+import (
+ "go/ast"
+ "go/token"
+ "io"
+)
+
+type PluginGen struct {
+ Base
+ Type Type // 类型
+ Path string // 文件路径
+ ImportPath string // 导包路径
+ RelativePath string // 相对路径
+ StructName string // 结构体名称
+ PackageName string // 包名
+ IsNew bool // 是否使用new关键字
+}
+
+func (a *PluginGen) Parse(filename string, writer io.Writer) (file *ast.File, err error) {
+ if filename == "" {
+ if a.RelativePath == "" {
+ filename = a.Path
+ a.RelativePath = a.Base.RelativePath(a.Path)
+ return a.Base.Parse(filename, writer)
+ }
+ a.Path = a.Base.AbsolutePath(a.RelativePath)
+ filename = a.Path
+ }
+ return a.Base.Parse(filename, writer)
+}
+func (a *PluginGen) Rollback(file *ast.File) error {
+ for i := 0; i < len(file.Decls); i++ {
+ v1, o1 := file.Decls[i].(*ast.FuncDecl)
+ if o1 {
+ for j := 0; j < len(v1.Body.List); j++ {
+ v2, o2 := v1.Body.List[j].(*ast.ExprStmt)
+ if o2 {
+ v3, o3 := v2.X.(*ast.CallExpr)
+ if o3 {
+ v4, o4 := v3.Fun.(*ast.SelectorExpr)
+ if o4 {
+ if v4.Sel.Name != "ApplyBasic" {
+ continue
+ }
+ for k := 0; k < len(v3.Args); k++ {
+ v5, o5 := v3.Args[k].(*ast.CallExpr)
+ if o5 {
+ v6, o6 := v5.Fun.(*ast.Ident)
+ if o6 {
+ if v6.Name != "new" {
+ continue
+ }
+ for l := 0; l < len(v5.Args); l++ {
+ v7, o7 := v5.Args[l].(*ast.SelectorExpr)
+ if o7 {
+ v8, o8 := v7.X.(*ast.Ident)
+ if o8 {
+ if v8.Name == a.PackageName && v7.Sel.Name == a.StructName {
+ v3.Args = append(v3.Args[:k], v3.Args[k+1:]...)
+ continue
+ }
+ }
+ }
+ }
+ }
+ }
+ if k >= len(v3.Args) {
+ break
+ }
+ v6, o6 := v3.Args[k].(*ast.CompositeLit)
+ if o6 {
+ v7, o7 := v6.Type.(*ast.SelectorExpr)
+ if o7 {
+ v8, o8 := v7.X.(*ast.Ident)
+ if o8 {
+ if v8.Name == a.PackageName && v7.Sel.Name == a.StructName {
+ v3.Args = append(v3.Args[:k], v3.Args[k+1:]...)
+ continue
+ }
+ }
+ }
+ }
+ }
+ if len(v3.Args) == 0 {
+ _ = NewImport(a.ImportPath).Rollback(file)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return nil
+}
+
+func (a *PluginGen) Injection(file *ast.File) error {
+ _ = NewImport(a.ImportPath).Injection(file)
+ for i := 0; i < len(file.Decls); i++ {
+ v1, o1 := file.Decls[i].(*ast.FuncDecl)
+ if o1 {
+ for j := 0; j < len(v1.Body.List); j++ {
+ v2, o2 := v1.Body.List[j].(*ast.ExprStmt)
+ if o2 {
+ v3, o3 := v2.X.(*ast.CallExpr)
+ if o3 {
+ v4, o4 := v3.Fun.(*ast.SelectorExpr)
+ if o4 {
+ if v4.Sel.Name != "ApplyBasic" {
+ continue
+ }
+ var has bool
+ for k := 0; k < len(v3.Args); k++ {
+ v5, o5 := v3.Args[k].(*ast.CallExpr)
+ if o5 {
+ v6, o6 := v5.Fun.(*ast.Ident)
+ if o6 {
+ if v6.Name != "new" {
+ continue
+ }
+ for l := 0; l < len(v5.Args); l++ {
+ v7, o7 := v5.Args[l].(*ast.SelectorExpr)
+ if o7 {
+ v8, o8 := v7.X.(*ast.Ident)
+ if o8 {
+ if v8.Name == a.PackageName && v7.Sel.Name == a.StructName {
+ has = true
+ break
+ }
+ }
+ }
+ }
+ }
+ }
+ v6, o6 := v3.Args[k].(*ast.CompositeLit)
+ if o6 {
+ v7, o7 := v6.Type.(*ast.SelectorExpr)
+ if o7 {
+ v8, o8 := v7.X.(*ast.Ident)
+ if o8 {
+ if v8.Name == a.PackageName && v7.Sel.Name == a.StructName {
+ has = true
+ break
+ }
+ }
+ }
+ }
+ }
+ if !has {
+ if a.IsNew {
+ arg := &ast.CallExpr{
+ Fun: &ast.Ident{Name: "\n\t\tnew"},
+ Args: []ast.Expr{
+ &ast.SelectorExpr{
+ X: &ast.Ident{Name: a.PackageName},
+ Sel: &ast.Ident{Name: a.StructName},
+ },
+ },
+ }
+ v3.Args = append(v3.Args, arg)
+ v3.Args = append(v3.Args, &ast.BasicLit{
+ Kind: token.STRING,
+ Value: "\n",
+ })
+ break
+ }
+ arg := &ast.CompositeLit{
+ Type: &ast.SelectorExpr{
+ X: &ast.Ident{Name: a.PackageName},
+ Sel: &ast.Ident{Name: a.StructName},
+ },
+ }
+ v3.Args = append(v3.Args, arg)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return nil
+}
+
+func (a *PluginGen) Format(filename string, writer io.Writer, file *ast.File) error {
+ if filename == "" {
+ filename = a.Path
+ }
+ return a.Base.Format(filename, writer, file)
+}
diff --git a/server/utils/ast/plugin_gen_test.go b/server/utils/ast/plugin_gen_test.go
new file mode 100644
index 0000000..d88a957
--- /dev/null
+++ b/server/utils/ast/plugin_gen_test.go
@@ -0,0 +1,128 @@
+package ast
+
+import (
+ "path/filepath"
+ "testing"
+
+ "git.echol.cn/loser/st/server/global"
+)
+
+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/st/server/plugin/gva/model"`,
+ PackageName: "model",
+ StructName: "User",
+ IsNew: false,
+ },
+ },
+ {
+ name: "测试 GvaUser 结构体注入",
+ fields: fields{
+ Type: TypePluginGen,
+ Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "gen", "main.go"),
+ ImportPath: `"git.echol.cn/loser/st/server/plugin/gva/model"`,
+ PackageName: "model",
+ StructName: "User",
+ IsNew: true,
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ a := &PluginGen{
+ Type: tt.fields.Type,
+ Path: tt.fields.Path,
+ ImportPath: tt.fields.ImportPath,
+ PackageName: tt.fields.PackageName,
+ StructName: tt.fields.StructName,
+ IsNew: tt.fields.IsNew,
+ }
+ file, err := a.Parse(a.Path, nil)
+ if err != nil {
+ t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ a.Injection(file)
+ err = a.Format(a.Path, nil, file)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
+
+func TestPluginGenModel_Rollback(t *testing.T) {
+ type fields struct {
+ Type Type
+ Path string
+ ImportPath string
+ PackageName string
+ StructName string
+ IsNew bool
+ }
+ tests := []struct {
+ name string
+ fields fields
+ wantErr bool
+ }{
+ {
+ name: "测试 GvaUser 回滚",
+ fields: fields{
+ Type: TypePluginGen,
+ Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "gen", "main.go"),
+ ImportPath: `"git.echol.cn/loser/st/server/plugin/gva/model"`,
+ PackageName: "model",
+ StructName: "User",
+ IsNew: false,
+ },
+ },
+ {
+ name: "测试 GvaUser 回滚",
+ fields: fields{
+ Type: TypePluginGen,
+ Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "gen", "main.go"),
+ ImportPath: `"git.echol.cn/loser/st/server/plugin/gva/model"`,
+ PackageName: "model",
+ StructName: "User",
+ IsNew: true,
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ a := &PluginGen{
+ Type: tt.fields.Type,
+ Path: tt.fields.Path,
+ ImportPath: tt.fields.ImportPath,
+ PackageName: tt.fields.PackageName,
+ StructName: tt.fields.StructName,
+ IsNew: tt.fields.IsNew,
+ }
+ file, err := a.Parse(a.Path, nil)
+ if err != nil {
+ t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ a.Rollback(file)
+ err = a.Format(a.Path, nil, file)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
diff --git a/server/utils/ast/plugin_initialize_gorm.go b/server/utils/ast/plugin_initialize_gorm.go
new file mode 100644
index 0000000..e342251
--- /dev/null
+++ b/server/utils/ast/plugin_initialize_gorm.go
@@ -0,0 +1,111 @@
+package ast
+
+import (
+ "go/ast"
+ "io"
+)
+
+type PluginInitializeGorm struct {
+ Base
+ Type Type // 类型
+ Path string // 文件路径
+ ImportPath string // 导包路径
+ RelativePath string // 相对路径
+ StructName string // 结构体名称
+ PackageName string // 包名
+ IsNew bool // 是否使用new关键字 true: new(PackageName.StructName) false: &PackageName.StructName{}
+}
+
+func (a *PluginInitializeGorm) Parse(filename string, writer io.Writer) (file *ast.File, err error) {
+ if filename == "" {
+ if a.RelativePath == "" {
+ filename = a.Path
+ a.RelativePath = a.Base.RelativePath(a.Path)
+ return a.Base.Parse(filename, writer)
+ }
+ a.Path = a.Base.AbsolutePath(a.RelativePath)
+ filename = a.Path
+ }
+ return a.Base.Parse(filename, writer)
+}
+
+func (a *PluginInitializeGorm) Rollback(file *ast.File) error {
+ var needRollBackImport bool
+ ast.Inspect(file, func(n ast.Node) bool {
+ callExpr, ok := n.(*ast.CallExpr)
+ if !ok {
+ return true
+ }
+
+ selExpr, seok := callExpr.Fun.(*ast.SelectorExpr)
+ if !seok || selExpr.Sel.Name != "AutoMigrate" {
+ return true
+ }
+ if len(callExpr.Args) <= 1 {
+ needRollBackImport = true
+ }
+ // 删除指定的参数
+ for i, arg := range callExpr.Args {
+ compLit, cok := arg.(*ast.CompositeLit)
+ if !cok {
+ continue
+ }
+
+ cselExpr, sok := compLit.Type.(*ast.SelectorExpr)
+ if !sok {
+ continue
+ }
+
+ ident, idok := cselExpr.X.(*ast.Ident)
+ if idok && ident.Name == a.PackageName && cselExpr.Sel.Name == a.StructName {
+ // 删除参数
+ callExpr.Args = append(callExpr.Args[:i], callExpr.Args[i+1:]...)
+ break
+ }
+ }
+
+ return true
+ })
+
+ if needRollBackImport {
+ _ = NewImport(a.ImportPath).Rollback(file)
+ }
+
+ return nil
+}
+
+func (a *PluginInitializeGorm) Injection(file *ast.File) error {
+ _ = NewImport(a.ImportPath).Injection(file)
+ var call *ast.CallExpr
+ ast.Inspect(file, func(n ast.Node) bool {
+ callExpr, ok := n.(*ast.CallExpr)
+ if !ok {
+ return true
+ }
+
+ selExpr, ok := callExpr.Fun.(*ast.SelectorExpr)
+ if ok && selExpr.Sel.Name == "AutoMigrate" {
+ call = callExpr
+ return false
+ }
+
+ return true
+ })
+
+ arg := &ast.CompositeLit{
+ Type: &ast.SelectorExpr{
+ X: &ast.Ident{Name: a.PackageName},
+ Sel: &ast.Ident{Name: a.StructName},
+ },
+ }
+
+ call.Args = append(call.Args, arg)
+ return nil
+}
+
+func (a *PluginInitializeGorm) Format(filename string, writer io.Writer, file *ast.File) error {
+ if filename == "" {
+ filename = a.Path
+ }
+ return a.Base.Format(filename, writer, file)
+}
diff --git a/server/utils/ast/plugin_initialize_gorm_test.go b/server/utils/ast/plugin_initialize_gorm_test.go
new file mode 100644
index 0000000..467c2df
--- /dev/null
+++ b/server/utils/ast/plugin_initialize_gorm_test.go
@@ -0,0 +1,139 @@
+package ast
+
+import (
+ "path/filepath"
+ "testing"
+
+ "git.echol.cn/loser/st/server/global"
+)
+
+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/st/server/plugin/gva/model"`,
+ StructName: "User",
+ PackageName: "model",
+ IsNew: false,
+ },
+ },
+ {
+ name: "测试 new(model.ExaCustomer) 注入",
+ fields: fields{
+ Type: TypePluginInitializeGorm,
+ Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "gorm.go"),
+ ImportPath: `"git.echol.cn/loser/st/server/plugin/gva/model"`,
+ StructName: "User",
+ PackageName: "model",
+ IsNew: true,
+ },
+ },
+ {
+ name: "测试 new(model.SysUsers) 注入",
+ fields: fields{
+ Type: TypePluginInitializeGorm,
+ Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "gorm.go"),
+ ImportPath: `"git.echol.cn/loser/st/server/plugin/gva/model"`,
+ StructName: "SysUser",
+ PackageName: "model",
+ IsNew: true,
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ a := &PluginInitializeGorm{
+ Type: tt.fields.Type,
+ Path: tt.fields.Path,
+ ImportPath: tt.fields.ImportPath,
+ StructName: tt.fields.StructName,
+ PackageName: tt.fields.PackageName,
+ IsNew: tt.fields.IsNew,
+ }
+ file, err := a.Parse(a.Path, nil)
+ if err != nil {
+ t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ a.Injection(file)
+ err = a.Format(a.Path, nil, file)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
+
+func TestPluginInitializeGorm_Rollback(t *testing.T) {
+ type fields struct {
+ Type Type
+ Path string
+ ImportPath string
+ StructName string
+ PackageName string
+ IsNew bool
+ }
+ tests := []struct {
+ name string
+ fields fields
+ wantErr bool
+ }{
+ {
+ name: "测试 &model.User{} 回滚",
+ fields: fields{
+ Type: TypePluginInitializeGorm,
+ Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "gorm.go"),
+ ImportPath: `"git.echol.cn/loser/st/server/plugin/gva/model"`,
+ StructName: "User",
+ PackageName: "model",
+ IsNew: false,
+ },
+ },
+ {
+ name: "测试 new(model.ExaCustomer) 回滚",
+ fields: fields{
+ Type: TypePluginInitializeGorm,
+ Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "gorm.go"),
+ ImportPath: `"git.echol.cn/loser/st/server/plugin/gva/model"`,
+ StructName: "User",
+ PackageName: "model",
+ IsNew: true,
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ a := &PluginInitializeGorm{
+ Type: tt.fields.Type,
+ Path: tt.fields.Path,
+ ImportPath: tt.fields.ImportPath,
+ StructName: tt.fields.StructName,
+ PackageName: tt.fields.PackageName,
+ IsNew: tt.fields.IsNew,
+ }
+ file, err := a.Parse(a.Path, nil)
+ if err != nil {
+ t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ a.Rollback(file)
+ err = a.Format(a.Path, nil, file)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
diff --git a/server/utils/ast/plugin_initialize_router.go b/server/utils/ast/plugin_initialize_router.go
new file mode 100644
index 0000000..6550789
--- /dev/null
+++ b/server/utils/ast/plugin_initialize_router.go
@@ -0,0 +1,124 @@
+package ast
+
+import (
+ "fmt"
+ "go/ast"
+ "io"
+)
+
+// PluginInitializeRouter 插件初始化路由
+// PackageName.AppName.GroupName.FunctionName()
+type PluginInitializeRouter struct {
+ Base
+ Type Type // 类型
+ Path string // 文件路径
+ ImportPath string // 导包路径
+ ImportGlobalPath string // 导包全局变量路径
+ ImportMiddlewarePath string // 导包中间件路径
+ RelativePath string // 相对路径
+ AppName string // 应用名称
+ GroupName string // 分组名称
+ PackageName string // 包名
+ FunctionName string // 函数名
+ LeftRouterGroupName string // 左路由分组名称
+ RightRouterGroupName string // 右路由分组名称
+}
+
+func (a *PluginInitializeRouter) Parse(filename string, writer io.Writer) (file *ast.File, err error) {
+ if filename == "" {
+ if a.RelativePath == "" {
+ filename = a.Path
+ a.RelativePath = a.Base.RelativePath(a.Path)
+ return a.Base.Parse(filename, writer)
+ }
+ a.Path = a.Base.AbsolutePath(a.RelativePath)
+ filename = a.Path
+ }
+ return a.Base.Parse(filename, writer)
+}
+
+func (a *PluginInitializeRouter) Rollback(file *ast.File) error {
+ funcDecl := FindFunction(file, "Router")
+ delI := 0
+ routerNum := 0
+ for i := len(funcDecl.Body.List) - 1; i >= 0; i-- {
+ stmt, ok := funcDecl.Body.List[i].(*ast.ExprStmt)
+ if !ok {
+ continue
+ }
+
+ callExpr, ok := stmt.X.(*ast.CallExpr)
+ if !ok {
+ continue
+ }
+
+ selExpr, ok := callExpr.Fun.(*ast.SelectorExpr)
+ if !ok {
+ continue
+ }
+
+ ident, ok := selExpr.X.(*ast.SelectorExpr)
+
+ if ok {
+ if iExpr, ieok := ident.X.(*ast.SelectorExpr); ieok {
+ if iden, idok := iExpr.X.(*ast.Ident); idok {
+ if iden.Name == "router" {
+ routerNum++
+ }
+ }
+ }
+ if ident.Sel.Name == a.GroupName && selExpr.Sel.Name == a.FunctionName {
+ // 删除语句
+ delI = i
+ }
+ }
+ }
+
+ funcDecl.Body.List = append(funcDecl.Body.List[:delI], funcDecl.Body.List[delI+1:]...)
+
+ if routerNum <= 1 {
+ _ = NewImport(a.ImportPath).Rollback(file)
+ }
+
+ return nil
+}
+
+func (a *PluginInitializeRouter) Injection(file *ast.File) error {
+ _ = NewImport(a.ImportPath).Injection(file)
+ funcDecl := FindFunction(file, "Router")
+
+ var exists bool
+
+ ast.Inspect(funcDecl, func(n ast.Node) bool {
+ callExpr, ok := n.(*ast.CallExpr)
+ if !ok {
+ return true
+ }
+
+ selExpr, ok := callExpr.Fun.(*ast.SelectorExpr)
+ if !ok {
+ return true
+ }
+
+ ident, ok := selExpr.X.(*ast.SelectorExpr)
+ if ok && ident.Sel.Name == a.GroupName && selExpr.Sel.Name == a.FunctionName {
+ exists = true
+ return false
+ }
+ return true
+ })
+
+ if !exists {
+ stmtStr := fmt.Sprintf("%s.%s.%s.%s(%s, %s)", a.PackageName, a.AppName, a.GroupName, a.FunctionName, a.LeftRouterGroupName, a.RightRouterGroupName)
+ stmt := CreateStmt(stmtStr)
+ funcDecl.Body.List = append(funcDecl.Body.List, stmt)
+ }
+ return nil
+}
+
+func (a *PluginInitializeRouter) Format(filename string, writer io.Writer, file *ast.File) error {
+ if filename == "" {
+ filename = a.Path
+ }
+ return a.Base.Format(filename, writer, file)
+}
diff --git a/server/utils/ast/plugin_initialize_router_test.go b/server/utils/ast/plugin_initialize_router_test.go
new file mode 100644
index 0000000..b407d63
--- /dev/null
+++ b/server/utils/ast/plugin_initialize_router_test.go
@@ -0,0 +1,156 @@
+package ast
+
+import (
+ "path/filepath"
+ "testing"
+
+ "git.echol.cn/loser/st/server/global"
+)
+
+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/st/server/plugin/gva/router"`,
+ AppName: "Router",
+ GroupName: "User",
+ PackageName: "router",
+ FunctionName: "Init",
+ LeftRouterGroupName: "public",
+ RightRouterGroupName: "private",
+ },
+ wantErr: false,
+ },
+ {
+ name: "测试 中文 注入",
+ fields: fields{
+ Type: TypePluginInitializeRouter,
+ Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "router.go"),
+ ImportPath: `"git.echol.cn/loser/st/server/plugin/gva/router"`,
+ AppName: "Router",
+ GroupName: "U中文",
+ PackageName: "router",
+ FunctionName: "Init",
+ LeftRouterGroupName: "public",
+ RightRouterGroupName: "private",
+ },
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ a := &PluginInitializeRouter{
+ Type: tt.fields.Type,
+ Path: tt.fields.Path,
+ ImportPath: tt.fields.ImportPath,
+ AppName: tt.fields.AppName,
+ GroupName: tt.fields.GroupName,
+ PackageName: tt.fields.PackageName,
+ FunctionName: tt.fields.FunctionName,
+ LeftRouterGroupName: tt.fields.LeftRouterGroupName,
+ RightRouterGroupName: tt.fields.RightRouterGroupName,
+ }
+ file, err := a.Parse(a.Path, nil)
+ if err != nil {
+ t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ a.Injection(file)
+ err = a.Format(a.Path, nil, file)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
+
+func TestPluginInitializeRouter_Rollback(t *testing.T) {
+ type fields struct {
+ Type Type
+ Path string
+ ImportPath string
+ AppName string
+ GroupName string
+ PackageName string
+ FunctionName string
+ LeftRouterGroupName string
+ RightRouterGroupName string
+ }
+ tests := []struct {
+ name string
+ fields fields
+ wantErr bool
+ }{
+ {
+ name: "测试 Gva插件User 回滚",
+ fields: fields{
+ Type: TypePluginInitializeRouter,
+ Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "router.go"),
+ ImportPath: `"git.echol.cn/loser/st/server/plugin/gva/router"`,
+ AppName: "Router",
+ GroupName: "User",
+ PackageName: "router",
+ FunctionName: "Init",
+ LeftRouterGroupName: "public",
+ RightRouterGroupName: "private",
+ },
+ wantErr: false,
+ },
+ {
+ name: "测试 中文 注入",
+ fields: fields{
+ Type: TypePluginInitializeRouter,
+ Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "router.go"),
+ ImportPath: `"git.echol.cn/loser/st/server/plugin/gva/router"`,
+ AppName: "Router",
+ GroupName: "U中文",
+ PackageName: "router",
+ FunctionName: "Init",
+ LeftRouterGroupName: "public",
+ RightRouterGroupName: "private",
+ },
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ a := &PluginInitializeRouter{
+ Type: tt.fields.Type,
+ Path: tt.fields.Path,
+ ImportPath: tt.fields.ImportPath,
+ AppName: tt.fields.AppName,
+ GroupName: tt.fields.GroupName,
+ PackageName: tt.fields.PackageName,
+ FunctionName: tt.fields.FunctionName,
+ LeftRouterGroupName: tt.fields.LeftRouterGroupName,
+ RightRouterGroupName: tt.fields.RightRouterGroupName,
+ }
+ file, err := a.Parse(a.Path, nil)
+ if err != nil {
+ t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ a.Rollback(file)
+ err = a.Format(a.Path, nil, file)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
diff --git a/server/utils/ast/plugin_initialize_v2.go b/server/utils/ast/plugin_initialize_v2.go
new file mode 100644
index 0000000..974f513
--- /dev/null
+++ b/server/utils/ast/plugin_initialize_v2.go
@@ -0,0 +1,82 @@
+package ast
+
+import (
+ "go/ast"
+ "go/token"
+ "io"
+ "strconv"
+ "strings"
+)
+
+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 {
+ importPath := strings.TrimSpace(a.ImportPath)
+ if importPath == "" {
+ return nil
+ }
+ importPath = strings.Trim(importPath, "\"")
+ if importPath == "" || CheckImport(file, importPath) {
+ return nil
+ }
+
+ importSpec := &ast.ImportSpec{
+ Name: ast.NewIdent("_"),
+ Path: &ast.BasicLit{Kind: token.STRING, Value: strconv.Quote(importPath)},
+ }
+ var importDecl *ast.GenDecl
+ for _, decl := range file.Decls {
+ genDecl, ok := decl.(*ast.GenDecl)
+ if !ok {
+ continue
+ }
+ if genDecl.Tok == token.IMPORT {
+ importDecl = genDecl
+ break
+ }
+ }
+ if importDecl == nil {
+ file.Decls = append([]ast.Decl{
+ &ast.GenDecl{
+ Tok: token.IMPORT,
+ Specs: []ast.Spec{importSpec},
+ },
+ }, file.Decls...)
+ return nil
+ }
+ importDecl.Specs = append(importDecl.Specs, importSpec)
+ 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/server/utils/ast/plugin_initialize_v2_test.go b/server/utils/ast/plugin_initialize_v2_test.go
new file mode 100644
index 0000000..55dc0c9
--- /dev/null
+++ b/server/utils/ast/plugin_initialize_v2_test.go
@@ -0,0 +1,101 @@
+package ast
+
+import (
+ "path/filepath"
+ "testing"
+
+ "git.echol.cn/loser/st/server/global"
+)
+
+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, "plugin", "gva", "plugin.go"),
+ PluginPath: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "register.go"),
+ ImportPath: `"git.echol.cn/loser/st/server/plugin/gva"`,
+ },
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ a := PluginInitializeV2{
+ Type: tt.fields.Type,
+ Path: tt.fields.Path,
+ PluginPath: tt.fields.PluginPath,
+ ImportPath: tt.fields.ImportPath,
+ }
+ file, err := a.Parse("", nil)
+ if err != nil {
+ t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ a.Injection(file)
+ err = a.Format("", 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, "plugin", "gva", "plugin.go"),
+ PluginPath: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "register.go"),
+ ImportPath: `"git.echol.cn/loser/st/server/plugin/gva"`,
+ },
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ a := PluginInitializeV2{
+ Type: tt.fields.Type,
+ Path: tt.fields.Path,
+ PluginPath: tt.fields.PluginPath,
+ ImportPath: tt.fields.ImportPath,
+ StructName: "Plugin",
+ PackageName: "gva",
+ }
+ file, err := a.Parse("", nil)
+ if err != nil {
+ t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ a.Rollback(file)
+ err = a.Format("", nil, file)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
diff --git a/server/utils/autocode/template_funcs.go b/server/utils/autocode/template_funcs.go
new file mode 100644
index 0000000..817918b
--- /dev/null
+++ b/server/utils/autocode/template_funcs.go
@@ -0,0 +1,714 @@
+package autocode
+
+import (
+ "fmt"
+ "slices"
+ "strings"
+ "text/template"
+
+ systemReq "git.echol.cn/loser/st/server/model/system/request"
+)
+
+// GetTemplateFuncMap 返回模板函数映射,用于在模板中使用
+func GetTemplateFuncMap() template.FuncMap {
+ return template.FuncMap{
+ "title": strings.Title,
+ "GenerateField": GenerateField,
+ "GenerateSearchField": GenerateSearchField,
+ "GenerateSearchConditions": GenerateSearchConditions,
+ "GenerateSearchFormItem": GenerateSearchFormItem,
+ "GenerateTableColumn": GenerateTableColumn,
+ "GenerateFormItem": GenerateFormItem,
+ "GenerateDescriptionItem": GenerateDescriptionItem,
+ "GenerateDefaultFormValue": GenerateDefaultFormValue,
+ }
+}
+
+// 渲染Model中的字段
+func GenerateField(field systemReq.AutoCodeField) string {
+ // 构建gorm标签
+ gormTag := ``
+
+ if field.FieldIndexType != "" {
+ gormTag += field.FieldIndexType + ";"
+ }
+
+ if field.PrimaryKey {
+ gormTag += "primarykey;"
+ }
+
+ if field.DefaultValue != "" {
+ gormTag += fmt.Sprintf("default:%s;", field.DefaultValue)
+ }
+
+ if field.Comment != "" {
+ gormTag += fmt.Sprintf("comment:%s;", field.Comment)
+ }
+
+ gormTag += "column:" + field.ColumnName + ";"
+
+ // 对于int类型,根据DataTypeLong决定具体的Go类型,不使用size标签
+ if field.DataTypeLong != "" && field.FieldType != "enum" && field.FieldType != "int" {
+ gormTag += fmt.Sprintf("size:%s;", field.DataTypeLong)
+ }
+
+ requireTag := ` binding:"required"` + "`"
+
+ // 根据字段类型构建不同的字段定义
+ var result string
+ switch field.FieldType {
+ case "enum":
+ result = fmt.Sprintf(`%s string `+"`"+`json:"%s" form:"%s" gorm:"%stype:enum(%s);"`+"`",
+ field.FieldName, field.FieldJson, field.FieldJson, gormTag, field.DataTypeLong)
+ case "picture", "video":
+ tagContent := fmt.Sprintf(`json:"%s" form:"%s" gorm:"%s"`,
+ field.FieldJson, field.FieldJson, gormTag)
+
+ result = fmt.Sprintf(`%s string `+"`"+`%s`+"`"+``, field.FieldName, tagContent)
+ case "file", "pictures", "array":
+ tagContent := fmt.Sprintf(`json:"%s" form:"%s" gorm:"%s"`,
+ field.FieldJson, field.FieldJson, gormTag)
+
+ result = fmt.Sprintf(`%s datatypes.JSON `+"`"+`%s swaggertype:"array,object"`+"`"+``,
+ field.FieldName, tagContent)
+ case "richtext":
+ tagContent := fmt.Sprintf(`json:"%s" form:"%s" gorm:"%s`,
+ field.FieldJson, field.FieldJson, gormTag)
+
+ result = fmt.Sprintf(`%s *string `+"`"+`%stype:text;"`+"`"+``,
+ field.FieldName, tagContent)
+ case "json":
+ tagContent := fmt.Sprintf(`json:"%s" form:"%s" gorm:"%s"`,
+ field.FieldJson, field.FieldJson, gormTag)
+
+ result = fmt.Sprintf(`%s datatypes.JSON `+"`"+`%s swaggertype:"object"`+"`"+``,
+ field.FieldName, tagContent)
+ default:
+ tagContent := fmt.Sprintf(`json:"%s" form:"%s" gorm:"%s"`,
+ field.FieldJson, field.FieldJson, gormTag)
+
+ // 对于int类型,根据DataTypeLong决定具体的Go类型
+ var fieldType string
+ if field.FieldType == "int" {
+ switch field.DataTypeLong {
+ case "1", "2", "3":
+ fieldType = "int8"
+ case "4", "5":
+ fieldType = "int16"
+ case "6", "7", "8", "9", "10":
+ fieldType = "int32"
+ case "11", "12", "13", "14", "15", "16", "17", "18", "19", "20":
+ fieldType = "int64"
+ default:
+ fieldType = "int64"
+ }
+ } else {
+ fieldType = field.FieldType
+ }
+
+ result = fmt.Sprintf(`%s *%s `+"`"+`%s`+"`"+``,
+ field.FieldName, fieldType, tagContent)
+ }
+
+ if field.Require {
+ result = result[0:len(result)-1] + requireTag
+ }
+
+ // 添加字段描述
+ if field.FieldDesc != "" {
+ result += fmt.Sprintf(" //%s", field.FieldDesc)
+ }
+
+ return result
+}
+
+// 格式化搜索条件语句
+func GenerateSearchConditions(fields []*systemReq.AutoCodeField) string {
+ var conditions []string
+
+ for _, field := range fields {
+ if field.FieldSearchType == "" {
+ continue
+ }
+
+ var condition string
+
+ if slices.Contains([]string{"enum", "pictures", "picture", "video", "json", "richtext", "array"}, field.FieldType) {
+ if field.FieldType == "enum" {
+ if field.FieldSearchType == "LIKE" {
+ condition = fmt.Sprintf(`
+ if info.%s != "" {
+ db = db.Where("%s LIKE ?", "%%"+ info.%s+"%%")
+ }`,
+ field.FieldName, field.ColumnName, field.FieldName)
+ } else {
+ condition = fmt.Sprintf(`
+ if info.%s != "" {
+ db = db.Where("%s %s ?", info.%s)
+ }`,
+ field.FieldName, field.ColumnName, field.FieldSearchType, field.FieldName)
+ }
+ } else {
+ condition = fmt.Sprintf(`
+ if info.%s != "" {
+ // TODO 数据类型为复杂类型,请根据业务需求自行实现复杂类型的查询业务
+ }`, field.FieldName)
+ }
+
+ } else if field.FieldSearchType == "BETWEEN" || field.FieldSearchType == "NOT BETWEEN" {
+ if field.FieldType == "time.Time" {
+ condition = fmt.Sprintf(`
+ if len(info.%sRange) == 2 {
+ db = db.Where("%s %s ? AND ? ", info.%sRange[0], info.%sRange[1])
+ }`,
+ field.FieldName, field.ColumnName, field.FieldSearchType, field.FieldName, field.FieldName)
+ } else {
+ condition = fmt.Sprintf(`
+ if info.Start%s != nil && info.End%s != nil {
+ db = db.Where("%s %s ? AND ? ", *info.Start%s, *info.End%s)
+ }`,
+ field.FieldName, field.FieldName, field.ColumnName,
+ field.FieldSearchType, field.FieldName, field.FieldName)
+ }
+ } else {
+ nullCheck := "info." + field.FieldName + " != nil"
+ if field.FieldType == "string" {
+ condition = fmt.Sprintf(`
+ if %s && *info.%s != "" {`, nullCheck, field.FieldName)
+ } else {
+ condition = fmt.Sprintf(`
+ if %s {`, nullCheck)
+ }
+
+ if field.FieldSearchType == "LIKE" {
+ condition += fmt.Sprintf(`
+ db = db.Where("%s LIKE ?", "%%"+ *info.%s+"%%")
+ }`,
+ field.ColumnName, field.FieldName)
+ } else {
+ condition += fmt.Sprintf(`
+ db = db.Where("%s %s ?", *info.%s)
+ }`,
+ field.ColumnName, field.FieldSearchType, field.FieldName)
+ }
+ }
+
+ conditions = append(conditions, condition)
+ }
+
+ return strings.Join(conditions, "")
+}
+
+// 格式化前端搜索条件
+func GenerateSearchFormItem(field systemReq.AutoCodeField) string {
+ // 开始构建表单项
+ result := fmt.Sprintf(`
+`, field.FieldDesc, field.FieldJson)
+
+ // 根据字段属性生成不同的输入类型
+ if field.FieldType == "bool" {
+ result += fmt.Sprintf(`
+`, field.FieldJson)
+ result += `
+`
+ result += `
+`
+ result += `
+`
+ } else if field.DictType != "" {
+ multipleAttr := ""
+ if field.FieldType == "array" {
+ multipleAttr = "multiple "
+ }
+ result += fmt.Sprintf(`
+`,
+ field.FieldJson, field.FieldDesc, field.DictType, field.Clearable, multipleAttr)
+ } else if field.CheckDataSource {
+ multipleAttr := ""
+ if field.DataSource.Association == 2 {
+ multipleAttr = "multiple "
+ }
+ result += fmt.Sprintf(`
+`,
+ multipleAttr, field.FieldJson, field.FieldDesc, field.Clearable)
+ result += fmt.Sprintf(`
+`,
+ field.FieldJson)
+ result += `
+`
+ } else if field.FieldType == "float64" || field.FieldType == "int" {
+ if field.FieldSearchType == "BETWEEN" || field.FieldSearchType == "NOT BETWEEN" {
+ result += fmt.Sprintf(`
+`, field.FieldName)
+ result += ` —
+`
+ result += fmt.Sprintf(`
+`, field.FieldName)
+ } else {
+ result += fmt.Sprintf(`
+`, field.FieldJson)
+ }
+ } else if field.FieldType == "time.Time" {
+ if field.FieldSearchType == "BETWEEN" || field.FieldSearchType == "NOT BETWEEN" {
+ result += `
+`
+ result += `
+`
+ result += fmt.Sprintf(` %s
+`, field.FieldDesc)
+ result += `
+`
+ result += `
+`
+ result += `
+`
+ result += `
+`
+ result += `
+`
+ result += fmt.Sprintf(``, field.FieldJson)
+ } else {
+ result += fmt.Sprintf(``, field.FieldJson)
+ }
+ } else {
+ result += fmt.Sprintf(`
+`, field.FieldJson)
+ }
+
+ // 关闭表单项
+ result += ``
+
+ return result
+}
+
+// GenerateTableColumn generates HTML for table column based on field properties
+func GenerateTableColumn(field systemReq.AutoCodeField) string {
+ // Add sortable attribute if needed
+ sortAttr := ""
+ if field.Sort {
+ sortAttr = " sortable"
+ }
+
+ // Handle different field types
+ if field.CheckDataSource {
+ result := fmt.Sprintf(`
+`,
+ sortAttr, field.FieldDesc, field.FieldJson)
+ result += `
+`
+
+ if field.DataSource.Association == 2 {
+ result += fmt.Sprintf(`
+`,
+ field.FieldJson, field.FieldJson)
+ result += ` {{ item }}
+`
+ result += `
+`
+ } else {
+ result += fmt.Sprintf(` {{ filterDataSource(dataSource.%s,scope.row.%s) }}
+`,
+ field.FieldJson, field.FieldJson)
+ }
+
+ result += `
+`
+ result += ``
+ return result
+ } else if field.DictType != "" {
+ result := fmt.Sprintf(`
+`,
+ sortAttr, field.FieldDesc, field.FieldJson)
+ result += `
+`
+
+ if field.FieldType == "array" {
+ result += fmt.Sprintf(` {{ filterDict(item,%sOptions) }}
+`,
+ field.FieldJson, field.DictType)
+ } else {
+ result += fmt.Sprintf(` {{ filterDict(scope.row.%s,%sOptions) }}
+`,
+ field.FieldJson, field.DictType)
+ }
+
+ result += `
+`
+ result += ``
+ return result
+ } else if field.FieldType == "bool" {
+ result := fmt.Sprintf(`
+`,
+ sortAttr, field.FieldDesc, field.FieldJson)
+ result += fmt.Sprintf(` {{ formatBoolean(scope.row.%s) }}
+`, field.FieldJson)
+ result += ``
+ return result
+ } else if field.FieldType == "time.Time" {
+ result := fmt.Sprintf(`
+`,
+ sortAttr, field.FieldDesc, field.FieldJson)
+ result += fmt.Sprintf(` {{ formatDate(scope.row.%s) }}
+`, field.FieldJson)
+ result += ``
+ return result
+ } else if field.FieldType == "picture" {
+ result := fmt.Sprintf(`
+`, field.FieldDesc, field.FieldJson)
+ result += `
+`
+ result += fmt.Sprintf(`
+`, field.FieldJson)
+ result += `
+`
+ result += ``
+ return result
+ } else if field.FieldType == "pictures" {
+ result := fmt.Sprintf(`
+`, field.FieldDesc, field.FieldJson)
+ result += `
+`
+ result += `
+`
+ result += fmt.Sprintf(`
+`, field.FieldJson)
+ result += `
+`
+ result += `
+`
+ result += ``
+ return result
+ } else if field.FieldType == "video" {
+ result := fmt.Sprintf(`
+`, field.FieldDesc, field.FieldJson)
+ result += `
+`
+ result += `
+`
+ result += `
+`
+ result += ``
+ return result
+ } else if field.FieldType == "richtext" {
+ result := fmt.Sprintf(`
+`, field.FieldDesc, field.FieldJson)
+ result += `
+`
+ result += ` [富文本内容]
+`
+ result += `
+`
+ result += ``
+ return result
+ } else if field.FieldType == "file" {
+ result := fmt.Sprintf(`
+`, field.FieldDesc, field.FieldJson)
+ result += `
+`
+ result += `
+`
+ result += fmt.Sprintf(` {{ file.name }}
+`, field.FieldJson)
+ result += `
+`
+ result += `
+`
+ result += ``
+ return result
+ } else if field.FieldType == "json" {
+ result := fmt.Sprintf(`
+`, field.FieldDesc, field.FieldJson)
+ result += `
+`
+ result += ` [JSON]
+`
+ result += `
+`
+ result += ``
+ return result
+ } else if field.FieldType == "array" {
+ result := fmt.Sprintf(`
+`, field.FieldDesc, field.FieldJson)
+ result += `
+`
+ result += fmt.Sprintf(`
+`, field.FieldJson)
+ result += `
+`
+ result += ``
+ return result
+ } else {
+ return fmt.Sprintf(`
+`,
+ sortAttr, field.FieldDesc, field.FieldJson)
+ }
+}
+
+func GenerateFormItem(field systemReq.AutoCodeField) string {
+ // 开始构建表单项
+ result := fmt.Sprintf(`
+`, field.FieldDesc, field.FieldJson)
+
+ // 处理不同字段类型
+ if field.CheckDataSource {
+ multipleAttr := ""
+ if field.DataSource.Association == 2 {
+ multipleAttr = " multiple"
+ }
+ result += fmt.Sprintf(`
+`,
+ multipleAttr, field.FieldJson, field.FieldDesc, field.Clearable)
+ result += fmt.Sprintf(`
+`,
+ field.FieldJson)
+ result += `
+`
+ } else {
+ switch field.FieldType {
+ case "bool":
+ result += fmt.Sprintf(`
+`,
+ field.FieldJson)
+
+ case "string":
+ if field.DictType != "" {
+ result += fmt.Sprintf(`
+`,
+ field.FieldJson, field.FieldDesc, field.DictType, field.Clearable)
+ } else {
+ result += fmt.Sprintf(`
+`,
+ field.FieldJson, field.Clearable, field.FieldDesc)
+ }
+
+ case "richtext":
+ result += fmt.Sprintf(`
+`, field.FieldJson)
+
+ case "json":
+ result += fmt.Sprintf(` // 此字段为json结构,可以前端自行控制展示和数据绑定模式 需绑定json的key为 formData.%s 后端会按照json的类型进行存取
+`, field.FieldJson)
+ result += fmt.Sprintf(` {{ formData.%s }}
+`, field.FieldJson)
+
+ case "array":
+ if field.DictType != "" {
+ result += fmt.Sprintf(`
+`,
+ field.FieldJson, field.FieldDesc, field.Clearable)
+ result += fmt.Sprintf(`
+`,
+ field.DictType)
+ result += `
+`
+ } else {
+ result += fmt.Sprintf(`
+`, field.FieldJson)
+ }
+
+ case "int":
+ result += fmt.Sprintf(`
+`,
+ field.FieldJson, field.Clearable, field.FieldDesc)
+
+ case "time.Time":
+ result += fmt.Sprintf(`
+`,
+ field.FieldJson, field.Clearable)
+
+ case "float64":
+ result += fmt.Sprintf(`
+`,
+ field.FieldJson, field.Clearable)
+
+ case "enum":
+ result += fmt.Sprintf(`
+`,
+ field.FieldJson, field.FieldDesc, field.Clearable)
+ result += fmt.Sprintf(`
+`,
+ field.DataTypeLong)
+ result += `
+`
+
+ case "picture":
+ result += fmt.Sprintf(`
+`, field.FieldJson)
+
+ case "pictures":
+ result += fmt.Sprintf(`
+`, field.FieldJson)
+
+ case "video":
+ result += fmt.Sprintf(`
+`, field.FieldJson)
+
+ case "file":
+ result += fmt.Sprintf(`
+`, field.FieldJson)
+ }
+ }
+
+ // 关闭表单项
+ result += ``
+
+ return result
+}
+
+func GenerateDescriptionItem(field systemReq.AutoCodeField) string {
+ // 开始构建描述项
+ result := fmt.Sprintf(`
+`, field.FieldDesc)
+
+ if field.CheckDataSource {
+ result += `
+`
+ if field.DataSource.Association == 2 {
+ result += fmt.Sprintf(`
+`,
+ field.FieldJson, field.FieldJson)
+ result += ` {{ item }}
+`
+ result += `
+`
+ } else {
+ result += fmt.Sprintf(` {{ filterDataSource(dataSource.%s,detailForm.%s) }}
+`,
+ field.FieldJson, field.FieldJson)
+ }
+ result += `
+`
+ } else if field.FieldType != "picture" && field.FieldType != "pictures" &&
+ field.FieldType != "file" && field.FieldType != "array" &&
+ field.FieldType != "richtext" {
+ result += fmt.Sprintf(` {{ detailForm.%s }}
+`, field.FieldJson)
+ } else {
+ switch field.FieldType {
+ case "picture":
+ result += fmt.Sprintf(`
+`,
+ field.FieldJson, field.FieldJson)
+ case "array":
+ result += fmt.Sprintf(`
+`, field.FieldJson)
+ case "pictures":
+ result += fmt.Sprintf(`
+`,
+ field.FieldJson, field.FieldJson)
+ case "richtext":
+ result += fmt.Sprintf(`
+`, field.FieldJson)
+ case "file":
+ result += fmt.Sprintf(`
+`, field.FieldJson)
+ result += `
+`
+ result += `
+`
+ result += ` {{ item.name }}
+`
+ result += `
+`
+ result += `
+`
+ }
+ }
+
+ // 关闭描述项
+ result += ``
+
+ return result
+}
+
+func GenerateDefaultFormValue(field systemReq.AutoCodeField) string {
+ // 根据字段类型确定默认值
+ var defaultValue string
+
+ switch field.FieldType {
+ case "bool":
+ defaultValue = "false"
+ case "string", "richtext":
+ defaultValue = "''"
+ case "int":
+ if field.DataSource != nil { // 检查数据源是否存在
+ defaultValue = "undefined"
+ } else {
+ defaultValue = "0"
+ }
+ case "time.Time":
+ defaultValue = "new Date()"
+ case "float64":
+ defaultValue = "0"
+ case "picture", "video":
+ defaultValue = "\"\""
+ case "pictures", "file", "array":
+ defaultValue = "[]"
+ case "json":
+ defaultValue = "{}"
+ default:
+ defaultValue = "null"
+ }
+
+ // 返回格式化后的默认值字符串
+ return fmt.Sprintf(`%s: %s,`, field.FieldJson, defaultValue)
+}
+
+// GenerateSearchField 根据字段属性生成搜索结构体中的字段定义
+func GenerateSearchField(field systemReq.AutoCodeField) string {
+ var result string
+
+ if field.FieldSearchType == "" {
+ return "" // 如果没有搜索类型,返回空字符串
+ }
+
+ if field.FieldSearchType == "BETWEEN" || field.FieldSearchType == "NOT BETWEEN" {
+ // 生成范围搜索字段
+ // time 的情况
+ if field.FieldType == "time.Time" {
+ result = fmt.Sprintf("%sRange []time.Time `json:\"%sRange\" form:\"%sRange[]\"`",
+ field.FieldName, field.FieldJson, field.FieldJson)
+ } else {
+ startField := fmt.Sprintf("Start%s *%s `json:\"start%s\" form:\"start%s\"`",
+ field.FieldName, field.FieldType, field.FieldName, field.FieldName)
+ endField := fmt.Sprintf("End%s *%s `json:\"end%s\" form:\"end%s\"`",
+ field.FieldName, field.FieldType, field.FieldName, field.FieldName)
+ result = startField + "\n" + endField
+ }
+ } else {
+ // 生成普通搜索字段
+ if field.FieldType == "enum" || field.FieldType == "picture" ||
+ field.FieldType == "pictures" || field.FieldType == "video" ||
+ field.FieldType == "json" || field.FieldType == "richtext" || field.FieldType == "array" || field.FieldType == "file" {
+ result = fmt.Sprintf("%s string `json:\"%s\" form:\"%s\"` ",
+ field.FieldName, field.FieldJson, field.FieldJson)
+ } else {
+ result = fmt.Sprintf("%s *%s `json:\"%s\" form:\"%s\"` ",
+ field.FieldName, field.FieldType, field.FieldJson, field.FieldJson)
+ }
+ }
+
+ return result
+}
diff --git a/server/utils/breakpoint_continue.go b/server/utils/breakpoint_continue.go
new file mode 100644
index 0000000..66368e2
--- /dev/null
+++ b/server/utils/breakpoint_continue.go
@@ -0,0 +1,121 @@
+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) {
+ if strings.Contains(fileName, "..") || strings.Contains(fileMd5, "..") {
+ return "", errors.New("文件名或路径不合法")
+ }
+ 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
+ }
+ defer f.Close()
+ _, err = f.Write(content)
+ if err != nil {
+ return path, err
+ }
+
+ 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) {
+ if strings.Contains(fileName, "..") || strings.Contains(FileMd5, "..") {
+ return "", errors.New("文件名或路径不合法")
+ }
+ 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 {
+ if strings.Contains(FileMd5, "..") {
+ return errors.New("路径不合法")
+ }
+ err := os.RemoveAll(breakpointDir + FileMd5)
+ return err
+}
diff --git a/server/utils/captcha/redis.go b/server/utils/captcha/redis.go
new file mode 100644
index 0000000..52fa2b4
--- /dev/null
+++ b/server/utils/captcha/redis.go
@@ -0,0 +1,61 @@
+package captcha
+
+import (
+ "context"
+ "time"
+
+ "git.echol.cn/loser/st/server/global"
+ "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) *RedisStore {
+ if ctx == nil {
+ 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/server/utils/casbin_util.go b/server/utils/casbin_util.go
new file mode 100644
index 0000000..18de021
--- /dev/null
+++ b/server/utils/casbin_util.go
@@ -0,0 +1,52 @@
+package utils
+
+import (
+ "sync"
+
+ "git.echol.cn/loser/st/server/global"
+ "github.com/casbin/casbin/v2"
+ "github.com/casbin/casbin/v2/model"
+ gormadapter "github.com/casbin/gorm-adapter/v3"
+ "go.uber.org/zap"
+)
+
+var (
+ syncedCachedEnforcer *casbin.SyncedCachedEnforcer
+ once sync.Once
+)
+
+// GetCasbin 获取casbin实例
+func GetCasbin() *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/server/utils/character_card.go b/server/utils/character_card.go
new file mode 100644
index 0000000..c124773
--- /dev/null
+++ b/server/utils/character_card.go
@@ -0,0 +1,285 @@
+package utils
+
+import (
+ "bytes"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "image"
+ "image/png"
+ "io"
+)
+
+// CharacterCardV2 SillyTavern 角色卡 V2 格式
+type CharacterCardV2 struct {
+ Spec string `json:"spec"`
+ SpecVersion string `json:"spec_version"`
+ Data CharacterCardV2Data `json:"data"`
+}
+
+type CharacterCardV2Data struct {
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Personality string `json:"personality"`
+ Scenario string `json:"scenario"`
+ FirstMes string `json:"first_mes"`
+ MesExample string `json:"mes_example"`
+ CreatorNotes string `json:"creator_notes"`
+ SystemPrompt string `json:"system_prompt"`
+ PostHistoryInstructions string `json:"post_history_instructions"`
+ Tags []string `json:"tags"`
+ Creator string `json:"creator"`
+ CharacterVersion string `json:"character_version"`
+ AlternateGreetings []string `json:"alternate_greetings"`
+ CharacterBook map[string]interface{} `json:"character_book,omitempty"`
+ Extensions map[string]interface{} `json:"extensions"`
+}
+
+// ExtractCharacterFromPNG 从 PNG 图片中提取角色卡数据
+func ExtractCharacterFromPNG(pngData []byte) (*CharacterCardV2, error) {
+ reader := bytes.NewReader(pngData)
+
+ // 验证 PNG 格式(解码但不保存图片)
+ _, err := png.Decode(reader)
+ if err != nil {
+ return nil, errors.New("无效的 PNG 文件")
+ }
+
+ // 重新读取以获取 tEXt chunks
+ reader.Seek(0, 0)
+
+ // 查找 tEXt chunk 中的 "chara" 字段
+ charaJSON, err := extractTextChunk(reader, "chara")
+ if err != nil {
+ return nil, errors.New("PNG 中没有找到角色卡数据")
+ }
+
+ // 尝试 Base64 解码
+ decodedJSON, err := base64.StdEncoding.DecodeString(charaJSON)
+ if err != nil {
+ // 如果不是 Base64,直接使用原始 JSON
+ decodedJSON = []byte(charaJSON)
+ }
+
+ // 解析 JSON
+ var card CharacterCardV2
+ err = json.Unmarshal(decodedJSON, &card)
+ if err != nil {
+ return nil, errors.New("解析角色卡数据失败: " + err.Error())
+ }
+
+ return &card, nil
+}
+
+// extractTextChunk 从 PNG 中提取指定 key 的 tEXt chunk
+func extractTextChunk(r io.Reader, key string) (string, error) {
+ // 跳过 PNG signature (8 bytes)
+ signature := make([]byte, 8)
+ if _, err := io.ReadFull(r, signature); err != nil {
+ return "", err
+ }
+
+ // 验证 PNG signature
+ expectedSig := []byte{137, 80, 78, 71, 13, 10, 26, 10}
+ if !bytes.Equal(signature, expectedSig) {
+ return "", errors.New("invalid PNG signature")
+ }
+
+ // 读取所有 chunks
+ for {
+ // 读取 chunk length (4 bytes)
+ lengthBytes := make([]byte, 4)
+ if _, err := io.ReadFull(r, lengthBytes); err != nil {
+ if err == io.EOF {
+ break
+ }
+ return "", err
+ }
+ length := uint32(lengthBytes[0])<<24 | uint32(lengthBytes[1])<<16 |
+ uint32(lengthBytes[2])<<8 | uint32(lengthBytes[3])
+
+ // 读取 chunk type (4 bytes)
+ chunkType := make([]byte, 4)
+ if _, err := io.ReadFull(r, chunkType); err != nil {
+ return "", err
+ }
+
+ // 读取 chunk data
+ data := make([]byte, length)
+ if _, err := io.ReadFull(r, data); err != nil {
+ return "", err
+ }
+
+ // 读取 CRC (4 bytes)
+ crc := make([]byte, 4)
+ if _, err := io.ReadFull(r, crc); err != nil {
+ return "", err
+ }
+
+ // 检查是否是 tEXt chunk
+ if string(chunkType) == "tEXt" {
+ // tEXt chunk 格式: keyword\0text
+ nullIndex := bytes.IndexByte(data, 0)
+ if nullIndex == -1 {
+ continue
+ }
+
+ keyword := string(data[:nullIndex])
+ text := string(data[nullIndex+1:])
+
+ if keyword == key {
+ return text, nil
+ }
+ }
+
+ // IEND chunk 表示结束
+ if string(chunkType) == "IEND" {
+ break
+ }
+ }
+
+ return "", errors.New("text chunk not found")
+}
+
+// EmbedCharacterToPNG 将角色卡数据嵌入到 PNG 图片中
+func EmbedCharacterToPNG(img image.Image, card *CharacterCardV2) ([]byte, error) {
+ // 序列化角色卡数据
+ cardJSON, err := json.Marshal(card)
+ if err != nil {
+ return nil, err
+ }
+
+ // Base64 编码
+ encodedJSON := base64.StdEncoding.EncodeToString(cardJSON)
+
+ // 创建一个 buffer 来写入 PNG
+ var buf bytes.Buffer
+
+ // 写入 PNG signature
+ buf.Write([]byte{137, 80, 78, 71, 13, 10, 26, 10})
+
+ // 编码原始图片到临时 buffer
+ var imgBuf bytes.Buffer
+ if err := png.Encode(&imgBuf, img); err != nil {
+ return nil, err
+ }
+
+ // 跳过原始 PNG 的 signature
+ imgData := imgBuf.Bytes()[8:]
+
+ // 将原始图片的 chunks 复制到输出,在 IEND 之前插入 tEXt chunk
+ r := bytes.NewReader(imgData)
+
+ for {
+ // 读取 chunk length
+ lengthBytes := make([]byte, 4)
+ if _, err := io.ReadFull(r, lengthBytes); err != nil {
+ if err == io.EOF {
+ break
+ }
+ return nil, err
+ }
+ length := uint32(lengthBytes[0])<<24 | uint32(lengthBytes[1])<<16 |
+ uint32(lengthBytes[2])<<8 | uint32(lengthBytes[3])
+
+ // 读取 chunk type
+ chunkType := make([]byte, 4)
+ if _, err := io.ReadFull(r, chunkType); err != nil {
+ return nil, err
+ }
+
+ // 读取 chunk data
+ data := make([]byte, length)
+ if _, err := io.ReadFull(r, data); err != nil {
+ return nil, err
+ }
+
+ // 读取 CRC
+ crc := make([]byte, 4)
+ if _, err := io.ReadFull(r, crc); err != nil {
+ return nil, err
+ }
+
+ // 如果是 IEND chunk,先写入 tEXt chunk
+ if string(chunkType) == "IEND" {
+ // 写入 tEXt chunk
+ writeTextChunk(&buf, "chara", encodedJSON)
+ }
+
+ // 写入原始 chunk
+ buf.Write(lengthBytes)
+ buf.Write(chunkType)
+ buf.Write(data)
+ buf.Write(crc)
+
+ if string(chunkType) == "IEND" {
+ break
+ }
+ }
+
+ return buf.Bytes(), nil
+}
+
+// writeTextChunk 写入 tEXt chunk
+func writeTextChunk(w io.Writer, keyword, text string) error {
+ data := append([]byte(keyword), 0)
+ data = append(data, []byte(text)...)
+
+ // 写入 length
+ length := uint32(len(data))
+ lengthBytes := []byte{
+ byte(length >> 24),
+ byte(length >> 16),
+ byte(length >> 8),
+ byte(length),
+ }
+ w.Write(lengthBytes)
+
+ // 写入 type
+ w.Write([]byte("tEXt"))
+
+ // 写入 data
+ w.Write(data)
+
+ // 计算并写入 CRC
+ crcData := append([]byte("tEXt"), data...)
+ crc := calculateCRC(crcData)
+ crcBytes := []byte{
+ byte(crc >> 24),
+ byte(crc >> 16),
+ byte(crc >> 8),
+ byte(crc),
+ }
+ w.Write(crcBytes)
+
+ return nil
+}
+
+// calculateCRC 计算 CRC32
+func calculateCRC(data []byte) uint32 {
+ crc := uint32(0xFFFFFFFF)
+
+ for _, b := range data {
+ crc ^= uint32(b)
+ for i := 0; i < 8; i++ {
+ if crc&1 != 0 {
+ crc = (crc >> 1) ^ 0xEDB88320
+ } else {
+ crc >>= 1
+ }
+ }
+ }
+
+ return crc ^ 0xFFFFFFFF
+}
+
+// ParseCharacterCardJSON 解析 JSON 格式的角色卡
+func ParseCharacterCardJSON(jsonData []byte) (*CharacterCardV2, error) {
+ var card CharacterCardV2
+ err := json.Unmarshal(jsonData, &card)
+ if err != nil {
+ return nil, errors.New("解析角色卡 JSON 失败: " + err.Error())
+ }
+
+ return &card, nil
+}
diff --git a/server/utils/claims.go b/server/utils/claims.go
new file mode 100644
index 0000000..4962c17
--- /dev/null
+++ b/server/utils/claims.go
@@ -0,0 +1,148 @@
+package utils
+
+import (
+ "net"
+ "time"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/model/system"
+ systemReq "git.echol.cn/loser/st/server/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()))
+ }
+ 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/server/utils/directory.go b/server/utils/directory.go
new file mode 100644
index 0000000..95e4178
--- /dev/null
+++ b/server/utils/directory.go
@@ -0,0 +1,124 @@
+package utils
+
+import (
+ "errors"
+ "os"
+ "path/filepath"
+ "reflect"
+ "strings"
+
+ "git.echol.cn/loser/st/server/global"
+ "go.uber.org/zap"
+)
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@function: PathExists
+//@description: 文件目录是否存在
+//@param: path string
+//@return: bool, error
+
+func PathExists(path string) (bool, error) {
+ fi, err := os.Stat(path)
+ if err == nil {
+ if fi.IsDir() {
+ return true, nil
+ }
+ return false, errors.New("存在同名文件")
+ }
+ if os.IsNotExist(err) {
+ return false, nil
+ }
+ return false, err
+}
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@function: CreateDir
+//@description: 批量创建文件夹
+//@param: dirs ...string
+//@return: err error
+
+func CreateDir(dirs ...string) (err error) {
+ for _, v := range dirs {
+ exist, err := PathExists(v)
+ if err != nil {
+ return err
+ }
+ if !exist {
+ global.GVA_LOG.Debug("create directory" + v)
+ if err := os.MkdirAll(v, os.ModePerm); err != nil {
+ global.GVA_LOG.Error("create directory"+v, zap.Any(" error:", err))
+ return err
+ }
+ }
+ }
+ return err
+}
+
+//@author: [songzhibin97](https://github.com/songzhibin97)
+//@function: FileMove
+//@description: 文件移动供外部调用
+//@param: src string, dst string(src: 源位置,绝对路径or相对路径, dst: 目标位置,绝对路径or相对路径,必须为文件夹)
+//@return: err error
+
+func FileMove(src string, dst string) (err error) {
+ if dst == "" {
+ return nil
+ }
+ src, err = filepath.Abs(src)
+ if err != nil {
+ return err
+ }
+ dst, err = filepath.Abs(dst)
+ if err != nil {
+ return err
+ }
+ revoke := false
+ dir := filepath.Dir(dst)
+Redirect:
+ _, err = os.Stat(dir)
+ if err != nil {
+ err = os.MkdirAll(dir, 0o755)
+ if err != nil {
+ return err
+ }
+ if !revoke {
+ revoke = true
+ goto Redirect
+ }
+ }
+ return os.Rename(src, dst)
+}
+
+func DeLFile(filePath string) error {
+ return os.RemoveAll(filePath)
+}
+
+//@author: [songzhibin97](https://github.com/songzhibin97)
+//@function: TrimSpace
+//@description: 去除结构体空格
+//@param: target interface (target: 目标结构体,传入必须是指针类型)
+//@return: null
+
+func TrimSpace(target interface{}) {
+ t := reflect.TypeOf(target)
+ if t.Kind() != reflect.Ptr {
+ return
+ }
+ t = t.Elem()
+ v := reflect.ValueOf(target).Elem()
+ for i := 0; i < t.NumField(); i++ {
+ switch v.Field(i).Kind() {
+ case reflect.String:
+ v.Field(i).SetString(strings.TrimSpace(v.Field(i).String()))
+ }
+ }
+}
+
+// FileExist 判断文件是否存在
+func FileExist(path string) bool {
+ fi, err := os.Lstat(path)
+ if err == nil {
+ return !fi.IsDir()
+ }
+ return !os.IsNotExist(err)
+}
diff --git a/server/utils/fmt_plus.go b/server/utils/fmt_plus.go
new file mode 100644
index 0000000..8ff29cd
--- /dev/null
+++ b/server/utils/fmt_plus.go
@@ -0,0 +1,126 @@
+package utils
+
+import (
+ "fmt"
+ "math/rand"
+ "reflect"
+ "strings"
+
+ "git.echol.cn/loser/st/server/model/common"
+)
+
+//@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, "")
+}
+
+// HumpToUnderscore 将驼峰命名转换为下划线分割模式
+func HumpToUnderscore(s string) string {
+ var result strings.Builder
+
+ for i, char := range s {
+ if i > 0 && char >= 'A' && char <= 'Z' {
+ // 在大写字母前添加下划线
+ result.WriteRune('_')
+ result.WriteRune(char - 'A' + 'a') // 转小写
+ } else {
+ result.WriteRune(char)
+ }
+ }
+
+ return strings.ToLower(result.String())
+}
+
+// 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/server/utils/hash.go b/server/utils/hash.go
new file mode 100644
index 0000000..e7f23aa
--- /dev/null
+++ b/server/utils/hash.go
@@ -0,0 +1,32 @@
+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/server/utils/human_duration.go b/server/utils/human_duration.go
new file mode 100644
index 0000000..0cdb055
--- /dev/null
+++ b/server/utils/human_duration.go
@@ -0,0 +1,29 @@
+package utils
+
+import (
+ "strconv"
+ "strings"
+ "time"
+)
+
+func ParseDuration(d string) (time.Duration, error) {
+ d = strings.TrimSpace(d)
+ dr, err := time.ParseDuration(d)
+ if err == nil {
+ return dr, nil
+ }
+ if strings.Contains(d, "d") {
+ index := strings.Index(d, "d")
+
+ hour, _ := strconv.Atoi(d[:index])
+ dr = time.Hour * 24 * time.Duration(hour)
+ ndr, err := time.ParseDuration(d[index+1:])
+ if err != nil {
+ return dr, nil
+ }
+ return dr + ndr, nil
+ }
+
+ dv, err := strconv.ParseInt(d, 10, 64)
+ return time.Duration(dv), err
+}
diff --git a/server/utils/human_duration_test.go b/server/utils/human_duration_test.go
new file mode 100644
index 0000000..8a5294b
--- /dev/null
+++ b/server/utils/human_duration_test.go
@@ -0,0 +1,49 @@
+package utils
+
+import (
+ "testing"
+ "time"
+)
+
+func TestParseDuration(t *testing.T) {
+ type args struct {
+ d string
+ }
+ tests := []struct {
+ name string
+ args args
+ want time.Duration
+ wantErr bool
+ }{
+ {
+ name: "5h20m",
+ args: args{"5h20m"},
+ want: time.Hour*5 + 20*time.Minute,
+ wantErr: false,
+ },
+ {
+ name: "1d5h20m",
+ args: args{"1d5h20m"},
+ want: 24*time.Hour + time.Hour*5 + 20*time.Minute,
+ wantErr: false,
+ },
+ {
+ name: "1d",
+ args: args{"1d"},
+ want: 24 * time.Hour,
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := ParseDuration(tt.args.d)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("ParseDuration() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if got != tt.want {
+ t.Errorf("ParseDuration() got = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/server/utils/json.go b/server/utils/json.go
new file mode 100644
index 0000000..8c4118c
--- /dev/null
+++ b/server/utils/json.go
@@ -0,0 +1,34 @@
+package utils
+
+import (
+ "encoding/json"
+ "strings"
+)
+
+func GetJSONKeys(jsonStr string) (keys []string, err error) {
+ // 使用json.Decoder,以便在解析过程中记录键的顺序
+ dec := json.NewDecoder(strings.NewReader(jsonStr))
+ t, err := dec.Token()
+ if err != nil {
+ return nil, err
+ }
+ // 确保数据是一个对象
+ if t != json.Delim('{') {
+ return nil, err
+ }
+ for dec.More() {
+ t, err = dec.Token()
+ if err != nil {
+ return nil, err
+ }
+ keys = append(keys, t.(string))
+
+ // 解析值
+ var value interface{}
+ err = dec.Decode(&value)
+ if err != nil {
+ return nil, err
+ }
+ }
+ return keys, nil
+}
diff --git a/server/utils/json_test.go b/server/utils/json_test.go
new file mode 100644
index 0000000..f21a679
--- /dev/null
+++ b/server/utils/json_test.go
@@ -0,0 +1,53 @@
+package utils
+
+import (
+ "fmt"
+ "testing"
+)
+
+func TestGetJSONKeys(t *testing.T) {
+ var jsonStr = `
+ {
+ "Name": "test",
+ "TableName": "test",
+ "TemplateID": "test",
+ "TemplateInfo": "test",
+ "Limit": 0
+}`
+ keys, err := GetJSONKeys(jsonStr)
+ if err != nil {
+ t.Errorf("GetJSONKeys failed" + err.Error())
+ return
+ }
+ if len(keys) != 5 {
+ t.Errorf("GetJSONKeys failed" + err.Error())
+ return
+ }
+ if keys[0] != "Name" {
+ t.Errorf("GetJSONKeys failed" + err.Error())
+
+ return
+ }
+ if keys[1] != "TableName" {
+ t.Errorf("GetJSONKeys failed" + err.Error())
+
+ return
+ }
+ if keys[2] != "TemplateID" {
+ t.Errorf("GetJSONKeys failed" + err.Error())
+
+ return
+ }
+ if keys[3] != "TemplateInfo" {
+ t.Errorf("GetJSONKeys failed" + err.Error())
+
+ return
+ }
+ if keys[4] != "Limit" {
+ t.Errorf("GetJSONKeys failed" + err.Error())
+
+ return
+ }
+
+ fmt.Println(keys)
+}
diff --git a/server/utils/jwt.go b/server/utils/jwt.go
new file mode 100644
index 0000000..effb72c
--- /dev/null
+++ b/server/utils/jwt.go
@@ -0,0 +1,105 @@
+package utils
+
+import (
+ "context"
+ "errors"
+ "time"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/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
+}
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@function: SetRedisJWT
+//@description: jwt存入redis并设置过期时间
+//@param: jwt string, userName string
+//@return: err error
+
+func SetRedisJWT(jwt string, userName string) (err error) {
+ // 此处过期时间等于jwt过期时间
+ dr, err := 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
+}
diff --git a/server/utils/plugin/plugin.go b/server/utils/plugin/plugin.go
new file mode 100644
index 0000000..a59d5b5
--- /dev/null
+++ b/server/utils/plugin/plugin.go
@@ -0,0 +1,18 @@
+package plugin
+
+import (
+ "github.com/gin-gonic/gin"
+)
+
+const (
+ OnlyFuncName = "Plugin"
+)
+
+// Plugin 插件模式接口化
+type Plugin interface {
+ // Register 注册路由
+ Register(group *gin.RouterGroup)
+
+ // RouterPath 用户返回注册路由
+ RouterPath() string
+}
diff --git a/server/utils/plugin/v2/plugin.go b/server/utils/plugin/v2/plugin.go
new file mode 100644
index 0000000..4dac0ab
--- /dev/null
+++ b/server/utils/plugin/v2/plugin.go
@@ -0,0 +1,11 @@
+package plugin
+
+import (
+ "github.com/gin-gonic/gin"
+)
+
+// Plugin 插件模式接口化v2
+type Plugin interface {
+ // Register 注册路由
+ Register(group *gin.Engine)
+}
diff --git a/server/utils/plugin/v2/registry.go b/server/utils/plugin/v2/registry.go
new file mode 100644
index 0000000..4ec5fce
--- /dev/null
+++ b/server/utils/plugin/v2/registry.go
@@ -0,0 +1,27 @@
+package plugin
+
+import "sync"
+
+var (
+ registryMu sync.RWMutex
+ registry []Plugin
+)
+
+// Register records a plugin for auto initialization.
+func Register(p Plugin) {
+ if p == nil {
+ return
+ }
+ registryMu.Lock()
+ registry = append(registry, p)
+ registryMu.Unlock()
+}
+
+// Registered returns a snapshot of all registered plugins.
+func Registered() []Plugin {
+ registryMu.RLock()
+ defer registryMu.RUnlock()
+ out := make([]Plugin, len(registry))
+ copy(out, registry)
+ return out
+}
diff --git a/server/utils/request/http.go b/server/utils/request/http.go
new file mode 100644
index 0000000..86d0d15
--- /dev/null
+++ b/server/utils/request/http.go
@@ -0,0 +1,62 @@
+package request
+
+import (
+ "bytes"
+ "encoding/json"
+ "net/http"
+ "net/url"
+)
+
+func HttpRequest(
+ urlStr string,
+ method string,
+ headers map[string]string,
+ params map[string]string,
+ data any) (*http.Response, error) {
+ // 创建URL
+ u, err := url.Parse(urlStr)
+ if err != nil {
+ return nil, err
+ }
+
+ // 添加查询参数
+ query := u.Query()
+ for k, v := range params {
+ query.Set(k, v)
+ }
+ u.RawQuery = query.Encode()
+
+ // 将数据编码为JSON
+ buf := new(bytes.Buffer)
+ if data != nil {
+ b, err := json.Marshal(data)
+ if err != nil {
+ return nil, err
+ }
+ buf = bytes.NewBuffer(b)
+ }
+
+ // 创建请求
+ req, err := http.NewRequest(method, u.String(), buf)
+
+ if err != nil {
+ return nil, err
+ }
+
+ for k, v := range headers {
+ req.Header.Set(k, v)
+ }
+
+ if data != nil {
+ req.Header.Set("Content-Type", "application/json")
+ }
+
+ // 发送请求
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ // 返回响应,让调用者处理
+ return resp, nil
+}
diff --git a/server/utils/server.go b/server/utils/server.go
new file mode 100644
index 0000000..4d84cb4
--- /dev/null
+++ b/server/utils/server.go
@@ -0,0 +1,127 @@
+package utils
+
+import (
+ "runtime"
+ "time"
+
+ "git.echol.cn/loser/st/server/global"
+
+ "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/server/utils/stacktrace/stacktrace.go b/server/utils/stacktrace/stacktrace.go
new file mode 100644
index 0000000..e9f4dbd
--- /dev/null
+++ b/server/utils/stacktrace/stacktrace.go
@@ -0,0 +1,79 @@
+package stacktrace
+
+import (
+ "regexp"
+ "strconv"
+ "strings"
+)
+
+// Frame 表示一次栈帧解析结果
+type Frame struct {
+ File string
+ Line int
+ Func string
+}
+
+var fileLineRe = regexp.MustCompile(`\s*(.+\.go):(\d+)\s*$`)
+
+// FindFinalCaller 从 zap 的 entry.Stack 文本中,解析“最终业务调用方”的文件与行号
+// 策略:自顶向下解析,优先选择第一条项目代码帧,过滤第三方库/标准库/框架中间件
+func FindFinalCaller(stack string) (Frame, bool) {
+ if stack == "" {
+ return Frame{}, false
+ }
+ lines := strings.Split(stack, "\n")
+ var currFunc string
+ for i := 0; i < len(lines); i++ {
+ line := strings.TrimSpace(lines[i])
+ if line == "" {
+ continue
+ }
+ if m := fileLineRe.FindStringSubmatch(line); m != nil {
+ file := m[1]
+ ln, _ := strconv.Atoi(m[2])
+ if shouldSkip(file) {
+ // 跳过此帧,同时重置函数名以避免错误配对
+ currFunc = ""
+ continue
+ }
+ return Frame{File: file, Line: ln, Func: currFunc}, true
+ }
+ // 记录函数名行,下一行通常是文件:行
+ currFunc = line
+ }
+ return Frame{}, false
+}
+
+func shouldSkip(file string) bool {
+ // 第三方库与 Go 模块缓存
+ if strings.Contains(file, "/go/pkg/mod/") {
+ return true
+ }
+ if strings.Contains(file, "/go.uber.org/") {
+ return true
+ }
+ if strings.Contains(file, "/gorm.io/") {
+ return true
+ }
+ // 标准库
+ if strings.Contains(file, "/go/go") && strings.Contains(file, "/src/") { // e.g. /Users/name/go/go1.24.2/src/net/http/server.go
+ return true
+ }
+ // 框架内不需要作为最终调用方的路径
+ if strings.Contains(file, "/server/core/zap.go") {
+ return true
+ }
+ if strings.Contains(file, "/server/core/") {
+ return true
+ }
+ if strings.Contains(file, "/server/utils/errorhook/") {
+ return true
+ }
+ if strings.Contains(file, "/server/middleware/") {
+ return true
+ }
+ if strings.Contains(file, "/server/router/") {
+ return true
+ }
+ return false
+}
diff --git a/server/utils/system_events.go b/server/utils/system_events.go
new file mode 100644
index 0000000..126d85b
--- /dev/null
+++ b/server/utils/system_events.go
@@ -0,0 +1,34 @@
+package utils
+
+import (
+ "sync"
+)
+
+// SystemEvents 定义系统级事件处理
+type SystemEvents struct {
+ reloadHandlers []func() error
+ mu sync.RWMutex
+}
+
+// 全局事件管理器
+var GlobalSystemEvents = &SystemEvents{}
+
+// RegisterReloadHandler 注册系统重载处理函数
+func (e *SystemEvents) RegisterReloadHandler(handler func() error) {
+ e.mu.Lock()
+ defer e.mu.Unlock()
+ e.reloadHandlers = append(e.reloadHandlers, handler)
+}
+
+// TriggerReload 触发所有注册的重载处理函数
+func (e *SystemEvents) TriggerReload() error {
+ e.mu.RLock()
+ defer e.mu.RUnlock()
+
+ for _, handler := range e.reloadHandlers {
+ if err := handler(); err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/server/utils/timer/timed_task.go b/server/utils/timer/timed_task.go
new file mode 100644
index 0000000..93a2b91
--- /dev/null
+++ b/server/utils/timer/timed_task.go
@@ -0,0 +1,230 @@
+package timer
+
+import (
+ "sync"
+
+ "github.com/robfig/cron/v3"
+)
+
+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/server/utils/timer/timed_task_test.go b/server/utils/timer/timed_task_test.go
new file mode 100644
index 0000000..9f2c02c
--- /dev/null
+++ b/server/utils/timer/timed_task_test.go
@@ -0,0 +1,72 @@
+package timer
+
+import (
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+var job = mockJob{}
+
+type mockJob struct{}
+
+func (job mockJob) Run() {
+ mockFunc()
+}
+
+func mockFunc() {
+ time.Sleep(time.Second)
+ fmt.Println("1s...")
+}
+
+func TestNewTimerTask(t *testing.T) {
+ tm := NewTimerTask()
+ _tm := tm.(*timer)
+
+ {
+ _, err := tm.AddTaskByFunc("func", "@every 1s", mockFunc, "测试mockfunc")
+ assert.Nil(t, err)
+ _, ok := _tm.cronList["func"]
+ if !ok {
+ t.Error("no find func")
+ }
+ }
+
+ {
+ _, err := tm.AddTaskByJob("job", "@every 1s", job, "测试job mockfunc")
+ assert.Nil(t, err)
+ _, ok := _tm.cronList["job"]
+ if !ok {
+ t.Error("no find job")
+ }
+ }
+
+ {
+ _, ok := tm.FindCron("func")
+ if !ok {
+ t.Error("no find func")
+ }
+ _, ok = tm.FindCron("job")
+ if !ok {
+ t.Error("no find job")
+ }
+ _, ok = tm.FindCron("none")
+ if ok {
+ t.Error("find none")
+ }
+ }
+ {
+ tm.Clear("func")
+ _, ok := tm.FindCron("func")
+ if ok {
+ t.Error("find func")
+ }
+ }
+ {
+ a := tm.FindCronList()
+ b, c := tm.FindCron("job")
+ fmt.Println(a, b, c)
+ }
+}
diff --git a/server/utils/upload/aliyun_oss.go b/server/utils/upload/aliyun_oss.go
new file mode 100644
index 0000000..db8b57b
--- /dev/null
+++ b/server/utils/upload/aliyun_oss.go
@@ -0,0 +1,75 @@
+package upload
+
+import (
+ "errors"
+ "mime/multipart"
+ "time"
+
+ "git.echol.cn/loser/st/server/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/server/utils/upload/aws_s3.go b/server/utils/upload/aws_s3.go
new file mode 100644
index 0000000..a031fb3
--- /dev/null
+++ b/server/utils/upload/aws_s3.go
@@ -0,0 +1,98 @@
+package upload
+
+import (
+ "errors"
+ "fmt"
+ "mime/multipart"
+ "time"
+
+ "git.echol.cn/loser/st/server/global"
+
+ "github.com/aws/aws-sdk-go/aws"
+ "github.com/aws/aws-sdk-go/aws/credentials"
+ "github.com/aws/aws-sdk-go/aws/session"
+ "github.com/aws/aws-sdk-go/service/s3"
+ "github.com/aws/aws-sdk-go/service/s3/s3manager"
+ "go.uber.org/zap"
+)
+
+type AwsS3 struct{}
+
+//@author: [WqyJh](https://github.com/WqyJh)
+//@object: *AwsS3
+//@function: UploadFile
+//@description: Upload file to Aws S3 using aws-sdk-go. See https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/s3-example-basic-bucket-operations.html#s3-examples-bucket-ops-upload-file-to-bucket
+//@param: file *multipart.FileHeader
+//@return: string, string, error
+
+func (*AwsS3) UploadFile(file *multipart.FileHeader) (string, string, error) {
+ session := newSession()
+ uploader := s3manager.NewUploader(session)
+
+ fileKey := fmt.Sprintf("%d%s", time.Now().Unix(), file.Filename)
+ filename := global.GVA_CONFIG.AwsS3.PathPrefix + "/" + fileKey
+ f, openError := file.Open()
+ if openError != nil {
+ global.GVA_LOG.Error("function file.Open() failed", zap.Any("err", openError.Error()))
+ return "", "", errors.New("function file.Open() failed, err:" + openError.Error())
+ }
+ defer f.Close() // 创建文件 defer 关闭
+
+ _, err := uploader.Upload(&s3manager.UploadInput{
+ Bucket: aws.String(global.GVA_CONFIG.AwsS3.Bucket),
+ Key: aws.String(filename),
+ Body: f,
+ ContentType: aws.String(file.Header.Get("Content-Type")),
+ })
+ 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/server/utils/upload/cloudflare_r2.go b/server/utils/upload/cloudflare_r2.go
new file mode 100644
index 0000000..536a529
--- /dev/null
+++ b/server/utils/upload/cloudflare_r2.go
@@ -0,0 +1,85 @@
+package upload
+
+import (
+ "errors"
+ "fmt"
+ "mime/multipart"
+ "time"
+
+ "git.echol.cn/loser/st/server/global"
+ "github.com/aws/aws-sdk-go/aws"
+ "github.com/aws/aws-sdk-go/aws/credentials"
+ "github.com/aws/aws-sdk-go/aws/session"
+ "github.com/aws/aws-sdk-go/service/s3"
+ "github.com/aws/aws-sdk-go/service/s3/s3manager"
+ "go.uber.org/zap"
+)
+
+type 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/server/utils/upload/local.go b/server/utils/upload/local.go
new file mode 100644
index 0000000..6ff8dfe
--- /dev/null
+++ b/server/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/st/server/global"
+ "git.echol.cn/loser/st/server/utils"
+ "go.uber.org/zap"
+)
+
+var mu sync.Mutex
+
+type Local struct{}
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@author: [ccfish86](https://github.com/ccfish86)
+//@author: [SliverHorn](https://github.com/SliverHorn)
+//@object: *Local
+//@function: UploadFile
+//@description: 上传文件
+//@param: file *multipart.FileHeader
+//@return: string, string, error
+
+func (*Local) UploadFile(file *multipart.FileHeader) (string, string, error) {
+ // 读取文件后缀
+ ext := filepath.Ext(file.Filename)
+ // 读取文件名并加密
+ name := strings.TrimSuffix(file.Filename, ext)
+ name = utils.MD5V([]byte(name))
+ // 拼接新文件名
+ filename := name + "_" + time.Now().Format("20060102150405") + ext
+ // 尝试创建此路径
+ mkdirErr := os.MkdirAll(global.GVA_CONFIG.Local.StorePath, os.ModePerm)
+ if mkdirErr != nil {
+ global.GVA_LOG.Error("function os.MkdirAll() failed", zap.Any("err", mkdirErr.Error()))
+ return "", "", errors.New("function os.MkdirAll() failed, err:" + mkdirErr.Error())
+ }
+ // 拼接路径和文件名
+ p := global.GVA_CONFIG.Local.StorePath + "/" + filename
+ filepath := global.GVA_CONFIG.Local.Path + "/" + filename
+
+ f, openError := file.Open() // 读取文件
+ if openError != nil {
+ global.GVA_LOG.Error("function file.Open() failed", zap.Any("err", openError.Error()))
+ return "", "", errors.New("function file.Open() failed, err:" + openError.Error())
+ }
+ defer f.Close() // 创建文件 defer 关闭
+
+ out, createErr := os.Create(p)
+ if createErr != nil {
+ global.GVA_LOG.Error("function os.Create() failed", zap.Any("err", createErr.Error()))
+
+ return "", "", errors.New("function os.Create() failed, err:" + createErr.Error())
+ }
+ defer out.Close() // 创建文件 defer 关闭
+
+ _, copyErr := io.Copy(out, f) // 传输(拷贝)文件
+ if copyErr != nil {
+ global.GVA_LOG.Error("function io.Copy() failed", zap.Any("err", copyErr.Error()))
+ return "", "", errors.New("function io.Copy() failed, err:" + copyErr.Error())
+ }
+ return filepath, filename, nil
+}
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@author: [ccfish86](https://github.com/ccfish86)
+//@author: [SliverHorn](https://github.com/SliverHorn)
+//@object: *Local
+//@function: DeleteFile
+//@description: 删除文件
+//@param: key string
+//@return: error
+
+func (*Local) DeleteFile(key string) error {
+ // 检查 key 是否为空
+ if key == "" {
+ return errors.New("key不能为空")
+ }
+
+ // 验证 key 是否包含非法字符或尝试访问存储路径之外的文件
+ if strings.Contains(key, "..") || strings.ContainsAny(key, `\/:*?"<>|`) {
+ return errors.New("非法的key")
+ }
+
+ p := filepath.Join(global.GVA_CONFIG.Local.StorePath, key)
+
+ // 检查文件是否存在
+ if _, err := os.Stat(p); os.IsNotExist(err) {
+ return errors.New("文件不存在")
+ }
+
+ // 使用文件锁防止并发删除
+ mu.Lock()
+ defer mu.Unlock()
+
+ err := os.Remove(p)
+ if err != nil {
+ return errors.New("文件删除失败: " + err.Error())
+ }
+
+ return nil
+}
diff --git a/server/utils/upload/minio_oss.go b/server/utils/upload/minio_oss.go
new file mode 100644
index 0000000..a142fe4
--- /dev/null
+++ b/server/utils/upload/minio_oss.go
@@ -0,0 +1,106 @@
+package upload
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "io"
+ "mime"
+ "mime/multipart"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "git.echol.cn/loser/st/server/global"
+ "git.echol.cn/loser/st/server/utils"
+ "github.com/minio/minio-go/v7"
+ "github.com/minio/minio-go/v7/pkg/credentials"
+ "go.uber.org/zap"
+)
+
+var MinioClient *Minio // 优化性能,但是不支持动态配置
+
+type Minio struct {
+ Client *minio.Client
+ bucket string
+}
+
+func GetMinio(endpoint, accessKeyID, secretAccessKey, bucketName string, useSSL bool) (*Minio, error) {
+ if MinioClient != nil {
+ return MinioClient, nil
+ }
+ // Initialize minio client object.
+ minioClient, err := minio.New(endpoint, &minio.Options{
+ Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""),
+ Secure: useSSL, // Set to true if using https
+ })
+ if err != nil {
+ return nil, err
+ }
+ // 尝试创建bucket
+ err = minioClient.MakeBucket(context.Background(), bucketName, minio.MakeBucketOptions{})
+ if err != nil {
+ // Check to see if we already own this bucket (which happens if you run this twice)
+ exists, errBucketExists := minioClient.BucketExists(context.Background(), bucketName)
+ if errBucketExists == nil && exists {
+ // log.Printf("We already own %s\n", bucketName)
+ } else {
+ return nil, err
+ }
+ }
+ MinioClient = &Minio{Client: minioClient, bucket: bucketName}
+ return MinioClient, nil
+}
+
+func (m *Minio) UploadFile(file *multipart.FileHeader) (filePathres, key string, uploadErr error) {
+ f, openError := file.Open()
+ // mutipart.File to os.File
+ if openError != nil {
+ global.GVA_LOG.Error("function file.Open() Failed", zap.Any("err", openError.Error()))
+ return "", "", errors.New("function file.Open() Failed, err:" + openError.Error())
+ }
+
+ filecontent := bytes.Buffer{}
+ _, err := io.Copy(&filecontent, f)
+ if err != nil {
+ global.GVA_LOG.Error("读取文件失败", zap.Any("err", err.Error()))
+ return "", "", errors.New("读取文件失败, err:" + err.Error())
+ }
+ f.Close() // 创建文件 defer 关闭
+
+ // 对文件名进行加密存储
+ ext := filepath.Ext(file.Filename)
+ filename := utils.MD5V([]byte(strings.TrimSuffix(file.Filename, ext))) + ext
+ if global.GVA_CONFIG.Minio.BasePath == "" {
+ filePathres = "uploads" + "/" + time.Now().Format("2006-01-02") + "/" + filename
+ } else {
+ filePathres = global.GVA_CONFIG.Minio.BasePath + "/" + time.Now().Format("2006-01-02") + "/" + filename
+ }
+
+ // 根据文件扩展名检测 MIME 类型
+ contentType := mime.TypeByExtension(ext)
+ if contentType == "" {
+ contentType = "application/octet-stream"
+ }
+
+ // 设置超时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: contentType})
+ 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/server/utils/upload/obs.go b/server/utils/upload/obs.go
new file mode 100644
index 0000000..59e1e36
--- /dev/null
+++ b/server/utils/upload/obs.go
@@ -0,0 +1,69 @@
+package upload
+
+import (
+ "mime/multipart"
+
+ "git.echol.cn/loser/st/server/global"
+ "github.com/huaweicloud/huaweicloud-sdk-go-obs/obs"
+ "github.com/pkg/errors"
+)
+
+var HuaWeiObs = new(Obs)
+
+type Obs struct{}
+
+func NewHuaWeiObsClient() (client *obs.ObsClient, err error) {
+ return obs.New(global.GVA_CONFIG.HuaWeiObs.AccessKey, global.GVA_CONFIG.HuaWeiObs.SecretKey, global.GVA_CONFIG.HuaWeiObs.Endpoint)
+}
+
+func (o *Obs) UploadFile(file *multipart.FileHeader) (string, string, error) {
+ // var open multipart.File
+ open, err := file.Open()
+ if err != nil {
+ return "", "", err
+ }
+ defer open.Close()
+ filename := file.Filename
+ input := &obs.PutObjectInput{
+ PutObjectBasicInput: obs.PutObjectBasicInput{
+ ObjectOperationInput: obs.ObjectOperationInput{
+ Bucket: global.GVA_CONFIG.HuaWeiObs.Bucket,
+ Key: filename,
+ },
+ HttpHeader: obs.HttpHeader{
+ ContentType: file.Header.Get("content-type"),
+ },
+ },
+ Body: open,
+ }
+
+ var client *obs.ObsClient
+ client, err = NewHuaWeiObsClient()
+ if err != nil {
+ return "", "", errors.Wrap(err, "获取华为对象存储对象失败!")
+ }
+
+ _, err = client.PutObject(input)
+ if err != nil {
+ return "", "", errors.Wrap(err, "文件上传失败!")
+ }
+ filepath := global.GVA_CONFIG.HuaWeiObs.Path + "/" + filename
+ return filepath, filename, err
+}
+
+func (o *Obs) DeleteFile(key string) error {
+ client, err := NewHuaWeiObsClient()
+ if err != nil {
+ return errors.Wrap(err, "获取华为对象存储对象失败!")
+ }
+ input := &obs.DeleteObjectInput{
+ Bucket: global.GVA_CONFIG.HuaWeiObs.Bucket,
+ Key: key,
+ }
+ var output *obs.DeleteObjectOutput
+ output, err = client.DeleteObject(input)
+ if err != nil {
+ return errors.Wrapf(err, "删除对象(%s)失败!, output: %v", key, output)
+ }
+ return nil
+}
diff --git a/server/utils/upload/qiniu.go b/server/utils/upload/qiniu.go
new file mode 100644
index 0000000..3e49de6
--- /dev/null
+++ b/server/utils/upload/qiniu.go
@@ -0,0 +1,96 @@
+package upload
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "mime/multipart"
+ "time"
+
+ "git.echol.cn/loser/st/server/global"
+ "github.com/qiniu/go-sdk/v7/auth/qbox"
+ "github.com/qiniu/go-sdk/v7/storage"
+ "go.uber.org/zap"
+)
+
+type Qiniu struct{}
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@author: [ccfish86](https://github.com/ccfish86)
+//@author: [SliverHorn](https://github.com/SliverHorn)
+//@object: *Qiniu
+//@function: UploadFile
+//@description: 上传文件
+//@param: file *multipart.FileHeader
+//@return: string, string, error
+
+func (*Qiniu) UploadFile(file *multipart.FileHeader) (string, string, error) {
+ putPolicy := storage.PutPolicy{Scope: global.GVA_CONFIG.Qiniu.Bucket}
+ mac := qbox.NewMac(global.GVA_CONFIG.Qiniu.AccessKey, global.GVA_CONFIG.Qiniu.SecretKey)
+ upToken := putPolicy.UploadToken(mac)
+ cfg := qiniuConfig()
+ formUploader := storage.NewFormUploader(cfg)
+ ret := storage.PutRet{}
+ putExtra := storage.PutExtra{Params: map[string]string{"x:name": "github logo"}}
+
+ f, openError := file.Open()
+ if openError != nil {
+ global.GVA_LOG.Error("function file.Open() failed", zap.Any("err", openError.Error()))
+
+ return "", "", errors.New("function file.Open() failed, err:" + openError.Error())
+ }
+ defer f.Close() // 创建文件 defer 关闭
+ fileKey := fmt.Sprintf("%d%s", time.Now().Unix(), file.Filename) // 文件名格式 自己可以改 建议保证唯一性
+ putErr := formUploader.Put(context.Background(), &ret, upToken, fileKey, f, file.Size, &putExtra)
+ if putErr != nil {
+ global.GVA_LOG.Error("function formUploader.Put() failed", zap.Any("err", putErr.Error()))
+ return "", "", errors.New("function formUploader.Put() failed, err:" + putErr.Error())
+ }
+ return global.GVA_CONFIG.Qiniu.ImgPath + "/" + ret.Key, ret.Key, nil
+}
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@author: [ccfish86](https://github.com/ccfish86)
+//@author: [SliverHorn](https://github.com/SliverHorn)
+//@object: *Qiniu
+//@function: DeleteFile
+//@description: 删除文件
+//@param: key string
+//@return: error
+
+func (*Qiniu) DeleteFile(key string) error {
+ mac := qbox.NewMac(global.GVA_CONFIG.Qiniu.AccessKey, global.GVA_CONFIG.Qiniu.SecretKey)
+ cfg := qiniuConfig()
+ bucketManager := storage.NewBucketManager(mac, cfg)
+ if err := bucketManager.Delete(global.GVA_CONFIG.Qiniu.Bucket, key); err != nil {
+ global.GVA_LOG.Error("function bucketManager.Delete() failed", zap.Any("err", err.Error()))
+ return errors.New("function bucketManager.Delete() failed, err:" + err.Error())
+ }
+ return nil
+}
+
+//@author: [SliverHorn](https://github.com/SliverHorn)
+//@object: *Qiniu
+//@function: qiniuConfig
+//@description: 根据配置文件进行返回七牛云的配置
+//@return: *storage.Config
+
+func qiniuConfig() *storage.Config {
+ cfg := storage.Config{
+ UseHTTPS: global.GVA_CONFIG.Qiniu.UseHTTPS,
+ UseCdnDomains: global.GVA_CONFIG.Qiniu.UseCdnDomains,
+ }
+ switch global.GVA_CONFIG.Qiniu.Zone { // 根据配置文件进行初始化空间对应的机房
+ case "ZoneHuadong":
+ cfg.Zone = &storage.ZoneHuadong
+ case "ZoneHuabei":
+ cfg.Zone = &storage.ZoneHuabei
+ case "ZoneHuanan":
+ cfg.Zone = &storage.ZoneHuanan
+ case "ZoneBeimei":
+ cfg.Zone = &storage.ZoneBeimei
+ case "ZoneXinjiapo":
+ cfg.Zone = &storage.ZoneXinjiapo
+ }
+ return &cfg
+}
diff --git a/server/utils/upload/tencent_cos.go b/server/utils/upload/tencent_cos.go
new file mode 100644
index 0000000..d8770a1
--- /dev/null
+++ b/server/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/st/server/global"
+
+ "github.com/tencentyun/cos-go-sdk-v5"
+ "go.uber.org/zap"
+)
+
+type TencentCOS struct{}
+
+// UploadFile upload file to COS
+func (*TencentCOS) UploadFile(file *multipart.FileHeader) (string, string, error) {
+ client := NewClient()
+ f, openError := file.Open()
+ if openError != nil {
+ global.GVA_LOG.Error("function file.Open() failed", zap.Any("err", openError.Error()))
+ return "", "", errors.New("function file.Open() failed, err:" + openError.Error())
+ }
+ defer f.Close() // 创建文件 defer 关闭
+ fileKey := fmt.Sprintf("%d%s", time.Now().Unix(), file.Filename)
+
+ _, err := client.Object.Put(context.Background(), global.GVA_CONFIG.TencentCOS.PathPrefix+"/"+fileKey, f, nil)
+ if err != nil {
+ panic(err)
+ }
+ return global.GVA_CONFIG.TencentCOS.BaseURL + "/" + global.GVA_CONFIG.TencentCOS.PathPrefix + "/" + fileKey, fileKey, nil
+}
+
+// DeleteFile delete file form COS
+func (*TencentCOS) DeleteFile(key string) error {
+ client := NewClient()
+ name := global.GVA_CONFIG.TencentCOS.PathPrefix + "/" + key
+ _, err := client.Object.Delete(context.Background(), name)
+ if err != nil {
+ global.GVA_LOG.Error("function bucketManager.Delete() failed", zap.Any("err", err.Error()))
+ return errors.New("function bucketManager.Delete() failed, err:" + err.Error())
+ }
+ return nil
+}
+
+// NewClient init COS client
+func NewClient() *cos.Client {
+ urlStr, _ := url.Parse("https://" + global.GVA_CONFIG.TencentCOS.Bucket + ".cos." + global.GVA_CONFIG.TencentCOS.Region + ".myqcloud.com")
+ baseURL := &cos.BaseURL{BucketURL: urlStr}
+ client := cos.NewClient(baseURL, &http.Client{
+ Transport: &cos.AuthorizationTransport{
+ SecretID: global.GVA_CONFIG.TencentCOS.SecretID,
+ SecretKey: global.GVA_CONFIG.TencentCOS.SecretKey,
+ },
+ })
+ return client
+}
diff --git a/server/utils/upload/upload.go b/server/utils/upload/upload.go
new file mode 100644
index 0000000..da5f6db
--- /dev/null
+++ b/server/utils/upload/upload.go
@@ -0,0 +1,46 @@
+package upload
+
+import (
+ "mime/multipart"
+
+ "git.echol.cn/loser/st/server/global"
+)
+
+// OSS 对象存储接口
+// Author [SliverHorn](https://github.com/SliverHorn)
+// Author [ccfish86](https://github.com/ccfish86)
+type OSS interface {
+ UploadFile(file *multipart.FileHeader) (string, string, error)
+ DeleteFile(key string) error
+}
+
+// NewOss OSS的实例化方法
+// Author [SliverHorn](https://github.com/SliverHorn)
+// Author [ccfish86](https://github.com/ccfish86)
+func NewOss() OSS {
+ switch global.GVA_CONFIG.System.OssType {
+ case "local":
+ return &Local{}
+ case "qiniu":
+ return &Qiniu{}
+ case "tencent-cos":
+ return &TencentCOS{}
+ case "aliyun-oss":
+ return &AliyunOSS{}
+ case "huawei-obs":
+ return HuaWeiObs
+ case "aws-s3":
+ return &AwsS3{}
+ case "cloudflare-r2":
+ return &CloudflareR2{}
+ case "minio":
+ minioClient, err := GetMinio(global.GVA_CONFIG.Minio.Endpoint, global.GVA_CONFIG.Minio.AccessKeyId, global.GVA_CONFIG.Minio.AccessKeySecret, global.GVA_CONFIG.Minio.BucketName, global.GVA_CONFIG.Minio.UseSSL)
+ if err != nil {
+ global.GVA_LOG.Warn("你配置了使用minio,但是初始化失败,请检查minio可用性或安全配置: " + err.Error())
+ panic("minio初始化失败") // 建议这样做,用户自己配置了minio,如果报错了还要把服务开起来,使用起来也很危险
+ }
+ return minioClient
+ default:
+ return &Local{}
+ }
+}
diff --git a/server/utils/validator.go b/server/utils/validator.go
new file mode 100644
index 0000000..a56dac0
--- /dev/null
+++ b/server/utils/validator.go
@@ -0,0 +1,294 @@
+package utils
+
+import (
+ "errors"
+ "reflect"
+ "regexp"
+ "strconv"
+ "strings"
+)
+
+type Rules map[string][]string
+
+type RulesMap map[string]Rules
+
+var CustomizeMap = make(map[string]Rules)
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@function: RegisterRule
+//@description: 注册自定义规则方案建议在路由初始化层即注册
+//@param: key string, rule Rules
+//@return: err error
+
+func RegisterRule(key string, rule Rules) (err error) {
+ if CustomizeMap[key] != nil {
+ return errors.New(key + "已注册,无法重复注册")
+ } else {
+ CustomizeMap[key] = rule
+ return nil
+ }
+}
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@function: NotEmpty
+//@description: 非空 不能为其对应类型的0值
+//@return: string
+
+func NotEmpty() string {
+ return "notEmpty"
+}
+
+// @author: [zooqkl](https://github.com/zooqkl)
+// @function: RegexpMatch
+// @description: 正则校验 校验输入项是否满足正则表达式
+// @param: rule string
+// @return: string
+
+func RegexpMatch(rule string) string {
+ return "regexp=" + rule
+}
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@function: Lt
+//@description: 小于入参(<) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较
+//@param: mark string
+//@return: string
+
+func Lt(mark string) string {
+ return "lt=" + mark
+}
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@function: Le
+//@description: 小于等于入参(<=) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较
+//@param: mark string
+//@return: string
+
+func Le(mark string) string {
+ return "le=" + mark
+}
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@function: Eq
+//@description: 等于入参(==) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较
+//@param: mark string
+//@return: string
+
+func Eq(mark string) string {
+ return "eq=" + mark
+}
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@function: Ne
+//@description: 不等于入参(!=) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较
+//@param: mark string
+//@return: string
+
+func Ne(mark string) string {
+ return "ne=" + mark
+}
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@function: Ge
+//@description: 大于等于入参(>=) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较
+//@param: mark string
+//@return: string
+
+func Ge(mark string) string {
+ return "ge=" + mark
+}
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@function: Gt
+//@description: 大于入参(>) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较
+//@param: mark string
+//@return: string
+
+func Gt(mark string) string {
+ return "gt=" + mark
+}
+
+//
+//@author: [piexlmax](https://github.com/piexlmax)
+//@function: Verify
+//@description: 校验方法
+//@param: st interface{}, roleMap Rules(入参实例,规则map)
+//@return: err error
+
+func Verify(st interface{}, roleMap Rules) (err error) {
+ compareMap := map[string]bool{
+ "lt": true,
+ "le": true,
+ "eq": true,
+ "ne": true,
+ "ge": true,
+ "gt": true,
+ }
+
+ typ := reflect.TypeOf(st)
+ val := reflect.ValueOf(st) // 获取reflect.Type类型
+
+ kd := val.Kind() // 获取到st对应的类别
+ if kd != reflect.Struct {
+ return errors.New("expect struct")
+ }
+ num := val.NumField()
+ // 遍历结构体的所有字段
+ for i := 0; i < num; i++ {
+ tagVal := typ.Field(i)
+ val := val.Field(i)
+ if tagVal.Type.Kind() == reflect.Struct {
+ if err = Verify(val.Interface(), roleMap); err != nil {
+ return err
+ }
+ }
+ if len(roleMap[tagVal.Name]) > 0 {
+ for _, v := range roleMap[tagVal.Name] {
+ switch {
+ case v == "notEmpty":
+ if isBlank(val) {
+ return errors.New(tagVal.Name + "值不能为空")
+ }
+ case strings.Split(v, "=")[0] == "regexp":
+ if !regexpMatch(strings.Split(v, "=")[1], val.String()) {
+ return errors.New(tagVal.Name + "格式校验不通过")
+ }
+ case compareMap[strings.Split(v, "=")[0]]:
+ if !compareVerify(val, v) {
+ return errors.New(tagVal.Name + "长度或值不在合法范围," + v)
+ }
+ }
+ }
+ }
+ }
+ return nil
+}
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@function: compareVerify
+//@description: 长度和数字的校验方法 根据类型自动校验
+//@param: value reflect.Value, VerifyStr string
+//@return: bool
+
+func compareVerify(value reflect.Value, VerifyStr string) bool {
+ switch value.Kind() {
+ case reflect.String:
+ return compare(len([]rune(value.String())), VerifyStr)
+ case reflect.Slice, reflect.Array:
+ return compare(value.Len(), VerifyStr)
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+ return compare(value.Uint(), VerifyStr)
+ case reflect.Float32, reflect.Float64:
+ return compare(value.Float(), VerifyStr)
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ return compare(value.Int(), VerifyStr)
+ default:
+ return false
+ }
+}
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@function: isBlank
+//@description: 非空校验
+//@param: value reflect.Value
+//@return: bool
+
+func isBlank(value reflect.Value) bool {
+ switch value.Kind() {
+ case reflect.String, reflect.Slice:
+ return value.Len() == 0
+ case reflect.Bool:
+ return !value.Bool()
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ return value.Int() == 0
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+ return value.Uint() == 0
+ case reflect.Float32, reflect.Float64:
+ return value.Float() == 0
+ case reflect.Interface, reflect.Ptr:
+ return value.IsNil()
+ }
+ return reflect.DeepEqual(value.Interface(), reflect.Zero(value.Type()).Interface())
+}
+
+//@author: [piexlmax](https://github.com/piexlmax)
+//@function: compare
+//@description: 比较函数
+//@param: value interface{}, VerifyStr string
+//@return: bool
+
+func compare(value interface{}, VerifyStr string) bool {
+ VerifyStrArr := strings.Split(VerifyStr, "=")
+ val := reflect.ValueOf(value)
+ switch val.Kind() {
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ VInt, VErr := strconv.ParseInt(VerifyStrArr[1], 10, 64)
+ if VErr != nil {
+ return false
+ }
+ switch {
+ case VerifyStrArr[0] == "lt":
+ return val.Int() < VInt
+ case VerifyStrArr[0] == "le":
+ return val.Int() <= VInt
+ case VerifyStrArr[0] == "eq":
+ return val.Int() == VInt
+ case VerifyStrArr[0] == "ne":
+ return val.Int() != VInt
+ case VerifyStrArr[0] == "ge":
+ return val.Int() >= VInt
+ case VerifyStrArr[0] == "gt":
+ return val.Int() > VInt
+ default:
+ return false
+ }
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+ VInt, VErr := strconv.Atoi(VerifyStrArr[1])
+ if VErr != nil {
+ return false
+ }
+ switch {
+ case VerifyStrArr[0] == "lt":
+ return val.Uint() < uint64(VInt)
+ case VerifyStrArr[0] == "le":
+ return val.Uint() <= uint64(VInt)
+ case VerifyStrArr[0] == "eq":
+ return val.Uint() == uint64(VInt)
+ case VerifyStrArr[0] == "ne":
+ return val.Uint() != uint64(VInt)
+ case VerifyStrArr[0] == "ge":
+ return val.Uint() >= uint64(VInt)
+ case VerifyStrArr[0] == "gt":
+ return val.Uint() > uint64(VInt)
+ default:
+ return false
+ }
+ case reflect.Float32, reflect.Float64:
+ VFloat, VErr := strconv.ParseFloat(VerifyStrArr[1], 64)
+ if VErr != nil {
+ return false
+ }
+ switch {
+ case VerifyStrArr[0] == "lt":
+ return val.Float() < VFloat
+ case VerifyStrArr[0] == "le":
+ return val.Float() <= VFloat
+ case VerifyStrArr[0] == "eq":
+ return val.Float() == VFloat
+ case VerifyStrArr[0] == "ne":
+ return val.Float() != VFloat
+ case VerifyStrArr[0] == "ge":
+ return val.Float() >= VFloat
+ case VerifyStrArr[0] == "gt":
+ return val.Float() > VFloat
+ default:
+ return false
+ }
+ default:
+ return false
+ }
+}
+
+func regexpMatch(rule, matchStr string) bool {
+ return regexp.MustCompile(rule).MatchString(matchStr)
+}
diff --git a/server/utils/validator_test.go b/server/utils/validator_test.go
new file mode 100644
index 0000000..70187db
--- /dev/null
+++ b/server/utils/validator_test.go
@@ -0,0 +1,38 @@
+package utils
+
+import (
+ "testing"
+
+ "git.echol.cn/loser/st/server/model/common/request"
+)
+
+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/server/utils/verify.go b/server/utils/verify.go
new file mode 100644
index 0000000..cc2cb78
--- /dev/null
+++ b/server/utils/verify.go
@@ -0,0 +1,19 @@
+package utils
+
+var (
+ IdVerify = Rules{"ID": []string{NotEmpty()}}
+ ApiVerify = Rules{"Path": {NotEmpty()}, "Description": {NotEmpty()}, "ApiGroup": {NotEmpty()}, "Method": {NotEmpty()}}
+ MenuVerify = Rules{"Path": {NotEmpty()}, "Name": {NotEmpty()}, "Component": {NotEmpty()}, "Sort": {Ge("0")}}
+ MenuMetaVerify = Rules{"Title": {NotEmpty()}}
+ LoginVerify = Rules{"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/server/utils/zip.go b/server/utils/zip.go
new file mode 100644
index 0000000..ef35d10
--- /dev/null
+++ b/server/utils/zip.go
@@ -0,0 +1,53 @@
+package utils
+
+import (
+ "archive/zip"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+// 解压
+func Unzip(zipFile string, destDir string) ([]string, error) {
+ zipReader, err := zip.OpenReader(zipFile)
+ var paths []string
+ if err != nil {
+ return []string{}, err
+ }
+ defer zipReader.Close()
+
+ for _, f := range zipReader.File {
+ if strings.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
+}
diff --git a/web-app/.env.development b/web-app/.env.development
new file mode 100644
index 0000000..f915f89
--- /dev/null
+++ b/web-app/.env.development
@@ -0,0 +1 @@
+VITE_API_BASE_URL=http://localhost:8888
diff --git a/web-app/.env.production b/web-app/.env.production
new file mode 100644
index 0000000..3083254
--- /dev/null
+++ b/web-app/.env.production
@@ -0,0 +1 @@
+VITE_API_BASE_URL=https://your-production-api.com
diff --git a/web-app/.vite/deps/_metadata.json b/web-app/.vite/deps/_metadata.json
new file mode 100644
index 0000000..c60efdd
--- /dev/null
+++ b/web-app/.vite/deps/_metadata.json
@@ -0,0 +1,8 @@
+{
+ "hash": "137c6446",
+ "configHash": "6b4f1a36",
+ "lockfileHash": "ed4ec364",
+ "browserHash": "4a9d650d",
+ "optimized": {},
+ "chunks": {}
+}
\ No newline at end of file
diff --git a/web-app/.vite/deps/package.json b/web-app/.vite/deps/package.json
new file mode 100644
index 0000000..3dbc1ca
--- /dev/null
+++ b/web-app/.vite/deps/package.json
@@ -0,0 +1,3 @@
+{
+ "type": "module"
+}
diff --git a/web-app/.vite/deps_temp_7925bc02/package.json b/web-app/.vite/deps_temp_7925bc02/package.json
new file mode 100644
index 0000000..3dbc1ca
--- /dev/null
+++ b/web-app/.vite/deps_temp_7925bc02/package.json
@@ -0,0 +1,3 @@
+{
+ "type": "module"
+}
diff --git a/web-app/README.md b/web-app/README.md
new file mode 100644
index 0000000..fd5f86e
--- /dev/null
+++ b/web-app/README.md
@@ -0,0 +1,55 @@
+# 云酒馆前端
+
+现代化的 SillyTavern UI,采用 Glassmorphism 风格和深色主题。
+
+## 技术栈
+
+- React 18 + TypeScript
+- Tailwind CSS
+- Vite
+- React Router
+- Lucide Icons
+
+## 开发
+
+```bash
+npm install
+npm run dev
+```
+
+## 构建
+
+```bash
+npm run build
+npm run preview
+```
+
+## 项目结构
+
+```
+src/
+├── components/ # 可复用组件
+│ ├── Navbar.tsx
+│ ├── Sidebar.tsx
+│ ├── ChatArea.tsx
+│ └── ...
+├── pages/ # 页面组件
+│ ├── HomePage.tsx
+│ ├── CharacterMarket.tsx
+│ ├── CharacterManagePage.tsx
+│ ├── PresetManagePage.tsx
+│ └── ...
+├── App.tsx # 路由配置
+├── main.tsx # 入口文件
+└── index.css # 全局样式
+```
+
+## 设计系统
+
+- 主色调: #7C3AED (紫色)
+- 次要色: #A78BFA (淡紫色)
+- 强调色: #F97316 (橙色)
+- 字体: Inter
+- 风格: Glassmorphism + 深色主题
+
+详见 [设计系统文档](../docs/design-system/)
diff --git a/web-app/index.html b/web-app/index.html
new file mode 100644
index 0000000..566d5ed
--- /dev/null
+++ b/web-app/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+ SillyTavern - Modern UI
+
+
+
+
+
+
diff --git a/web-app/package-lock.json b/web-app/package-lock.json
new file mode 100644
index 0000000..417a586
--- /dev/null
+++ b/web-app/package-lock.json
@@ -0,0 +1,4652 @@
+{
+ "name": "sillytavern-modern-ui",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "sillytavern-modern-ui",
+ "version": "0.0.0",
+ "dependencies": {
+ "axios": "^1.13.5",
+ "lucide-react": "^0.344.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-markdown": "^10.1.0",
+ "react-router-dom": "^7.13.1",
+ "rehype-raw": "^7.0.0",
+ "rehype-sanitize": "^6.0.0",
+ "remark-gfm": "^4.0.1"
+ },
+ "devDependencies": {
+ "@types/react": "^18.3.3",
+ "@types/react-dom": "^18.3.0",
+ "@vitejs/plugin-react": "^4.3.1",
+ "autoprefixer": "^10.4.19",
+ "postcss": "^8.4.38",
+ "tailwindcss": "^3.4.3",
+ "typescript": "^5.5.3",
+ "vite": "^5.3.1"
+ }
+ },
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.1",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz",
+ "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
+ "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
+ "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
+ "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
+ "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
+ "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
+ "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
+ "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
+ "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
+ "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
+ "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
+ "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
+ "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
+ "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
+ "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
+ "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
+ "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
+ "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
+ "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
+ "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
+ "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
+ "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
+ "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
+ "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
+ "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
+ "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
+ "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/debug": {
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
+ "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/ms": "*"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "license": "MIT"
+ },
+ "node_modules/@types/estree-jsx": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
+ "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "*"
+ }
+ },
+ "node_modules/@types/hast": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/@types/mdast": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
+ "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/@types/ms": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.28",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
+ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.3.7",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
+ "node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+ "license": "MIT"
+ },
+ "node_modules/@ungap/structured-clone": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
+ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
+ "license": "ISC"
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/any-promise": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/arg": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.4.27",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz",
+ "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.28.1",
+ "caniuse-lite": "^1.0.30001774",
+ "fraction.js": "^5.3.4",
+ "picocolors": "^1.1.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/axios": {
+ "version": "1.13.5",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
+ "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.11",
+ "form-data": "^4.0.5",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/bail": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
+ "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz",
+ "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.9.0",
+ "caniuse-lite": "^1.0.30001759",
+ "electron-to-chromium": "^1.5.263",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.2.0"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/camelcase-css": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001774",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz",
+ "integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/ccount": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
+ "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
+ "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-html4": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
+ "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-legacy": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
+ "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-reference-invalid": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
+ "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/comma-separated-tokens": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
+ "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decode-named-character-reference": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz",
+ "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/devlop": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
+ "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
+ "license": "MIT",
+ "dependencies": {
+ "dequal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/didyoumean": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.302",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz",
+ "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
+ "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/estree-util-is-identifier-name": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
+ "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "license": "MIT"
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fastq": {
+ "version": "1.20.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
+ "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fraction.js": {
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
+ "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hast-util-from-parse5": {
+ "version": "8.0.3",
+ "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz",
+ "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "devlop": "^1.0.0",
+ "hastscript": "^9.0.0",
+ "property-information": "^7.0.0",
+ "vfile": "^6.0.0",
+ "vfile-location": "^5.0.0",
+ "web-namespaces": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-parse-selector": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz",
+ "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-raw": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz",
+ "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "@ungap/structured-clone": "^1.0.0",
+ "hast-util-from-parse5": "^8.0.0",
+ "hast-util-to-parse5": "^8.0.0",
+ "html-void-elements": "^3.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "parse5": "^7.0.0",
+ "unist-util-position": "^5.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0",
+ "web-namespaces": "^2.0.0",
+ "zwitch": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-sanitize": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-5.0.2.tgz",
+ "integrity": "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@ungap/structured-clone": "^1.0.0",
+ "unist-util-position": "^5.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-to-jsx-runtime": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
+ "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "devlop": "^1.0.0",
+ "estree-util-is-identifier-name": "^3.0.0",
+ "hast-util-whitespace": "^3.0.0",
+ "mdast-util-mdx-expression": "^2.0.0",
+ "mdast-util-mdx-jsx": "^3.0.0",
+ "mdast-util-mdxjs-esm": "^2.0.0",
+ "property-information": "^7.0.0",
+ "space-separated-tokens": "^2.0.0",
+ "style-to-js": "^1.0.0",
+ "unist-util-position": "^5.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-to-parse5": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz",
+ "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "devlop": "^1.0.0",
+ "property-information": "^7.0.0",
+ "space-separated-tokens": "^2.0.0",
+ "web-namespaces": "^2.0.0",
+ "zwitch": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-whitespace": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
+ "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hastscript": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz",
+ "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "hast-util-parse-selector": "^4.0.0",
+ "property-information": "^7.0.0",
+ "space-separated-tokens": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/html-url-attributes": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
+ "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/html-void-elements": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
+ "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/inline-style-parser": {
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
+ "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
+ "license": "MIT"
+ },
+ "node_modules/is-alphabetical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
+ "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-alphanumerical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
+ "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-alphabetical": "^2.0.0",
+ "is-decimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-decimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
+ "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-hexadecimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
+ "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-plain-obj": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
+ "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "1.21.7",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/lilconfig": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antonk52"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/longest-streak": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
+ "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "0.344.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.344.0.tgz",
+ "integrity": "sha512-6YyBnn91GB45VuVT96bYCOKElbJzUHqp65vX8cDcu55MQL9T969v4dhGClpljamuI/+KMO9P6w9Acq1CVQGvIQ==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/markdown-table": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
+ "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mdast-util-find-and-replace": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
+ "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "escape-string-regexp": "^5.0.0",
+ "unist-util-is": "^6.0.0",
+ "unist-util-visit-parents": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-from-markdown": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz",
+ "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark": "^4.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz",
+ "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==",
+ "license": "MIT",
+ "dependencies": {
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-gfm-autolink-literal": "^2.0.0",
+ "mdast-util-gfm-footnote": "^2.0.0",
+ "mdast-util-gfm-strikethrough": "^2.0.0",
+ "mdast-util-gfm-table": "^2.0.0",
+ "mdast-util-gfm-task-list-item": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-autolink-literal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz",
+ "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "ccount": "^2.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-find-and-replace": "^3.0.0",
+ "micromark-util-character": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-footnote": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz",
+ "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.1.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-strikethrough": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz",
+ "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-table": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz",
+ "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "markdown-table": "^3.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-task-list-item": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz",
+ "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-expression": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
+ "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-jsx": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz",
+ "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "ccount": "^2.0.0",
+ "devlop": "^1.1.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "parse-entities": "^4.0.0",
+ "stringify-entities": "^4.0.0",
+ "unist-util-stringify-position": "^4.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdxjs-esm": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",
+ "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-phrasing": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
+ "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-hast": {
+ "version": "13.2.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
+ "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@ungap/structured-clone": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "trim-lines": "^3.0.0",
+ "unist-util-position": "^5.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-markdown": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",
+ "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "longest-streak": "^3.0.0",
+ "mdast-util-phrasing": "^4.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "unist-util-visit": "^5.0.0",
+ "zwitch": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
+ "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromark": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
+ "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@types/debug": "^4.0.0",
+ "debug": "^4.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-core-commonmark": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-combine-extensions": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-core-commonmark": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz",
+ "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-factory-destination": "^2.0.0",
+ "micromark-factory-label": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-factory-title": "^2.0.0",
+ "micromark-factory-whitespace": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-html-tag-name": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-extension-gfm": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",
+ "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-extension-gfm-autolink-literal": "^2.0.0",
+ "micromark-extension-gfm-footnote": "^2.0.0",
+ "micromark-extension-gfm-strikethrough": "^2.0.0",
+ "micromark-extension-gfm-table": "^2.0.0",
+ "micromark-extension-gfm-tagfilter": "^2.0.0",
+ "micromark-extension-gfm-task-list-item": "^2.0.0",
+ "micromark-util-combine-extensions": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-autolink-literal": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz",
+ "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-footnote": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz",
+ "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-core-commonmark": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-strikethrough": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz",
+ "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-table": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz",
+ "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-tagfilter": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz",
+ "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-task-list-item": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz",
+ "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-factory-destination": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
+ "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-label": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz",
+ "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-space": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",
+ "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-title": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz",
+ "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-whitespace": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz",
+ "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-character": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
+ "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-chunked": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz",
+ "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-classify-character": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz",
+ "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-combine-extensions": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz",
+ "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-numeric-character-reference": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz",
+ "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-string": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz",
+ "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-encode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
+ "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-html-tag-name": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
+ "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-normalize-identifier": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz",
+ "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-resolve-all": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz",
+ "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-sanitize-uri": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
+ "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-subtokenize": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz",
+ "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-symbol": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
+ "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-types": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
+ "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/mz": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "thenify-all": "^1.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.27",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
+ "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-hash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/parse-entities": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
+ "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^2.0.0",
+ "character-entities-legacy": "^3.0.0",
+ "character-reference-invalid": "^2.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "is-alphanumerical": "^2.0.0",
+ "is-decimal": "^2.0.0",
+ "is-hexadecimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/parse-entities/node_modules/@types/unist": {
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
+ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
+ "license": "MIT"
+ },
+ "node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
+ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-import": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.0.0",
+ "read-cache": "^1.0.0",
+ "resolve": "^1.1.7"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
+ },
+ "node_modules/postcss-js": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
+ "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "camelcase-css": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >= 16"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.21"
+ }
+ },
+ "node_modules/postcss-load-config": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
+ "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "lilconfig": "^3.1.1"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "jiti": ">=1.21.0",
+ "postcss": ">=8.0.9",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss-nested": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "postcss-selector-parser": "^6.1.1"
+ },
+ "engines": {
+ "node": ">=12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.14"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/property-information": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
+ "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-markdown": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
+ "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "hast-util-to-jsx-runtime": "^2.0.0",
+ "html-url-attributes": "^3.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "remark-parse": "^11.0.0",
+ "remark-rehype": "^11.0.0",
+ "unified": "^11.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18",
+ "react": ">=18"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-router": {
+ "version": "7.13.1",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz",
+ "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.13.1",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz",
+ "integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.13.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "node_modules/read-cache": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pify": "^2.3.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/rehype-raw": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz",
+ "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "hast-util-raw": "^9.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/rehype-sanitize": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/rehype-sanitize/-/rehype-sanitize-6.0.0.tgz",
+ "integrity": "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "hast-util-sanitize": "^5.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-gfm": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
+ "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-gfm": "^3.0.0",
+ "micromark-extension-gfm": "^3.0.0",
+ "remark-parse": "^11.0.0",
+ "remark-stringify": "^11.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-parse": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
+ "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-rehype": {
+ "version": "11.1.2",
+ "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz",
+ "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "unified": "^11.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-stringify": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
+ "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.11",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
+ "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
+ "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.59.0",
+ "@rollup/rollup-android-arm64": "4.59.0",
+ "@rollup/rollup-darwin-arm64": "4.59.0",
+ "@rollup/rollup-darwin-x64": "4.59.0",
+ "@rollup/rollup-freebsd-arm64": "4.59.0",
+ "@rollup/rollup-freebsd-x64": "4.59.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.59.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.59.0",
+ "@rollup/rollup-linux-arm64-musl": "4.59.0",
+ "@rollup/rollup-linux-loong64-gnu": "4.59.0",
+ "@rollup/rollup-linux-loong64-musl": "4.59.0",
+ "@rollup/rollup-linux-ppc64-gnu": "4.59.0",
+ "@rollup/rollup-linux-ppc64-musl": "4.59.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.59.0",
+ "@rollup/rollup-linux-riscv64-musl": "4.59.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.59.0",
+ "@rollup/rollup-linux-x64-gnu": "4.59.0",
+ "@rollup/rollup-linux-x64-musl": "4.59.0",
+ "@rollup/rollup-openbsd-x64": "4.59.0",
+ "@rollup/rollup-openharmony-arm64": "4.59.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.59.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.59.0",
+ "@rollup/rollup-win32-x64-gnu": "4.59.0",
+ "@rollup/rollup-win32-x64-msvc": "4.59.0",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
+ "license": "MIT"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/space-separated-tokens": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
+ "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/stringify-entities": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
+ "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities-html4": "^2.0.0",
+ "character-entities-legacy": "^3.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/style-to-js": {
+ "version": "1.1.21",
+ "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz",
+ "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "style-to-object": "1.0.14"
+ }
+ },
+ "node_modules/style-to-object": {
+ "version": "1.0.14",
+ "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz",
+ "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==",
+ "license": "MIT",
+ "dependencies": {
+ "inline-style-parser": "0.2.7"
+ }
+ },
+ "node_modules/sucrase": {
+ "version": "3.35.1",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
+ "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "commander": "^4.0.0",
+ "lines-and-columns": "^1.1.6",
+ "mz": "^2.7.0",
+ "pirates": "^4.0.1",
+ "tinyglobby": "^0.2.11",
+ "ts-interface-checker": "^0.1.9"
+ },
+ "bin": {
+ "sucrase": "bin/sucrase",
+ "sucrase-node": "bin/sucrase-node"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "3.4.19",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
+ "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "arg": "^5.0.2",
+ "chokidar": "^3.6.0",
+ "didyoumean": "^1.2.2",
+ "dlv": "^1.1.3",
+ "fast-glob": "^3.3.2",
+ "glob-parent": "^6.0.2",
+ "is-glob": "^4.0.3",
+ "jiti": "^1.21.7",
+ "lilconfig": "^3.1.3",
+ "micromatch": "^4.0.8",
+ "normalize-path": "^3.0.0",
+ "object-hash": "^3.0.0",
+ "picocolors": "^1.1.1",
+ "postcss": "^8.4.47",
+ "postcss-import": "^15.1.0",
+ "postcss-js": "^4.0.1",
+ "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
+ "postcss-nested": "^6.2.0",
+ "postcss-selector-parser": "^6.1.2",
+ "resolve": "^1.22.8",
+ "sucrase": "^3.35.0"
+ },
+ "bin": {
+ "tailwind": "lib/cli.js",
+ "tailwindcss": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/thenify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0"
+ }
+ },
+ "node_modules/thenify-all": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "thenify": ">= 3.1.0 < 4"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/trim-lines": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
+ "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/trough": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
+ "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/ts-interface-checker": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/unified": {
+ "version": "11.0.5",
+ "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
+ "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "bail": "^2.0.0",
+ "devlop": "^1.0.0",
+ "extend": "^3.0.0",
+ "is-plain-obj": "^4.0.0",
+ "trough": "^2.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-is": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
+ "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-position": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
+ "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-stringify-position": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
+ "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz",
+ "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0",
+ "unist-util-visit-parents": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit-parents": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
+ "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vfile": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
+ "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vfile-location": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz",
+ "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vfile-message": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
+ "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/web-namespaces": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz",
+ "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/zwitch": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
+ "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ }
+ }
+}
diff --git a/web-app/package.json b/web-app/package.json
new file mode 100644
index 0000000..9111f49
--- /dev/null
+++ b/web-app/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "sillytavern-modern-ui",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "axios": "^1.13.5",
+ "lucide-react": "^0.344.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-markdown": "^10.1.0",
+ "react-router-dom": "^7.13.1",
+ "rehype-raw": "^7.0.0",
+ "rehype-sanitize": "^6.0.0",
+ "remark-gfm": "^4.0.1"
+ },
+ "devDependencies": {
+ "@types/react": "^18.3.3",
+ "@types/react-dom": "^18.3.0",
+ "@vitejs/plugin-react": "^4.3.1",
+ "autoprefixer": "^10.4.19",
+ "postcss": "^8.4.38",
+ "tailwindcss": "^3.4.3",
+ "typescript": "^5.5.3",
+ "vite": "^5.3.1"
+ }
+}
diff --git a/web-app/postcss.config.js b/web-app/postcss.config.js
new file mode 100644
index 0000000..2e7af2b
--- /dev/null
+++ b/web-app/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/web-app/src/App.tsx b/web-app/src/App.tsx
new file mode 100644
index 0000000..72fd35c
--- /dev/null
+++ b/web-app/src/App.tsx
@@ -0,0 +1,36 @@
+import {BrowserRouter, Route, Routes} from 'react-router-dom'
+import HomePage from './pages/HomePage'
+import CharacterMarket from './pages/CharacterMarket'
+import CharacterDetail from './pages/CharacterDetail'
+import CharacterDetailPage from './pages/CharacterDetailPage'
+import ChatPage from './pages/ChatPage'
+import LoginPage from './pages/LoginPage'
+import RegisterPage from './pages/RegisterPage'
+import ForgotPasswordPage from './pages/ForgotPasswordPage'
+import ProfilePage from './pages/ProfilePage'
+import CharacterManagePage from './pages/CharacterManagePage'
+import PresetManagePage from './pages/PresetManagePage'
+import AdminPage from './pages/AdminPage'
+
+function App() {
+ return (
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+ )
+}
+
+export default App
diff --git a/web-app/src/api/aiConfig.ts b/web-app/src/api/aiConfig.ts
new file mode 100644
index 0000000..0425ff6
--- /dev/null
+++ b/web-app/src/api/aiConfig.ts
@@ -0,0 +1,114 @@
+import apiClient from './client'
+
+export interface AIConfig {
+ id: number
+ name: string
+ provider: 'openai' | 'anthropic' | 'custom'
+ baseUrl: string // 注意:后端返回的是 baseUrl 而不是 baseURL
+ apiKey: string
+ models?: string[]
+ defaultModel: string
+ settings?: Record
+ isActive: boolean
+ isDefault: boolean
+ createdAt: string
+ updatedAt: string
+}
+
+export interface CreateAIConfigRequest {
+ name: string
+ provider: 'openai' | 'anthropic' | 'custom'
+ baseUrl: string // 注意:后端使用 baseUrl 而不是 baseURL
+ apiKey: string
+ defaultModel?: string
+ settings?: Record
+ isActive?: boolean
+ isDefault?: boolean
+}
+
+export interface UpdateAIConfigRequest {
+ name?: string
+ provider?: 'openai' | 'anthropic' | 'custom'
+ baseUrl?: string // 注意:后端使用 baseUrl 而不是 baseURL
+ apiKey?: string
+ defaultModel?: string
+ settings?: Record
+ isActive?: boolean
+ isDefault?: boolean
+}
+
+export interface ModelInfo {
+ id: string
+ name: string
+ ownedBy: string
+}
+
+export interface GetModelsRequest {
+ baseUrl: string // 注意:后端使用 baseUrl 而不是 baseURL
+ apiKey: string
+ provider: 'openai' | 'anthropic' | 'custom'
+}
+
+export interface GetModelsResponse {
+ models: ModelInfo[]
+}
+
+export interface TestAIConfigRequest {
+ baseUrl: string // 注意:后端使用 baseUrl 而不是 baseURL
+ apiKey: string
+ provider: 'openai' | 'anthropic' | 'custom'
+ model: string
+}
+
+export interface TestAIConfigResponse {
+ success: boolean
+ message: string
+ latency: number
+}
+
+export interface AIConfigListResponse {
+ list: AIConfig[]
+ total: number
+}
+
+export const aiConfigApi = {
+ // 创建AI配置
+ createAIConfig: (data: CreateAIConfigRequest) => {
+ return apiClient.post('/app/ai-config', data)
+ },
+
+ // 获取AI配置列表
+ getAIConfigList: () => {
+ return apiClient.get('/app/ai-config')
+ },
+
+ // 更新AI配置
+ updateAIConfig: (id: number, data: UpdateAIConfigRequest) => {
+ return apiClient.put(`/app/ai-config/${id}`, data)
+ },
+
+ // 删除AI配置
+ deleteAIConfig: (id: number) => {
+ return apiClient.delete(`/app/ai-config/${id}`)
+ },
+
+ // 获取模型列表
+ getModels: (data: GetModelsRequest) => {
+ return apiClient.post('/app/ai-config/models', data)
+ },
+
+ // 测试AI配置(用于新建时,需要传递完整信息)
+ testAIConfig: (data: TestAIConfigRequest) => {
+ return apiClient.post('/app/ai-config/test', data)
+ },
+
+ // 通过ID测试AI配置(用于已保存的配置,后端会从数据库获取完整API Key)
+ testAIConfigById: (id: number) => {
+ return apiClient.post(`/app/ai-config/${id}/test`)
+ },
+
+ // 通过ID获取模型列表(用于已保存的配置,后端会从数据库获取完整API Key)
+ getModelsByConfigId: (id: number) => {
+ return apiClient.get(`/app/ai-config/${id}/models`)
+ },
+}
diff --git a/web-app/src/api/auth.ts b/web-app/src/api/auth.ts
new file mode 100644
index 0000000..ff7f556
--- /dev/null
+++ b/web-app/src/api/auth.ts
@@ -0,0 +1,92 @@
+import apiClient from './client'
+
+// 类型定义
+export interface RegisterRequest {
+ username: string
+ password: string
+ nickName?: string
+ email?: string
+ phone?: string
+}
+
+export interface LoginRequest {
+ username: string
+ password: string
+}
+
+export interface UpdateProfileRequest {
+ nickName?: string
+ email?: string
+ phone?: string
+ avatar?: string
+ preferences?: string
+ aiSettings?: string
+}
+
+export interface ChangePasswordRequest {
+ oldPassword: string
+ newPassword: string
+}
+
+export interface User {
+ id: number
+ uuid: string
+ username: string
+ nickName: string
+ email: string
+ phone: string
+ avatar: string
+ status: string
+ enable: boolean
+ isAdmin: boolean
+ lastLoginAt: string | null
+ lastLoginIp: string
+ chatCount: number
+ messageCount: number
+ createdAt: string
+}
+
+export interface LoginResponse {
+ user: User
+ token: string
+ refreshToken: string
+ expiresAt: number
+}
+
+// API 方法
+export const authApi = {
+ // 用户注册
+ register: (data: RegisterRequest) => {
+ return apiClient.post('/app/auth/register', data)
+ },
+
+ // 用户登录
+ login: (data: LoginRequest): Promise<{ data: LoginResponse }> => {
+ return apiClient.post('/app/auth/login', data)
+ },
+
+ // 刷新 Token
+ refreshToken: (refreshToken: string): Promise<{ data: LoginResponse }> => {
+ return apiClient.post('/app/auth/refresh', { refreshToken })
+ },
+
+ // 用户登出
+ logout: () => {
+ return apiClient.post('/app/auth/logout')
+ },
+
+ // 获取用户信息
+ getUserInfo: (): Promise<{ data: User }> => {
+ return apiClient.get('/app/auth/userinfo')
+ },
+
+ // 更新用户资料
+ updateProfile: (data: UpdateProfileRequest) => {
+ return apiClient.put('/app/user/profile', data)
+ },
+
+ // 修改密码
+ changePassword: (data: ChangePasswordRequest) => {
+ return apiClient.post('/app/user/change-password', data)
+ },
+}
diff --git a/web-app/src/api/character.ts b/web-app/src/api/character.ts
new file mode 100644
index 0000000..975e08c
--- /dev/null
+++ b/web-app/src/api/character.ts
@@ -0,0 +1,130 @@
+import apiClient from './client'
+
+// 类型定义
+export interface Character {
+ id: number
+ name: string
+ avatar: string
+ creator: string
+ version: string
+ description: string
+ personality: string
+ scenario: string
+ firstMes: string
+ mesExample: string
+ creatorNotes: string
+ systemPrompt: string
+ postHistoryInstructions: string
+ tags: string[]
+ alternateGreetings: string[]
+ characterBook: Record
+ extensions: Record
+ spec: string
+ specVersion: string
+ isPublic: boolean
+ useCount: number
+ favoriteCount: number
+ createdAt: string
+ updatedAt: string
+}
+
+export interface CreateCharacterRequest {
+ name: string
+ avatar?: string
+ creator?: string
+ version?: string
+ description?: string
+ personality?: string
+ scenario?: string
+ firstMes?: string
+ mesExample?: string
+ creatorNotes?: string
+ systemPrompt?: string
+ postHistoryInstructions?: string
+ tags?: string[]
+ alternateGreetings?: string[]
+ characterBook?: Record
+ extensions?: Record
+ isPublic?: boolean
+}
+
+export interface UpdateCharacterRequest {
+ name?: string
+ avatar?: string
+ creator?: string
+ version?: string
+ description?: string
+ personality?: string
+ scenario?: string
+ firstMes?: string
+ mesExample?: string
+ creatorNotes?: string
+ systemPrompt?: string
+ postHistoryInstructions?: string
+ tags?: string[]
+ alternateGreetings?: string[]
+ characterBook?: Record
+ extensions?: Record
+ isPublic?: boolean
+}
+
+export interface GetCharacterListRequest {
+ page?: number
+ pageSize?: number
+ keyword?: string
+ tag?: string
+ isPublic?: boolean
+}
+
+export interface CharacterListResponse {
+ list: Character[]
+ total: number
+ page: number
+ pageSize: number
+}
+
+// API 方法
+export const characterApi = {
+ // 创建角色卡
+ createCharacter: (data: CreateCharacterRequest): Promise<{ data: Character }> => {
+ return apiClient.post('/app/character', data)
+ },
+
+ // 获取角色卡列表
+ getCharacterList: (params?: GetCharacterListRequest): Promise<{ data: CharacterListResponse }> => {
+ return apiClient.get('/app/character', { params })
+ },
+
+ // 获取角色卡详情
+ getCharacterById: (id: number): Promise<{ data: Character }> => {
+ return apiClient.get(`/app/character/${id}`)
+ },
+
+ // 更新角色卡
+ updateCharacter: (id: number, data: UpdateCharacterRequest) => {
+ return apiClient.put(`/app/character/${id}`, data)
+ },
+
+ // 删除角色卡
+ deleteCharacter: (id: number) => {
+ return apiClient.delete(`/app/character/${id}`)
+ },
+
+ // 上传角色卡文件(PNG/JSON)
+ uploadCharacter: (file: File): Promise<{ data: Character }> => {
+ const formData = new FormData()
+ formData.append('file', file)
+ return apiClient.post('/app/character/upload', formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ })
+ },
+
+ // 导出角色卡为 JSON
+ exportCharacter: (id: number) => {
+ return apiClient.get(`/app/character/${id}/export`, {
+ responseType: 'blob',
+ })
+ },
+}
diff --git a/web-app/src/api/client.ts b/web-app/src/api/client.ts
new file mode 100644
index 0000000..a6fdba9
--- /dev/null
+++ b/web-app/src/api/client.ts
@@ -0,0 +1,76 @@
+import axios, {AxiosError, AxiosInstance} from 'axios'
+
+// API 基础配置
+const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8888'
+
+// 创建 axios 实例
+const apiClient: AxiosInstance = axios.create({
+ baseURL: API_BASE_URL,
+ timeout: 30000,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+})
+
+// 请求拦截器 - 添加 Token
+apiClient.interceptors.request.use(
+ (config) => {
+ const token = localStorage.getItem('token')
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`
+ }
+ return config
+ },
+ (error) => {
+ return Promise.reject(error)
+ }
+)
+
+// 响应拦截器 - 统一错误处理
+apiClient.interceptors.response.use(
+ (response) => {
+ return response.data
+ },
+ async (error: AxiosError) => {
+ if (error.response) {
+ const { status, data } = error.response
+
+ // Token 过期,尝试刷新
+ if (status === 401 && data?.data?.reload) {
+ const refreshToken = localStorage.getItem('refreshToken')
+ if (refreshToken) {
+ try {
+ const response = await axios.post(`${API_BASE_URL}/app/auth/refresh`, {
+ refreshToken,
+ })
+ const { token, refreshToken: newRefreshToken } = response.data.data
+ localStorage.setItem('token', token)
+ localStorage.setItem('refreshToken', newRefreshToken)
+
+ // 重试原请求
+ if (error.config) {
+ error.config.headers.Authorization = `Bearer ${token}`
+ return apiClient.request(error.config)
+ }
+ } catch (refreshError) {
+ // 刷新失败,清除 Token 并跳转登录
+ localStorage.removeItem('token')
+ localStorage.removeItem('refreshToken')
+ window.location.href = '/login'
+ return Promise.reject(refreshError)
+ }
+ } else {
+ // 没有 refreshToken,跳转登录
+ window.location.href = '/login'
+ }
+ }
+
+ // 返回错误信息
+ return Promise.reject(data?.msg || '请求失败')
+ }
+
+ return Promise.reject(error.message || '网络错误')
+ }
+)
+
+export default apiClient
diff --git a/web-app/src/api/conversation.ts b/web-app/src/api/conversation.ts
new file mode 100644
index 0000000..434f3cf
--- /dev/null
+++ b/web-app/src/api/conversation.ts
@@ -0,0 +1,109 @@
+import apiClient from './client'
+
+// 简化的角色信息(用于列表)
+export interface CharacterSimple {
+ id: number
+ name: string
+ avatar: string
+ description: string
+ createdAt: string
+ updatedAt: string
+}
+
+// 对话列表项(轻量级)
+export interface ConversationListItem {
+ id: number
+ characterId: number
+ title: string
+ messageCount: number
+ tokenCount: number
+ createdAt: string
+ updatedAt: string
+ character?: CharacterSimple
+}
+
+export interface Conversation {
+ id: number
+ userId?: number
+ characterId: number
+ title: string
+ presetId?: number
+ aiProvider: string
+ model: string
+ settings?: Record
+ messageCount: number
+ tokenCount: number
+ createdAt: string
+ updatedAt: string
+}
+
+export interface Message {
+ id: number
+ conversationId: number
+ role: 'user' | 'assistant' | 'system'
+ content: string
+ tokenCount: number
+ createdAt: string
+}
+
+export interface CreateConversationRequest {
+ characterId: number
+ title?: string
+ presetId?: number
+ aiProvider?: string
+ model?: string
+ settings?: Record
+}
+
+export interface SendMessageRequest {
+ content: string
+}
+
+export interface ConversationListResponse {
+ list: ConversationListItem[]
+ total: number
+ page: number
+ pageSize: number
+}
+
+export interface MessageListResponse {
+ list: Message[]
+ total: number
+}
+
+export const conversationApi = {
+ // 创建对话
+ createConversation: (data: CreateConversationRequest) => {
+ return apiClient.post('/app/conversation', data)
+ },
+
+ // 获取对话列表
+ getConversationList: (params: { page?: number; pageSize?: number }) => {
+ return apiClient.get('/app/conversation', { params })
+ },
+
+ // 获取对话详情
+ getConversationById: (id: number) => {
+ return apiClient.get(`/app/conversation/${id}`)
+ },
+
+ // 删除对话
+ deleteConversation: (id: number) => {
+ return apiClient.delete(`/app/conversation/${id}`)
+ },
+
+ // 获取消息列表
+ getMessageList: (conversationId: number, params: { page?: number; pageSize?: number }) => {
+ return apiClient.get(`/app/conversation/${conversationId}/messages`, { params })
+ },
+
+ // 发送消息
+ sendMessage: (conversationId: number, data: SendMessageRequest) => {
+ return apiClient.post(`/app/conversation/${conversationId}/message`, data)
+ },
+
+ // 更新对话设置
+ updateConversationSettings: (conversationId: number, settings: Record) => {
+ return apiClient.put(`/app/conversation/${conversationId}/settings`, { settings })
+ },
+}
diff --git a/web-app/src/api/preset.ts b/web-app/src/api/preset.ts
new file mode 100644
index 0000000..087169c
--- /dev/null
+++ b/web-app/src/api/preset.ts
@@ -0,0 +1,102 @@
+import apiClient from './client'
+
+// 预设接口定义
+export interface Preset {
+ id: number
+ userId: number
+ name: string
+ description: string
+ isPublic: boolean
+ isDefault: boolean
+ temperature: number
+ topP: number
+ topK: number
+ frequencyPenalty: number
+ presencePenalty: number
+ maxTokens: number
+ repetitionPenalty: number
+ minP: number
+ topA: number
+ systemPrompt: string
+ stopSequences: string[]
+ extensions: Record
+ useCount: number
+ createdAt: string
+ updatedAt: string
+}
+
+// 创建预设请求
+export interface CreatePresetRequest {
+ name: string
+ description?: string
+ isPublic?: boolean
+ temperature?: number
+ topP?: number
+ topK?: number
+ frequencyPenalty?: number
+ presencePenalty?: number
+ maxTokens?: number
+ repetitionPenalty?: number
+ minP?: number
+ topA?: number
+ systemPrompt?: string
+ stopSequences?: string[]
+ extensions?: Record
+}
+
+// 预设列表响应
+export interface PresetListResponse {
+ list: Preset[]
+ total: number
+ page: number
+ pageSize: number
+}
+
+// 预设 API
+export const presetApi = {
+ // 创建预设
+ createPreset: (data: CreatePresetRequest) => {
+ return apiClient.post('/app/preset', data)
+ },
+
+ // 获取预设列表
+ getPresetList: (params: { page?: number; pageSize?: number; keyword?: string; isPublic?: boolean }) => {
+ return apiClient.get('/app/preset', { params })
+ },
+
+ // 根据ID获取预设
+ getPresetById: (id: number) => {
+ return apiClient.get(`/app/preset/${id}`)
+ },
+
+ // 更新预设
+ updatePreset: (id: number, data: Partial) => {
+ return apiClient.put(`/app/preset/${id}`, data)
+ },
+
+ // 删除预设
+ deletePreset: (id: number) => {
+ return apiClient.delete(`/app/preset/${id}`)
+ },
+
+ // 设置默认预设
+ setDefaultPreset: (id: number) => {
+ return apiClient.post(`/app/preset/${id}/default`)
+ },
+
+ // 导入预设
+ importPreset: (file: File) => {
+ const formData = new FormData()
+ formData.append('file', file)
+ return apiClient.post('/app/preset/import', formData, {
+ headers: { 'Content-Type': 'multipart/form-data' }
+ })
+ },
+
+ // 导出预设
+ exportPreset: (id: number) => {
+ return apiClient.get(`/app/preset/${id}/export`, {
+ responseType: 'blob'
+ })
+ }
+}
diff --git a/web-app/src/api/upload.ts b/web-app/src/api/upload.ts
new file mode 100644
index 0000000..e3df34d
--- /dev/null
+++ b/web-app/src/api/upload.ts
@@ -0,0 +1,18 @@
+import apiClient from './client'
+
+// 上传图片接口
+export interface UploadImageResponse {
+ url: string
+}
+
+// 上传 API
+export const uploadApi = {
+ // 上传图片到 OSS
+ uploadImage: (file: File) => {
+ const formData = new FormData()
+ formData.append('file', file)
+ return apiClient.post('/app/upload/image', formData, {
+ headers: { 'Content-Type': 'multipart/form-data' }
+ })
+ }
+}
diff --git a/web-app/src/components/CharacterPanel.tsx b/web-app/src/components/CharacterPanel.tsx
new file mode 100644
index 0000000..8000f57
--- /dev/null
+++ b/web-app/src/components/CharacterPanel.tsx
@@ -0,0 +1,131 @@
+import {PanelRightClose, Settings, Sparkles} from 'lucide-react'
+import {type Character} from '../api/character'
+import {type Conversation} from '../api/conversation'
+
+interface CharacterPanelProps {
+ character: Character
+ conversation: Conversation
+ onOpenSettings: () => void
+ onClose?: () => void
+}
+
+export default function CharacterPanel({ character, conversation, onOpenSettings, onClose }: CharacterPanelProps) {
+ const tags = Array.isArray(character.tags) ? character.tags : []
+
+ return (
+
+
+
角色信息
+
+
+ {onClose && (
+
+ )}
+
+
+
+
+
+ {character.avatar ? (
+

+ ) : (
+
+ {character.name ? character.name.charAt(0) : '?'}
+
+ )}
+
{character.name || '未命名角色'}
+
{character.description || '暂无描述'}
+
+
+
+ 在线
+
+
+
+
+
+ {tags.length > 0 && (
+
+
标签
+
+ {tags.map((tag, index) => (
+
+ {tag}
+
+ ))}
+
+
+ )}
+
+ {character.personality && (
+
+
性格特征
+
{character.personality}
+
+ )}
+
+ {character.scenario && (
+
+
场景设定
+
{character.scenario}
+
+ )}
+
+ {character.creator && (
+
+
创建者
+
{character.creator}
+
+ )}
+
+
+
对话统计
+
+
+ 消息数
+ {conversation.messageCount}
+
+
+ Token 使用
+ {conversation.tokenCount}
+
+
+ AI 模型
+ {conversation.model || 'GPT-4'}
+
+
+
+
+ {character.characterBook && (
+
+
世界书
+
+ {typeof character.characterBook === 'object' && character.characterBook.entries
+ ? `${character.characterBook.entries.length} 个条目`
+ : '已启用'}
+
+
+ )}
+
+
+ )
+}
diff --git a/web-app/src/components/ChatArea.tsx b/web-app/src/components/ChatArea.tsx
new file mode 100644
index 0000000..abcdd59
--- /dev/null
+++ b/web-app/src/components/ChatArea.tsx
@@ -0,0 +1,719 @@
+import {
+ Check,
+ Copy,
+ Download,
+ Mic,
+ MoreVertical,
+ Paperclip,
+ RefreshCw,
+ Send,
+ Settings,
+ Trash2,
+ Waves,
+ Zap
+} from 'lucide-react'
+import {useEffect, useRef, useState} from 'react'
+import {createPortal} from 'react-dom'
+import {type Conversation, conversationApi, type Message} from '../api/conversation'
+import {type Character} from '../api/character'
+import {type AIConfig, aiConfigApi} from '../api/aiConfig'
+import {type Preset, presetApi} from '../api/preset'
+import MessageContent from './MessageContent'
+
+interface ChatAreaProps {
+ conversation: Conversation
+ character: Character
+ onConversationUpdate: (conversation: Conversation) => void
+}
+
+export default function ChatArea({ conversation, character, onConversationUpdate }: ChatAreaProps) {
+ const [messages, setMessages] = useState([])
+ const [inputValue, setInputValue] = useState('')
+ const [sending, setSending] = useState(false)
+ const [loading, setLoading] = useState(true)
+ const [showMenu, setShowMenu] = useState(false)
+ const [copiedId, setCopiedId] = useState(null)
+ const [aiConfigs, setAiConfigs] = useState([])
+ const [selectedConfigId, setSelectedConfigId] = useState()
+ const [showModelSelector, setShowModelSelector] = useState(false)
+ const [streamEnabled, setStreamEnabled] = useState(true) // 默认启用流式传输
+ const [presets, setPresets] = useState([])
+ const [selectedPresetId, setSelectedPresetId] = useState()
+ const [showPresetSelector, setShowPresetSelector] = useState(false)
+ const messagesEndRef = useRef(null)
+ const textareaRef = useRef(null)
+ const modelSelectorRef = useRef(null)
+ const presetSelectorRef = useRef(null)
+ const menuRef = useRef(null)
+
+ // 点击外部关闭下拉菜单
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ const target = event.target as HTMLElement
+
+ // 检查是否点击在模型选择器外部
+ if (showModelSelector && modelSelectorRef.current && !modelSelectorRef.current.contains(target)) {
+ setShowModelSelector(false)
+ }
+
+ // 检查是否点击在预设选择器外部
+ if (showPresetSelector && presetSelectorRef.current && !presetSelectorRef.current.contains(target)) {
+ setShowPresetSelector(false)
+ }
+
+ // 检查是否点击在菜单外部
+ if (showMenu && menuRef.current && !menuRef.current.contains(target)) {
+ setShowMenu(false)
+ }
+ }
+
+ document.addEventListener('mousedown', handleClickOutside)
+ return () => document.removeEventListener('mousedown', handleClickOutside)
+ }, [showModelSelector, showPresetSelector, showMenu])
+
+ useEffect(() => {
+ loadMessages()
+ loadAIConfigs()
+ loadCurrentConfig()
+ loadPresets()
+ loadCurrentPreset()
+ }, [conversation.id])
+
+ useEffect(() => {
+ scrollToBottom()
+ }, [messages])
+
+ const loadMessages = async () => {
+ try {
+ setLoading(true)
+ const response = await conversationApi.getMessageList(conversation.id, {
+ page: 1,
+ pageSize: 100,
+ })
+ setMessages(response.data.list || [])
+ } catch (err) {
+ console.error('加载消息失败:', err)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const loadAIConfigs = async () => {
+ try {
+ const response = await aiConfigApi.getAIConfigList()
+ const activeConfigs = response.data.list.filter(config => config.isActive)
+ setAiConfigs(activeConfigs)
+ } catch (err) {
+ console.error('加载 AI 配置失败:', err)
+ }
+ }
+
+ const loadCurrentConfig = () => {
+ if (conversation.settings) {
+ try {
+ const settings = typeof conversation.settings === 'string'
+ ? JSON.parse(conversation.settings)
+ : conversation.settings
+ if (settings.aiConfigId) {
+ setSelectedConfigId(settings.aiConfigId)
+ }
+ } catch (e) {
+ console.error('解析设置失败:', e)
+ }
+ }
+ }
+
+ const loadPresets = async () => {
+ try {
+ const response = await presetApi.getPresetList({ page: 1, pageSize: 100 })
+ setPresets(response.data.list)
+ } catch (err) {
+ console.error('加载预设失败:', err)
+ }
+ }
+
+ const loadCurrentPreset = () => {
+ if (conversation.settings) {
+ try {
+ const settings = typeof conversation.settings === 'string'
+ ? JSON.parse(conversation.settings)
+ : conversation.settings
+ if (settings.presetId) {
+ setSelectedPresetId(settings.presetId)
+ }
+ } catch (e) {
+ console.error('解析设置失败:', e)
+ }
+ }
+ }
+
+ const handlePresetChange = async (presetId: number) => {
+ try {
+ const settings = conversation.settings
+ ? (typeof conversation.settings === 'string' ? JSON.parse(conversation.settings) : conversation.settings)
+ : {}
+
+ settings.presetId = presetId
+
+ await conversationApi.updateConversationSettings(conversation.id, settings)
+ setSelectedPresetId(presetId)
+ setShowPresetSelector(false)
+
+ const convResp = await conversationApi.getConversationById(conversation.id)
+ onConversationUpdate(convResp.data)
+ } catch (err) {
+ console.error('更新预设失败:', err)
+ alert('更新失败,请重试')
+ }
+ }
+
+ const handleModelChange = async (configId: number) => {
+ try {
+ const settings = conversation.settings
+ ? (typeof conversation.settings === 'string' ? JSON.parse(conversation.settings) : conversation.settings)
+ : {}
+
+ settings.aiConfigId = configId
+
+ await conversationApi.updateConversationSettings(conversation.id, settings)
+ setSelectedConfigId(configId)
+ setShowModelSelector(false)
+
+ const convResp = await conversationApi.getConversationById(conversation.id)
+ onConversationUpdate(convResp.data)
+ } catch (err) {
+ console.error('更新模型配置失败:', err)
+ alert('更新失败,请重试')
+ }
+ }
+
+ const scrollToBottom = () => {
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
+ }
+
+ const handleSend = async () => {
+ // 防止重复发送
+ if (!inputValue.trim() || sending) return
+
+ const userMessage = inputValue.trim()
+
+ // 立即清空输入框和设置发送状态,防止重复触发
+ setInputValue('')
+ setSending(true)
+
+ // 立即显示用户消息
+ const tempUserMessage: Message = {
+ id: Date.now(),
+ conversationId: conversation.id,
+ role: 'user',
+ content: userMessage,
+ tokenCount: 0,
+ createdAt: new Date().toISOString(),
+ }
+ setMessages((prev) => [...prev, tempUserMessage])
+
+ // 创建临时AI消息用于流式显示
+ const tempAIMessage: Message = {
+ id: Date.now() + 1,
+ conversationId: conversation.id,
+ role: 'assistant',
+ content: '',
+ tokenCount: 0,
+ createdAt: new Date().toISOString(),
+ }
+
+ try {
+ if (streamEnabled) {
+ // 流式传输
+ console.log('[Stream] 开始流式传输...')
+ setMessages((prev) => [...prev, tempAIMessage])
+
+ const response = await fetch(
+ `${import.meta.env.VITE_API_BASE_URL || '/api'}/app/conversation/${conversation.id}/message?stream=true`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`,
+ },
+ body: JSON.stringify({ content: userMessage }),
+ }
+ )
+
+ if (!response.ok) {
+ throw new Error('流式传输失败')
+ }
+
+ console.log('[Stream] 连接成功,开始接收数据...')
+ const reader = response.body?.getReader()
+ const decoder = new TextDecoder()
+
+ if (reader) {
+ let fullContent = ''
+ let buffer = ''
+
+ while (true) {
+ const { done, value } = await reader.read()
+ if (done) {
+ console.log('[Stream] 传输完成')
+ break
+ }
+
+ buffer += decoder.decode(value, { stream: true })
+ const lines = buffer.split('\n')
+
+ // 保留最后一行(可能不完整)
+ buffer = lines.pop() || ''
+
+ let currentEvent = ''
+ for (const line of lines) {
+ if (line.startsWith('event: ')) {
+ currentEvent = line.slice(7).trim()
+ console.log('[Stream] 事件类型:', currentEvent)
+ } else if (line.startsWith('data: ')) {
+ const data = line.slice(6).trim()
+
+ if (currentEvent === 'message') {
+ // 消息内容 - 后端现在发送的是纯文本,不再是JSON
+ fullContent += data
+ console.log('[Stream] 接收内容片段:', data)
+ // 实时更新临时AI消息的内容
+ setMessages((prev) =>
+ prev.map((m) =>
+ m.id === tempAIMessage.id ? { ...m, content: fullContent } : m
+ )
+ )
+ } else if (currentEvent === 'done') {
+ // 流式传输完成
+ console.log('[Stream] 收到完成信号,重新加载消息')
+ await loadMessages()
+ break
+ } else if (currentEvent === 'error') {
+ // 错误处理
+ console.error('[Stream] 错误:', data)
+ throw new Error(data)
+ }
+ currentEvent = ''
+ }
+ }
+ }
+ }
+
+ // 更新对话信息
+ const convResp = await conversationApi.getConversationById(conversation.id)
+ onConversationUpdate(convResp.data)
+ } else {
+ // 普通传输
+ const response = await conversationApi.sendMessage(conversation.id, {
+ content: userMessage,
+ })
+
+ // 更新消息列表(包含AI回复)
+ await loadMessages()
+
+ // 更新对话信息
+ const convResp = await conversationApi.getConversationById(conversation.id)
+ onConversationUpdate(convResp.data)
+ }
+ } catch (err: any) {
+ console.error('发送消息失败:', err)
+ alert(err.response?.data?.msg || '发送消息失败,请重试')
+ // 移除临时消息
+ setMessages((prev) => prev.filter((m) => m.id !== tempUserMessage.id && m.id !== tempAIMessage.id))
+ } finally {
+ setSending(false)
+ }
+ }
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter' && !e.shiftKey && !sending && inputValue.trim()) {
+ e.preventDefault()
+ handleSend()
+ }
+ }
+
+ const handleCopyMessage = (content: string, id: number) => {
+ navigator.clipboard.writeText(content)
+ setCopiedId(id)
+ setTimeout(() => setCopiedId(null), 2000)
+ }
+
+ const handleRegenerateResponse = async () => {
+ if (messages.length === 0 || sending) return
+
+ // 找到最后一条用户消息
+ const lastUserMessage = [...messages].reverse().find(m => m.role === 'user')
+ if (!lastUserMessage) return
+
+ setSending(true)
+ try {
+ await conversationApi.sendMessage(conversation.id, {
+ content: lastUserMessage.content,
+ })
+ await loadMessages()
+ const convResp = await conversationApi.getConversationById(conversation.id)
+ onConversationUpdate(convResp.data)
+ } catch (err) {
+ console.error('重新生成失败:', err)
+ alert('重新生成失败,请重试')
+ } finally {
+ setSending(false)
+ }
+ }
+
+ const handleDeleteConversation = async () => {
+ if (!confirm('确定要删除这个对话吗?')) return
+
+ try {
+ await conversationApi.deleteConversation(conversation.id)
+ window.location.href = '/chat'
+ } catch (err) {
+ console.error('删除对话失败:', err)
+ alert('删除失败')
+ }
+ }
+
+ const handleExportConversation = () => {
+ const content = messages
+ .map((msg) => `[${msg.role}] ${msg.content}`)
+ .join('\n\n')
+ const blob = new Blob([content], { type: 'text/plain' })
+ const url = URL.createObjectURL(blob)
+ const a = document.createElement('a')
+ a.href = url
+ a.download = `${conversation.title}.txt`
+ a.click()
+ URL.revokeObjectURL(url)
+ }
+
+ const formatTime = (dateString: string) => {
+ const date = new Date(dateString)
+ const now = new Date()
+ const diff = now.getTime() - date.getTime()
+ const minutes = Math.floor(diff / 60000)
+ const hours = Math.floor(diff / 3600000)
+ const days = Math.floor(diff / 86400000)
+
+ if (minutes < 1) return '刚刚'
+ if (minutes < 60) return `${minutes}分钟前`
+ if (hours < 24) return `${hours}小时前`
+ if (days < 7) return `${days}天前`
+ return date.toLocaleDateString('zh-CN')
+ }
+
+ return (
+
+
+
+
+
{conversation.title}
+
+ 与 {character.name} 对话中
+
+
+
+
+
+ {showModelSelector && createPortal(
+
e.stopPropagation()}
+ >
+ {aiConfigs.length === 0 ? (
+
+ 暂无可用配置
+
+ ) : (
+ aiConfigs.map((config) => (
+
+ ))
+ )}
+
,
+ document.body
+ )}
+
+
+ {/* 预设选择器 */}
+
+
+ {showPresetSelector && createPortal(
+
e.stopPropagation()}
+ >
+ {presets.length === 0 ? (
+
+ 暂无可用预设
+
+ ) : (
+ presets.map((preset) => (
+
+ ))
+ )}
+
,
+ document.body
+ )}
+
+
+
+
+
+
+ {showMenu && createPortal(
+
e.stopPropagation()}
+ >
+
+
+
,
+ document.body
+ )}
+
+
+
+
+
+
+ {loading ? (
+
加载消息中...
+ ) : messages.length === 0 ? (
+
+
还没有消息
+
发送第一条消息开始对话吧!
+
+ ) : (
+ messages.map((msg) => (
+
+
+ {msg.role === 'assistant' && (
+
+ {character.avatar && (
+

+ )}
+
{character.name}
+
+ )}
+
{
+ setInputValue(choice)
+ // 自动聚焦到输入框
+ textareaRef.current?.focus()
+ }}
+ />
+
+ {formatTime(msg.createdAt)}
+
+
+
+
+ ))
+ )}
+ {sending && (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 消息: {conversation.messageCount} | Token: {conversation.tokenCount}
+ {sending && AI 正在思考...}
+
+
+
+ )
+}
diff --git a/web-app/src/components/MessageContent.tsx b/web-app/src/components/MessageContent.tsx
new file mode 100644
index 0000000..740888b
--- /dev/null
+++ b/web-app/src/components/MessageContent.tsx
@@ -0,0 +1,341 @@
+import {useEffect, useRef, useState} from 'react'
+import {Code, Eye, Play} from 'lucide-react'
+
+interface MessageContentProps {
+ content: string
+ role: 'user' | 'assistant'
+ onChoiceSelect?: (choice: string) => void
+}
+
+interface Choice {
+ label: string
+ text: string
+}
+
+// 解析并高亮对白的组件
+function DialogueText({ text }: { text: string }) {
+ // 匹配各种引号格式的对白
+ const dialogueRegex = /([""「『])(.*?)([""」』])|(")(.*?)(")|(')(.*?)(')/g
+
+ const parts: JSX.Element[] = []
+ let lastIndex = 0
+ let match
+ let key = 0
+
+ while ((match = dialogueRegex.exec(text)) !== null) {
+ // 添加对白之前的文本
+ if (match.index > lastIndex) {
+ parts.push(
+
+ {text.substring(lastIndex, match.index)}
+
+ )
+ }
+
+ // 提取对白内容(处理不同的引号组)
+ const dialogue = match[2] || match[5] || match[8]
+ const openQuote = match[1] || match[4] || match[7]
+ const closeQuote = match[3] || match[6] || match[9]
+
+ // 添加高亮的对白
+ parts.push(
+
+ {openQuote}
+ {dialogue}
+ {closeQuote}
+
+ )
+
+ lastIndex = match.index + match[0].length
+ }
+
+ // 添加剩余的文本
+ if (lastIndex < text.length) {
+ parts.push(
+
+ {text.substring(lastIndex)}
+
+ )
+ }
+
+ return <>{parts.length > 0 ? parts : {text}}>
+}
+
+// 解析选择项的函数
+function parseChoices(content: string): { choices: Choice[]; cleanContent: string } {
+ // 匹配 ... 或 [choice]...[/choice] 格式
+ const choiceRegex = /(?:|\\[choice\\])([\s\S]*?)(?:<\/choice>|\\[\/choice\\])/i
+ const match = content.match(choiceRegex)
+
+ if (!match) {
+ // 如果没有标准格式,尝试从HTML中提取选择项
+ // 匹配类似 "1.xxx" 或 "A.xxx" 的列表项
+ const htmlChoiceRegex = /\s*(\d+|[A-Z])\s*[.、::]\s*([^<]+)/gi
+ const choices: Choice[] = []
+ let htmlMatch
+
+ while ((htmlMatch = htmlChoiceRegex.exec(content)) !== null) {
+ choices.push({
+ label: htmlMatch[1],
+ text: htmlMatch[2].trim()
+ })
+ }
+
+ if (choices.length > 0) {
+ return { choices, cleanContent: content }
+ }
+
+ return { choices: [], cleanContent: content }
+ }
+
+ const choiceBlock = match[1]
+ const choices: Choice[] = []
+
+ // 匹配 A. text, B. text 等格式
+ const optionRegex = /^([A-Z])[.、::]\s*(.+)$/gm
+ let optionMatch
+
+ while ((optionMatch = optionRegex.exec(choiceBlock)) !== null) {
+ choices.push({
+ label: optionMatch[1],
+ text: optionMatch[2].trim()
+ })
+ }
+
+ // 移除选择块,返回清理后的内容
+ const cleanContent = content.replace(choiceRegex, '').trim()
+
+ return { choices, cleanContent }
+}
+
+// 清理脚本输出内容
+function cleanScriptOutput(content: string): string {
+ // 移除 ... 块
+ let cleaned = content.replace(/[\s\S]*?<\/UpdateVariable>/gi, '')
+
+ // 移除 ... 块
+ cleaned = cleaned.replace(/[\s\S]*?<\/Analysis>/gi, '')
+
+ // 移除 _.set() 调用
+ cleaned = cleaned.replace(/^\s*_.set\([^)]+\);\s*$/gm, '')
+
+ return cleaned.trim()
+}
+
+// 解析状态面板数据
+function parseStatusPanel(content: string): { status: any; cleanContent: string } {
+ const statusRegex = /([\s\S]*?)<\/status_current_variable>/i
+ const match = content.match(statusRegex)
+
+ if (!match) {
+ return { status: null, cleanContent: content }
+ }
+
+ try {
+ const statusData = JSON.parse(match[1].trim())
+ const cleanContent = content.replace(statusRegex, '').trim()
+ return { status: statusData, cleanContent }
+ } catch (e) {
+ console.error('解析状态面板失败:', e)
+ return { status: null, cleanContent: content }
+ }
+}
+
+export default function MessageContent({ content, role, onChoiceSelect }: MessageContentProps) {
+ const [showRaw, setShowRaw] = useState(false)
+ const [hasHtml, setHasHtml] = useState(false)
+ const [hasScript, setHasScript] = useState(false)
+ const [allowScript, setAllowScript] = useState(true) // 默认启用脚本
+ const [choices, setChoices] = useState([])
+ const [displayContent, setDisplayContent] = useState(content)
+ const [statusPanel, setStatusPanel] = useState(null)
+ const iframeRef = useRef(null)
+
+ useEffect(() => {
+ console.log('[MessageContent] 原始内容:', content)
+
+ // 解析状态面板
+ const { status, cleanContent: contentAfterStatus } = parseStatusPanel(content)
+ console.log('[MessageContent] 状态面板:', status)
+ setStatusPanel(status)
+
+ // 解析选择项
+ const { choices: parsedChoices, cleanContent } = parseChoices(contentAfterStatus)
+ console.log('[MessageContent] 选择项:', parsedChoices)
+ setChoices(parsedChoices)
+
+ // 清理脚本输出
+ const finalContent = cleanScriptOutput(cleanContent)
+ console.log('[MessageContent] 清理后内容:', finalContent)
+ setDisplayContent(finalContent)
+
+ // 检测内容类型
+ const htmlRegex = /<[^>]+>/g
+ const scriptRegex = /