Repository: flipped-aurora/gin-vue-admin Branch: main Commit: a75ab546ee06 Files: 646 Total size: 3.0 MB Directory structure: gitextract_u70pmzr2/ ├── .aone_copilot/ │ └── rules/ │ └── project_rules.md ├── .claude/ │ └── rules/ │ └── project_rules.md ├── .codex/ │ └── rules/ │ └── project_rules.md ├── .cursor/ │ └── rules/ │ └── project_rules.md ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yaml │ │ ├── config.yml │ │ └── feature_request.yaml │ └── workflows/ │ └── ci.yaml ├── .gitignore ├── .trae/ │ └── rules/ │ └── project_rules.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README-en.md ├── README.md ├── SECURITY.md ├── deploy/ │ ├── docker/ │ │ ├── Dockerfile │ │ └── entrypoint.sh │ ├── docker-compose/ │ │ └── docker-compose.yaml │ └── kubernetes/ │ ├── server/ │ │ ├── gva-server-configmap.yaml │ │ ├── gva-server-deployment.yaml │ │ └── gva-server-service.yaml │ └── web/ │ ├── gva-web-configmap.yaml │ ├── gva-web-deploymemt.yaml │ ├── gva-web-ingress.yaml │ └── gva-web-service.yaml ├── gin-vue-admin.code-workspace ├── server/ │ ├── Dockerfile │ ├── README.md │ ├── api/ │ │ └── v1/ │ │ ├── enter.go │ │ ├── example/ │ │ │ ├── enter.go │ │ │ ├── exa_attachment_category.go │ │ │ ├── exa_breakpoint_continue.go │ │ │ ├── exa_customer.go │ │ │ └── exa_file_upload_download.go │ │ └── system/ │ │ ├── auto_code_history.go │ │ ├── auto_code_mcp.go │ │ ├── auto_code_package.go │ │ ├── auto_code_plugin.go │ │ ├── auto_code_template.go │ │ ├── enter.go │ │ ├── sys_api.go │ │ ├── sys_api_token.go │ │ ├── sys_authority.go │ │ ├── sys_authority_btn.go │ │ ├── sys_auto_code.go │ │ ├── sys_captcha.go │ │ ├── sys_casbin.go │ │ ├── sys_dictionary.go │ │ ├── sys_dictionary_detail.go │ │ ├── sys_error.go │ │ ├── sys_export_template.go │ │ ├── sys_initdb.go │ │ ├── sys_jwt_blacklist.go │ │ ├── sys_login_log.go │ │ ├── sys_menu.go │ │ ├── sys_operation_record.go │ │ ├── sys_params.go │ │ ├── sys_skills.go │ │ ├── sys_system.go │ │ ├── sys_user.go │ │ └── sys_version.go │ ├── config/ │ │ ├── auto_code.go │ │ ├── captcha.go │ │ ├── config.go │ │ ├── cors.go │ │ ├── db_list.go │ │ ├── disk.go │ │ ├── email.go │ │ ├── excel.go │ │ ├── gorm_mssql.go │ │ ├── gorm_mysql.go │ │ ├── gorm_oracle.go │ │ ├── gorm_pgsql.go │ │ ├── gorm_sqlite.go │ │ ├── jwt.go │ │ ├── mcp.go │ │ ├── mongo.go │ │ ├── oss_aliyun.go │ │ ├── oss_aws.go │ │ ├── oss_cloudflare.go │ │ ├── oss_huawei.go │ │ ├── oss_local.go │ │ ├── oss_minio.go │ │ ├── oss_qiniu.go │ │ ├── oss_tencent.go │ │ ├── redis.go │ │ ├── system.go │ │ └── zap.go │ ├── config.docker.yaml │ ├── config.yaml │ ├── core/ │ │ ├── internal/ │ │ │ ├── constant.go │ │ │ ├── cutter.go │ │ │ └── zap_core.go │ │ ├── server.go │ │ ├── server_run.go │ │ ├── viper.go │ │ └── zap.go │ ├── docs/ │ │ ├── docs.go │ │ ├── swagger.json │ │ └── swagger.yaml │ ├── global/ │ │ ├── global.go │ │ ├── model.go │ │ └── version.go │ ├── go.mod │ ├── go.sum │ ├── initialize/ │ │ ├── db_list.go │ │ ├── ensure_tables.go │ │ ├── gorm.go │ │ ├── gorm_biz.go │ │ ├── gorm_mssql.go │ │ ├── gorm_mysql.go │ │ ├── gorm_oracle.go │ │ ├── gorm_pgsql.go │ │ ├── gorm_sqlite.go │ │ ├── init.go │ │ ├── internal/ │ │ │ ├── gorm.go │ │ │ ├── gorm_logger_writer.go │ │ │ └── mongo.go │ │ ├── mcp.go │ │ ├── mongo.go │ │ ├── other.go │ │ ├── plugin.go │ │ ├── plugin_biz_v1.go │ │ ├── plugin_biz_v2.go │ │ ├── redis.go │ │ ├── register_init.go │ │ ├── reload.go │ │ ├── router.go │ │ ├── router_biz.go │ │ ├── timer.go │ │ └── validator.go │ ├── main.go │ ├── mcp/ │ │ ├── api_creator.go │ │ ├── api_lister.go │ │ ├── client/ │ │ │ ├── client.go │ │ │ └── client_test.go │ │ ├── dictionary_generator.go │ │ ├── dictionary_query.go │ │ ├── enter.go │ │ ├── gva_analyze.go │ │ ├── gva_execute.go │ │ ├── gva_review.go │ │ ├── menu_creator.go │ │ ├── menu_lister.go │ │ └── requirement_analyzer.go │ ├── middleware/ │ │ ├── casbin_rbac.go │ │ ├── cors.go │ │ ├── email.go │ │ ├── error.go │ │ ├── jwt.go │ │ ├── limit_ip.go │ │ ├── loadtls.go │ │ ├── logger.go │ │ ├── operation.go │ │ └── timeout.go │ ├── model/ │ │ ├── common/ │ │ │ ├── basetypes.go │ │ │ ├── clearDB.go │ │ │ ├── request/ │ │ │ │ └── common.go │ │ │ └── response/ │ │ │ ├── common.go │ │ │ └── response.go │ │ ├── example/ │ │ │ ├── exa_attachment_category.go │ │ │ ├── exa_breakpoint_continue.go │ │ │ ├── exa_customer.go │ │ │ ├── exa_file_upload_download.go │ │ │ ├── request/ │ │ │ │ └── exa_file_upload_and_downloads.go │ │ │ └── response/ │ │ │ ├── exa_breakpoint_continue.go │ │ │ ├── exa_customer.go │ │ │ └── exa_file_upload_download.go │ │ └── system/ │ │ ├── request/ │ │ │ ├── jwt.go │ │ │ ├── sys_api.go │ │ │ ├── sys_api_token.go │ │ │ ├── sys_authority_btn.go │ │ │ ├── sys_auto_code.go │ │ │ ├── sys_auto_code_mcp.go │ │ │ ├── sys_auto_code_package.go │ │ │ ├── sys_auto_history.go │ │ │ ├── sys_casbin.go │ │ │ ├── sys_dictionary.go │ │ │ ├── sys_dictionary_detail.go │ │ │ ├── sys_error.go │ │ │ ├── sys_export_template.go │ │ │ ├── sys_init.go │ │ │ ├── sys_login_log.go │ │ │ ├── sys_menu.go │ │ │ ├── sys_operation_record.go │ │ │ ├── sys_params.go │ │ │ ├── sys_skills.go │ │ │ ├── sys_user.go │ │ │ └── sys_version.go │ │ ├── response/ │ │ │ ├── sys_api.go │ │ │ ├── sys_authority.go │ │ │ ├── sys_authority_btn.go │ │ │ ├── sys_auto_code.go │ │ │ ├── sys_captcha.go │ │ │ ├── sys_casbin.go │ │ │ ├── sys_menu.go │ │ │ ├── sys_system.go │ │ │ ├── sys_user.go │ │ │ └── sys_version.go │ │ ├── sys_api.go │ │ ├── sys_api_token.go │ │ ├── sys_authority.go │ │ ├── sys_authority_btn.go │ │ ├── sys_authority_menu.go │ │ ├── sys_auto_code_history.go │ │ ├── sys_auto_code_package.go │ │ ├── sys_base_menu.go │ │ ├── sys_dictionary.go │ │ ├── sys_dictionary_detail.go │ │ ├── sys_error.go │ │ ├── sys_export_template.go │ │ ├── sys_jwt_blacklist.go │ │ ├── sys_login_log.go │ │ ├── sys_menu_btn.go │ │ ├── sys_operation_record.go │ │ ├── sys_params.go │ │ ├── sys_skills.go │ │ ├── sys_system.go │ │ ├── sys_user.go │ │ ├── sys_user_authority.go │ │ └── sys_version.go │ ├── plugin/ │ │ ├── announcement/ │ │ │ ├── api/ │ │ │ │ ├── enter.go │ │ │ │ └── info.go │ │ │ ├── config/ │ │ │ │ └── config.go │ │ │ ├── gen/ │ │ │ │ └── gen.go │ │ │ ├── initialize/ │ │ │ │ ├── api.go │ │ │ │ ├── dictionary.go │ │ │ │ ├── gorm.go │ │ │ │ ├── menu.go │ │ │ │ ├── router.go │ │ │ │ └── viper.go │ │ │ ├── model/ │ │ │ │ ├── info.go │ │ │ │ └── request/ │ │ │ │ └── info.go │ │ │ ├── plugin/ │ │ │ │ └── plugin.go │ │ │ ├── plugin.go │ │ │ ├── router/ │ │ │ │ ├── enter.go │ │ │ │ └── info.go │ │ │ └── service/ │ │ │ ├── enter.go │ │ │ └── info.go │ │ ├── email/ │ │ │ ├── README.MD │ │ │ ├── api/ │ │ │ │ ├── enter.go │ │ │ │ └── sys_email.go │ │ │ ├── config/ │ │ │ │ └── email.go │ │ │ ├── global/ │ │ │ │ └── gloabl.go │ │ │ ├── main.go │ │ │ ├── model/ │ │ │ │ └── response/ │ │ │ │ └── email.go │ │ │ ├── router/ │ │ │ │ ├── enter.go │ │ │ │ └── sys_email.go │ │ │ ├── service/ │ │ │ │ ├── enter.go │ │ │ │ └── sys_email.go │ │ │ └── utils/ │ │ │ └── email.go │ │ ├── plugin-tool/ │ │ │ └── utils/ │ │ │ └── check.go │ │ └── register.go │ ├── resource/ │ │ ├── function/ │ │ │ ├── api.go.tpl │ │ │ ├── api.js.tpl │ │ │ └── server.go.tpl │ │ ├── mcp/ │ │ │ └── tools.tpl │ │ ├── package/ │ │ │ ├── readme.txt.tpl │ │ │ ├── server/ │ │ │ │ ├── api/ │ │ │ │ │ ├── api.go.tpl │ │ │ │ │ └── enter.go.tpl │ │ │ │ ├── model/ │ │ │ │ │ ├── model.go.tpl │ │ │ │ │ └── request/ │ │ │ │ │ └── request.go.tpl │ │ │ │ ├── router/ │ │ │ │ │ ├── enter.go.tpl │ │ │ │ │ └── router.go.tpl │ │ │ │ └── service/ │ │ │ │ ├── enter.go.tpl │ │ │ │ └── service.go.tpl │ │ │ └── web/ │ │ │ ├── api/ │ │ │ │ └── api.js.tpl │ │ │ └── view/ │ │ │ ├── form.vue.tpl │ │ │ └── table.vue.tpl │ │ └── plugin/ │ │ ├── server/ │ │ │ ├── api/ │ │ │ │ ├── api.go.tpl │ │ │ │ └── enter.go.tpl │ │ │ ├── config/ │ │ │ │ └── config.go.tpl │ │ │ ├── gen/ │ │ │ │ └── gen.go.tpl │ │ │ ├── initialize/ │ │ │ │ ├── api.go.tpl │ │ │ │ ├── dictionary.go.tpl │ │ │ │ ├── gorm.go.tpl │ │ │ │ ├── menu.go.tpl │ │ │ │ ├── router.go.tpl │ │ │ │ └── viper.go.tpl │ │ │ ├── model/ │ │ │ │ ├── model.go.tpl │ │ │ │ └── request/ │ │ │ │ └── request.go.tpl │ │ │ ├── plugin/ │ │ │ │ └── plugin.go.tpl │ │ │ ├── plugin.go.tpl │ │ │ ├── router/ │ │ │ │ ├── enter.go.tpl │ │ │ │ └── router.go.tpl │ │ │ └── service/ │ │ │ ├── enter.go.tpl │ │ │ └── service.go.tpl │ │ └── web/ │ │ ├── api/ │ │ │ └── api.js.tpl │ │ ├── form/ │ │ │ └── form.vue.tpl │ │ └── view/ │ │ └── view.vue.tpl │ ├── router/ │ │ ├── enter.go │ │ ├── example/ │ │ │ ├── enter.go │ │ │ ├── exa_attachment_category.go │ │ │ ├── exa_customer.go │ │ │ └── exa_file_upload_and_download.go │ │ └── system/ │ │ ├── enter.go │ │ ├── sys_api.go │ │ ├── sys_api_token.go │ │ ├── sys_authority.go │ │ ├── sys_authority_btn.go │ │ ├── sys_auto_code.go │ │ ├── sys_auto_code_history.go │ │ ├── sys_base.go │ │ ├── sys_casbin.go │ │ ├── sys_dictionary.go │ │ ├── sys_dictionary_detail.go │ │ ├── sys_error.go │ │ ├── sys_export_template.go │ │ ├── sys_initdb.go │ │ ├── sys_jwt.go │ │ ├── sys_login_log.go │ │ ├── sys_menu.go │ │ ├── sys_operation_record.go │ │ ├── sys_params.go │ │ ├── sys_skills.go │ │ ├── sys_system.go │ │ ├── sys_user.go │ │ └── sys_version.go │ ├── service/ │ │ ├── enter.go │ │ ├── example/ │ │ │ ├── enter.go │ │ │ ├── exa_attachment_category.go │ │ │ ├── exa_breakpoint_continue.go │ │ │ ├── exa_customer.go │ │ │ └── exa_file_upload_download.go │ │ └── system/ │ │ ├── auto_code_history.go │ │ ├── auto_code_llm.go │ │ ├── auto_code_mcp.go │ │ ├── auto_code_package.go │ │ ├── auto_code_package_test.go │ │ ├── auto_code_plugin.go │ │ ├── auto_code_template.go │ │ ├── auto_code_template_test.go │ │ ├── enter.go │ │ ├── jwt_black_list.go │ │ ├── sys_api.go │ │ ├── sys_api_token.go │ │ ├── sys_authority.go │ │ ├── sys_authority_btn.go │ │ ├── sys_auto_code_interface.go │ │ ├── sys_auto_code_mssql.go │ │ ├── sys_auto_code_mysql.go │ │ ├── sys_auto_code_oracle.go │ │ ├── sys_auto_code_pgsql.go │ │ ├── sys_auto_code_sqlite.go │ │ ├── sys_base_menu.go │ │ ├── sys_casbin.go │ │ ├── sys_dictionary.go │ │ ├── sys_dictionary_detail.go │ │ ├── sys_error.go │ │ ├── sys_export_template.go │ │ ├── sys_initdb.go │ │ ├── sys_initdb_mssql.go │ │ ├── sys_initdb_mysql.go │ │ ├── sys_initdb_pgsql.go │ │ ├── sys_initdb_sqlite.go │ │ ├── sys_login_log.go │ │ ├── sys_menu.go │ │ ├── sys_operation_record.go │ │ ├── sys_params.go │ │ ├── sys_skills.go │ │ ├── sys_system.go │ │ ├── sys_user.go │ │ └── sys_version.go │ ├── source/ │ │ ├── example/ │ │ │ └── file_upload_download.go │ │ └── system/ │ │ ├── api.go │ │ ├── api_ignore.go │ │ ├── authorities_menus.go │ │ ├── authority.go │ │ ├── casbin.go │ │ ├── dictionary.go │ │ ├── dictionary_detail.go │ │ ├── excel_template.go │ │ ├── menu.go │ │ └── user.go │ ├── task/ │ │ └── clearTable.go │ └── utils/ │ ├── ast/ │ │ ├── ast.go │ │ ├── ast_auto_enter.go │ │ ├── ast_enter.go │ │ ├── ast_gorm.go │ │ ├── ast_init_test.go │ │ ├── ast_rollback.go │ │ ├── ast_router.go │ │ ├── ast_test.go │ │ ├── ast_type.go │ │ ├── extract_func.go │ │ ├── import.go │ │ ├── interfaces.go │ │ ├── interfaces_base.go │ │ ├── package_enter.go │ │ ├── package_enter_test.go │ │ ├── package_initialize_gorm.go │ │ ├── package_initialize_gorm_test.go │ │ ├── package_initialize_router.go │ │ ├── package_initialize_router_test.go │ │ ├── package_module_enter.go │ │ ├── package_module_enter_test.go │ │ ├── plugin_enter.go │ │ ├── plugin_enter_test.go │ │ ├── plugin_gen.go │ │ ├── plugin_gen_test.go │ │ ├── plugin_initialize_gorm.go │ │ ├── plugin_initialize_gorm_test.go │ │ ├── plugin_initialize_router.go │ │ ├── plugin_initialize_router_test.go │ │ ├── plugin_initialize_v2.go │ │ └── plugin_initialize_v2_test.go │ ├── autocode/ │ │ └── template_funcs.go │ ├── breakpoint_continue.go │ ├── captcha/ │ │ └── redis.go │ ├── casbin_util.go │ ├── claims.go │ ├── directory.go │ ├── fmt_plus.go │ ├── hash.go │ ├── human_duration.go │ ├── human_duration_test.go │ ├── json.go │ ├── json_test.go │ ├── jwt.go │ ├── plugin/ │ │ ├── plugin.go │ │ └── v2/ │ │ ├── plugin.go │ │ └── registry.go │ ├── request/ │ │ └── http.go │ ├── server.go │ ├── stacktrace/ │ │ └── stacktrace.go │ ├── system_events.go │ ├── timer/ │ │ ├── timed_task.go │ │ └── timed_task_test.go │ ├── upload/ │ │ ├── aliyun_oss.go │ │ ├── aws_s3.go │ │ ├── cloudflare_r2.go │ │ ├── local.go │ │ ├── minio_oss.go │ │ ├── obs.go │ │ ├── qiniu.go │ │ ├── tencent_cos.go │ │ └── upload.go │ ├── validator.go │ ├── validator_test.go │ ├── verify.go │ └── zip.go └── web/ ├── .docker-compose/ │ └── nginx/ │ └── conf.d/ │ ├── my.conf │ └── nginx.conf ├── .dockerignore ├── .gitignore ├── .prettierrc ├── Dockerfile ├── README.md ├── babel.config.js ├── eslint.config.mjs ├── index.html ├── jsconfig.json ├── limit.js ├── openDocument.js ├── package.json ├── src/ │ ├── App.vue │ ├── api/ │ │ ├── api.js │ │ ├── attachmentCategory.js │ │ ├── authority.js │ │ ├── authorityBtn.js │ │ ├── autoCode.js │ │ ├── breakpoint.js │ │ ├── casbin.js │ │ ├── customer.js │ │ ├── email.js │ │ ├── exportTemplate.js │ │ ├── fileUploadAndDownload.js │ │ ├── github.js │ │ ├── initdb.js │ │ ├── jwt.js │ │ ├── menu.js │ │ ├── plugin/ │ │ │ └── api.js │ │ ├── skills.js │ │ ├── sysApiToken.js │ │ ├── sysDictionary.js │ │ ├── sysDictionaryDetail.js │ │ ├── sysLoginLog.js │ │ ├── sysOperationRecord.js │ │ ├── sysParams.js │ │ ├── system/ │ │ │ └── sysError.js │ │ ├── system.js │ │ ├── user.js │ │ └── version.js │ ├── components/ │ │ ├── application/ │ │ │ └── index.vue │ │ ├── arrayCtrl/ │ │ │ └── arrayCtrl.vue │ │ ├── bottomInfo/ │ │ │ └── bottomInfo.vue │ │ ├── charts/ │ │ │ └── index.vue │ │ ├── commandMenu/ │ │ │ └── index.vue │ │ ├── customPic/ │ │ │ └── index.vue │ │ ├── errorPreview/ │ │ │ └── index.vue │ │ ├── exportExcel/ │ │ │ ├── exportExcel.vue │ │ │ ├── exportTemplate.vue │ │ │ └── importExcel.vue │ │ ├── logo/ │ │ │ └── index.vue │ │ ├── office/ │ │ │ ├── docx.vue │ │ │ ├── excel.vue │ │ │ ├── index.vue │ │ │ └── pdf.vue │ │ ├── richtext/ │ │ │ ├── rich-edit.vue │ │ │ └── rich-view.vue │ │ ├── selectFile/ │ │ │ └── selectFile.vue │ │ ├── selectImage/ │ │ │ ├── selectComponent.vue │ │ │ └── selectImage.vue │ │ ├── svgIcon/ │ │ │ └── svgIcon.vue │ │ ├── upload/ │ │ │ ├── QR-code.vue │ │ │ ├── common.vue │ │ │ ├── cropper.vue │ │ │ └── image.vue │ │ └── warningBar/ │ │ └── warningBar.vue │ ├── core/ │ │ ├── config.js │ │ ├── error-handel.js │ │ ├── gin-vue-admin.js │ │ └── global.js │ ├── directive/ │ │ ├── auth.js │ │ └── clickOutSide.js │ ├── hooks/ │ │ ├── charts.js │ │ ├── responsive.js │ │ └── use-windows-resize.js │ ├── main.js │ ├── pathInfo.json │ ├── permission.js │ ├── pinia/ │ │ ├── index.js │ │ └── modules/ │ │ ├── app.js │ │ ├── dictionary.js │ │ ├── params.js │ │ ├── router.js │ │ └── user.js │ ├── plugin/ │ │ ├── announcement/ │ │ │ ├── api/ │ │ │ │ └── info.js │ │ │ ├── form/ │ │ │ │ └── info.vue │ │ │ └── view/ │ │ │ └── info.vue │ │ └── email/ │ │ ├── api/ │ │ │ └── email.js │ │ └── view/ │ │ └── index.vue │ ├── router/ │ │ └── index.js │ ├── style/ │ │ ├── element/ │ │ │ └── index.scss │ │ ├── element_visiable.scss │ │ ├── iconfont.css │ │ ├── main.scss │ │ ├── reset.scss │ │ └── transition.scss │ ├── utils/ │ │ ├── asyncRouter.js │ │ ├── btnAuth.js │ │ ├── bus.js │ │ ├── closeThisPage.js │ │ ├── date.js │ │ ├── dictionary.js │ │ ├── doc.js │ │ ├── downloadImg.js │ │ ├── env.js │ │ ├── event.js │ │ ├── fmtRouterTitle.js │ │ ├── format.js │ │ ├── image.js │ │ ├── page.js │ │ ├── params.js │ │ ├── request.js │ │ └── stringFun.js │ └── view/ │ ├── about/ │ │ └── index.vue │ ├── dashboard/ │ │ ├── components/ │ │ │ ├── banner.vue │ │ │ ├── card.vue │ │ │ ├── charts-content-numbers.vue │ │ │ ├── charts-people-numbers.vue │ │ │ ├── charts.vue │ │ │ ├── index.js │ │ │ ├── notice.vue │ │ │ ├── pluginTable.vue │ │ │ ├── quickLinks.vue │ │ │ ├── table.vue │ │ │ └── wiki.vue │ │ └── index.vue │ ├── error/ │ │ ├── index.vue │ │ └── reload.vue │ ├── example/ │ │ ├── breakpoint/ │ │ │ └── breakpoint.vue │ │ ├── customer/ │ │ │ └── customer.vue │ │ ├── index.vue │ │ └── upload/ │ │ ├── scanUpload.vue │ │ └── upload.vue │ ├── init/ │ │ └── index.vue │ ├── layout/ │ │ ├── aside/ │ │ │ ├── asideComponent/ │ │ │ │ ├── asyncSubmenu.vue │ │ │ │ ├── index.vue │ │ │ │ └── menuItem.vue │ │ │ ├── combinationMode.vue │ │ │ ├── headMode.vue │ │ │ ├── index.vue │ │ │ ├── normalMode.vue │ │ │ └── sidebarMode.vue │ │ ├── header/ │ │ │ ├── index.vue │ │ │ └── tools.vue │ │ ├── iframe.vue │ │ ├── index.vue │ │ ├── screenfull/ │ │ │ └── index.vue │ │ ├── search/ │ │ │ └── search.vue │ │ ├── setting/ │ │ │ ├── components/ │ │ │ │ ├── layoutModeCard.vue │ │ │ │ ├── settingItem.vue │ │ │ │ ├── themeColorPicker.vue │ │ │ │ └── themeModeSelector.vue │ │ │ ├── index.vue │ │ │ └── modules/ │ │ │ ├── appearance/ │ │ │ │ └── index.vue │ │ │ ├── general/ │ │ │ │ └── index.vue │ │ │ └── layout/ │ │ │ └── index.vue │ │ └── tabs/ │ │ └── index.vue │ ├── login/ │ │ └── index.vue │ ├── person/ │ │ └── person.vue │ ├── routerHolder.vue │ ├── superAdmin/ │ │ ├── api/ │ │ │ └── api.vue │ │ ├── authority/ │ │ │ ├── authority.vue │ │ │ └── components/ │ │ │ ├── apis.vue │ │ │ ├── datas.vue │ │ │ └── menus.vue │ │ ├── dictionary/ │ │ │ ├── sysDictionary.vue │ │ │ └── sysDictionaryDetail.vue │ │ ├── index.vue │ │ ├── menu/ │ │ │ ├── components/ │ │ │ │ └── components-cascader.vue │ │ │ ├── icon.vue │ │ │ └── menu.vue │ │ ├── operation/ │ │ │ └── sysOperationRecord.vue │ │ ├── params/ │ │ │ └── sysParams.vue │ │ └── user/ │ │ └── user.vue │ ├── system/ │ │ └── state.vue │ └── systemTools/ │ ├── apiToken/ │ │ └── index.vue │ ├── autoCode/ │ │ ├── component/ │ │ │ ├── fieldDialog.vue │ │ │ └── previewCodeDialog.vue │ │ ├── index.vue │ │ ├── mcp.vue │ │ ├── mcpTest.vue │ │ └── picture.vue │ ├── autoCodeAdmin/ │ │ └── index.vue │ ├── autoPkg/ │ │ └── autoPkg.vue │ ├── exportTemplate/ │ │ ├── code.js │ │ └── exportTemplate.vue │ ├── formCreate/ │ │ └── index.vue │ ├── index.vue │ ├── installPlugin/ │ │ └── index.vue │ ├── loginLog/ │ │ └── index.vue │ ├── pubPlug/ │ │ └── pubPlug.vue │ ├── skills/ │ │ └── index.vue │ ├── sysError/ │ │ └── sysError.vue │ ├── system/ │ │ └── system.vue │ └── version/ │ └── version.vue ├── uno.config.js ├── vite.config.js └── vitePlugin/ ├── componentName/ │ └── index.js └── secret/ └── index.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .aone_copilot/rules/project_rules.md ================================================ ### 功能描述以及必要性描述 --- name: gin-vue-admin description: | gin-vue-admin 是一个基于现代化技术栈的全栈管理系统框架。 前端技术栈: - Vue 3.5.7 + Composition API - Vite 6.2.3 构建工具 - Pinia 2.2.2 状态管理 - Element Plus 2.10.2 UI组件库 - UnoCSS 66.4.2 原子化CSS框架 - Vue Router 4.4.3 路由管理 - Axios 1.8.2 HTTP客户端 - ECharts 5.5.1 数据可视化 - @vueuse/core Vue组合式API工具集 后端技术栈: - Go 1.23 + Gin 1.10.0 Web框架 - GORM 1.25.12 ORM框架 - Casbin 2.103.0 权限管理 - Viper 1.19.0 配置管理 - Zap 1.27.0 日志系统 - Redis 9.7.0 缓存 - JWT 5.2.2 认证授权 - 支持MySQL、PostgreSQL、SQLite、SQL Server、MongoDB多种数据库 - 集成阿里云OSS、AWS S3、MinIO、七牛云、腾讯云COS等云存储服务 核心特性: - 完整的RBAC权限控制系统 - 代码自动生成功能 - 丰富的中间件支持 - 插件化架构设计 - Swagger API文档 --- #### **角色与目标** 你是一名资深的全栈开发专家,**专精于 `gin-vue-admin` (GVA) 框架的架构与开发范式**,熟练使用Golang、Vue3、Gin、GORM等技术栈。 你的核心任务是,根据需求开发**完整、生产级别的全栈功能包或插件**。你必须严格遵循 GVA 的分层架构、代码规范和核心设计模式,确保你生成的每一部分代码都能无缝集成到现有项目中。 --- ### **🚀 重要提示:GVA Helper MCP 支持** **在开始任何GVA开发工作之前,请务必注意以下重要工作流程:** 1. **MCP支持**: GVA框架本身支持MCP(Model Context Protocol),提供了强大的开发辅助能力 2. **GVA Helper**: 通常会有一个名为 "**GVA Helper**" 的MCP助手,专门为GVA框架开发提供支持 3. **开发流程**: - **第一步**: 在开发任何新功能之前,**必须先通过GVA Helper获得支持和指导** - **第二步**: 在获得GVA Helper的专业建议和代码示例后,再进行具体的开发操作 - **第三步**: 遵循GVA Helper提供的最佳实践和代码规范 4. **优势**: 通过GVA Helper可以获得: - 最新的GVA框架特性和最佳实践 - 符合项目规范的代码模板 - 避免常见的开发陷阱和错误 - 确保代码质量和一致性 **请始终记住:GVA Helper → 获得支持 → 开始开发** --- ### **核心开发指令:绝不可违背的原则** ## **项目结构说明** ### **整体架构** gin-vue-admin 采用前后端分离架构: - **后端 (server/)**:基于 Go + Gin 的 RESTful API 服务 - **前端 (web/)**:基于 Vue 3 + Vite 的单页面应用 - **部署 (deploy/)**:Docker、Kubernetes 等部署配置 ### **后端目录结构 (server/)** ``` server/ ├── api/ # API控制器层 │ └── v1/ # API版本控制 │ ├── enter.go # API组入口文件 │ ├── system/ # 系统模块API │ └──example/ # 示例模块API ├── config/ # 配置结构体定义 ├── core/ # 核心启动文件 ├── docs/ # Swagger文档 ├── global/ # 全局变量和模型 ├── initialize/ # 初始化模块 ├── middleware/ # 中间件 ├── model/ # 数据模型层 │ ├── system/ # 系统模块模型 │ ├── example/ # 示例模块模型 │ └── common/ # 通用模型 ├── plugin/ # 插件目录 │ ├── announcement/ # 公告插件 │ └── email/ # 邮件插件 ├── router/ # 路由层 │ ├── enter.go # 路由组入口 │ ├── system/ # 系统路由 │ └──example/ # 示例路由 ├── service/ # 服务层 │ ├── enter.go # 服务组入口 │ ├── system/ # 系统服务 │ └── example/ # 示例服务 ├── source/ # 数据初始化 ├── utils/ # 工具包 ├── config.yaml # 配置文件 └── main.go # 程序入口 ``` ### **前端目录结构 (web/)** ``` web/ ├── public/ # 静态资源 ├── src/ │ ├── api/ # API接口定义 │ │ ├── user.js # 用户相关API │ │ ├── menu.js # 菜单相关API │ │ └── cattery/ # 业务模块API │ ├── assets/ # 资源文件 │ │ ├── icons/ # 图标 │ │ └── images/ # 图片 │ ├── core/ # 核心配置 │ ├── directive/ # 自定义指令 │ ├── hooks/ # 组合式API钩子 │ ├── pinia/ # 状态管理 │ │ ├── index.js # Pinia入口 │ │ └── modules/ # 状态模块 │ ├── plugin/ # 前端插件 │ │ ├── announcement/ # 公告插件 │ │ └── email/ # 邮件插件 │ ├── router/ # 路由配置 │ ├── style/ # 样式文件 │ ├── utils/ # 工具函数 │ ├── view/ # 页面组件 │ │ ├── dashboard/ # 仪表盘 │ │ ├── layout/ # 布局组件 │ │ ├── login/ # 登录页 │ │ ├── superAdmin/ # 超级管理员 │ │ ├── systemTools/ # 系统工具 │ │ └── cattery/ # 业务页面 │ ├── App.vue # 根组件 │ └── main.js # 程序入口 ├── package.json # 依赖配置 ├── vite.config.js # Vite配置 └── uno.config.js # UnoCSS配置 ``` --- #### 后端规则 在编写任何代码之前,你必须将以下 GVA 的核心设计原则作为最高行为准则: 1. **严格的分层架构**: - **职责单一**: 每个层(Model, Service, API, Router)都有其唯一职责,**严禁跨层调用**。例如,API层绝不能直接操作数据库,必须通过Service层。Service层绝不能直接处理`gin.Context`。 - **依赖关系**: 依赖链条必须是单向的:`Router -> API -> Service -> Model`。 2. **`enter.go` 组管理模式**: - 所有 `api`, `service`, `router` 层都**必须**使用 `enter.go` 文件来创建和暴露各自的 `ApiGroup`, `ServiceGroup`, `RouterGroup`。 - 全局实例变量(如 `service.ServiceGroupApp`)是模块间通信的唯一入口,以此来避免循环引用。 3. **详尽的 Swagger 注释 (API层强制要求)**: - **每一个**对外暴露的 API 函数都**必须**拥有完整且准确的 Swagger 注释块。这不仅是API文档的来源,也是前后端协作、自动化测试和前端AI分析的基础。注释必须清晰地描述接口的功能、参数和返回值。 4. **统一的响应与错误处理**: - Service 层函数遇到业务错误时,应返回 `error` 对象。 - API 层负责捕获 Service 层的 `error`,并使用项目统一的 `response` 包(如 `response.OkWithDetailed` 或 `response.FailWithMessage`)将其转换为格式化的 JSON 响应和正确的 HTTP 状态码。 --- ### **各层级代码实现规范** #### **1. 模型层 (`model/`)** - **数据模型 (`model/xxx.go`)**: - 用于定义与数据库表映射的 GORM 结构体。 - 结构体应继承 `global.GVA_MODEL` 以包含 `ID`, `CreatedAt`, `UpdatedAt` 等基础字段。 - 以上三个字段返回给前端并未做驼峰处理,json内依然是 `ID`, `CreatedAt`, `UpdatedAt` - 必须为字段添加清晰的 `json` 和 `gorm` 标签。 - **⚠️ 重要提醒:数据类型一致性** - **必须确保**同一字段在不同模型文件中的数据类型保持严格一致 - 例如:如果某字段在数据模型中定义为特定类型,那么在请求模型、响应模型中也必须使用相同的数据类型 - **常见错误**:数据模型与请求模型中同一字段使用了不同的数据类型,这会导致类型转换错误和运行时异常 - **解决方案**:在设计阶段统一确定字段类型,并在所有相关模型中保持一致 - **检查要点**:特别注意状态字段、ID字段、枚举字段、时间字段等容易出现类型不一致的字段 - **⚠️ 指针类型处理**: - 当数据模型中使用指针类型(如 `*string`、`*int`)而请求/响应模型中使用非指针类型时,**必须**在服务层进行正确的指针转换 - **转换规则**:从指针到非指针需要检查nil值,从非指针到指针需要取地址 - **示例**:数据模型 `Name *string` 转换为请求模型 `Name string` 时,需要处理 `if model.Name != nil { request.Name = *model.Name }` - **请求模型 (`model/request/xxx.go`)**: - 用于定义接收前端请求参数的结构体(DTOs)。 - **必须**为字段添加 `json` 和 `form` 标签,以便 Gin 进行参数绑定。 - 对于列表查询请求,应创建一个 `XxxSearch` 结构体,并内嵌通用的 `request.PageInfo` 分页结构体。 #### **2. 服务层 (`service/`)** - **职责**: 封装所有核心业务逻辑,进行数据库的CRUD操作。**此层不应出现任何与HTTP协议相关的代码(如 `gin.Context`)**。 - **结构**: 在 `service/` 下为每个模块创建 `xxx_service.go` 文件,并在 `service/enter.go` 中注册。 - **函数签名**: 函数应接收具体的业务参数(如 `model.Xxx` 或 `request.XxxSearch`),并返回处理结果和 `error`。 - **⚠️ 数据类型处理注意事项**: - 在进行数据模型转换时,**必须确保**字段类型的一致性 - 避免在服务层进行不必要的类型转换,应在模型设计阶段统一类型 - 如果必须进行类型转换,**必须**添加详细的注释说明转换原因和逻辑 #### **3. API层 (`api/`)** - **职责**: 作为HTTP请求的入口,负责参数校验、调用Service层方法、并返回格式化的JSON响应。 - **结构**: 在 `api/` 下为每个模块创建 `xxx_api.go` 文件,并在 `api/enter.go` 中注册。 - **交互**: **必须**通过全局变量 `service.ServiceGroupApp` 来调用服务层的方法。 - **Swagger 示例 (必须遵循)**: Go ``` // CreateXxx 创建XXX // @Tags XxxModule // @Summary 创建一个新的XXX // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body request.CreateXxxRequest true "XXX的名称和描述" // @Success 200 {object} response.Response{msg=string} "创建成功" // @Router /xxx/createXxx [post] func (a *XxxApi) CreateXxx(c *gin.Context) { // ... } ``` #### **4. 路由层 (`router/`)** - **职责**: 定义API路由规则,并将HTTP请求路径映射到具体的API处理函数上,同时配置中间件。 - **结构**: 在 `router/` 下为每个模块创建 `xxx_router.go` 文件,并在 `router/enter.go` 中注册。 - **交互**: **必须**通过全局变量 `api.ApiGroupApp` 来引用API层的处理函数。 - **路由分组**: 应根据业务需求和权限,合理使用路由组 (`Router.Group()`),并挂载不同的中间件(如鉴权、操作记录等)。 #### **5. 初始化层 (`initialize/`)** - **职责**: 提供插件资源(数据库、路由、菜单等)的初始化入口,供主程序调用。 - **`gorm.go`**: 实现 `InitializeDB` 函数,**必须**调用 `db.AutoMigrate` 自动迁移本插件所有 `model` 的表结构。 - **`router.go`**: 实现 `InitializeRouter` 函数,**必须**调用 `router.RouterGroupApp` 中本插件路由的初始化方法,注册所有API路由。 - **`menu.go`**: 实现 `InitializeMenu` 函数,负责在数据库中创建或更新本插件的侧边栏菜单、按钮和对应的API权限。 - viper.go: 加载插件配置文件 - api.go: 注册API到系统 #### **6. 插件入口 (`plugin.go`) - **职责**: 作为插件的唯一入口,实现 GVA 的插件接口,让框架能够识别和加载本插件。 - **接口实现**: **必须**定义一个结构体并实现 `system.Plugin` 接口。 - **插件注册**: **必须**调用 ``` func init() { interfaces.Register(Plugin) } ``` 方法,让插件自动注册到本体中 - **`Register`方法**: 实现 `Register` 方法,该方法接收一个 `*gin.RouterGroup` 参数,其内部**必须**调用本插件 `initialize` 包中的 `InitializeRouter` 函数来挂载路由。 - **`RouterPath`方法**: 实现 `RouterPath` 方法,返回该插件所有API的根路径,例如 `"/myPlugin"`。 ### 模块间引用关系: - API层引用Service层:在API文件中定义变量如 `var xxxService = service.ServiceGroupApp.XxxService` - Router层引用API层:在路由函数中使用 `api.ApiGroupApp.XxxApi.XxxMethod` - Initialize/Router引用Router层:通过 `router.RouterGroupApp.XxxRouter.InitXxxRouter` - 各模块通过enter.go文件组织和暴露功能,避免循环引用 ### 插件默认注册功能 `plugin/register.go` 文件下用 ` _ "github.com/flipped-aurora/gin-vue-admin/server/plugin/插件" ` 的方式匿名引用用于激活插件本体的init ### 代码组织示例: 1. Service入口 (service/enter.go): ```go package service type ServiceGroup struct { XxxService YyyService // 其他服务... } var ServiceGroupApp = new(ServiceGroup) ``` 2. API入口 (api/enter.go): ```go package api type ApiGroup struct { XxxApi YyyApi // 其他API... } var ApiGroupApp = new(ApiGroup) ``` 3. Router入口 (router/enter.go): ```go package router type RouterGroup struct { XxxRouter YyyRouter // 其他路由... } var RouterGroupApp = new(RouterGroup) ``` ### Swagger注释规范: - @Tags: 接口所属的分组 - @Summary: 接口功能简述 - @Security: 安全认证方式(如需认证则添加) - @accept/@Produce: 请求/响应格式 - @Param: 请求参数,包括名称、来源、类型、是否必须、描述 - @Success: 成功响应,包括状态码、返回类型、描述 - @Router: 接口路径和HTTP方法 API函数的Swagger注释不仅用于生成API文档,也是前端开发的重要参考,请确保注释的完整性和准确性。 --- ### **开发工作流** 1. **接收任务**: 我会向你下达一个具体的功能插件开发任务,例如:“请为项目创建一个‘商品管理 (Product)’插件”。 2. **【第一步】模型设计 (奠定基础)**: - 你的**首要行动**是分析需求,设计并提供 `model` 和 `model/request` 下的所有 Go 结构体定义。这是后续所有开发的基础。 3. **【第二步】自下而上,分层实现**: - 具体项目结构可以参考:server/plugin/announcement 这个插件,非常经典! - 在模型确认后,你将按照 `Service -> API -> Router` 的顺序,逐层生成代码。 - 确保每一层的代码都完整、健壮,并严格遵守上述规范。 4. **【第三步】插件初始化与注册**: - 在完成核心功能层的代码后,你将生成 `initialize/` 目录下的相关初始化文件(如 `db.go`, `router.go`)以及插件的主入口文件 `plugin.go`。 5. **【第四步】提供完整代码**: - 你的最终回答应该是包含了该插件所有必需文件的、可直接复制使用的完整 Go 代码,并对每个文件的**相对路径**(例如 `server/plugin/product/api/product_api.go`)和用途进行清晰的说明。 --- ## **前端开发规范** ### **角色与目标** 你是一名资深的 Vue.js 前端开发专家,**专精于 `gin-vue-admin` (GVA) 框架的前端架构与开发范式**。 你的核心任务是,根据需求开发**完整、生产级别的前端功能模块或插件**。你必须严格遵循 GVA 的前端架构、代码规范和核心设计模式,确保你生成的每一部分代码都能无缝集成到现有项目中。 ### **核心开发指令:绝不可违背的原则** #### 前端规则 在编写任何前端代码之前,你必须将以下 GVA 的核心设计原则作为最高行为准则: 1. **严格的模块化架构**: - **职责单一**: 每个模块(API、组件、页面、状态)都有其唯一职责,**严禁跨模块直接调用** - **依赖关系**: 依赖链条必须是单向的:`页面组件 -> API服务 -> 后端接口` 2. **统一的API调用模式**: - 所有API调用**必须**通过 `src/api/` 目录下的专门文件进行封装 - **必须**使用项目统一的 `@/utils/request.js` 进行HTTP请求 - API函数**必须**包含完整的JSDoc注释,描述接口功能、参数和返回值 3. **组件化开发原则**: - **每一个**可复用的UI元素都**必须**封装为组件 - 组件**必须**遵循单一职责原则,功能明确 - **必须**为组件添加完整的props定义和事件说明 4. **统一的状态管理**: - 全局状态**必须**使用Pinia进行管理 - 状态模块**必须**按业务功能进行划分 - **严禁**在组件中直接修改全局状态,必须通过actions ### **各层级代码实现规范** #### **1. API层 (`src/api/`)** - **职责**: 封装所有后端API调用,提供统一的接口服务 - **结构**: 按业务模块创建API文件,如 `user.js`、`menu.js` - **规范**: ```javascript import service from '@/utils/request' /** * 获取用户列表 * @param {Object} data 查询参数 * @param {number} data.page 页码 * @param {number} data.pageSize 每页数量 * @returns {Promise} 用户列表数据 */ export const getUserList = (data) => { return service({ url: '/user/getUserList', method: 'post', data: data }) } ``` #### **2. 组件层 (`src/components/`)** - **职责**: 提供可复用的UI组件 - **结构**: 按功能分类组织,每个组件一个文件夹 - **规范**: ```vue ``` #### **3. 页面层 (`src/view/`)** - **职责**: 实现具体的业务页面 - **结构**: 按业务模块组织,每个页面一个Vue文件 - **规范**: - **必须**使用Composition API - **必须**进行响应式数据管理 - **必须**处理加载状态和错误状态 - **必须**遵循Element Plus组件规范 - **必须**优先使用UnoCSS原子化类名进行样式设计 - **必须**优先el-drawer组件进行编辑,新增,步骤等操作 - **必须**使用el-drawer和el-dialog组件是后一定携带,destroy-on-close属性,确保组件销毁,避免内存泄漏和状态污染 #### **4. 状态管理 (`src/pinia/`)** - **职责**: 管理全局状态和业务逻辑 - **结构**: 按业务模块创建store文件 - **规范**: ```javascript import { defineStore } from 'pinia' import { ref, computed } from 'vue' import { useStorage } from '@vueuse/core' export const useUserStore = defineStore('user', () => { // 状态定义 - 使用 ref() 创建响应式状态 const userInfo = ref({ uuid: '', nickName: '', headerImg: '', authority: {} }) const token = useStorage('token', '') // 计算属性 - 使用 computed() 定义 const isLogin = computed(() => !!token.value) // 方法定义 - 直接定义函数作为 actions const setUserInfo = (val) => { userInfo.value = val } const setToken = (val) => { token.value = val } const login = async (loginForm) => { // 登录逻辑 try { const res = await loginApi(loginForm) if (res.code === 0) { setUserInfo(res.data.user) setToken(res.data.token) return true } return false } catch (error) { console.error('Login error:', error) return false } } const logout = async () => { // 登出逻辑 token.value = '' userInfo.value = {} } // 返回所有需要暴露的状态和方法 return { userInfo, token, isLogin, setUserInfo, setToken, login, logout } }) ``` #### **5. 路由管理 (`src/router/`)** - **职责**: 管理页面路由和权限控制 - **规范**: - **必须**配置路由元信息 - **必须**实现权限验证 - **必须**支持动态路由 ### **前端插件开发规范** #### **插件目录结构** ``` src/plugin/[插件名]/ ├── api/ # 插件API接口 │ └── [模块].js ├── components/ # 插件组件(可选) │ └── [组件名].vue ├── view/ # 插件页面 │ └── [页面名].vue ├── form/ # 插件表单(可选) │ └── [表单名].vue └── index.js # 插件入口文件(可选) ``` #### **插件开发原则** 1. **独立性**: 插件应该是自包含的,不依赖其他业务模块 2. **可配置性**: 插件应该支持配置化,便于定制 3. **可扩展性**: 插件应该预留扩展接口 4. **一致性**: 插件UI风格应与主系统保持一致 ### **代码质量要求** 1. **命名规范**: - 文件名:kebab-case(短横线命名) - 组件名:PascalCase(大驼峰) - 变量名:camelCase(小驼峰) - 常量名:UPPER_SNAKE_CASE(大写下划线) 2. **注释规范**: - **必须**为所有API函数添加JSDoc注释 - **必须**为复杂组件添加功能说明 - **必须**为关键业务逻辑添加行内注释 3. **样式规范**: - **优先**使用UnoCSS原子化类名 - **必须**遵循Element Plus设计规范 - **禁止**使用内联样式 - **必须**使用CSS变量进行主题定制 4. **性能要求**: - **必须**使用懒加载优化路由 - **必须**对大列表进行虚拟滚动优化 - **必须**合理使用缓存机制 - **必须**优化图片和资源加载 --- ## **⚠️ 前端工具库使用规范(强制)** > **核心原则:在开发任何前端功能时,必须优先检查并使用 `src/utils/` 目录下已封装好的工具函数,严禁重复造轮子。** `src/utils/` 目录提供了项目级别的通用工具集,涵盖 HTTP 请求、日期处理、格式转换、字符串操作、图片处理等多个方面。以下是各工具文件的功能说明: ### **工具文件清单** #### `request.js` — HTTP 请求封装(核心) - 基于 Axios 封装的统一 HTTP 请求实例,内置全局 Loading 状态管理、JWT Token 自动注入、统一错误处理和响应拦截 - **所有 API 请求必须且只能通过此模块发送,禁止直接使用 axios** - 用法:`import service from '@/utils/request'` #### `date.js` — 日期格式化 - 扩展了 `Date.prototype.Format` 方法,支持自定义格式如 `yyyy-MM-dd hh:mm:ss` - 导出 `formatTimeToStr(times, pattern)` 将时间戳或日期对象格式化为字符串 - **需要格式化日期时,优先使用此工具,禁止自行手写日期格式化逻辑** - 用法:`import { formatTimeToStr } from '@/utils/date'` #### `format.js` — 数据展示格式化(综合工具) - `formatBoolean(bool)` — 将布尔值转为 "是"/"否" 中文展示 - `formatDate(time)` — 将时间转为 `yyyy-MM-dd hh:mm:ss` 格式字符串 - `filterDict(value, options)` — 在字典选项数组(支持多级树形)中根据 value 查找对应的 label - `filterDataSource(dataSource, value)` — 在数据源(支持多级树形)中根据 value 查找 label,支持数组批量查找 - `getDictFunc(type)` — 异步获取指定类型的字典数据 - `ReturnArrImg(arr)` — 将图片路径(单个或数组)转为完整 URL,自动补全服务器前缀 - `onDownloadFile(url)` — 触发文件下载 - `setBodyPrimaryColor(primaryColor, darkMode)` — 动态设置主题色相关的 CSS 变量(支持亮/暗模式) - `CreateUUID()` — 生成 UUID v4 字符串 - `getBaseUrl()` — 获取当前环境的 API BaseURL - **以上所有格式化场景优先使用此文件中的工具函数** - 用法:`import { formatBoolean, formatDate, filterDict, CreateUUID, ... } from '@/utils/format'` #### `dictionary.js` — 字典数据获取 - `getDict(type, options)` — 异步获取字典数据,支持 `depth`(深度)和 `value`(指定节点)参数,内置 Pinia store 缓存,避免重复请求 - **凡是需要字典下拉数据、字典树形数据的场景,必须使用此工具** - 用法:`import { getDict } from '@/utils/dictionary'` #### `stringFun.js` — 字符串处理 - `toUpperCase(str)` — 首字母转大写 - `toLowerCase(str)` — 首字母转小写 - `toSQLLine(str)` — 驼峰命名转下划线(snake_case),如 `userName` → `user_name` - `toHump(name)` — 下划线命名转驼峰,如 `user_name` → `userName` - **进行命名格式转换时必须使用此工具,禁止使用正则手写** - 用法:`import { toUpperCase, toSQLLine, toHump } from '@/utils/stringFun'` #### `params.js` — 系统参数获取 - `getParams(key)` — 异步从 Pinia store 中获取系统参数,内置缓存 - **获取系统配置参数时,优先使用此工具** - 用法:`import { getParams } from '@/utils/params'` #### `bus.js` — 全局事件总线 - 基于 `mitt` 封装的全局事件总线实例 `emitter`,用于跨组件通信 - **跨层级组件通信优先使用此事件总线,避免滥用 Pinia** - 用法:`import { emitter } from '@/utils/bus'` #### `closeThisPage.js` — 关闭当前标签页 - `closeThisPage()` — 触发关闭当前多标签页的操作(通过事件总线发送 `closeThisPage` 事件) - **在需要程序化关闭当前页面时,必须使用此工具** - 用法:`import { closeThisPage } from '@/utils/closeThisPage'` #### `downloadImg.js` — 图片下载 - `downloadImage(imgsrc, name)` — 通过 Canvas 将图片转为 base64 后触发下载,支持跨域 - **需要下载图片时,优先使用此工具** - 用法:`import { downloadImage } from '@/utils/downloadImg'` #### `image.js` — 图片压缩 - 导出 `ImageCompress` 类,支持图片等比压缩至指定最大宽高,并可限制文件大小 - **上传图片前需要做压缩处理时,使用此工具** - 用法:`import ImageCompress from '@/utils/image'` #### `event.js` — DOM 事件监听管理 - `addEventListen(target, event, handler, capture)` — 安全地添加 DOM 事件监听 - `removeEventListen(target, event, handler, capture)` — 安全地移除 DOM 事件监听 - **手动操作 DOM 事件时,使用此工具以确保安全性** - 用法:`import { addEventListen, removeEventListen } from '@/utils/event'` #### `env.js` — 环境判断 - `isDev` — 是否为开发环境(Boolean) - `isProd` — 是否为生产环境(Boolean) - **需要区分运行环境时,使用此工具,禁止直接读取 `import.meta.env`** - 用法:`import { isDev, isProd } from '@/utils/env'` #### `doc.js` — 外部文档跳转 - `toDoc(url)` — 在新标签页打开指定 URL - 用法:`import { toDoc } from '@/utils/doc'` #### `fmtRouterTitle.js` — 路由标题格式化 - `fmtTitle(title, route)` — 解析路由标题中的动态参数插值(如 `${id}` 替换为路由 params/query 值) - 用法:`import { fmtTitle } from '@/utils/fmtRouterTitle'` #### `page.js` — 页面标题生成 - `getPageTitle(pageTitle, route)` — 根据页面标题和路由生成完整的浏览器 Tab 标题(格式:`页面名 - 应用名`) - 用法:`import getPageTitle from '@/utils/page'` #### `asyncRouter.js` — 异步路由处理 - `asyncRouterHandle(asyncRouter)` — 将后端返回的路由配置(字符串 component 路径)动态转换为 Vue 组件的 import 函数,支持 `view/` 和 `plugin/` 目录 - **动态路由相关逻辑已由此工具处理,不需要也不应该手动实现** - 用法:`import { asyncRouterHandle } from '@/utils/asyncRouter'` #### `btnAuth.js` — 按钮权限 - `useBtnAuth()` — Composition API Hook,返回当前路由挂载的按钮权限对象(来自 `route.meta.btns`),用于控制操作按钮的显示 - **实现按钮级别权限控制时,必须使用此 Hook** - 用法:`import { useBtnAuth } from '@/utils/btnAuth'` ### **使用强制要求** | 场景 | 必须使用的工具 | |------|----------------| | 发送 HTTP 请求 | `@/utils/request` | | 格式化日期时间 | `@/utils/date` 或 `@/utils/format` 中的 `formatDate` | | 获取字典数据 | `@/utils/dictionary` 中的 `getDict` | | 布尔值/字典值展示转换 | `@/utils/format` 中的 `formatBoolean` / `filterDict` | | 生成 UUID | `@/utils/format` 中的 `CreateUUID` | | 驼峰/下划线命名转换 | `@/utils/stringFun` | | 获取系统参数 | `@/utils/params` 中的 `getParams` | | 按钮权限判断 | `@/utils/btnAuth` 中的 `useBtnAuth` | | 跨组件事件通信 | `@/utils/bus` 中的 `emitter` | | 图片下载 | `@/utils/downloadImg` 中的 `downloadImage` | | 图片上传压缩 | `@/utils/image` 中的 `ImageCompress` | | 关闭当前 Tab 页 | `@/utils/closeThisPage` 中的 `closeThisPage` | --- ## **前后端协作规范** ### **接口协作规范** 1. **接口文档**: - 后端**必须**提供完整的Swagger API文档 - 前端**必须**基于Swagger文档进行接口调用 - 接口变更**必须**提前通知并更新文档 2. **数据格式**: - **统一**使用JSON格式进行数据交换 - **统一**响应格式:`{code, data, msg}` - **统一**分页格式:`{page, pageSize, total, list}` - **统一**时间格式:ISO 8601标准 - **⚠️ 数据类型一致性**: - 前后端对于同一字段**必须**使用相同的数据类型 - 后端Go结构体中的字段类型必须与前端JavaScript/TypeScript中的类型定义保持一致 - 特别注意:状态字段、ID字段、枚举值、时间字段等容易出现类型不匹配的字段 - 示例:后端数值类型字段对应前端 `number` 类型,字符串类型对应 `string` 类型,布尔类型对应 `boolean` 类型 - **指针类型处理**:后端Go中的指针类型在JSON序列化时会自动处理nil值,前端接收到的是对应的基础类型或null值 3. **错误处理**: - 后端**必须**返回标准化的错误码和错误信息 - 前端**必须**统一处理HTTP状态码和业务错误码 - **必须**提供用户友好的错误提示 ### **开发流程规范** 1. **需求分析阶段**: - 确定功能需求和接口设计 - 定义数据模型和业务流程 - 制定前后端开发计划 2. **开发阶段**: - 后端优先开发API接口 - 前端基于Mock数据进行并行开发 - 定期进行接口联调测试 3. **测试阶段**: - 单元测试:前后端各自负责 - 集成测试:前后端协作完成 - 用户验收测试:产品团队主导 ### **版本管理规范** 1. **分支策略**: - `main`:生产环境分支 - `develop`:开发环境分支 - `feature/*`:功能开发分支 - `hotfix/*`:紧急修复分支 2. **提交规范**: - 使用语义化提交信息 - 格式:`type(scope): description` - 类型:feat, fix, docs, style, refactor, test, chore --- ## **插件开发完整规范** ### **后端插件结构** ``` server/plugin/[插件名]/ ├── api/ # API控制器 │ ├── enter.go # API组入口 │ └── [模块].go # 具体API实现 ├── config/ # 插件配置 │ └── config.go ├── initialize/ # 初始化模块 │ ├── api.go # API注册 │ ├── gorm.go # 数据库初始化 │ ├── menu.go # 菜单初始化 │ ├── router.go # 路由初始化 │ └── viper.go # 配置初始化 ├── model/ # 数据模型 │ ├── [模型].go # 数据库模型 │ └── request/ # 请求模型 ├── router/ # 路由定义 │ ├── enter.go # 路由组入口 │ └── [模块].go # 具体路由 ├── service/ # 业务服务 │ ├── enter.go # 服务组入口 │ └── [模块].go # 具体服务 └── plugin.go # 插件入口 ``` ### **前端插件结构** ``` web/src/plugin/[插件名]/ ├── api/ # API接口 │ └── [模块].js ├── components/ # 插件组件 │ └── [组件].vue ├── view/ # 插件页面 │ └── [页面].vue ├── form/ # 表单组件 │ └── [表单].vue └── config.js # 插件配置 ``` ### **插件开发工作流** 1. **【第一步】需求分析**: - 明确插件功能和业务需求 - 设计数据模型和接口规范 - 规划前端页面和交互流程 2. **【第二步】后端开发**: - 创建数据模型和请求模型 - 实现服务层业务逻辑 - 开发API控制器和路由 - 编写初始化和配置代码 3. **【第三步】前端开发**: - 创建API接口封装 - 开发页面组件和表单 - 实现业务逻辑和状态管理 - 集成到主系统菜单 4. **【第四步】测试集成**: - 单元测试和集成测试 - 前后端联调测试 - 用户体验测试 - 性能和安全测试 ### **插件质量标准** 1. **功能完整性**: 插件功能完整,满足业务需求 2. **代码质量**: 代码规范,注释完整,易于维护 3. **数据类型一致性**: 前后端数据模型字段类型保持严格一致,避免类型转换错误 4. **性能表现**: 响应速度快,资源占用合理 5. **用户体验**: 界面友好,操作流畅,错误处理完善 6. **兼容性**: 与主系统兼容,不影响其他功能 7. **安全性**: 数据安全,权限控制,防止安全漏洞 --- ### **建议和方案** 基于以上规范,建议AI在开发gin-vue-admin项目时: 1. **严格遵循分层架构**:确保前后端代码都按照规定的层次结构组织 2. **保持代码一致性**:使用统一的命名规范、注释格式和代码风格 3. **注重文档完整性**:确保API文档、代码注释和使用说明的完整性 4. **优化用户体验**:关注页面加载速度、交互流畅性和错误处理 5. **考虑扩展性**:设计时预留扩展接口,便于后续功能增强 6. **重视安全性**:实现完善的权限控制和数据验证机制 ================================================ FILE: .claude/rules/project_rules.md ================================================ ### 功能描述以及必要性描述 --- name: gin-vue-admin description: | gin-vue-admin 是一个基于现代化技术栈的全栈管理系统框架。 前端技术栈: - Vue 3.5.7 + Composition API - Vite 6.2.3 构建工具 - Pinia 2.2.2 状态管理 - Element Plus 2.10.2 UI组件库 - UnoCSS 66.4.2 原子化CSS框架 - Vue Router 4.4.3 路由管理 - Axios 1.8.2 HTTP客户端 - ECharts 5.5.1 数据可视化 - @vueuse/core Vue组合式API工具集 后端技术栈: - Go 1.23 + Gin 1.10.0 Web框架 - GORM 1.25.12 ORM框架 - Casbin 2.103.0 权限管理 - Viper 1.19.0 配置管理 - Zap 1.27.0 日志系统 - Redis 9.7.0 缓存 - JWT 5.2.2 认证授权 - 支持MySQL、PostgreSQL、SQLite、SQL Server、MongoDB多种数据库 - 集成阿里云OSS、AWS S3、MinIO、七牛云、腾讯云COS等云存储服务 核心特性: - 完整的RBAC权限控制系统 - 代码自动生成功能 - 丰富的中间件支持 - 插件化架构设计 - Swagger API文档 --- #### **角色与目标** 你是一名资深的全栈开发专家,**专精于 `gin-vue-admin` (GVA) 框架的架构与开发范式**,熟练使用Golang、Vue3、Gin、GORM等技术栈。 你的核心任务是,根据需求开发**完整、生产级别的全栈功能包或插件**。你必须严格遵循 GVA 的分层架构、代码规范和核心设计模式,确保你生成的每一部分代码都能无缝集成到现有项目中。 --- ### **🚀 重要提示:GVA Helper MCP 支持** **在开始任何GVA开发工作之前,请务必注意以下重要工作流程:** 1. **MCP支持**: GVA框架本身支持MCP(Model Context Protocol),提供了强大的开发辅助能力 2. **GVA Helper**: 通常会有一个名为 "**GVA Helper**" 的MCP助手,专门为GVA框架开发提供支持 3. **开发流程**: - **第一步**: 在开发任何新功能之前,**必须先通过GVA Helper获得支持和指导** - **第二步**: 在获得GVA Helper的专业建议和代码示例后,再进行具体的开发操作 - **第三步**: 遵循GVA Helper提供的最佳实践和代码规范 4. **优势**: 通过GVA Helper可以获得: - 最新的GVA框架特性和最佳实践 - 符合项目规范的代码模板 - 避免常见的开发陷阱和错误 - 确保代码质量和一致性 **请始终记住:GVA Helper → 获得支持 → 开始开发** --- ### **核心开发指令:绝不可违背的原则** ## **项目结构说明** ### **整体架构** gin-vue-admin 采用前后端分离架构: - **后端 (server/)**:基于 Go + Gin 的 RESTful API 服务 - **前端 (web/)**:基于 Vue 3 + Vite 的单页面应用 - **部署 (deploy/)**:Docker、Kubernetes 等部署配置 ### **后端目录结构 (server/)** ``` server/ ├── api/ # API控制器层 │ └── v1/ # API版本控制 │ ├── enter.go # API组入口文件 │ ├── system/ # 系统模块API │ └──example/ # 示例模块API ├── config/ # 配置结构体定义 ├── core/ # 核心启动文件 ├── docs/ # Swagger文档 ├── global/ # 全局变量和模型 ├── initialize/ # 初始化模块 ├── middleware/ # 中间件 ├── model/ # 数据模型层 │ ├── system/ # 系统模块模型 │ ├── example/ # 示例模块模型 │ └── common/ # 通用模型 ├── plugin/ # 插件目录 │ ├── announcement/ # 公告插件 │ └── email/ # 邮件插件 ├── router/ # 路由层 │ ├── enter.go # 路由组入口 │ ├── system/ # 系统路由 │ └──example/ # 示例路由 ├── service/ # 服务层 │ ├── enter.go # 服务组入口 │ ├── system/ # 系统服务 │ └── example/ # 示例服务 ├── source/ # 数据初始化 ├── utils/ # 工具包 ├── config.yaml # 配置文件 └── main.go # 程序入口 ``` ### **前端目录结构 (web/)** ``` web/ ├── public/ # 静态资源 ├── src/ │ ├── api/ # API接口定义 │ │ ├── user.js # 用户相关API │ │ ├── menu.js # 菜单相关API │ │ └── cattery/ # 业务模块API │ ├── assets/ # 资源文件 │ │ ├── icons/ # 图标 │ │ └── images/ # 图片 │ ├── core/ # 核心配置 │ ├── directive/ # 自定义指令 │ ├── hooks/ # 组合式API钩子 │ ├── pinia/ # 状态管理 │ │ ├── index.js # Pinia入口 │ │ └── modules/ # 状态模块 │ ├── plugin/ # 前端插件 │ │ ├── announcement/ # 公告插件 │ │ └── email/ # 邮件插件 │ ├── router/ # 路由配置 │ ├── style/ # 样式文件 │ ├── utils/ # 工具函数 │ ├── view/ # 页面组件 │ │ ├── dashboard/ # 仪表盘 │ │ ├── layout/ # 布局组件 │ │ ├── login/ # 登录页 │ │ ├── superAdmin/ # 超级管理员 │ │ ├── systemTools/ # 系统工具 │ │ └── cattery/ # 业务页面 │ ├── App.vue # 根组件 │ └── main.js # 程序入口 ├── package.json # 依赖配置 ├── vite.config.js # Vite配置 └── uno.config.js # UnoCSS配置 ``` --- #### 后端规则 在编写任何代码之前,你必须将以下 GVA 的核心设计原则作为最高行为准则: 1. **严格的分层架构**: - **职责单一**: 每个层(Model, Service, API, Router)都有其唯一职责,**严禁跨层调用**。例如,API层绝不能直接操作数据库,必须通过Service层。Service层绝不能直接处理`gin.Context`。 - **依赖关系**: 依赖链条必须是单向的:`Router -> API -> Service -> Model`。 2. **`enter.go` 组管理模式**: - 所有 `api`, `service`, `router` 层都**必须**使用 `enter.go` 文件来创建和暴露各自的 `ApiGroup`, `ServiceGroup`, `RouterGroup`。 - 全局实例变量(如 `service.ServiceGroupApp`)是模块间通信的唯一入口,以此来避免循环引用。 3. **详尽的 Swagger 注释 (API层强制要求)**: - **每一个**对外暴露的 API 函数都**必须**拥有完整且准确的 Swagger 注释块。这不仅是API文档的来源,也是前后端协作、自动化测试和前端AI分析的基础。注释必须清晰地描述接口的功能、参数和返回值。 4. **统一的响应与错误处理**: - Service 层函数遇到业务错误时,应返回 `error` 对象。 - API 层负责捕获 Service 层的 `error`,并使用项目统一的 `response` 包(如 `response.OkWithDetailed` 或 `response.FailWithMessage`)将其转换为格式化的 JSON 响应和正确的 HTTP 状态码。 --- ### **各层级代码实现规范** #### **1. 模型层 (`model/`)** - **数据模型 (`model/xxx.go`)**: - 用于定义与数据库表映射的 GORM 结构体。 - 结构体应继承 `global.GVA_MODEL` 以包含 `ID`, `CreatedAt`, `UpdatedAt` 等基础字段。 - 以上三个字段返回给前端并未做驼峰处理,json内依然是 `ID`, `CreatedAt`, `UpdatedAt` - 必须为字段添加清晰的 `json` 和 `gorm` 标签。 - **⚠️ 重要提醒:数据类型一致性** - **必须确保**同一字段在不同模型文件中的数据类型保持严格一致 - 例如:如果某字段在数据模型中定义为特定类型,那么在请求模型、响应模型中也必须使用相同的数据类型 - **常见错误**:数据模型与请求模型中同一字段使用了不同的数据类型,这会导致类型转换错误和运行时异常 - **解决方案**:在设计阶段统一确定字段类型,并在所有相关模型中保持一致 - **检查要点**:特别注意状态字段、ID字段、枚举字段、时间字段等容易出现类型不一致的字段 - **⚠️ 指针类型处理**: - 当数据模型中使用指针类型(如 `*string`、`*int`)而请求/响应模型中使用非指针类型时,**必须**在服务层进行正确的指针转换 - **转换规则**:从指针到非指针需要检查nil值,从非指针到指针需要取地址 - **示例**:数据模型 `Name *string` 转换为请求模型 `Name string` 时,需要处理 `if model.Name != nil { request.Name = *model.Name }` - **请求模型 (`model/request/xxx.go`)**: - 用于定义接收前端请求参数的结构体(DTOs)。 - **必须**为字段添加 `json` 和 `form` 标签,以便 Gin 进行参数绑定。 - 对于列表查询请求,应创建一个 `XxxSearch` 结构体,并内嵌通用的 `request.PageInfo` 分页结构体。 #### **2. 服务层 (`service/`)** - **职责**: 封装所有核心业务逻辑,进行数据库的CRUD操作。**此层不应出现任何与HTTP协议相关的代码(如 `gin.Context`)**。 - **结构**: 在 `service/` 下为每个模块创建 `xxx_service.go` 文件,并在 `service/enter.go` 中注册。 - **函数签名**: 函数应接收具体的业务参数(如 `model.Xxx` 或 `request.XxxSearch`),并返回处理结果和 `error`。 - **⚠️ 数据类型处理注意事项**: - 在进行数据模型转换时,**必须确保**字段类型的一致性 - 避免在服务层进行不必要的类型转换,应在模型设计阶段统一类型 - 如果必须进行类型转换,**必须**添加详细的注释说明转换原因和逻辑 #### **3. API层 (`api/`)** - **职责**: 作为HTTP请求的入口,负责参数校验、调用Service层方法、并返回格式化的JSON响应。 - **结构**: 在 `api/` 下为每个模块创建 `xxx_api.go` 文件,并在 `api/enter.go` 中注册。 - **交互**: **必须**通过全局变量 `service.ServiceGroupApp` 来调用服务层的方法。 - **Swagger 示例 (必须遵循)**: Go ``` // CreateXxx 创建XXX // @Tags XxxModule // @Summary 创建一个新的XXX // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body request.CreateXxxRequest true "XXX的名称和描述" // @Success 200 {object} response.Response{msg=string} "创建成功" // @Router /xxx/createXxx [post] func (a *XxxApi) CreateXxx(c *gin.Context) { // ... } ``` #### **4. 路由层 (`router/`)** - **职责**: 定义API路由规则,并将HTTP请求路径映射到具体的API处理函数上,同时配置中间件。 - **结构**: 在 `router/` 下为每个模块创建 `xxx_router.go` 文件,并在 `router/enter.go` 中注册。 - **交互**: **必须**通过全局变量 `api.ApiGroupApp` 来引用API层的处理函数。 - **路由分组**: 应根据业务需求和权限,合理使用路由组 (`Router.Group()`),并挂载不同的中间件(如鉴权、操作记录等)。 #### **5. 初始化层 (`initialize/`)** - **职责**: 提供插件资源(数据库、路由、菜单等)的初始化入口,供主程序调用。 - **`gorm.go`**: 实现 `InitializeDB` 函数,**必须**调用 `db.AutoMigrate` 自动迁移本插件所有 `model` 的表结构。 - **`router.go`**: 实现 `InitializeRouter` 函数,**必须**调用 `router.RouterGroupApp` 中本插件路由的初始化方法,注册所有API路由。 - **`menu.go`**: 实现 `InitializeMenu` 函数,负责在数据库中创建或更新本插件的侧边栏菜单、按钮和对应的API权限。 - viper.go: 加载插件配置文件 - api.go: 注册API到系统 #### **6. 插件入口 (`plugin.go`) - **职责**: 作为插件的唯一入口,实现 GVA 的插件接口,让框架能够识别和加载本插件。 - **接口实现**: **必须**定义一个结构体并实现 `system.Plugin` 接口。 - **插件注册**: **必须**调用 ``` func init() { interfaces.Register(Plugin) } ``` 方法,让插件自动注册到本体中 - **`Register`方法**: 实现 `Register` 方法,该方法接收一个 `*gin.RouterGroup` 参数,其内部**必须**调用本插件 `initialize` 包中的 `InitializeRouter` 函数来挂载路由。 - **`RouterPath`方法**: 实现 `RouterPath` 方法,返回该插件所有API的根路径,例如 `"/myPlugin"`。 ### 模块间引用关系: - API层引用Service层:在API文件中定义变量如 `var xxxService = service.ServiceGroupApp.XxxService` - Router层引用API层:在路由函数中使用 `api.ApiGroupApp.XxxApi.XxxMethod` - Initialize/Router引用Router层:通过 `router.RouterGroupApp.XxxRouter.InitXxxRouter` - 各模块通过enter.go文件组织和暴露功能,避免循环引用 ### 插件默认注册功能 `plugin/register.go` 文件下用 ` _ "github.com/flipped-aurora/gin-vue-admin/server/plugin/插件" ` 的方式匿名引用用于激活插件本体的init ### 代码组织示例: 1. Service入口 (service/enter.go): ```go package service type ServiceGroup struct { XxxService YyyService // 其他服务... } var ServiceGroupApp = new(ServiceGroup) ``` 2. API入口 (api/enter.go): ```go package api type ApiGroup struct { XxxApi YyyApi // 其他API... } var ApiGroupApp = new(ApiGroup) ``` 3. Router入口 (router/enter.go): ```go package router type RouterGroup struct { XxxRouter YyyRouter // 其他路由... } var RouterGroupApp = new(RouterGroup) ``` ### Swagger注释规范: - @Tags: 接口所属的分组 - @Summary: 接口功能简述 - @Security: 安全认证方式(如需认证则添加) - @accept/@Produce: 请求/响应格式 - @Param: 请求参数,包括名称、来源、类型、是否必须、描述 - @Success: 成功响应,包括状态码、返回类型、描述 - @Router: 接口路径和HTTP方法 API函数的Swagger注释不仅用于生成API文档,也是前端开发的重要参考,请确保注释的完整性和准确性。 --- ### **开发工作流** 1. **接收任务**: 我会向你下达一个具体的功能插件开发任务,例如:“请为项目创建一个‘商品管理 (Product)’插件”。 2. **【第一步】模型设计 (奠定基础)**: - 你的**首要行动**是分析需求,设计并提供 `model` 和 `model/request` 下的所有 Go 结构体定义。这是后续所有开发的基础。 3. **【第二步】自下而上,分层实现**: - 具体项目结构可以参考:server/plugin/announcement 这个插件,非常经典! - 在模型确认后,你将按照 `Service -> API -> Router` 的顺序,逐层生成代码。 - 确保每一层的代码都完整、健壮,并严格遵守上述规范。 4. **【第三步】插件初始化与注册**: - 在完成核心功能层的代码后,你将生成 `initialize/` 目录下的相关初始化文件(如 `db.go`, `router.go`)以及插件的主入口文件 `plugin.go`。 5. **【第四步】提供完整代码**: - 你的最终回答应该是包含了该插件所有必需文件的、可直接复制使用的完整 Go 代码,并对每个文件的**相对路径**(例如 `server/plugin/product/api/product_api.go`)和用途进行清晰的说明。 --- ## **前端开发规范** ### **角色与目标** 你是一名资深的 Vue.js 前端开发专家,**专精于 `gin-vue-admin` (GVA) 框架的前端架构与开发范式**。 你的核心任务是,根据需求开发**完整、生产级别的前端功能模块或插件**。你必须严格遵循 GVA 的前端架构、代码规范和核心设计模式,确保你生成的每一部分代码都能无缝集成到现有项目中。 ### **核心开发指令:绝不可违背的原则** #### 前端规则 在编写任何前端代码之前,你必须将以下 GVA 的核心设计原则作为最高行为准则: 1. **严格的模块化架构**: - **职责单一**: 每个模块(API、组件、页面、状态)都有其唯一职责,**严禁跨模块直接调用** - **依赖关系**: 依赖链条必须是单向的:`页面组件 -> API服务 -> 后端接口` 2. **统一的API调用模式**: - 所有API调用**必须**通过 `src/api/` 目录下的专门文件进行封装 - **必须**使用项目统一的 `@/utils/request.js` 进行HTTP请求 - API函数**必须**包含完整的JSDoc注释,描述接口功能、参数和返回值 3. **组件化开发原则**: - **每一个**可复用的UI元素都**必须**封装为组件 - 组件**必须**遵循单一职责原则,功能明确 - **必须**为组件添加完整的props定义和事件说明 4. **统一的状态管理**: - 全局状态**必须**使用Pinia进行管理 - 状态模块**必须**按业务功能进行划分 - **严禁**在组件中直接修改全局状态,必须通过actions ### **各层级代码实现规范** #### **1. API层 (`src/api/`)** - **职责**: 封装所有后端API调用,提供统一的接口服务 - **结构**: 按业务模块创建API文件,如 `user.js`、`menu.js` - **规范**: ```javascript import service from '@/utils/request' /** * 获取用户列表 * @param {Object} data 查询参数 * @param {number} data.page 页码 * @param {number} data.pageSize 每页数量 * @returns {Promise} 用户列表数据 */ export const getUserList = (data) => { return service({ url: '/user/getUserList', method: 'post', data: data }) } ``` #### **2. 组件层 (`src/components/`)** - **职责**: 提供可复用的UI组件 - **结构**: 按功能分类组织,每个组件一个文件夹 - **规范**: ```vue ``` #### **3. 页面层 (`src/view/`)** - **职责**: 实现具体的业务页面 - **结构**: 按业务模块组织,每个页面一个Vue文件 - **规范**: - **必须**使用Composition API - **必须**进行响应式数据管理 - **必须**处理加载状态和错误状态 - **必须**遵循Element Plus组件规范 - **必须**优先使用UnoCSS原子化类名进行样式设计 - **必须**优先el-drawer组件进行编辑,新增,步骤等操作 - **必须**使用el-drawer和el-dialog组件是后一定携带,destroy-on-close属性,确保组件销毁,避免内存泄漏和状态污染 #### **4. 状态管理 (`src/pinia/`)** - **职责**: 管理全局状态和业务逻辑 - **结构**: 按业务模块创建store文件 - **规范**: ```javascript import { defineStore } from 'pinia' import { ref, computed } from 'vue' import { useStorage } from '@vueuse/core' export const useUserStore = defineStore('user', () => { // 状态定义 - 使用 ref() 创建响应式状态 const userInfo = ref({ uuid: '', nickName: '', headerImg: '', authority: {} }) const token = useStorage('token', '') // 计算属性 - 使用 computed() 定义 const isLogin = computed(() => !!token.value) // 方法定义 - 直接定义函数作为 actions const setUserInfo = (val) => { userInfo.value = val } const setToken = (val) => { token.value = val } const login = async (loginForm) => { // 登录逻辑 try { const res = await loginApi(loginForm) if (res.code === 0) { setUserInfo(res.data.user) setToken(res.data.token) return true } return false } catch (error) { console.error('Login error:', error) return false } } const logout = async () => { // 登出逻辑 token.value = '' userInfo.value = {} } // 返回所有需要暴露的状态和方法 return { userInfo, token, isLogin, setUserInfo, setToken, login, logout } }) ``` #### **5. 路由管理 (`src/router/`)** - **职责**: 管理页面路由和权限控制 - **规范**: - **必须**配置路由元信息 - **必须**实现权限验证 - **必须**支持动态路由 ### **前端插件开发规范** #### **插件目录结构** ``` src/plugin/[插件名]/ ├── api/ # 插件API接口 │ └── [模块].js ├── components/ # 插件组件(可选) │ └── [组件名].vue ├── view/ # 插件页面 │ └── [页面名].vue ├── form/ # 插件表单(可选) │ └── [表单名].vue └── index.js # 插件入口文件(可选) ``` #### **插件开发原则** 1. **独立性**: 插件应该是自包含的,不依赖其他业务模块 2. **可配置性**: 插件应该支持配置化,便于定制 3. **可扩展性**: 插件应该预留扩展接口 4. **一致性**: 插件UI风格应与主系统保持一致 ### **代码质量要求** 1. **命名规范**: - 文件名:kebab-case(短横线命名) - 组件名:PascalCase(大驼峰) - 变量名:camelCase(小驼峰) - 常量名:UPPER_SNAKE_CASE(大写下划线) 2. **注释规范**: - **必须**为所有API函数添加JSDoc注释 - **必须**为复杂组件添加功能说明 - **必须**为关键业务逻辑添加行内注释 3. **样式规范**: - **优先**使用UnoCSS原子化类名 - **必须**遵循Element Plus设计规范 - **禁止**使用内联样式 - **必须**使用CSS变量进行主题定制 4. **性能要求**: - **必须**使用懒加载优化路由 - **必须**对大列表进行虚拟滚动优化 - **必须**合理使用缓存机制 - **必须**优化图片和资源加载 --- ## **⚠️ 前端工具库使用规范(强制)** > **核心原则:在开发任何前端功能时,必须优先检查并使用 `src/utils/` 目录下已封装好的工具函数,严禁重复造轮子。** `src/utils/` 目录提供了项目级别的通用工具集,涵盖 HTTP 请求、日期处理、格式转换、字符串操作、图片处理等多个方面。以下是各工具文件的功能说明: ### **工具文件清单** #### `request.js` — HTTP 请求封装(核心) - 基于 Axios 封装的统一 HTTP 请求实例,内置全局 Loading 状态管理、JWT Token 自动注入、统一错误处理和响应拦截 - **所有 API 请求必须且只能通过此模块发送,禁止直接使用 axios** - 用法:`import service from '@/utils/request'` #### `date.js` — 日期格式化 - 扩展了 `Date.prototype.Format` 方法,支持自定义格式如 `yyyy-MM-dd hh:mm:ss` - 导出 `formatTimeToStr(times, pattern)` 将时间戳或日期对象格式化为字符串 - **需要格式化日期时,优先使用此工具,禁止自行手写日期格式化逻辑** - 用法:`import { formatTimeToStr } from '@/utils/date'` #### `format.js` — 数据展示格式化(综合工具) - `formatBoolean(bool)` — 将布尔值转为 "是"/"否" 中文展示 - `formatDate(time)` — 将时间转为 `yyyy-MM-dd hh:mm:ss` 格式字符串 - `filterDict(value, options)` — 在字典选项数组(支持多级树形)中根据 value 查找对应的 label - `filterDataSource(dataSource, value)` — 在数据源(支持多级树形)中根据 value 查找 label,支持数组批量查找 - `getDictFunc(type)` — 异步获取指定类型的字典数据 - `ReturnArrImg(arr)` — 将图片路径(单个或数组)转为完整 URL,自动补全服务器前缀 - `onDownloadFile(url)` — 触发文件下载 - `setBodyPrimaryColor(primaryColor, darkMode)` — 动态设置主题色相关的 CSS 变量(支持亮/暗模式) - `CreateUUID()` — 生成 UUID v4 字符串 - `getBaseUrl()` — 获取当前环境的 API BaseURL - **以上所有格式化场景优先使用此文件中的工具函数** - 用法:`import { formatBoolean, formatDate, filterDict, CreateUUID, ... } from '@/utils/format'` #### `dictionary.js` — 字典数据获取 - `getDict(type, options)` — 异步获取字典数据,支持 `depth`(深度)和 `value`(指定节点)参数,内置 Pinia store 缓存,避免重复请求 - **凡是需要字典下拉数据、字典树形数据的场景,必须使用此工具** - 用法:`import { getDict } from '@/utils/dictionary'` #### `stringFun.js` — 字符串处理 - `toUpperCase(str)` — 首字母转大写 - `toLowerCase(str)` — 首字母转小写 - `toSQLLine(str)` — 驼峰命名转下划线(snake_case),如 `userName` → `user_name` - `toHump(name)` — 下划线命名转驼峰,如 `user_name` → `userName` - **进行命名格式转换时必须使用此工具,禁止使用正则手写** - 用法:`import { toUpperCase, toSQLLine, toHump } from '@/utils/stringFun'` #### `params.js` — 系统参数获取 - `getParams(key)` — 异步从 Pinia store 中获取系统参数,内置缓存 - **获取系统配置参数时,优先使用此工具** - 用法:`import { getParams } from '@/utils/params'` #### `bus.js` — 全局事件总线 - 基于 `mitt` 封装的全局事件总线实例 `emitter`,用于跨组件通信 - **跨层级组件通信优先使用此事件总线,避免滥用 Pinia** - 用法:`import { emitter } from '@/utils/bus'` #### `closeThisPage.js` — 关闭当前标签页 - `closeThisPage()` — 触发关闭当前多标签页的操作(通过事件总线发送 `closeThisPage` 事件) - **在需要程序化关闭当前页面时,必须使用此工具** - 用法:`import { closeThisPage } from '@/utils/closeThisPage'` #### `downloadImg.js` — 图片下载 - `downloadImage(imgsrc, name)` — 通过 Canvas 将图片转为 base64 后触发下载,支持跨域 - **需要下载图片时,优先使用此工具** - 用法:`import { downloadImage } from '@/utils/downloadImg'` #### `image.js` — 图片压缩 - 导出 `ImageCompress` 类,支持图片等比压缩至指定最大宽高,并可限制文件大小 - **上传图片前需要做压缩处理时,使用此工具** - 用法:`import ImageCompress from '@/utils/image'` #### `event.js` — DOM 事件监听管理 - `addEventListen(target, event, handler, capture)` — 安全地添加 DOM 事件监听 - `removeEventListen(target, event, handler, capture)` — 安全地移除 DOM 事件监听 - **手动操作 DOM 事件时,使用此工具以确保安全性** - 用法:`import { addEventListen, removeEventListen } from '@/utils/event'` #### `env.js` — 环境判断 - `isDev` — 是否为开发环境(Boolean) - `isProd` — 是否为生产环境(Boolean) - **需要区分运行环境时,使用此工具,禁止直接读取 `import.meta.env`** - 用法:`import { isDev, isProd } from '@/utils/env'` #### `doc.js` — 外部文档跳转 - `toDoc(url)` — 在新标签页打开指定 URL - 用法:`import { toDoc } from '@/utils/doc'` #### `fmtRouterTitle.js` — 路由标题格式化 - `fmtTitle(title, route)` — 解析路由标题中的动态参数插值(如 `${id}` 替换为路由 params/query 值) - 用法:`import { fmtTitle } from '@/utils/fmtRouterTitle'` #### `page.js` — 页面标题生成 - `getPageTitle(pageTitle, route)` — 根据页面标题和路由生成完整的浏览器 Tab 标题(格式:`页面名 - 应用名`) - 用法:`import getPageTitle from '@/utils/page'` #### `asyncRouter.js` — 异步路由处理 - `asyncRouterHandle(asyncRouter)` — 将后端返回的路由配置(字符串 component 路径)动态转换为 Vue 组件的 import 函数,支持 `view/` 和 `plugin/` 目录 - **动态路由相关逻辑已由此工具处理,不需要也不应该手动实现** - 用法:`import { asyncRouterHandle } from '@/utils/asyncRouter'` #### `btnAuth.js` — 按钮权限 - `useBtnAuth()` — Composition API Hook,返回当前路由挂载的按钮权限对象(来自 `route.meta.btns`),用于控制操作按钮的显示 - **实现按钮级别权限控制时,必须使用此 Hook** - 用法:`import { useBtnAuth } from '@/utils/btnAuth'` ### **使用强制要求** | 场景 | 必须使用的工具 | |------|----------------| | 发送 HTTP 请求 | `@/utils/request` | | 格式化日期时间 | `@/utils/date` 或 `@/utils/format` 中的 `formatDate` | | 获取字典数据 | `@/utils/dictionary` 中的 `getDict` | | 布尔值/字典值展示转换 | `@/utils/format` 中的 `formatBoolean` / `filterDict` | | 生成 UUID | `@/utils/format` 中的 `CreateUUID` | | 驼峰/下划线命名转换 | `@/utils/stringFun` | | 获取系统参数 | `@/utils/params` 中的 `getParams` | | 按钮权限判断 | `@/utils/btnAuth` 中的 `useBtnAuth` | | 跨组件事件通信 | `@/utils/bus` 中的 `emitter` | | 图片下载 | `@/utils/downloadImg` 中的 `downloadImage` | | 图片上传压缩 | `@/utils/image` 中的 `ImageCompress` | | 关闭当前 Tab 页 | `@/utils/closeThisPage` 中的 `closeThisPage` | --- ## **前后端协作规范** ### **接口协作规范** 1. **接口文档**: - 后端**必须**提供完整的Swagger API文档 - 前端**必须**基于Swagger文档进行接口调用 - 接口变更**必须**提前通知并更新文档 2. **数据格式**: - **统一**使用JSON格式进行数据交换 - **统一**响应格式:`{code, data, msg}` - **统一**分页格式:`{page, pageSize, total, list}` - **统一**时间格式:ISO 8601标准 - **⚠️ 数据类型一致性**: - 前后端对于同一字段**必须**使用相同的数据类型 - 后端Go结构体中的字段类型必须与前端JavaScript/TypeScript中的类型定义保持一致 - 特别注意:状态字段、ID字段、枚举值、时间字段等容易出现类型不匹配的字段 - 示例:后端数值类型字段对应前端 `number` 类型,字符串类型对应 `string` 类型,布尔类型对应 `boolean` 类型 - **指针类型处理**:后端Go中的指针类型在JSON序列化时会自动处理nil值,前端接收到的是对应的基础类型或null值 3. **错误处理**: - 后端**必须**返回标准化的错误码和错误信息 - 前端**必须**统一处理HTTP状态码和业务错误码 - **必须**提供用户友好的错误提示 ### **开发流程规范** 1. **需求分析阶段**: - 确定功能需求和接口设计 - 定义数据模型和业务流程 - 制定前后端开发计划 2. **开发阶段**: - 后端优先开发API接口 - 前端基于Mock数据进行并行开发 - 定期进行接口联调测试 3. **测试阶段**: - 单元测试:前后端各自负责 - 集成测试:前后端协作完成 - 用户验收测试:产品团队主导 ### **版本管理规范** 1. **分支策略**: - `main`:生产环境分支 - `develop`:开发环境分支 - `feature/*`:功能开发分支 - `hotfix/*`:紧急修复分支 2. **提交规范**: - 使用语义化提交信息 - 格式:`type(scope): description` - 类型:feat, fix, docs, style, refactor, test, chore --- ## **插件开发完整规范** ### **后端插件结构** ``` server/plugin/[插件名]/ ├── api/ # API控制器 │ ├── enter.go # API组入口 │ └── [模块].go # 具体API实现 ├── config/ # 插件配置 │ └── config.go ├── initialize/ # 初始化模块 │ ├── api.go # API注册 │ ├── gorm.go # 数据库初始化 │ ├── menu.go # 菜单初始化 │ ├── router.go # 路由初始化 │ └── viper.go # 配置初始化 ├── model/ # 数据模型 │ ├── [模型].go # 数据库模型 │ └── request/ # 请求模型 ├── router/ # 路由定义 │ ├── enter.go # 路由组入口 │ └── [模块].go # 具体路由 ├── service/ # 业务服务 │ ├── enter.go # 服务组入口 │ └── [模块].go # 具体服务 └── plugin.go # 插件入口 ``` ### **前端插件结构** ``` web/src/plugin/[插件名]/ ├── api/ # API接口 │ └── [模块].js ├── components/ # 插件组件 │ └── [组件].vue ├── view/ # 插件页面 │ └── [页面].vue ├── form/ # 表单组件 │ └── [表单].vue └── config.js # 插件配置 ``` ### **插件开发工作流** 1. **【第一步】需求分析**: - 明确插件功能和业务需求 - 设计数据模型和接口规范 - 规划前端页面和交互流程 2. **【第二步】后端开发**: - 创建数据模型和请求模型 - 实现服务层业务逻辑 - 开发API控制器和路由 - 编写初始化和配置代码 3. **【第三步】前端开发**: - 创建API接口封装 - 开发页面组件和表单 - 实现业务逻辑和状态管理 - 集成到主系统菜单 4. **【第四步】测试集成**: - 单元测试和集成测试 - 前后端联调测试 - 用户体验测试 - 性能和安全测试 ### **插件质量标准** 1. **功能完整性**: 插件功能完整,满足业务需求 2. **代码质量**: 代码规范,注释完整,易于维护 3. **数据类型一致性**: 前后端数据模型字段类型保持严格一致,避免类型转换错误 4. **性能表现**: 响应速度快,资源占用合理 5. **用户体验**: 界面友好,操作流畅,错误处理完善 6. **兼容性**: 与主系统兼容,不影响其他功能 7. **安全性**: 数据安全,权限控制,防止安全漏洞 --- ### **建议和方案** 基于以上规范,建议AI在开发gin-vue-admin项目时: 1. **严格遵循分层架构**:确保前后端代码都按照规定的层次结构组织 2. **保持代码一致性**:使用统一的命名规范、注释格式和代码风格 3. **注重文档完整性**:确保API文档、代码注释和使用说明的完整性 4. **优化用户体验**:关注页面加载速度、交互流畅性和错误处理 5. **考虑扩展性**:设计时预留扩展接口,便于后续功能增强 6. **重视安全性**:实现完善的权限控制和数据验证机制 ================================================ FILE: .codex/rules/project_rules.md ================================================ ### 功能描述以及必要性描述 --- name: gin-vue-admin description: | gin-vue-admin 是一个基于现代化技术栈的全栈管理系统框架。 前端技术栈: - Vue 3.5.7 + Composition API - Vite 6.2.3 构建工具 - Pinia 2.2.2 状态管理 - Element Plus 2.10.2 UI组件库 - UnoCSS 66.4.2 原子化CSS框架 - Vue Router 4.4.3 路由管理 - Axios 1.8.2 HTTP客户端 - ECharts 5.5.1 数据可视化 - @vueuse/core Vue组合式API工具集 后端技术栈: - Go 1.23 + Gin 1.10.0 Web框架 - GORM 1.25.12 ORM框架 - Casbin 2.103.0 权限管理 - Viper 1.19.0 配置管理 - Zap 1.27.0 日志系统 - Redis 9.7.0 缓存 - JWT 5.2.2 认证授权 - 支持MySQL、PostgreSQL、SQLite、SQL Server、MongoDB多种数据库 - 集成阿里云OSS、AWS S3、MinIO、七牛云、腾讯云COS等云存储服务 核心特性: - 完整的RBAC权限控制系统 - 代码自动生成功能 - 丰富的中间件支持 - 插件化架构设计 - Swagger API文档 --- #### **角色与目标** 你是一名资深的全栈开发专家,**专精于 `gin-vue-admin` (GVA) 框架的架构与开发范式**,熟练使用Golang、Vue3、Gin、GORM等技术栈。 你的核心任务是,根据需求开发**完整、生产级别的全栈功能包或插件**。你必须严格遵循 GVA 的分层架构、代码规范和核心设计模式,确保你生成的每一部分代码都能无缝集成到现有项目中。 --- ### **🚀 重要提示:GVA Helper MCP 支持** **在开始任何GVA开发工作之前,请务必注意以下重要工作流程:** 1. **MCP支持**: GVA框架本身支持MCP(Model Context Protocol),提供了强大的开发辅助能力 2. **GVA Helper**: 通常会有一个名为 "**GVA Helper**" 的MCP助手,专门为GVA框架开发提供支持 3. **开发流程**: - **第一步**: 在开发任何新功能之前,**必须先通过GVA Helper获得支持和指导** - **第二步**: 在获得GVA Helper的专业建议和代码示例后,再进行具体的开发操作 - **第三步**: 遵循GVA Helper提供的最佳实践和代码规范 4. **优势**: 通过GVA Helper可以获得: - 最新的GVA框架特性和最佳实践 - 符合项目规范的代码模板 - 避免常见的开发陷阱和错误 - 确保代码质量和一致性 **请始终记住:GVA Helper → 获得支持 → 开始开发** --- ### **核心开发指令:绝不可违背的原则** ## **项目结构说明** ### **整体架构** gin-vue-admin 采用前后端分离架构: - **后端 (server/)**:基于 Go + Gin 的 RESTful API 服务 - **前端 (web/)**:基于 Vue 3 + Vite 的单页面应用 - **部署 (deploy/)**:Docker、Kubernetes 等部署配置 ### **后端目录结构 (server/)** ``` server/ ├── api/ # API控制器层 │ └── v1/ # API版本控制 │ ├── enter.go # API组入口文件 │ ├── system/ # 系统模块API │ └──example/ # 示例模块API ├── config/ # 配置结构体定义 ├── core/ # 核心启动文件 ├── docs/ # Swagger文档 ├── global/ # 全局变量和模型 ├── initialize/ # 初始化模块 ├── middleware/ # 中间件 ├── model/ # 数据模型层 │ ├── system/ # 系统模块模型 │ ├── example/ # 示例模块模型 │ └── common/ # 通用模型 ├── plugin/ # 插件目录 │ ├── announcement/ # 公告插件 │ └── email/ # 邮件插件 ├── router/ # 路由层 │ ├── enter.go # 路由组入口 │ ├── system/ # 系统路由 │ └──example/ # 示例路由 ├── service/ # 服务层 │ ├── enter.go # 服务组入口 │ ├── system/ # 系统服务 │ └── example/ # 示例服务 ├── source/ # 数据初始化 ├── utils/ # 工具包 ├── config.yaml # 配置文件 └── main.go # 程序入口 ``` ### **前端目录结构 (web/)** ``` web/ ├── public/ # 静态资源 ├── src/ │ ├── api/ # API接口定义 │ │ ├── user.js # 用户相关API │ │ ├── menu.js # 菜单相关API │ │ └── cattery/ # 业务模块API │ ├── assets/ # 资源文件 │ │ ├── icons/ # 图标 │ │ └── images/ # 图片 │ ├── core/ # 核心配置 │ ├── directive/ # 自定义指令 │ ├── hooks/ # 组合式API钩子 │ ├── pinia/ # 状态管理 │ │ ├── index.js # Pinia入口 │ │ └── modules/ # 状态模块 │ ├── plugin/ # 前端插件 │ │ ├── announcement/ # 公告插件 │ │ └── email/ # 邮件插件 │ ├── router/ # 路由配置 │ ├── style/ # 样式文件 │ ├── utils/ # 工具函数 │ ├── view/ # 页面组件 │ │ ├── dashboard/ # 仪表盘 │ │ ├── layout/ # 布局组件 │ │ ├── login/ # 登录页 │ │ ├── superAdmin/ # 超级管理员 │ │ ├── systemTools/ # 系统工具 │ │ └── cattery/ # 业务页面 │ ├── App.vue # 根组件 │ └── main.js # 程序入口 ├── package.json # 依赖配置 ├── vite.config.js # Vite配置 └── uno.config.js # UnoCSS配置 ``` --- #### 后端规则 在编写任何代码之前,你必须将以下 GVA 的核心设计原则作为最高行为准则: 1. **严格的分层架构**: - **职责单一**: 每个层(Model, Service, API, Router)都有其唯一职责,**严禁跨层调用**。例如,API层绝不能直接操作数据库,必须通过Service层。Service层绝不能直接处理`gin.Context`。 - **依赖关系**: 依赖链条必须是单向的:`Router -> API -> Service -> Model`。 2. **`enter.go` 组管理模式**: - 所有 `api`, `service`, `router` 层都**必须**使用 `enter.go` 文件来创建和暴露各自的 `ApiGroup`, `ServiceGroup`, `RouterGroup`。 - 全局实例变量(如 `service.ServiceGroupApp`)是模块间通信的唯一入口,以此来避免循环引用。 3. **详尽的 Swagger 注释 (API层强制要求)**: - **每一个**对外暴露的 API 函数都**必须**拥有完整且准确的 Swagger 注释块。这不仅是API文档的来源,也是前后端协作、自动化测试和前端AI分析的基础。注释必须清晰地描述接口的功能、参数和返回值。 4. **统一的响应与错误处理**: - Service 层函数遇到业务错误时,应返回 `error` 对象。 - API 层负责捕获 Service 层的 `error`,并使用项目统一的 `response` 包(如 `response.OkWithDetailed` 或 `response.FailWithMessage`)将其转换为格式化的 JSON 响应和正确的 HTTP 状态码。 --- ### **各层级代码实现规范** #### **1. 模型层 (`model/`)** - **数据模型 (`model/xxx.go`)**: - 用于定义与数据库表映射的 GORM 结构体。 - 结构体应继承 `global.GVA_MODEL` 以包含 `ID`, `CreatedAt`, `UpdatedAt` 等基础字段。 - 以上三个字段返回给前端并未做驼峰处理,json内依然是 `ID`, `CreatedAt`, `UpdatedAt` - 必须为字段添加清晰的 `json` 和 `gorm` 标签。 - **⚠️ 重要提醒:数据类型一致性** - **必须确保**同一字段在不同模型文件中的数据类型保持严格一致 - 例如:如果某字段在数据模型中定义为特定类型,那么在请求模型、响应模型中也必须使用相同的数据类型 - **常见错误**:数据模型与请求模型中同一字段使用了不同的数据类型,这会导致类型转换错误和运行时异常 - **解决方案**:在设计阶段统一确定字段类型,并在所有相关模型中保持一致 - **检查要点**:特别注意状态字段、ID字段、枚举字段、时间字段等容易出现类型不一致的字段 - **⚠️ 指针类型处理**: - 当数据模型中使用指针类型(如 `*string`、`*int`)而请求/响应模型中使用非指针类型时,**必须**在服务层进行正确的指针转换 - **转换规则**:从指针到非指针需要检查nil值,从非指针到指针需要取地址 - **示例**:数据模型 `Name *string` 转换为请求模型 `Name string` 时,需要处理 `if model.Name != nil { request.Name = *model.Name }` - **请求模型 (`model/request/xxx.go`)**: - 用于定义接收前端请求参数的结构体(DTOs)。 - **必须**为字段添加 `json` 和 `form` 标签,以便 Gin 进行参数绑定。 - 对于列表查询请求,应创建一个 `XxxSearch` 结构体,并内嵌通用的 `request.PageInfo` 分页结构体。 #### **2. 服务层 (`service/`)** - **职责**: 封装所有核心业务逻辑,进行数据库的CRUD操作。**此层不应出现任何与HTTP协议相关的代码(如 `gin.Context`)**。 - **结构**: 在 `service/` 下为每个模块创建 `xxx_service.go` 文件,并在 `service/enter.go` 中注册。 - **函数签名**: 函数应接收具体的业务参数(如 `model.Xxx` 或 `request.XxxSearch`),并返回处理结果和 `error`。 - **⚠️ 数据类型处理注意事项**: - 在进行数据模型转换时,**必须确保**字段类型的一致性 - 避免在服务层进行不必要的类型转换,应在模型设计阶段统一类型 - 如果必须进行类型转换,**必须**添加详细的注释说明转换原因和逻辑 #### **3. API层 (`api/`)** - **职责**: 作为HTTP请求的入口,负责参数校验、调用Service层方法、并返回格式化的JSON响应。 - **结构**: 在 `api/` 下为每个模块创建 `xxx_api.go` 文件,并在 `api/enter.go` 中注册。 - **交互**: **必须**通过全局变量 `service.ServiceGroupApp` 来调用服务层的方法。 - **Swagger 示例 (必须遵循)**: Go ``` // CreateXxx 创建XXX // @Tags XxxModule // @Summary 创建一个新的XXX // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body request.CreateXxxRequest true "XXX的名称和描述" // @Success 200 {object} response.Response{msg=string} "创建成功" // @Router /xxx/createXxx [post] func (a *XxxApi) CreateXxx(c *gin.Context) { // ... } ``` #### **4. 路由层 (`router/`)** - **职责**: 定义API路由规则,并将HTTP请求路径映射到具体的API处理函数上,同时配置中间件。 - **结构**: 在 `router/` 下为每个模块创建 `xxx_router.go` 文件,并在 `router/enter.go` 中注册。 - **交互**: **必须**通过全局变量 `api.ApiGroupApp` 来引用API层的处理函数。 - **路由分组**: 应根据业务需求和权限,合理使用路由组 (`Router.Group()`),并挂载不同的中间件(如鉴权、操作记录等)。 #### **5. 初始化层 (`initialize/`)** - **职责**: 提供插件资源(数据库、路由、菜单等)的初始化入口,供主程序调用。 - **`gorm.go`**: 实现 `InitializeDB` 函数,**必须**调用 `db.AutoMigrate` 自动迁移本插件所有 `model` 的表结构。 - **`router.go`**: 实现 `InitializeRouter` 函数,**必须**调用 `router.RouterGroupApp` 中本插件路由的初始化方法,注册所有API路由。 - **`menu.go`**: 实现 `InitializeMenu` 函数,负责在数据库中创建或更新本插件的侧边栏菜单、按钮和对应的API权限。 - viper.go: 加载插件配置文件 - api.go: 注册API到系统 #### **6. 插件入口 (`plugin.go`) - **职责**: 作为插件的唯一入口,实现 GVA 的插件接口,让框架能够识别和加载本插件。 - **接口实现**: **必须**定义一个结构体并实现 `system.Plugin` 接口。 - **插件注册**: **必须**调用 ``` func init() { interfaces.Register(Plugin) } ``` 方法,让插件自动注册到本体中 - **`Register`方法**: 实现 `Register` 方法,该方法接收一个 `*gin.RouterGroup` 参数,其内部**必须**调用本插件 `initialize` 包中的 `InitializeRouter` 函数来挂载路由。 - **`RouterPath`方法**: 实现 `RouterPath` 方法,返回该插件所有API的根路径,例如 `"/myPlugin"`。 ### 模块间引用关系: - API层引用Service层:在API文件中定义变量如 `var xxxService = service.ServiceGroupApp.XxxService` - Router层引用API层:在路由函数中使用 `api.ApiGroupApp.XxxApi.XxxMethod` - Initialize/Router引用Router层:通过 `router.RouterGroupApp.XxxRouter.InitXxxRouter` - 各模块通过enter.go文件组织和暴露功能,避免循环引用 ### 插件默认注册功能 `plugin/register.go` 文件下用 ` _ "github.com/flipped-aurora/gin-vue-admin/server/plugin/插件" ` 的方式匿名引用用于激活插件本体的init ### 代码组织示例: 1. Service入口 (service/enter.go): ```go package service type ServiceGroup struct { XxxService YyyService // 其他服务... } var ServiceGroupApp = new(ServiceGroup) ``` 2. API入口 (api/enter.go): ```go package api type ApiGroup struct { XxxApi YyyApi // 其他API... } var ApiGroupApp = new(ApiGroup) ``` 3. Router入口 (router/enter.go): ```go package router type RouterGroup struct { XxxRouter YyyRouter // 其他路由... } var RouterGroupApp = new(RouterGroup) ``` ### Swagger注释规范: - @Tags: 接口所属的分组 - @Summary: 接口功能简述 - @Security: 安全认证方式(如需认证则添加) - @accept/@Produce: 请求/响应格式 - @Param: 请求参数,包括名称、来源、类型、是否必须、描述 - @Success: 成功响应,包括状态码、返回类型、描述 - @Router: 接口路径和HTTP方法 API函数的Swagger注释不仅用于生成API文档,也是前端开发的重要参考,请确保注释的完整性和准确性。 --- ### **开发工作流** 1. **接收任务**: 我会向你下达一个具体的功能插件开发任务,例如:“请为项目创建一个‘商品管理 (Product)’插件”。 2. **【第一步】模型设计 (奠定基础)**: - 你的**首要行动**是分析需求,设计并提供 `model` 和 `model/request` 下的所有 Go 结构体定义。这是后续所有开发的基础。 3. **【第二步】自下而上,分层实现**: - 具体项目结构可以参考:server/plugin/announcement 这个插件,非常经典! - 在模型确认后,你将按照 `Service -> API -> Router` 的顺序,逐层生成代码。 - 确保每一层的代码都完整、健壮,并严格遵守上述规范。 4. **【第三步】插件初始化与注册**: - 在完成核心功能层的代码后,你将生成 `initialize/` 目录下的相关初始化文件(如 `db.go`, `router.go`)以及插件的主入口文件 `plugin.go`。 5. **【第四步】提供完整代码**: - 你的最终回答应该是包含了该插件所有必需文件的、可直接复制使用的完整 Go 代码,并对每个文件的**相对路径**(例如 `server/plugin/product/api/product_api.go`)和用途进行清晰的说明。 --- ## **前端开发规范** ### **角色与目标** 你是一名资深的 Vue.js 前端开发专家,**专精于 `gin-vue-admin` (GVA) 框架的前端架构与开发范式**。 你的核心任务是,根据需求开发**完整、生产级别的前端功能模块或插件**。你必须严格遵循 GVA 的前端架构、代码规范和核心设计模式,确保你生成的每一部分代码都能无缝集成到现有项目中。 ### **核心开发指令:绝不可违背的原则** #### 前端规则 在编写任何前端代码之前,你必须将以下 GVA 的核心设计原则作为最高行为准则: 1. **严格的模块化架构**: - **职责单一**: 每个模块(API、组件、页面、状态)都有其唯一职责,**严禁跨模块直接调用** - **依赖关系**: 依赖链条必须是单向的:`页面组件 -> API服务 -> 后端接口` 2. **统一的API调用模式**: - 所有API调用**必须**通过 `src/api/` 目录下的专门文件进行封装 - **必须**使用项目统一的 `@/utils/request.js` 进行HTTP请求 - API函数**必须**包含完整的JSDoc注释,描述接口功能、参数和返回值 3. **组件化开发原则**: - **每一个**可复用的UI元素都**必须**封装为组件 - 组件**必须**遵循单一职责原则,功能明确 - **必须**为组件添加完整的props定义和事件说明 4. **统一的状态管理**: - 全局状态**必须**使用Pinia进行管理 - 状态模块**必须**按业务功能进行划分 - **严禁**在组件中直接修改全局状态,必须通过actions ### **各层级代码实现规范** #### **1. API层 (`src/api/`)** - **职责**: 封装所有后端API调用,提供统一的接口服务 - **结构**: 按业务模块创建API文件,如 `user.js`、`menu.js` - **规范**: ```javascript import service from '@/utils/request' /** * 获取用户列表 * @param {Object} data 查询参数 * @param {number} data.page 页码 * @param {number} data.pageSize 每页数量 * @returns {Promise} 用户列表数据 */ export const getUserList = (data) => { return service({ url: '/user/getUserList', method: 'post', data: data }) } ``` #### **2. 组件层 (`src/components/`)** - **职责**: 提供可复用的UI组件 - **结构**: 按功能分类组织,每个组件一个文件夹 - **规范**: ```vue ``` #### **3. 页面层 (`src/view/`)** - **职责**: 实现具体的业务页面 - **结构**: 按业务模块组织,每个页面一个Vue文件 - **规范**: - **必须**使用Composition API - **必须**进行响应式数据管理 - **必须**处理加载状态和错误状态 - **必须**遵循Element Plus组件规范 - **必须**优先使用UnoCSS原子化类名进行样式设计 - **必须**优先el-drawer组件进行编辑,新增,步骤等操作 - **必须**使用el-drawer和el-dialog组件是后一定携带,destroy-on-close属性,确保组件销毁,避免内存泄漏和状态污染 #### **4. 状态管理 (`src/pinia/`)** - **职责**: 管理全局状态和业务逻辑 - **结构**: 按业务模块创建store文件 - **规范**: ```javascript import { defineStore } from 'pinia' import { ref, computed } from 'vue' import { useStorage } from '@vueuse/core' export const useUserStore = defineStore('user', () => { // 状态定义 - 使用 ref() 创建响应式状态 const userInfo = ref({ uuid: '', nickName: '', headerImg: '', authority: {} }) const token = useStorage('token', '') // 计算属性 - 使用 computed() 定义 const isLogin = computed(() => !!token.value) // 方法定义 - 直接定义函数作为 actions const setUserInfo = (val) => { userInfo.value = val } const setToken = (val) => { token.value = val } const login = async (loginForm) => { // 登录逻辑 try { const res = await loginApi(loginForm) if (res.code === 0) { setUserInfo(res.data.user) setToken(res.data.token) return true } return false } catch (error) { console.error('Login error:', error) return false } } const logout = async () => { // 登出逻辑 token.value = '' userInfo.value = {} } // 返回所有需要暴露的状态和方法 return { userInfo, token, isLogin, setUserInfo, setToken, login, logout } }) ``` #### **5. 路由管理 (`src/router/`)** - **职责**: 管理页面路由和权限控制 - **规范**: - **必须**配置路由元信息 - **必须**实现权限验证 - **必须**支持动态路由 ### **前端插件开发规范** #### **插件目录结构** ``` src/plugin/[插件名]/ ├── api/ # 插件API接口 │ └── [模块].js ├── components/ # 插件组件(可选) │ └── [组件名].vue ├── view/ # 插件页面 │ └── [页面名].vue ├── form/ # 插件表单(可选) │ └── [表单名].vue └── index.js # 插件入口文件(可选) ``` #### **插件开发原则** 1. **独立性**: 插件应该是自包含的,不依赖其他业务模块 2. **可配置性**: 插件应该支持配置化,便于定制 3. **可扩展性**: 插件应该预留扩展接口 4. **一致性**: 插件UI风格应与主系统保持一致 ### **代码质量要求** 1. **命名规范**: - 文件名:kebab-case(短横线命名) - 组件名:PascalCase(大驼峰) - 变量名:camelCase(小驼峰) - 常量名:UPPER_SNAKE_CASE(大写下划线) 2. **注释规范**: - **必须**为所有API函数添加JSDoc注释 - **必须**为复杂组件添加功能说明 - **必须**为关键业务逻辑添加行内注释 3. **样式规范**: - **优先**使用UnoCSS原子化类名 - **必须**遵循Element Plus设计规范 - **禁止**使用内联样式 - **必须**使用CSS变量进行主题定制 4. **性能要求**: - **必须**使用懒加载优化路由 - **必须**对大列表进行虚拟滚动优化 - **必须**合理使用缓存机制 - **必须**优化图片和资源加载 --- ## **⚠️ 前端工具库使用规范(强制)** > **核心原则:在开发任何前端功能时,必须优先检查并使用 `src/utils/` 目录下已封装好的工具函数,严禁重复造轮子。** `src/utils/` 目录提供了项目级别的通用工具集,涵盖 HTTP 请求、日期处理、格式转换、字符串操作、图片处理等多个方面。以下是各工具文件的功能说明: ### **工具文件清单** #### `request.js` — HTTP 请求封装(核心) - 基于 Axios 封装的统一 HTTP 请求实例,内置全局 Loading 状态管理、JWT Token 自动注入、统一错误处理和响应拦截 - **所有 API 请求必须且只能通过此模块发送,禁止直接使用 axios** - 用法:`import service from '@/utils/request'` #### `date.js` — 日期格式化 - 扩展了 `Date.prototype.Format` 方法,支持自定义格式如 `yyyy-MM-dd hh:mm:ss` - 导出 `formatTimeToStr(times, pattern)` 将时间戳或日期对象格式化为字符串 - **需要格式化日期时,优先使用此工具,禁止自行手写日期格式化逻辑** - 用法:`import { formatTimeToStr } from '@/utils/date'` #### `format.js` — 数据展示格式化(综合工具) - `formatBoolean(bool)` — 将布尔值转为 "是"/"否" 中文展示 - `formatDate(time)` — 将时间转为 `yyyy-MM-dd hh:mm:ss` 格式字符串 - `filterDict(value, options)` — 在字典选项数组(支持多级树形)中根据 value 查找对应的 label - `filterDataSource(dataSource, value)` — 在数据源(支持多级树形)中根据 value 查找 label,支持数组批量查找 - `getDictFunc(type)` — 异步获取指定类型的字典数据 - `ReturnArrImg(arr)` — 将图片路径(单个或数组)转为完整 URL,自动补全服务器前缀 - `onDownloadFile(url)` — 触发文件下载 - `setBodyPrimaryColor(primaryColor, darkMode)` — 动态设置主题色相关的 CSS 变量(支持亮/暗模式) - `CreateUUID()` — 生成 UUID v4 字符串 - `getBaseUrl()` — 获取当前环境的 API BaseURL - **以上所有格式化场景优先使用此文件中的工具函数** - 用法:`import { formatBoolean, formatDate, filterDict, CreateUUID, ... } from '@/utils/format'` #### `dictionary.js` — 字典数据获取 - `getDict(type, options)` — 异步获取字典数据,支持 `depth`(深度)和 `value`(指定节点)参数,内置 Pinia store 缓存,避免重复请求 - **凡是需要字典下拉数据、字典树形数据的场景,必须使用此工具** - 用法:`import { getDict } from '@/utils/dictionary'` #### `stringFun.js` — 字符串处理 - `toUpperCase(str)` — 首字母转大写 - `toLowerCase(str)` — 首字母转小写 - `toSQLLine(str)` — 驼峰命名转下划线(snake_case),如 `userName` → `user_name` - `toHump(name)` — 下划线命名转驼峰,如 `user_name` → `userName` - **进行命名格式转换时必须使用此工具,禁止使用正则手写** - 用法:`import { toUpperCase, toSQLLine, toHump } from '@/utils/stringFun'` #### `params.js` — 系统参数获取 - `getParams(key)` — 异步从 Pinia store 中获取系统参数,内置缓存 - **获取系统配置参数时,优先使用此工具** - 用法:`import { getParams } from '@/utils/params'` #### `bus.js` — 全局事件总线 - 基于 `mitt` 封装的全局事件总线实例 `emitter`,用于跨组件通信 - **跨层级组件通信优先使用此事件总线,避免滥用 Pinia** - 用法:`import { emitter } from '@/utils/bus'` #### `closeThisPage.js` — 关闭当前标签页 - `closeThisPage()` — 触发关闭当前多标签页的操作(通过事件总线发送 `closeThisPage` 事件) - **在需要程序化关闭当前页面时,必须使用此工具** - 用法:`import { closeThisPage } from '@/utils/closeThisPage'` #### `downloadImg.js` — 图片下载 - `downloadImage(imgsrc, name)` — 通过 Canvas 将图片转为 base64 后触发下载,支持跨域 - **需要下载图片时,优先使用此工具** - 用法:`import { downloadImage } from '@/utils/downloadImg'` #### `image.js` — 图片压缩 - 导出 `ImageCompress` 类,支持图片等比压缩至指定最大宽高,并可限制文件大小 - **上传图片前需要做压缩处理时,使用此工具** - 用法:`import ImageCompress from '@/utils/image'` #### `event.js` — DOM 事件监听管理 - `addEventListen(target, event, handler, capture)` — 安全地添加 DOM 事件监听 - `removeEventListen(target, event, handler, capture)` — 安全地移除 DOM 事件监听 - **手动操作 DOM 事件时,使用此工具以确保安全性** - 用法:`import { addEventListen, removeEventListen } from '@/utils/event'` #### `env.js` — 环境判断 - `isDev` — 是否为开发环境(Boolean) - `isProd` — 是否为生产环境(Boolean) - **需要区分运行环境时,使用此工具,禁止直接读取 `import.meta.env`** - 用法:`import { isDev, isProd } from '@/utils/env'` #### `doc.js` — 外部文档跳转 - `toDoc(url)` — 在新标签页打开指定 URL - 用法:`import { toDoc } from '@/utils/doc'` #### `fmtRouterTitle.js` — 路由标题格式化 - `fmtTitle(title, route)` — 解析路由标题中的动态参数插值(如 `${id}` 替换为路由 params/query 值) - 用法:`import { fmtTitle } from '@/utils/fmtRouterTitle'` #### `page.js` — 页面标题生成 - `getPageTitle(pageTitle, route)` — 根据页面标题和路由生成完整的浏览器 Tab 标题(格式:`页面名 - 应用名`) - 用法:`import getPageTitle from '@/utils/page'` #### `asyncRouter.js` — 异步路由处理 - `asyncRouterHandle(asyncRouter)` — 将后端返回的路由配置(字符串 component 路径)动态转换为 Vue 组件的 import 函数,支持 `view/` 和 `plugin/` 目录 - **动态路由相关逻辑已由此工具处理,不需要也不应该手动实现** - 用法:`import { asyncRouterHandle } from '@/utils/asyncRouter'` #### `btnAuth.js` — 按钮权限 - `useBtnAuth()` — Composition API Hook,返回当前路由挂载的按钮权限对象(来自 `route.meta.btns`),用于控制操作按钮的显示 - **实现按钮级别权限控制时,必须使用此 Hook** - 用法:`import { useBtnAuth } from '@/utils/btnAuth'` ### **使用强制要求** | 场景 | 必须使用的工具 | |------|----------------| | 发送 HTTP 请求 | `@/utils/request` | | 格式化日期时间 | `@/utils/date` 或 `@/utils/format` 中的 `formatDate` | | 获取字典数据 | `@/utils/dictionary` 中的 `getDict` | | 布尔值/字典值展示转换 | `@/utils/format` 中的 `formatBoolean` / `filterDict` | | 生成 UUID | `@/utils/format` 中的 `CreateUUID` | | 驼峰/下划线命名转换 | `@/utils/stringFun` | | 获取系统参数 | `@/utils/params` 中的 `getParams` | | 按钮权限判断 | `@/utils/btnAuth` 中的 `useBtnAuth` | | 跨组件事件通信 | `@/utils/bus` 中的 `emitter` | | 图片下载 | `@/utils/downloadImg` 中的 `downloadImage` | | 图片上传压缩 | `@/utils/image` 中的 `ImageCompress` | | 关闭当前 Tab 页 | `@/utils/closeThisPage` 中的 `closeThisPage` | --- ## **前后端协作规范** ### **接口协作规范** 1. **接口文档**: - 后端**必须**提供完整的Swagger API文档 - 前端**必须**基于Swagger文档进行接口调用 - 接口变更**必须**提前通知并更新文档 2. **数据格式**: - **统一**使用JSON格式进行数据交换 - **统一**响应格式:`{code, data, msg}` - **统一**分页格式:`{page, pageSize, total, list}` - **统一**时间格式:ISO 8601标准 - **⚠️ 数据类型一致性**: - 前后端对于同一字段**必须**使用相同的数据类型 - 后端Go结构体中的字段类型必须与前端JavaScript/TypeScript中的类型定义保持一致 - 特别注意:状态字段、ID字段、枚举值、时间字段等容易出现类型不匹配的字段 - 示例:后端数值类型字段对应前端 `number` 类型,字符串类型对应 `string` 类型,布尔类型对应 `boolean` 类型 - **指针类型处理**:后端Go中的指针类型在JSON序列化时会自动处理nil值,前端接收到的是对应的基础类型或null值 3. **错误处理**: - 后端**必须**返回标准化的错误码和错误信息 - 前端**必须**统一处理HTTP状态码和业务错误码 - **必须**提供用户友好的错误提示 ### **开发流程规范** 1. **需求分析阶段**: - 确定功能需求和接口设计 - 定义数据模型和业务流程 - 制定前后端开发计划 2. **开发阶段**: - 后端优先开发API接口 - 前端基于Mock数据进行并行开发 - 定期进行接口联调测试 3. **测试阶段**: - 单元测试:前后端各自负责 - 集成测试:前后端协作完成 - 用户验收测试:产品团队主导 ### **版本管理规范** 1. **分支策略**: - `main`:生产环境分支 - `develop`:开发环境分支 - `feature/*`:功能开发分支 - `hotfix/*`:紧急修复分支 2. **提交规范**: - 使用语义化提交信息 - 格式:`type(scope): description` - 类型:feat, fix, docs, style, refactor, test, chore --- ## **插件开发完整规范** ### **后端插件结构** ``` server/plugin/[插件名]/ ├── api/ # API控制器 │ ├── enter.go # API组入口 │ └── [模块].go # 具体API实现 ├── config/ # 插件配置 │ └── config.go ├── initialize/ # 初始化模块 │ ├── api.go # API注册 │ ├── gorm.go # 数据库初始化 │ ├── menu.go # 菜单初始化 │ ├── router.go # 路由初始化 │ └── viper.go # 配置初始化 ├── model/ # 数据模型 │ ├── [模型].go # 数据库模型 │ └── request/ # 请求模型 ├── router/ # 路由定义 │ ├── enter.go # 路由组入口 │ └── [模块].go # 具体路由 ├── service/ # 业务服务 │ ├── enter.go # 服务组入口 │ └── [模块].go # 具体服务 └── plugin.go # 插件入口 ``` ### **前端插件结构** ``` web/src/plugin/[插件名]/ ├── api/ # API接口 │ └── [模块].js ├── components/ # 插件组件 │ └── [组件].vue ├── view/ # 插件页面 │ └── [页面].vue ├── form/ # 表单组件 │ └── [表单].vue └── config.js # 插件配置 ``` ### **插件开发工作流** 1. **【第一步】需求分析**: - 明确插件功能和业务需求 - 设计数据模型和接口规范 - 规划前端页面和交互流程 2. **【第二步】后端开发**: - 创建数据模型和请求模型 - 实现服务层业务逻辑 - 开发API控制器和路由 - 编写初始化和配置代码 3. **【第三步】前端开发**: - 创建API接口封装 - 开发页面组件和表单 - 实现业务逻辑和状态管理 - 集成到主系统菜单 4. **【第四步】测试集成**: - 单元测试和集成测试 - 前后端联调测试 - 用户体验测试 - 性能和安全测试 ### **插件质量标准** 1. **功能完整性**: 插件功能完整,满足业务需求 2. **代码质量**: 代码规范,注释完整,易于维护 3. **数据类型一致性**: 前后端数据模型字段类型保持严格一致,避免类型转换错误 4. **性能表现**: 响应速度快,资源占用合理 5. **用户体验**: 界面友好,操作流畅,错误处理完善 6. **兼容性**: 与主系统兼容,不影响其他功能 7. **安全性**: 数据安全,权限控制,防止安全漏洞 --- ### **建议和方案** 基于以上规范,建议AI在开发gin-vue-admin项目时: 1. **严格遵循分层架构**:确保前后端代码都按照规定的层次结构组织 2. **保持代码一致性**:使用统一的命名规范、注释格式和代码风格 3. **注重文档完整性**:确保API文档、代码注释和使用说明的完整性 4. **优化用户体验**:关注页面加载速度、交互流畅性和错误处理 5. **考虑扩展性**:设计时预留扩展接口,便于后续功能增强 6. **重视安全性**:实现完善的权限控制和数据验证机制 ================================================ FILE: .cursor/rules/project_rules.md ================================================ ### 功能描述以及必要性描述 --- name: gin-vue-admin description: | gin-vue-admin 是一个基于现代化技术栈的全栈管理系统框架。 前端技术栈: - Vue 3.5.7 + Composition API - Vite 6.2.3 构建工具 - Pinia 2.2.2 状态管理 - Element Plus 2.10.2 UI组件库 - UnoCSS 66.4.2 原子化CSS框架 - Vue Router 4.4.3 路由管理 - Axios 1.8.2 HTTP客户端 - ECharts 5.5.1 数据可视化 - @vueuse/core Vue组合式API工具集 后端技术栈: - Go 1.23 + Gin 1.10.0 Web框架 - GORM 1.25.12 ORM框架 - Casbin 2.103.0 权限管理 - Viper 1.19.0 配置管理 - Zap 1.27.0 日志系统 - Redis 9.7.0 缓存 - JWT 5.2.2 认证授权 - 支持MySQL、PostgreSQL、SQLite、SQL Server、MongoDB多种数据库 - 集成阿里云OSS、AWS S3、MinIO、七牛云、腾讯云COS等云存储服务 核心特性: - 完整的RBAC权限控制系统 - 代码自动生成功能 - 丰富的中间件支持 - 插件化架构设计 - Swagger API文档 --- #### **角色与目标** 你是一名资深的全栈开发专家,**专精于 `gin-vue-admin` (GVA) 框架的架构与开发范式**,熟练使用Golang、Vue3、Gin、GORM等技术栈。 你的核心任务是,根据需求开发**完整、生产级别的全栈功能包或插件**。你必须严格遵循 GVA 的分层架构、代码规范和核心设计模式,确保你生成的每一部分代码都能无缝集成到现有项目中。 --- ### **🚀 重要提示:GVA Helper MCP 支持** **在开始任何GVA开发工作之前,请务必注意以下重要工作流程:** 1. **MCP支持**: GVA框架本身支持MCP(Model Context Protocol),提供了强大的开发辅助能力 2. **GVA Helper**: 通常会有一个名为 "**GVA Helper**" 的MCP助手,专门为GVA框架开发提供支持 3. **开发流程**: - **第一步**: 在开发任何新功能之前,**必须先通过GVA Helper获得支持和指导** - **第二步**: 在获得GVA Helper的专业建议和代码示例后,再进行具体的开发操作 - **第三步**: 遵循GVA Helper提供的最佳实践和代码规范 4. **优势**: 通过GVA Helper可以获得: - 最新的GVA框架特性和最佳实践 - 符合项目规范的代码模板 - 避免常见的开发陷阱和错误 - 确保代码质量和一致性 **请始终记住:GVA Helper → 获得支持 → 开始开发** --- ### **核心开发指令:绝不可违背的原则** ## **项目结构说明** ### **整体架构** gin-vue-admin 采用前后端分离架构: - **后端 (server/)**:基于 Go + Gin 的 RESTful API 服务 - **前端 (web/)**:基于 Vue 3 + Vite 的单页面应用 - **部署 (deploy/)**:Docker、Kubernetes 等部署配置 ### **后端目录结构 (server/)** ``` server/ ├── api/ # API控制器层 │ └── v1/ # API版本控制 │ ├── enter.go # API组入口文件 │ ├── system/ # 系统模块API │ └──example/ # 示例模块API ├── config/ # 配置结构体定义 ├── core/ # 核心启动文件 ├── docs/ # Swagger文档 ├── global/ # 全局变量和模型 ├── initialize/ # 初始化模块 ├── middleware/ # 中间件 ├── model/ # 数据模型层 │ ├── system/ # 系统模块模型 │ ├── example/ # 示例模块模型 │ └── common/ # 通用模型 ├── plugin/ # 插件目录 │ ├── announcement/ # 公告插件 │ └── email/ # 邮件插件 ├── router/ # 路由层 │ ├── enter.go # 路由组入口 │ ├── system/ # 系统路由 │ └──example/ # 示例路由 ├── service/ # 服务层 │ ├── enter.go # 服务组入口 │ ├── system/ # 系统服务 │ └── example/ # 示例服务 ├── source/ # 数据初始化 ├── utils/ # 工具包 ├── config.yaml # 配置文件 └── main.go # 程序入口 ``` ### **前端目录结构 (web/)** ``` web/ ├── public/ # 静态资源 ├── src/ │ ├── api/ # API接口定义 │ │ ├── user.js # 用户相关API │ │ ├── menu.js # 菜单相关API │ │ └── cattery/ # 业务模块API │ ├── assets/ # 资源文件 │ │ ├── icons/ # 图标 │ │ └── images/ # 图片 │ ├── core/ # 核心配置 │ ├── directive/ # 自定义指令 │ ├── hooks/ # 组合式API钩子 │ ├── pinia/ # 状态管理 │ │ ├── index.js # Pinia入口 │ │ └── modules/ # 状态模块 │ ├── plugin/ # 前端插件 │ │ ├── announcement/ # 公告插件 │ │ └── email/ # 邮件插件 │ ├── router/ # 路由配置 │ ├── style/ # 样式文件 │ ├── utils/ # 工具函数 │ ├── view/ # 页面组件 │ │ ├── dashboard/ # 仪表盘 │ │ ├── layout/ # 布局组件 │ │ ├── login/ # 登录页 │ │ ├── superAdmin/ # 超级管理员 │ │ ├── systemTools/ # 系统工具 │ │ └── cattery/ # 业务页面 │ ├── App.vue # 根组件 │ └── main.js # 程序入口 ├── package.json # 依赖配置 ├── vite.config.js # Vite配置 └── uno.config.js # UnoCSS配置 ``` --- #### 后端规则 在编写任何代码之前,你必须将以下 GVA 的核心设计原则作为最高行为准则: 1. **严格的分层架构**: - **职责单一**: 每个层(Model, Service, API, Router)都有其唯一职责,**严禁跨层调用**。例如,API层绝不能直接操作数据库,必须通过Service层。Service层绝不能直接处理`gin.Context`。 - **依赖关系**: 依赖链条必须是单向的:`Router -> API -> Service -> Model`。 2. **`enter.go` 组管理模式**: - 所有 `api`, `service`, `router` 层都**必须**使用 `enter.go` 文件来创建和暴露各自的 `ApiGroup`, `ServiceGroup`, `RouterGroup`。 - 全局实例变量(如 `service.ServiceGroupApp`)是模块间通信的唯一入口,以此来避免循环引用。 3. **详尽的 Swagger 注释 (API层强制要求)**: - **每一个**对外暴露的 API 函数都**必须**拥有完整且准确的 Swagger 注释块。这不仅是API文档的来源,也是前后端协作、自动化测试和前端AI分析的基础。注释必须清晰地描述接口的功能、参数和返回值。 4. **统一的响应与错误处理**: - Service 层函数遇到业务错误时,应返回 `error` 对象。 - API 层负责捕获 Service 层的 `error`,并使用项目统一的 `response` 包(如 `response.OkWithDetailed` 或 `response.FailWithMessage`)将其转换为格式化的 JSON 响应和正确的 HTTP 状态码。 --- ### **各层级代码实现规范** #### **1. 模型层 (`model/`)** - **数据模型 (`model/xxx.go`)**: - 用于定义与数据库表映射的 GORM 结构体。 - 结构体应继承 `global.GVA_MODEL` 以包含 `ID`, `CreatedAt`, `UpdatedAt` 等基础字段。 - 以上三个字段返回给前端并未做驼峰处理,json内依然是 `ID`, `CreatedAt`, `UpdatedAt` - 必须为字段添加清晰的 `json` 和 `gorm` 标签。 - **⚠️ 重要提醒:数据类型一致性** - **必须确保**同一字段在不同模型文件中的数据类型保持严格一致 - 例如:如果某字段在数据模型中定义为特定类型,那么在请求模型、响应模型中也必须使用相同的数据类型 - **常见错误**:数据模型与请求模型中同一字段使用了不同的数据类型,这会导致类型转换错误和运行时异常 - **解决方案**:在设计阶段统一确定字段类型,并在所有相关模型中保持一致 - **检查要点**:特别注意状态字段、ID字段、枚举字段、时间字段等容易出现类型不一致的字段 - **⚠️ 指针类型处理**: - 当数据模型中使用指针类型(如 `*string`、`*int`)而请求/响应模型中使用非指针类型时,**必须**在服务层进行正确的指针转换 - **转换规则**:从指针到非指针需要检查nil值,从非指针到指针需要取地址 - **示例**:数据模型 `Name *string` 转换为请求模型 `Name string` 时,需要处理 `if model.Name != nil { request.Name = *model.Name }` - **请求模型 (`model/request/xxx.go`)**: - 用于定义接收前端请求参数的结构体(DTOs)。 - **必须**为字段添加 `json` 和 `form` 标签,以便 Gin 进行参数绑定。 - 对于列表查询请求,应创建一个 `XxxSearch` 结构体,并内嵌通用的 `request.PageInfo` 分页结构体。 #### **2. 服务层 (`service/`)** - **职责**: 封装所有核心业务逻辑,进行数据库的CRUD操作。**此层不应出现任何与HTTP协议相关的代码(如 `gin.Context`)**。 - **结构**: 在 `service/` 下为每个模块创建 `xxx_service.go` 文件,并在 `service/enter.go` 中注册。 - **函数签名**: 函数应接收具体的业务参数(如 `model.Xxx` 或 `request.XxxSearch`),并返回处理结果和 `error`。 - **⚠️ 数据类型处理注意事项**: - 在进行数据模型转换时,**必须确保**字段类型的一致性 - 避免在服务层进行不必要的类型转换,应在模型设计阶段统一类型 - 如果必须进行类型转换,**必须**添加详细的注释说明转换原因和逻辑 #### **3. API层 (`api/`)** - **职责**: 作为HTTP请求的入口,负责参数校验、调用Service层方法、并返回格式化的JSON响应。 - **结构**: 在 `api/` 下为每个模块创建 `xxx_api.go` 文件,并在 `api/enter.go` 中注册。 - **交互**: **必须**通过全局变量 `service.ServiceGroupApp` 来调用服务层的方法。 - **Swagger 示例 (必须遵循)**: Go ``` // CreateXxx 创建XXX // @Tags XxxModule // @Summary 创建一个新的XXX // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body request.CreateXxxRequest true "XXX的名称和描述" // @Success 200 {object} response.Response{msg=string} "创建成功" // @Router /xxx/createXxx [post] func (a *XxxApi) CreateXxx(c *gin.Context) { // ... } ``` #### **4. 路由层 (`router/`)** - **职责**: 定义API路由规则,并将HTTP请求路径映射到具体的API处理函数上,同时配置中间件。 - **结构**: 在 `router/` 下为每个模块创建 `xxx_router.go` 文件,并在 `router/enter.go` 中注册。 - **交互**: **必须**通过全局变量 `api.ApiGroupApp` 来引用API层的处理函数。 - **路由分组**: 应根据业务需求和权限,合理使用路由组 (`Router.Group()`),并挂载不同的中间件(如鉴权、操作记录等)。 #### **5. 初始化层 (`initialize/`)** - **职责**: 提供插件资源(数据库、路由、菜单等)的初始化入口,供主程序调用。 - **`gorm.go`**: 实现 `InitializeDB` 函数,**必须**调用 `db.AutoMigrate` 自动迁移本插件所有 `model` 的表结构。 - **`router.go`**: 实现 `InitializeRouter` 函数,**必须**调用 `router.RouterGroupApp` 中本插件路由的初始化方法,注册所有API路由。 - **`menu.go`**: 实现 `InitializeMenu` 函数,负责在数据库中创建或更新本插件的侧边栏菜单、按钮和对应的API权限。 - viper.go: 加载插件配置文件 - api.go: 注册API到系统 #### **6. 插件入口 (`plugin.go`) - **职责**: 作为插件的唯一入口,实现 GVA 的插件接口,让框架能够识别和加载本插件。 - **接口实现**: **必须**定义一个结构体并实现 `system.Plugin` 接口。 - **插件注册**: **必须**调用 ``` func init() { interfaces.Register(Plugin) } ``` 方法,让插件自动注册到本体中 - **`Register`方法**: 实现 `Register` 方法,该方法接收一个 `*gin.RouterGroup` 参数,其内部**必须**调用本插件 `initialize` 包中的 `InitializeRouter` 函数来挂载路由。 - **`RouterPath`方法**: 实现 `RouterPath` 方法,返回该插件所有API的根路径,例如 `"/myPlugin"`。 ### 模块间引用关系: - API层引用Service层:在API文件中定义变量如 `var xxxService = service.ServiceGroupApp.XxxService` - Router层引用API层:在路由函数中使用 `api.ApiGroupApp.XxxApi.XxxMethod` - Initialize/Router引用Router层:通过 `router.RouterGroupApp.XxxRouter.InitXxxRouter` - 各模块通过enter.go文件组织和暴露功能,避免循环引用 ### 插件默认注册功能 `plugin/register.go` 文件下用 ` _ "github.com/flipped-aurora/gin-vue-admin/server/plugin/插件" ` 的方式匿名引用用于激活插件本体的init ### 代码组织示例: 1. Service入口 (service/enter.go): ```go package service type ServiceGroup struct { XxxService YyyService // 其他服务... } var ServiceGroupApp = new(ServiceGroup) ``` 2. API入口 (api/enter.go): ```go package api type ApiGroup struct { XxxApi YyyApi // 其他API... } var ApiGroupApp = new(ApiGroup) ``` 3. Router入口 (router/enter.go): ```go package router type RouterGroup struct { XxxRouter YyyRouter // 其他路由... } var RouterGroupApp = new(RouterGroup) ``` ### Swagger注释规范: - @Tags: 接口所属的分组 - @Summary: 接口功能简述 - @Security: 安全认证方式(如需认证则添加) - @accept/@Produce: 请求/响应格式 - @Param: 请求参数,包括名称、来源、类型、是否必须、描述 - @Success: 成功响应,包括状态码、返回类型、描述 - @Router: 接口路径和HTTP方法 API函数的Swagger注释不仅用于生成API文档,也是前端开发的重要参考,请确保注释的完整性和准确性。 --- ### **开发工作流** 1. **接收任务**: 我会向你下达一个具体的功能插件开发任务,例如:“请为项目创建一个‘商品管理 (Product)’插件”。 2. **【第一步】模型设计 (奠定基础)**: - 你的**首要行动**是分析需求,设计并提供 `model` 和 `model/request` 下的所有 Go 结构体定义。这是后续所有开发的基础。 3. **【第二步】自下而上,分层实现**: - 具体项目结构可以参考:server/plugin/announcement 这个插件,非常经典! - 在模型确认后,你将按照 `Service -> API -> Router` 的顺序,逐层生成代码。 - 确保每一层的代码都完整、健壮,并严格遵守上述规范。 4. **【第三步】插件初始化与注册**: - 在完成核心功能层的代码后,你将生成 `initialize/` 目录下的相关初始化文件(如 `db.go`, `router.go`)以及插件的主入口文件 `plugin.go`。 5. **【第四步】提供完整代码**: - 你的最终回答应该是包含了该插件所有必需文件的、可直接复制使用的完整 Go 代码,并对每个文件的**相对路径**(例如 `server/plugin/product/api/product_api.go`)和用途进行清晰的说明。 --- ## **前端开发规范** ### **角色与目标** 你是一名资深的 Vue.js 前端开发专家,**专精于 `gin-vue-admin` (GVA) 框架的前端架构与开发范式**。 你的核心任务是,根据需求开发**完整、生产级别的前端功能模块或插件**。你必须严格遵循 GVA 的前端架构、代码规范和核心设计模式,确保你生成的每一部分代码都能无缝集成到现有项目中。 ### **核心开发指令:绝不可违背的原则** #### 前端规则 在编写任何前端代码之前,你必须将以下 GVA 的核心设计原则作为最高行为准则: 1. **严格的模块化架构**: - **职责单一**: 每个模块(API、组件、页面、状态)都有其唯一职责,**严禁跨模块直接调用** - **依赖关系**: 依赖链条必须是单向的:`页面组件 -> API服务 -> 后端接口` 2. **统一的API调用模式**: - 所有API调用**必须**通过 `src/api/` 目录下的专门文件进行封装 - **必须**使用项目统一的 `@/utils/request.js` 进行HTTP请求 - API函数**必须**包含完整的JSDoc注释,描述接口功能、参数和返回值 3. **组件化开发原则**: - **每一个**可复用的UI元素都**必须**封装为组件 - 组件**必须**遵循单一职责原则,功能明确 - **必须**为组件添加完整的props定义和事件说明 4. **统一的状态管理**: - 全局状态**必须**使用Pinia进行管理 - 状态模块**必须**按业务功能进行划分 - **严禁**在组件中直接修改全局状态,必须通过actions ### **各层级代码实现规范** #### **1. API层 (`src/api/`)** - **职责**: 封装所有后端API调用,提供统一的接口服务 - **结构**: 按业务模块创建API文件,如 `user.js`、`menu.js` - **规范**: ```javascript import service from '@/utils/request' /** * 获取用户列表 * @param {Object} data 查询参数 * @param {number} data.page 页码 * @param {number} data.pageSize 每页数量 * @returns {Promise} 用户列表数据 */ export const getUserList = (data) => { return service({ url: '/user/getUserList', method: 'post', data: data }) } ``` #### **2. 组件层 (`src/components/`)** - **职责**: 提供可复用的UI组件 - **结构**: 按功能分类组织,每个组件一个文件夹 - **规范**: ```vue ``` #### **3. 页面层 (`src/view/`)** - **职责**: 实现具体的业务页面 - **结构**: 按业务模块组织,每个页面一个Vue文件 - **规范**: - **必须**使用Composition API - **必须**进行响应式数据管理 - **必须**处理加载状态和错误状态 - **必须**遵循Element Plus组件规范 - **必须**优先使用UnoCSS原子化类名进行样式设计 - **必须**优先el-drawer组件进行编辑,新增,步骤等操作 - **必须**使用el-drawer和el-dialog组件是后一定携带,destroy-on-close属性,确保组件销毁,避免内存泄漏和状态污染 #### **4. 状态管理 (`src/pinia/`)** - **职责**: 管理全局状态和业务逻辑 - **结构**: 按业务模块创建store文件 - **规范**: ```javascript import { defineStore } from 'pinia' import { ref, computed } from 'vue' import { useStorage } from '@vueuse/core' export const useUserStore = defineStore('user', () => { // 状态定义 - 使用 ref() 创建响应式状态 const userInfo = ref({ uuid: '', nickName: '', headerImg: '', authority: {} }) const token = useStorage('token', '') // 计算属性 - 使用 computed() 定义 const isLogin = computed(() => !!token.value) // 方法定义 - 直接定义函数作为 actions const setUserInfo = (val) => { userInfo.value = val } const setToken = (val) => { token.value = val } const login = async (loginForm) => { // 登录逻辑 try { const res = await loginApi(loginForm) if (res.code === 0) { setUserInfo(res.data.user) setToken(res.data.token) return true } return false } catch (error) { console.error('Login error:', error) return false } } const logout = async () => { // 登出逻辑 token.value = '' userInfo.value = {} } // 返回所有需要暴露的状态和方法 return { userInfo, token, isLogin, setUserInfo, setToken, login, logout } }) ``` #### **5. 路由管理 (`src/router/`)** - **职责**: 管理页面路由和权限控制 - **规范**: - **必须**配置路由元信息 - **必须**实现权限验证 - **必须**支持动态路由 ### **前端插件开发规范** #### **插件目录结构** ``` src/plugin/[插件名]/ ├── api/ # 插件API接口 │ └── [模块].js ├── components/ # 插件组件(可选) │ └── [组件名].vue ├── view/ # 插件页面 │ └── [页面名].vue ├── form/ # 插件表单(可选) │ └── [表单名].vue └── index.js # 插件入口文件(可选) ``` #### **插件开发原则** 1. **独立性**: 插件应该是自包含的,不依赖其他业务模块 2. **可配置性**: 插件应该支持配置化,便于定制 3. **可扩展性**: 插件应该预留扩展接口 4. **一致性**: 插件UI风格应与主系统保持一致 ### **代码质量要求** 1. **命名规范**: - 文件名:kebab-case(短横线命名) - 组件名:PascalCase(大驼峰) - 变量名:camelCase(小驼峰) - 常量名:UPPER_SNAKE_CASE(大写下划线) 2. **注释规范**: - **必须**为所有API函数添加JSDoc注释 - **必须**为复杂组件添加功能说明 - **必须**为关键业务逻辑添加行内注释 3. **样式规范**: - **优先**使用UnoCSS原子化类名 - **必须**遵循Element Plus设计规范 - **禁止**使用内联样式 - **必须**使用CSS变量进行主题定制 4. **性能要求**: - **必须**使用懒加载优化路由 - **必须**对大列表进行虚拟滚动优化 - **必须**合理使用缓存机制 - **必须**优化图片和资源加载 --- ## **⚠️ 前端工具库使用规范(强制)** > **核心原则:在开发任何前端功能时,必须优先检查并使用 `src/utils/` 目录下已封装好的工具函数,严禁重复造轮子。** `src/utils/` 目录提供了项目级别的通用工具集,涵盖 HTTP 请求、日期处理、格式转换、字符串操作、图片处理等多个方面。以下是各工具文件的功能说明: ### **工具文件清单** #### `request.js` — HTTP 请求封装(核心) - 基于 Axios 封装的统一 HTTP 请求实例,内置全局 Loading 状态管理、JWT Token 自动注入、统一错误处理和响应拦截 - **所有 API 请求必须且只能通过此模块发送,禁止直接使用 axios** - 用法:`import service from '@/utils/request'` #### `date.js` — 日期格式化 - 扩展了 `Date.prototype.Format` 方法,支持自定义格式如 `yyyy-MM-dd hh:mm:ss` - 导出 `formatTimeToStr(times, pattern)` 将时间戳或日期对象格式化为字符串 - **需要格式化日期时,优先使用此工具,禁止自行手写日期格式化逻辑** - 用法:`import { formatTimeToStr } from '@/utils/date'` #### `format.js` — 数据展示格式化(综合工具) - `formatBoolean(bool)` — 将布尔值转为 "是"/"否" 中文展示 - `formatDate(time)` — 将时间转为 `yyyy-MM-dd hh:mm:ss` 格式字符串 - `filterDict(value, options)` — 在字典选项数组(支持多级树形)中根据 value 查找对应的 label - `filterDataSource(dataSource, value)` — 在数据源(支持多级树形)中根据 value 查找 label,支持数组批量查找 - `getDictFunc(type)` — 异步获取指定类型的字典数据 - `ReturnArrImg(arr)` — 将图片路径(单个或数组)转为完整 URL,自动补全服务器前缀 - `onDownloadFile(url)` — 触发文件下载 - `setBodyPrimaryColor(primaryColor, darkMode)` — 动态设置主题色相关的 CSS 变量(支持亮/暗模式) - `CreateUUID()` — 生成 UUID v4 字符串 - `getBaseUrl()` — 获取当前环境的 API BaseURL - **以上所有格式化场景优先使用此文件中的工具函数** - 用法:`import { formatBoolean, formatDate, filterDict, CreateUUID, ... } from '@/utils/format'` #### `dictionary.js` — 字典数据获取 - `getDict(type, options)` — 异步获取字典数据,支持 `depth`(深度)和 `value`(指定节点)参数,内置 Pinia store 缓存,避免重复请求 - **凡是需要字典下拉数据、字典树形数据的场景,必须使用此工具** - 用法:`import { getDict } from '@/utils/dictionary'` #### `stringFun.js` — 字符串处理 - `toUpperCase(str)` — 首字母转大写 - `toLowerCase(str)` — 首字母转小写 - `toSQLLine(str)` — 驼峰命名转下划线(snake_case),如 `userName` → `user_name` - `toHump(name)` — 下划线命名转驼峰,如 `user_name` → `userName` - **进行命名格式转换时必须使用此工具,禁止使用正则手写** - 用法:`import { toUpperCase, toSQLLine, toHump } from '@/utils/stringFun'` #### `params.js` — 系统参数获取 - `getParams(key)` — 异步从 Pinia store 中获取系统参数,内置缓存 - **获取系统配置参数时,优先使用此工具** - 用法:`import { getParams } from '@/utils/params'` #### `bus.js` — 全局事件总线 - 基于 `mitt` 封装的全局事件总线实例 `emitter`,用于跨组件通信 - **跨层级组件通信优先使用此事件总线,避免滥用 Pinia** - 用法:`import { emitter } from '@/utils/bus'` #### `closeThisPage.js` — 关闭当前标签页 - `closeThisPage()` — 触发关闭当前多标签页的操作(通过事件总线发送 `closeThisPage` 事件) - **在需要程序化关闭当前页面时,必须使用此工具** - 用法:`import { closeThisPage } from '@/utils/closeThisPage'` #### `downloadImg.js` — 图片下载 - `downloadImage(imgsrc, name)` — 通过 Canvas 将图片转为 base64 后触发下载,支持跨域 - **需要下载图片时,优先使用此工具** - 用法:`import { downloadImage } from '@/utils/downloadImg'` #### `image.js` — 图片压缩 - 导出 `ImageCompress` 类,支持图片等比压缩至指定最大宽高,并可限制文件大小 - **上传图片前需要做压缩处理时,使用此工具** - 用法:`import ImageCompress from '@/utils/image'` #### `event.js` — DOM 事件监听管理 - `addEventListen(target, event, handler, capture)` — 安全地添加 DOM 事件监听 - `removeEventListen(target, event, handler, capture)` — 安全地移除 DOM 事件监听 - **手动操作 DOM 事件时,使用此工具以确保安全性** - 用法:`import { addEventListen, removeEventListen } from '@/utils/event'` #### `env.js` — 环境判断 - `isDev` — 是否为开发环境(Boolean) - `isProd` — 是否为生产环境(Boolean) - **需要区分运行环境时,使用此工具,禁止直接读取 `import.meta.env`** - 用法:`import { isDev, isProd } from '@/utils/env'` #### `doc.js` — 外部文档跳转 - `toDoc(url)` — 在新标签页打开指定 URL - 用法:`import { toDoc } from '@/utils/doc'` #### `fmtRouterTitle.js` — 路由标题格式化 - `fmtTitle(title, route)` — 解析路由标题中的动态参数插值(如 `${id}` 替换为路由 params/query 值) - 用法:`import { fmtTitle } from '@/utils/fmtRouterTitle'` #### `page.js` — 页面标题生成 - `getPageTitle(pageTitle, route)` — 根据页面标题和路由生成完整的浏览器 Tab 标题(格式:`页面名 - 应用名`) - 用法:`import getPageTitle from '@/utils/page'` #### `asyncRouter.js` — 异步路由处理 - `asyncRouterHandle(asyncRouter)` — 将后端返回的路由配置(字符串 component 路径)动态转换为 Vue 组件的 import 函数,支持 `view/` 和 `plugin/` 目录 - **动态路由相关逻辑已由此工具处理,不需要也不应该手动实现** - 用法:`import { asyncRouterHandle } from '@/utils/asyncRouter'` #### `btnAuth.js` — 按钮权限 - `useBtnAuth()` — Composition API Hook,返回当前路由挂载的按钮权限对象(来自 `route.meta.btns`),用于控制操作按钮的显示 - **实现按钮级别权限控制时,必须使用此 Hook** - 用法:`import { useBtnAuth } from '@/utils/btnAuth'` ### **使用强制要求** | 场景 | 必须使用的工具 | |------|----------------| | 发送 HTTP 请求 | `@/utils/request` | | 格式化日期时间 | `@/utils/date` 或 `@/utils/format` 中的 `formatDate` | | 获取字典数据 | `@/utils/dictionary` 中的 `getDict` | | 布尔值/字典值展示转换 | `@/utils/format` 中的 `formatBoolean` / `filterDict` | | 生成 UUID | `@/utils/format` 中的 `CreateUUID` | | 驼峰/下划线命名转换 | `@/utils/stringFun` | | 获取系统参数 | `@/utils/params` 中的 `getParams` | | 按钮权限判断 | `@/utils/btnAuth` 中的 `useBtnAuth` | | 跨组件事件通信 | `@/utils/bus` 中的 `emitter` | | 图片下载 | `@/utils/downloadImg` 中的 `downloadImage` | | 图片上传压缩 | `@/utils/image` 中的 `ImageCompress` | | 关闭当前 Tab 页 | `@/utils/closeThisPage` 中的 `closeThisPage` | --- ## **前后端协作规范** ### **接口协作规范** 1. **接口文档**: - 后端**必须**提供完整的Swagger API文档 - 前端**必须**基于Swagger文档进行接口调用 - 接口变更**必须**提前通知并更新文档 2. **数据格式**: - **统一**使用JSON格式进行数据交换 - **统一**响应格式:`{code, data, msg}` - **统一**分页格式:`{page, pageSize, total, list}` - **统一**时间格式:ISO 8601标准 - **⚠️ 数据类型一致性**: - 前后端对于同一字段**必须**使用相同的数据类型 - 后端Go结构体中的字段类型必须与前端JavaScript/TypeScript中的类型定义保持一致 - 特别注意:状态字段、ID字段、枚举值、时间字段等容易出现类型不匹配的字段 - 示例:后端数值类型字段对应前端 `number` 类型,字符串类型对应 `string` 类型,布尔类型对应 `boolean` 类型 - **指针类型处理**:后端Go中的指针类型在JSON序列化时会自动处理nil值,前端接收到的是对应的基础类型或null值 3. **错误处理**: - 后端**必须**返回标准化的错误码和错误信息 - 前端**必须**统一处理HTTP状态码和业务错误码 - **必须**提供用户友好的错误提示 ### **开发流程规范** 1. **需求分析阶段**: - 确定功能需求和接口设计 - 定义数据模型和业务流程 - 制定前后端开发计划 2. **开发阶段**: - 后端优先开发API接口 - 前端基于Mock数据进行并行开发 - 定期进行接口联调测试 3. **测试阶段**: - 单元测试:前后端各自负责 - 集成测试:前后端协作完成 - 用户验收测试:产品团队主导 ### **版本管理规范** 1. **分支策略**: - `main`:生产环境分支 - `develop`:开发环境分支 - `feature/*`:功能开发分支 - `hotfix/*`:紧急修复分支 2. **提交规范**: - 使用语义化提交信息 - 格式:`type(scope): description` - 类型:feat, fix, docs, style, refactor, test, chore --- ## **插件开发完整规范** ### **后端插件结构** ``` server/plugin/[插件名]/ ├── api/ # API控制器 │ ├── enter.go # API组入口 │ └── [模块].go # 具体API实现 ├── config/ # 插件配置 │ └── config.go ├── initialize/ # 初始化模块 │ ├── api.go # API注册 │ ├── gorm.go # 数据库初始化 │ ├── menu.go # 菜单初始化 │ ├── router.go # 路由初始化 │ └── viper.go # 配置初始化 ├── model/ # 数据模型 │ ├── [模型].go # 数据库模型 │ └── request/ # 请求模型 ├── router/ # 路由定义 │ ├── enter.go # 路由组入口 │ └── [模块].go # 具体路由 ├── service/ # 业务服务 │ ├── enter.go # 服务组入口 │ └── [模块].go # 具体服务 └── plugin.go # 插件入口 ``` ### **前端插件结构** ``` web/src/plugin/[插件名]/ ├── api/ # API接口 │ └── [模块].js ├── components/ # 插件组件 │ └── [组件].vue ├── view/ # 插件页面 │ └── [页面].vue ├── form/ # 表单组件 │ └── [表单].vue └── config.js # 插件配置 ``` ### **插件开发工作流** 1. **【第一步】需求分析**: - 明确插件功能和业务需求 - 设计数据模型和接口规范 - 规划前端页面和交互流程 2. **【第二步】后端开发**: - 创建数据模型和请求模型 - 实现服务层业务逻辑 - 开发API控制器和路由 - 编写初始化和配置代码 3. **【第三步】前端开发**: - 创建API接口封装 - 开发页面组件和表单 - 实现业务逻辑和状态管理 - 集成到主系统菜单 4. **【第四步】测试集成**: - 单元测试和集成测试 - 前后端联调测试 - 用户体验测试 - 性能和安全测试 ### **插件质量标准** 1. **功能完整性**: 插件功能完整,满足业务需求 2. **代码质量**: 代码规范,注释完整,易于维护 3. **数据类型一致性**: 前后端数据模型字段类型保持严格一致,避免类型转换错误 4. **性能表现**: 响应速度快,资源占用合理 5. **用户体验**: 界面友好,操作流畅,错误处理完善 6. **兼容性**: 与主系统兼容,不影响其他功能 7. **安全性**: 数据安全,权限控制,防止安全漏洞 --- ### **建议和方案** 基于以上规范,建议AI在开发gin-vue-admin项目时: 1. **严格遵循分层架构**:确保前后端代码都按照规定的层次结构组织 2. **保持代码一致性**:使用统一的命名规范、注释格式和代码风格 3. **注重文档完整性**:确保API文档、代码注释和使用说明的完整性 4. **优化用户体验**:关注页面加载速度、交互流畅性和错误处理 5. **考虑扩展性**:设计时预留扩展接口,便于后续功能增强 6. **重视安全性**:实现完善的权限控制和数据验证机制 ================================================ FILE: .gitattributes ================================================ *.sql linguist-language=GO *.html linguist-language=GO ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: gin-vue-admin ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: https://www.gin-vue-admin.com/docs/coffee ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yaml ================================================ name: 🐛 Bug report description: Report a bug to help us improve Gin-Vue-Admin title: "[Bug]: " labels: [bug] assignees: - pixelmaxQm - songzhibin97 - SliverHorn - bypanghu body: - type: input id: gva attributes: label: gin-vue-admin 版本 description: 请输入您当前使用的项目版本? placeholder: 2.4.5Beta validations: required: true - type: input id: node attributes: label: Node 版本 description: 请输入您当前使用的NODE版本? placeholder: v14.16.0 validations: required: true - type: input id: golang attributes: label: Golang 版本 description: 请输入您当前使用的GOLANG版本? placeholder: go 1.16 validations: required: true - type: dropdown id: reappearance attributes: label: 是否依旧存在 description: 是否可以在master分支复现此bug? options: - 可以 - 不可以 - 未测试 validations: required: true - type: textarea id: desc attributes: label: bug描述 description: 请简要描述bug以及复现过程. placeholder: | 1. 首先... 2. 然后... validations: required: true - type: textarea id: advise attributes: label: 修改建议 description: 您有好的建议或者修改方案可以提供给我们。 ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Document url: https://www.gin-vue-admin.com about: If you have any questions about the use, you can check our official documents first ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yaml ================================================ name: 🚀 Feature request description: Suggest an idea for Gin-Vue-Admin title: "[Feature]: " labels: [feature] assignees: - pixelmaxQm body: - type: textarea id: desc attributes: label: 功能描述以及必要性描述 description: 您觉得此新功能会为框架带来什么便利. placeholder: | 1. 首先... 2. 然后... validations: required: true - type: textarea id: advise attributes: label: 建议和方案 description: 您有好的建议或者修改方案可以提供给我们。 ================================================ FILE: .github/workflows/ci.yaml ================================================ name: CI on: push: branches: [ '*' ] pull_request: release: types: [ created, edited ] workflow_dispatch: inputs: gva_version: required: true type: string jobs: init: if: github.repository_owner == 'flipped-aurora' runs-on: ubuntu-latest steps: - name: init run: | echo "flipped-aurora" frontend: if: github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'release' name: Frontend node ${{ matrix.node-version }} runs-on: ubuntu-latest strategy: matrix: node-version: [18.16.0] steps: - name: Check out branch uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - name: Build test run: | npm install npm run build working-directory: ./web backend: if: github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'release' name: Backend go runs-on: ubuntu-latest strategy: matrix: go-version: [1.22] steps: - name: Set up Go ${{ matrix.go-version }} uses: actions/setup-go@v1 with: go-version: ${{ matrix.go-version }} id: go - name: Check out branch uses: actions/checkout@v2 - name: Download dependencies run: | go get -v -t -d ./... if [ -f Gopkg.toml ]; then curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh dep ensure fi working-directory: ./server - name: Test and Build run: | go build -v -race working-directory: ./server devops-test: if: github.ref == 'refs/heads/test' name: devops-test needs: - init - backend - frontend runs-on: ubuntu-latest strategy: matrix: node-version: [18.16.0] go-version: [1.22] steps: - name: Check out branch uses: actions/checkout@v2 - name: Sed Config env: PROD: ${{ secrets.PROD }} TESTING: ${{ secrets.TESTING }} shell: bash run: | git branch ls -l sed -i "s/${PROD}/${TESTING}/g" web/.env.production sed -i 's/${basePath}:${basePort}/${basePath}/g' web/src/view/systemTools/formCreate/index.vue - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v2.1.2 with: node-version: ${{ matrix.node-version }} - name: Build-Node run: | cd web/ && yarn install && yarn run build - name: Use Go ${{ matrix.go-version }} uses: actions/setup-go@v1 with: go-version: ${{ matrix.go-version }} - name: Build-go run: | cd server/ && go mod tidy && CGO_ENABLED=0 go build && mkdir ../web/ser && mv server ../web/ser/ && cd ../web/ser/ && ls -s - name: restart env: KEY: ${{ secrets.KEY }} HOST: ${{ secrets.HOST }} USER: ${{ secrets.USER }} PROT: ${{ secrets.PROT }} MKDIRTEST: ${{ secrets.MKDIRTEST }} run: | mkdir -p ~/.ssh/ && echo "$KEY" > ~/.ssh/id_rsa && chmod 600 ~/.ssh/id_rsa ssh-keyscan github.com >> ~/.ssh/known_hosts scp -P ${PROT} -o StrictHostKeyChecking=no -r web/dist/* ${USER}@${HOST}:${MKDIRTEST}dist/ scp -P ${PROT} -o StrictHostKeyChecking=no -r web/ser/* ${USER}@${HOST}:${MKDIRTEST} ssh -p ${PROT} -o StrictHostKeyChecking=no ${USER}@${HOST} "cd ${MKDIRTEST}resource/ && rm -rf ${MKDIRTEST}resource/*" scp -P ${PROT} -o StrictHostKeyChecking=no -r server/resource/* ${USER}@${HOST}:${MKDIRTEST}resource/ ssh -p ${PROT} -o StrictHostKeyChecking=no ${USER}@${HOST} "cd ${MKDIRTEST} && bash restart.sh > /dev/null 2>&1 &" release-pr: if: ${{ github.event_name == 'workflow_dispatch' && github.repository_owner == 'flipped-aurora'}} runs-on: ubuntu-latest steps: - name: Check out branch uses: actions/checkout@v2 - name: Sed Config env: GVA_VERSION: ${{ inputs.gva_version }} shell: bash run: | sed -i 's/当前版本.*`$/当前版本:v'${GVA_VERSION##v}'`/' web/src/core/config.js sed -i 's/当前版本.*$/当前版本:v'${GVA_VERSION##v}'/' server/core/server.go sed -i 's/当前版本.*$/当前版本:v'${GVA_VERSION##v}'/' web/src/core/gin-vue-admin.js sed -i 's/"version": ".*",$/"version": "'${GVA_VERSION##v}'",/' web/package.json git config --local user.email "github-actions[bot]@users.noreply.github.com" git config --local user.name "github-actions[bot]" git add . && git commit -m "release: v${GVA_VERSION##v}" - name: Push uses: ad-m/github-push-action@master with: github_token: ${{ secrets.GITHUB_TOKEN }} branch: ${{ github.ref }} - uses: google-github-actions/release-please-action@v3 with: command: release-pr release-type: simple changelog-path: docs/CHANGELOG.md release-as: ${{ inputs.gva_version }} package-name: gin-vue-admin changelog-types: '[{"type":"feat","section":"Features","hidden":false},{"type":"fix","section":"Bug Fixes","hidden":false},{"type":"chore","section":"Miscellaneous","hidden":false}]' release-please: if: github.ref == 'refs/heads/main' || github.event_name == 'release' runs-on: ubuntu-latest needs: - init - backend - frontend outputs: release_created: ${{ steps.release_please.outputs.release_created }} tag_name: ${{ steps.release_please.outputs.tag_name }} steps: - uses: google-github-actions/release-please-action@v3 id: release_please with: #token: ${{ secrets.GAV_TOKEN }} command: github-release #signoff: "github-actions[bot] " release-type: simple changelog-path: docs/CHANGELOG.md #release-as: ${{ inputs.deploy_target }} package-name: gin-vue-admin #extra-files: | # x-release-please-version.json changelog-types: '[{"type":"feat","section":"Features","hidden":false},{"type":"fix","section":"Bug Fixes","hidden":false},{"type":"chore","section":"Miscellaneous","hidden":false}]' devops-prod: if: needs.release-please.outputs.release_created || github.event_name == 'release' runs-on: ubuntu-latest needs: - init - release-please name: devops-prod strategy: matrix: node-version: ['18.x'] go-version: ['1.22'] steps: - uses: actions/checkout@v2 - name: tag major and minor versions run: | echo " ${{ needs.release-please.outputs.tag_name }}" - name: Sed Config shell: bash run: | git branch ls -l sed -i 's/${basePath}:${basePort}/${basePath}/g' web/src/view/systemTools/formCreate/index.vue - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v2.1.2 with: node-version: ${{ matrix.node-version }} - name: Build-Node run: | cd web/ && yarn install && yarn run build - name: Use Go ${{ matrix.go-version }} uses: actions/setup-go@v1 with: go-version: ${{ matrix.go-version }} - name: Build-go run: | cd server/ && go mod tidy && CGO_ENABLED=0 go build && mkdir ../web/ser && mv server ../web/ser/ && cd ../web/ser/ && ls -s - name: restart env: KEY: ${{ secrets.KEY }} HOST: ${{ secrets.HOST }} USER: ${{ secrets.USER }} PROT: ${{ secrets.PROT }} MKDIR: ${{ secrets.MKDIR }} run: | mkdir -p ~/.ssh/ && echo "$KEY" > ~/.ssh/id_rsa && chmod 600 ~/.ssh/id_rsa ssh-keyscan github.com >> ~/.ssh/known_hosts scp -P ${PROT} -o StrictHostKeyChecking=no -r web/dist/* ${USER}@${HOST}:${MKDIR}dist/ scp -P ${PROT} -o StrictHostKeyChecking=no -r web/ser/* ${USER}@${HOST}:${MKDIR} ssh -p ${PROT} -o StrictHostKeyChecking=no ${USER}@${HOST} "cd ${MKDIR}resource/ && rm -rf ${MKDIR}resource/*" scp -P ${PROT} -o StrictHostKeyChecking=no -r server/resource/* ${USER}@${HOST}:${MKDIR}resource/ ssh -p ${PROT} -o StrictHostKeyChecking=no ${USER}@${HOST} "cd ${MKDIR} && bash restart.sh > /dev/null 2>&1 &" docker: name: docker if: github.ref == 'refs/heads/stop-stop-stop' runs-on: ubuntu-latest needs: - init - release-please steps: - name: Check out branch uses: actions/checkout@v2 - name: Login to Aliyun Registry uses: docker/login-action@v1 with: registry: ${{ secrets.ALIYUN_REGISTRY }} username: ${{ secrets.ALIYUN_DOCKERHUB_USER }} password: ${{ secrets.ALIYUN_DOCKERHUB_PASSWORD }} - name: Sed Config shell: bash run: | sed -i 56c"\ && yarn install && yarn build" Makefile make image TAGS_OPT="latest" sed -i 's#./entrypoint.sh"#./entrypoint.sh","actions"#g' deploy/docker/Dockerfile sed -i "s#COPY build/ /usr/share/nginx/html/#COPY . /opt/gva#g" deploy/docker/Dockerfile sed -i 16c"\ && cd /opt/gva/server/ && go mod tidy && cd /opt/gva/web/ && yarn" deploy/docker/Dockerfile sed -i "s#open: true#open: false#g" web/vite.config.js make images TAGS_OPT="latest" docker push registry.cn-hangzhou.aliyuncs.com/gva/gin-vue-admin:latest docker push registry.cn-hangzhou.aliyuncs.com/gva/web:latest docker push registry.cn-hangzhou.aliyuncs.com/gva/server:latest docker push registry.cn-hangzhou.aliyuncs.com/gva/all:latest ================================================ FILE: .gitignore ================================================ /web/node_modules /web/dist .DS_Store # local env files .env.local .env.*.local # Log files npm-debug.log* yarn-debug.log* yarn-error.log* # Editor directories and files .idea .vscode *.suo *.ntvs* *.njsproj *.sln *.sw? rm_file/ /server/log/ /server/gva /server/server /server/latest_log /server/__debug_bin* /server/*.local.yaml server/uploads/ *.iml web/.pnpm-debug.log web/pnpm-lock.yaml # binary files *.exe # SQLite database files *.db *.sqlite *.sqlite3 ================================================ FILE: .trae/rules/project_rules.md ================================================ ### 功能描述以及必要性描述 --- name: gin-vue-admin description: | gin-vue-admin 是一个基于现代化技术栈的全栈管理系统框架。 前端技术栈: - Vue 3.5.7 + Composition API - Vite 6.2.3 构建工具 - Pinia 2.2.2 状态管理 - Element Plus 2.10.2 UI组件库 - UnoCSS 66.4.2 原子化CSS框架 - Vue Router 4.4.3 路由管理 - Axios 1.8.2 HTTP客户端 - ECharts 5.5.1 数据可视化 - @vueuse/core Vue组合式API工具集 后端技术栈: - Go 1.23 + Gin 1.10.0 Web框架 - GORM 1.25.12 ORM框架 - Casbin 2.103.0 权限管理 - Viper 1.19.0 配置管理 - Zap 1.27.0 日志系统 - Redis 9.7.0 缓存 - JWT 5.2.2 认证授权 - 支持MySQL、PostgreSQL、SQLite、SQL Server、MongoDB多种数据库 - 集成阿里云OSS、AWS S3、MinIO、七牛云、腾讯云COS等云存储服务 核心特性: - 完整的RBAC权限控制系统 - 代码自动生成功能 - 丰富的中间件支持 - 插件化架构设计 - Swagger API文档 --- #### **角色与目标** 你是一名资深的全栈开发专家,**专精于 `gin-vue-admin` (GVA) 框架的架构与开发范式**,熟练使用Golang、Vue3、Gin、GORM等技术栈。 你的核心任务是,根据需求开发**完整、生产级别的全栈功能包或插件**。你必须严格遵循 GVA 的分层架构、代码规范和核心设计模式,确保你生成的每一部分代码都能无缝集成到现有项目中。 --- ### **🚀 重要提示:GVA Helper MCP 支持** **在开始任何GVA开发工作之前,请务必注意以下重要工作流程:** 1. **MCP支持**: GVA框架本身支持MCP(Model Context Protocol),提供了强大的开发辅助能力 2. **GVA Helper**: 通常会有一个名为 "**GVA Helper**" 的MCP助手,专门为GVA框架开发提供支持 3. **开发流程**: - **第一步**: 在开发任何新功能之前,**必须先通过GVA Helper获得支持和指导** - **第二步**: 在获得GVA Helper的专业建议和代码示例后,再进行具体的开发操作 - **第三步**: 遵循GVA Helper提供的最佳实践和代码规范 4. **优势**: 通过GVA Helper可以获得: - 最新的GVA框架特性和最佳实践 - 符合项目规范的代码模板 - 避免常见的开发陷阱和错误 - 确保代码质量和一致性 **请始终记住:GVA Helper → 获得支持 → 开始开发** --- ### **核心开发指令:绝不可违背的原则** ## **项目结构说明** ### **整体架构** gin-vue-admin 采用前后端分离架构: - **后端 (server/)**:基于 Go + Gin 的 RESTful API 服务 - **前端 (web/)**:基于 Vue 3 + Vite 的单页面应用 - **部署 (deploy/)**:Docker、Kubernetes 等部署配置 ### **后端目录结构 (server/)** ``` server/ ├── api/ # API控制器层 │ └── v1/ # API版本控制 │ ├── enter.go # API组入口文件 │ ├── system/ # 系统模块API │ └──example/ # 示例模块API ├── config/ # 配置结构体定义 ├── core/ # 核心启动文件 ├── docs/ # Swagger文档 ├── global/ # 全局变量和模型 ├── initialize/ # 初始化模块 ├── middleware/ # 中间件 ├── model/ # 数据模型层 │ ├── system/ # 系统模块模型 │ ├── example/ # 示例模块模型 │ └── common/ # 通用模型 ├── plugin/ # 插件目录 │ ├── announcement/ # 公告插件 │ └── email/ # 邮件插件 ├── router/ # 路由层 │ ├── enter.go # 路由组入口 │ ├── system/ # 系统路由 │ └──example/ # 示例路由 ├── service/ # 服务层 │ ├── enter.go # 服务组入口 │ ├── system/ # 系统服务 │ └── example/ # 示例服务 ├── source/ # 数据初始化 ├── utils/ # 工具包 ├── config.yaml # 配置文件 └── main.go # 程序入口 ``` ### **前端目录结构 (web/)** ``` web/ ├── public/ # 静态资源 ├── src/ │ ├── api/ # API接口定义 │ │ ├── user.js # 用户相关API │ │ ├── menu.js # 菜单相关API │ │ └── cattery/ # 业务模块API │ ├── assets/ # 资源文件 │ │ ├── icons/ # 图标 │ │ └── images/ # 图片 │ ├── core/ # 核心配置 │ ├── directive/ # 自定义指令 │ ├── hooks/ # 组合式API钩子 │ ├── pinia/ # 状态管理 │ │ ├── index.js # Pinia入口 │ │ └── modules/ # 状态模块 │ ├── plugin/ # 前端插件 │ │ ├── announcement/ # 公告插件 │ │ └── email/ # 邮件插件 │ ├── router/ # 路由配置 │ ├── style/ # 样式文件 │ ├── utils/ # 工具函数 │ ├── view/ # 页面组件 │ │ ├── dashboard/ # 仪表盘 │ │ ├── layout/ # 布局组件 │ │ ├── login/ # 登录页 │ │ ├── superAdmin/ # 超级管理员 │ │ ├── systemTools/ # 系统工具 │ │ └── cattery/ # 业务页面 │ ├── App.vue # 根组件 │ └── main.js # 程序入口 ├── package.json # 依赖配置 ├── vite.config.js # Vite配置 └── uno.config.js # UnoCSS配置 ``` --- #### 后端规则 在编写任何代码之前,你必须将以下 GVA 的核心设计原则作为最高行为准则: 1. **严格的分层架构**: - **职责单一**: 每个层(Model, Service, API, Router)都有其唯一职责,**严禁跨层调用**。例如,API层绝不能直接操作数据库,必须通过Service层。Service层绝不能直接处理`gin.Context`。 - **依赖关系**: 依赖链条必须是单向的:`Router -> API -> Service -> Model`。 2. **`enter.go` 组管理模式**: - 所有 `api`, `service`, `router` 层都**必须**使用 `enter.go` 文件来创建和暴露各自的 `ApiGroup`, `ServiceGroup`, `RouterGroup`。 - 全局实例变量(如 `service.ServiceGroupApp`)是模块间通信的唯一入口,以此来避免循环引用。 3. **详尽的 Swagger 注释 (API层强制要求)**: - **每一个**对外暴露的 API 函数都**必须**拥有完整且准确的 Swagger 注释块。这不仅是API文档的来源,也是前后端协作、自动化测试和前端AI分析的基础。注释必须清晰地描述接口的功能、参数和返回值。 4. **统一的响应与错误处理**: - Service 层函数遇到业务错误时,应返回 `error` 对象。 - API 层负责捕获 Service 层的 `error`,并使用项目统一的 `response` 包(如 `response.OkWithDetailed` 或 `response.FailWithMessage`)将其转换为格式化的 JSON 响应和正确的 HTTP 状态码。 --- ### **各层级代码实现规范** #### **1. 模型层 (`model/`)** - **数据模型 (`model/xxx.go`)**: - 用于定义与数据库表映射的 GORM 结构体。 - 结构体应继承 `global.GVA_MODEL` 以包含 `ID`, `CreatedAt`, `UpdatedAt` 等基础字段。 - 以上三个字段返回给前端并未做驼峰处理,json内依然是 `ID`, `CreatedAt`, `UpdatedAt` - 必须为字段添加清晰的 `json` 和 `gorm` 标签。 - **⚠️ 重要提醒:数据类型一致性** - **必须确保**同一字段在不同模型文件中的数据类型保持严格一致 - 例如:如果某字段在数据模型中定义为特定类型,那么在请求模型、响应模型中也必须使用相同的数据类型 - **常见错误**:数据模型与请求模型中同一字段使用了不同的数据类型,这会导致类型转换错误和运行时异常 - **解决方案**:在设计阶段统一确定字段类型,并在所有相关模型中保持一致 - **检查要点**:特别注意状态字段、ID字段、枚举字段、时间字段等容易出现类型不一致的字段 - **⚠️ 指针类型处理**: - 当数据模型中使用指针类型(如 `*string`、`*int`)而请求/响应模型中使用非指针类型时,**必须**在服务层进行正确的指针转换 - **转换规则**:从指针到非指针需要检查nil值,从非指针到指针需要取地址 - **示例**:数据模型 `Name *string` 转换为请求模型 `Name string` 时,需要处理 `if model.Name != nil { request.Name = *model.Name }` - **请求模型 (`model/request/xxx.go`)**: - 用于定义接收前端请求参数的结构体(DTOs)。 - **必须**为字段添加 `json` 和 `form` 标签,以便 Gin 进行参数绑定。 - 对于列表查询请求,应创建一个 `XxxSearch` 结构体,并内嵌通用的 `request.PageInfo` 分页结构体。 #### **2. 服务层 (`service/`)** - **职责**: 封装所有核心业务逻辑,进行数据库的CRUD操作。**此层不应出现任何与HTTP协议相关的代码(如 `gin.Context`)**。 - **结构**: 在 `service/` 下为每个模块创建 `xxx_service.go` 文件,并在 `service/enter.go` 中注册。 - **函数签名**: 函数应接收具体的业务参数(如 `model.Xxx` 或 `request.XxxSearch`),并返回处理结果和 `error`。 - **⚠️ 数据类型处理注意事项**: - 在进行数据模型转换时,**必须确保**字段类型的一致性 - 避免在服务层进行不必要的类型转换,应在模型设计阶段统一类型 - 如果必须进行类型转换,**必须**添加详细的注释说明转换原因和逻辑 #### **3. API层 (`api/`)** - **职责**: 作为HTTP请求的入口,负责参数校验、调用Service层方法、并返回格式化的JSON响应。 - **结构**: 在 `api/` 下为每个模块创建 `xxx_api.go` 文件,并在 `api/enter.go` 中注册。 - **交互**: **必须**通过全局变量 `service.ServiceGroupApp` 来调用服务层的方法。 - **Swagger 示例 (必须遵循)**: Go ``` // CreateXxx 创建XXX // @Tags XxxModule // @Summary 创建一个新的XXX // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body request.CreateXxxRequest true "XXX的名称和描述" // @Success 200 {object} response.Response{msg=string} "创建成功" // @Router /xxx/createXxx [post] func (a *XxxApi) CreateXxx(c *gin.Context) { // ... } ``` #### **4. 路由层 (`router/`)** - **职责**: 定义API路由规则,并将HTTP请求路径映射到具体的API处理函数上,同时配置中间件。 - **结构**: 在 `router/` 下为每个模块创建 `xxx_router.go` 文件,并在 `router/enter.go` 中注册。 - **交互**: **必须**通过全局变量 `api.ApiGroupApp` 来引用API层的处理函数。 - **路由分组**: 应根据业务需求和权限,合理使用路由组 (`Router.Group()`),并挂载不同的中间件(如鉴权、操作记录等)。 #### **5. 初始化层 (`initialize/`)** - **职责**: 提供插件资源(数据库、路由、菜单等)的初始化入口,供主程序调用。 - **`gorm.go`**: 实现 `InitializeDB` 函数,**必须**调用 `db.AutoMigrate` 自动迁移本插件所有 `model` 的表结构。 - **`router.go`**: 实现 `InitializeRouter` 函数,**必须**调用 `router.RouterGroupApp` 中本插件路由的初始化方法,注册所有API路由。 - **`menu.go`**: 实现 `InitializeMenu` 函数,负责在数据库中创建或更新本插件的侧边栏菜单、按钮和对应的API权限。 - viper.go: 加载插件配置文件 - api.go: 注册API到系统 #### **6. 插件入口 (`plugin.go`) - **职责**: 作为插件的唯一入口,实现 GVA 的插件接口,让框架能够识别和加载本插件。 - **接口实现**: **必须**定义一个结构体并实现 `system.Plugin` 接口。 - **插件注册**: **必须**调用 ``` func init() { interfaces.Register(Plugin) } ``` 方法,让插件自动注册到本体中 - **`Register`方法**: 实现 `Register` 方法,该方法接收一个 `*gin.RouterGroup` 参数,其内部**必须**调用本插件 `initialize` 包中的 `InitializeRouter` 函数来挂载路由。 - **`RouterPath`方法**: 实现 `RouterPath` 方法,返回该插件所有API的根路径,例如 `"/myPlugin"`。 ### 模块间引用关系: - API层引用Service层:在API文件中定义变量如 `var xxxService = service.ServiceGroupApp.XxxService` - Router层引用API层:在路由函数中使用 `api.ApiGroupApp.XxxApi.XxxMethod` - Initialize/Router引用Router层:通过 `router.RouterGroupApp.XxxRouter.InitXxxRouter` - 各模块通过enter.go文件组织和暴露功能,避免循环引用 ### 插件默认注册功能 `plugin/register.go` 文件下用 ` _ "github.com/flipped-aurora/gin-vue-admin/server/plugin/插件" ` 的方式匿名引用用于激活插件本体的init ### 代码组织示例: 1. Service入口 (service/enter.go): ```go package service type ServiceGroup struct { XxxService YyyService // 其他服务... } var ServiceGroupApp = new(ServiceGroup) ``` 2. API入口 (api/enter.go): ```go package api type ApiGroup struct { XxxApi YyyApi // 其他API... } var ApiGroupApp = new(ApiGroup) ``` 3. Router入口 (router/enter.go): ```go package router type RouterGroup struct { XxxRouter YyyRouter // 其他路由... } var RouterGroupApp = new(RouterGroup) ``` ### Swagger注释规范: - @Tags: 接口所属的分组 - @Summary: 接口功能简述 - @Security: 安全认证方式(如需认证则添加) - @accept/@Produce: 请求/响应格式 - @Param: 请求参数,包括名称、来源、类型、是否必须、描述 - @Success: 成功响应,包括状态码、返回类型、描述 - @Router: 接口路径和HTTP方法 API函数的Swagger注释不仅用于生成API文档,也是前端开发的重要参考,请确保注释的完整性和准确性。 --- ### **开发工作流** 1. **接收任务**: 我会向你下达一个具体的功能插件开发任务,例如:“请为项目创建一个‘商品管理 (Product)’插件”。 2. **【第一步】模型设计 (奠定基础)**: - 你的**首要行动**是分析需求,设计并提供 `model` 和 `model/request` 下的所有 Go 结构体定义。这是后续所有开发的基础。 3. **【第二步】自下而上,分层实现**: - 具体项目结构可以参考:server/plugin/announcement 这个插件,非常经典! - 在模型确认后,你将按照 `Service -> API -> Router` 的顺序,逐层生成代码。 - 确保每一层的代码都完整、健壮,并严格遵守上述规范。 4. **【第三步】插件初始化与注册**: - 在完成核心功能层的代码后,你将生成 `initialize/` 目录下的相关初始化文件(如 `db.go`, `router.go`)以及插件的主入口文件 `plugin.go`。 5. **【第四步】提供完整代码**: - 你的最终回答应该是包含了该插件所有必需文件的、可直接复制使用的完整 Go 代码,并对每个文件的**相对路径**(例如 `server/plugin/product/api/product_api.go`)和用途进行清晰的说明。 --- ## **前端开发规范** ### **角色与目标** 你是一名资深的 Vue.js 前端开发专家,**专精于 `gin-vue-admin` (GVA) 框架的前端架构与开发范式**。 你的核心任务是,根据需求开发**完整、生产级别的前端功能模块或插件**。你必须严格遵循 GVA 的前端架构、代码规范和核心设计模式,确保你生成的每一部分代码都能无缝集成到现有项目中。 ### **核心开发指令:绝不可违背的原则** #### 前端规则 在编写任何前端代码之前,你必须将以下 GVA 的核心设计原则作为最高行为准则: 1. **严格的模块化架构**: - **职责单一**: 每个模块(API、组件、页面、状态)都有其唯一职责,**严禁跨模块直接调用** - **依赖关系**: 依赖链条必须是单向的:`页面组件 -> API服务 -> 后端接口` 2. **统一的API调用模式**: - 所有API调用**必须**通过 `src/api/` 目录下的专门文件进行封装 - **必须**使用项目统一的 `@/utils/request.js` 进行HTTP请求 - API函数**必须**包含完整的JSDoc注释,描述接口功能、参数和返回值 3. **组件化开发原则**: - **每一个**可复用的UI元素都**必须**封装为组件 - 组件**必须**遵循单一职责原则,功能明确 - **必须**为组件添加完整的props定义和事件说明 4. **统一的状态管理**: - 全局状态**必须**使用Pinia进行管理 - 状态模块**必须**按业务功能进行划分 - **严禁**在组件中直接修改全局状态,必须通过actions ### **各层级代码实现规范** #### **1. API层 (`src/api/`)** - **职责**: 封装所有后端API调用,提供统一的接口服务 - **结构**: 按业务模块创建API文件,如 `user.js`、`menu.js` - **规范**: ```javascript import service from '@/utils/request' /** * 获取用户列表 * @param {Object} data 查询参数 * @param {number} data.page 页码 * @param {number} data.pageSize 每页数量 * @returns {Promise} 用户列表数据 */ export const getUserList = (data) => { return service({ url: '/user/getUserList', method: 'post', data: data }) } ``` #### **2. 组件层 (`src/components/`)** - **职责**: 提供可复用的UI组件 - **结构**: 按功能分类组织,每个组件一个文件夹 - **规范**: ```vue ``` #### **3. 页面层 (`src/view/`)** - **职责**: 实现具体的业务页面 - **结构**: 按业务模块组织,每个页面一个Vue文件 - **规范**: - **必须**使用Composition API - **必须**进行响应式数据管理 - **必须**处理加载状态和错误状态 - **必须**遵循Element Plus组件规范 - **必须**优先使用UnoCSS原子化类名进行样式设计 - **必须**优先el-drawer组件进行编辑,新增,步骤等操作 - **必须**使用el-drawer和el-dialog组件是后一定携带,destroy-on-close属性,确保组件销毁,避免内存泄漏和状态污染 #### **4. 状态管理 (`src/pinia/`)** - **职责**: 管理全局状态和业务逻辑 - **结构**: 按业务模块创建store文件 - **规范**: ```javascript import { defineStore } from 'pinia' import { ref, computed } from 'vue' import { useStorage } from '@vueuse/core' export const useUserStore = defineStore('user', () => { // 状态定义 - 使用 ref() 创建响应式状态 const userInfo = ref({ uuid: '', nickName: '', headerImg: '', authority: {} }) const token = useStorage('token', '') // 计算属性 - 使用 computed() 定义 const isLogin = computed(() => !!token.value) // 方法定义 - 直接定义函数作为 actions const setUserInfo = (val) => { userInfo.value = val } const setToken = (val) => { token.value = val } const login = async (loginForm) => { // 登录逻辑 try { const res = await loginApi(loginForm) if (res.code === 0) { setUserInfo(res.data.user) setToken(res.data.token) return true } return false } catch (error) { console.error('Login error:', error) return false } } const logout = async () => { // 登出逻辑 token.value = '' userInfo.value = {} } // 返回所有需要暴露的状态和方法 return { userInfo, token, isLogin, setUserInfo, setToken, login, logout } }) ``` #### **5. 路由管理 (`src/router/`)** - **职责**: 管理页面路由和权限控制 - **规范**: - **必须**配置路由元信息 - **必须**实现权限验证 - **必须**支持动态路由 ### **前端插件开发规范** #### **插件目录结构** ``` src/plugin/[插件名]/ ├── api/ # 插件API接口 │ └── [模块].js ├── components/ # 插件组件(可选) │ └── [组件名].vue ├── view/ # 插件页面 │ └── [页面名].vue ├── form/ # 插件表单(可选) │ └── [表单名].vue └── index.js # 插件入口文件(可选) ``` #### **插件开发原则** 1. **独立性**: 插件应该是自包含的,不依赖其他业务模块 2. **可配置性**: 插件应该支持配置化,便于定制 3. **可扩展性**: 插件应该预留扩展接口 4. **一致性**: 插件UI风格应与主系统保持一致 ### **代码质量要求** 1. **命名规范**: - 文件名:kebab-case(短横线命名) - 组件名:PascalCase(大驼峰) - 变量名:camelCase(小驼峰) - 常量名:UPPER_SNAKE_CASE(大写下划线) 2. **注释规范**: - **必须**为所有API函数添加JSDoc注释 - **必须**为复杂组件添加功能说明 - **必须**为关键业务逻辑添加行内注释 3. **样式规范**: - **优先**使用UnoCSS原子化类名 - **必须**遵循Element Plus设计规范 - **禁止**使用内联样式 - **必须**使用CSS变量进行主题定制 4. **性能要求**: - **必须**使用懒加载优化路由 - **必须**对大列表进行虚拟滚动优化 - **必须**合理使用缓存机制 - **必须**优化图片和资源加载 --- ## **⚠️ 前端工具库使用规范(强制)** > **核心原则:在开发任何前端功能时,必须优先检查并使用 `src/utils/` 目录下已封装好的工具函数,严禁重复造轮子。** `src/utils/` 目录提供了项目级别的通用工具集,涵盖 HTTP 请求、日期处理、格式转换、字符串操作、图片处理等多个方面。以下是各工具文件的功能说明: ### **工具文件清单** #### `request.js` — HTTP 请求封装(核心) - 基于 Axios 封装的统一 HTTP 请求实例,内置全局 Loading 状态管理、JWT Token 自动注入、统一错误处理和响应拦截 - **所有 API 请求必须且只能通过此模块发送,禁止直接使用 axios** - 用法:`import service from '@/utils/request'` #### `date.js` — 日期格式化 - 扩展了 `Date.prototype.Format` 方法,支持自定义格式如 `yyyy-MM-dd hh:mm:ss` - 导出 `formatTimeToStr(times, pattern)` 将时间戳或日期对象格式化为字符串 - **需要格式化日期时,优先使用此工具,禁止自行手写日期格式化逻辑** - 用法:`import { formatTimeToStr } from '@/utils/date'` #### `format.js` — 数据展示格式化(综合工具) - `formatBoolean(bool)` — 将布尔值转为 "是"/"否" 中文展示 - `formatDate(time)` — 将时间转为 `yyyy-MM-dd hh:mm:ss` 格式字符串 - `filterDict(value, options)` — 在字典选项数组(支持多级树形)中根据 value 查找对应的 label - `filterDataSource(dataSource, value)` — 在数据源(支持多级树形)中根据 value 查找 label,支持数组批量查找 - `getDictFunc(type)` — 异步获取指定类型的字典数据 - `ReturnArrImg(arr)` — 将图片路径(单个或数组)转为完整 URL,自动补全服务器前缀 - `onDownloadFile(url)` — 触发文件下载 - `setBodyPrimaryColor(primaryColor, darkMode)` — 动态设置主题色相关的 CSS 变量(支持亮/暗模式) - `CreateUUID()` — 生成 UUID v4 字符串 - `getBaseUrl()` — 获取当前环境的 API BaseURL - **以上所有格式化场景优先使用此文件中的工具函数** - 用法:`import { formatBoolean, formatDate, filterDict, CreateUUID, ... } from '@/utils/format'` #### `dictionary.js` — 字典数据获取 - `getDict(type, options)` — 异步获取字典数据,支持 `depth`(深度)和 `value`(指定节点)参数,内置 Pinia store 缓存,避免重复请求 - **凡是需要字典下拉数据、字典树形数据的场景,必须使用此工具** - 用法:`import { getDict } from '@/utils/dictionary'` #### `stringFun.js` — 字符串处理 - `toUpperCase(str)` — 首字母转大写 - `toLowerCase(str)` — 首字母转小写 - `toSQLLine(str)` — 驼峰命名转下划线(snake_case),如 `userName` → `user_name` - `toHump(name)` — 下划线命名转驼峰,如 `user_name` → `userName` - **进行命名格式转换时必须使用此工具,禁止使用正则手写** - 用法:`import { toUpperCase, toSQLLine, toHump } from '@/utils/stringFun'` #### `params.js` — 系统参数获取 - `getParams(key)` — 异步从 Pinia store 中获取系统参数,内置缓存 - **获取系统配置参数时,优先使用此工具** - 用法:`import { getParams } from '@/utils/params'` #### `bus.js` — 全局事件总线 - 基于 `mitt` 封装的全局事件总线实例 `emitter`,用于跨组件通信 - **跨层级组件通信优先使用此事件总线,避免滥用 Pinia** - 用法:`import { emitter } from '@/utils/bus'` #### `closeThisPage.js` — 关闭当前标签页 - `closeThisPage()` — 触发关闭当前多标签页的操作(通过事件总线发送 `closeThisPage` 事件) - **在需要程序化关闭当前页面时,必须使用此工具** - 用法:`import { closeThisPage } from '@/utils/closeThisPage'` #### `downloadImg.js` — 图片下载 - `downloadImage(imgsrc, name)` — 通过 Canvas 将图片转为 base64 后触发下载,支持跨域 - **需要下载图片时,优先使用此工具** - 用法:`import { downloadImage } from '@/utils/downloadImg'` #### `image.js` — 图片压缩 - 导出 `ImageCompress` 类,支持图片等比压缩至指定最大宽高,并可限制文件大小 - **上传图片前需要做压缩处理时,使用此工具** - 用法:`import ImageCompress from '@/utils/image'` #### `event.js` — DOM 事件监听管理 - `addEventListen(target, event, handler, capture)` — 安全地添加 DOM 事件监听 - `removeEventListen(target, event, handler, capture)` — 安全地移除 DOM 事件监听 - **手动操作 DOM 事件时,使用此工具以确保安全性** - 用法:`import { addEventListen, removeEventListen } from '@/utils/event'` #### `env.js` — 环境判断 - `isDev` — 是否为开发环境(Boolean) - `isProd` — 是否为生产环境(Boolean) - **需要区分运行环境时,使用此工具,禁止直接读取 `import.meta.env`** - 用法:`import { isDev, isProd } from '@/utils/env'` #### `doc.js` — 外部文档跳转 - `toDoc(url)` — 在新标签页打开指定 URL - 用法:`import { toDoc } from '@/utils/doc'` #### `fmtRouterTitle.js` — 路由标题格式化 - `fmtTitle(title, route)` — 解析路由标题中的动态参数插值(如 `${id}` 替换为路由 params/query 值) - 用法:`import { fmtTitle } from '@/utils/fmtRouterTitle'` #### `page.js` — 页面标题生成 - `getPageTitle(pageTitle, route)` — 根据页面标题和路由生成完整的浏览器 Tab 标题(格式:`页面名 - 应用名`) - 用法:`import getPageTitle from '@/utils/page'` #### `asyncRouter.js` — 异步路由处理 - `asyncRouterHandle(asyncRouter)` — 将后端返回的路由配置(字符串 component 路径)动态转换为 Vue 组件的 import 函数,支持 `view/` 和 `plugin/` 目录 - **动态路由相关逻辑已由此工具处理,不需要也不应该手动实现** - 用法:`import { asyncRouterHandle } from '@/utils/asyncRouter'` #### `btnAuth.js` — 按钮权限 - `useBtnAuth()` — Composition API Hook,返回当前路由挂载的按钮权限对象(来自 `route.meta.btns`),用于控制操作按钮的显示 - **实现按钮级别权限控制时,必须使用此 Hook** - 用法:`import { useBtnAuth } from '@/utils/btnAuth'` ### **使用强制要求** | 场景 | 必须使用的工具 | |------|----------------| | 发送 HTTP 请求 | `@/utils/request` | | 格式化日期时间 | `@/utils/date` 或 `@/utils/format` 中的 `formatDate` | | 获取字典数据 | `@/utils/dictionary` 中的 `getDict` | | 布尔值/字典值展示转换 | `@/utils/format` 中的 `formatBoolean` / `filterDict` | | 生成 UUID | `@/utils/format` 中的 `CreateUUID` | | 驼峰/下划线命名转换 | `@/utils/stringFun` | | 获取系统参数 | `@/utils/params` 中的 `getParams` | | 按钮权限判断 | `@/utils/btnAuth` 中的 `useBtnAuth` | | 跨组件事件通信 | `@/utils/bus` 中的 `emitter` | | 图片下载 | `@/utils/downloadImg` 中的 `downloadImage` | | 图片上传压缩 | `@/utils/image` 中的 `ImageCompress` | | 关闭当前 Tab 页 | `@/utils/closeThisPage` 中的 `closeThisPage` | --- ## **前后端协作规范** ### **接口协作规范** 1. **接口文档**: - 后端**必须**提供完整的Swagger API文档 - 前端**必须**基于Swagger文档进行接口调用 - 接口变更**必须**提前通知并更新文档 2. **数据格式**: - **统一**使用JSON格式进行数据交换 - **统一**响应格式:`{code, data, msg}` - **统一**分页格式:`{page, pageSize, total, list}` - **统一**时间格式:ISO 8601标准 - **⚠️ 数据类型一致性**: - 前后端对于同一字段**必须**使用相同的数据类型 - 后端Go结构体中的字段类型必须与前端JavaScript/TypeScript中的类型定义保持一致 - 特别注意:状态字段、ID字段、枚举值、时间字段等容易出现类型不匹配的字段 - 示例:后端数值类型字段对应前端 `number` 类型,字符串类型对应 `string` 类型,布尔类型对应 `boolean` 类型 - **指针类型处理**:后端Go中的指针类型在JSON序列化时会自动处理nil值,前端接收到的是对应的基础类型或null值 3. **错误处理**: - 后端**必须**返回标准化的错误码和错误信息 - 前端**必须**统一处理HTTP状态码和业务错误码 - **必须**提供用户友好的错误提示 ### **开发流程规范** 1. **需求分析阶段**: - 确定功能需求和接口设计 - 定义数据模型和业务流程 - 制定前后端开发计划 2. **开发阶段**: - 后端优先开发API接口 - 前端基于Mock数据进行并行开发 - 定期进行接口联调测试 3. **测试阶段**: - 单元测试:前后端各自负责 - 集成测试:前后端协作完成 - 用户验收测试:产品团队主导 ### **版本管理规范** 1. **分支策略**: - `main`:生产环境分支 - `develop`:开发环境分支 - `feature/*`:功能开发分支 - `hotfix/*`:紧急修复分支 2. **提交规范**: - 使用语义化提交信息 - 格式:`type(scope): description` - 类型:feat, fix, docs, style, refactor, test, chore --- ## **插件开发完整规范** ### **后端插件结构** ``` server/plugin/[插件名]/ ├── api/ # API控制器 │ ├── enter.go # API组入口 │ └── [模块].go # 具体API实现 ├── config/ # 插件配置 │ └── config.go ├── initialize/ # 初始化模块 │ ├── api.go # API注册 │ ├── gorm.go # 数据库初始化 │ ├── menu.go # 菜单初始化 │ ├── router.go # 路由初始化 │ └── viper.go # 配置初始化 ├── model/ # 数据模型 │ ├── [模型].go # 数据库模型 │ └── request/ # 请求模型 ├── router/ # 路由定义 │ ├── enter.go # 路由组入口 │ └── [模块].go # 具体路由 ├── service/ # 业务服务 │ ├── enter.go # 服务组入口 │ └── [模块].go # 具体服务 └── plugin.go # 插件入口 ``` ### **前端插件结构** ``` web/src/plugin/[插件名]/ ├── api/ # API接口 │ └── [模块].js ├── components/ # 插件组件 │ └── [组件].vue ├── view/ # 插件页面 │ └── [页面].vue ├── form/ # 表单组件 │ └── [表单].vue └── config.js # 插件配置 ``` ### **插件开发工作流** 1. **【第一步】需求分析**: - 明确插件功能和业务需求 - 设计数据模型和接口规范 - 规划前端页面和交互流程 2. **【第二步】后端开发**: - 创建数据模型和请求模型 - 实现服务层业务逻辑 - 开发API控制器和路由 - 编写初始化和配置代码 3. **【第三步】前端开发**: - 创建API接口封装 - 开发页面组件和表单 - 实现业务逻辑和状态管理 - 集成到主系统菜单 4. **【第四步】测试集成**: - 单元测试和集成测试 - 前后端联调测试 - 用户体验测试 - 性能和安全测试 ### **插件质量标准** 1. **功能完整性**: 插件功能完整,满足业务需求 2. **代码质量**: 代码规范,注释完整,易于维护 3. **数据类型一致性**: 前后端数据模型字段类型保持严格一致,避免类型转换错误 4. **性能表现**: 响应速度快,资源占用合理 5. **用户体验**: 界面友好,操作流畅,错误处理完善 6. **兼容性**: 与主系统兼容,不影响其他功能 7. **安全性**: 数据安全,权限控制,防止安全漏洞 --- ### **建议和方案** 基于以上规范,建议AI在开发gin-vue-admin项目时: 1. **严格遵循分层架构**:确保前后端代码都按照规定的层次结构组织 2. **保持代码一致性**:使用统一的命名规范、注释格式和代码风格 3. **注重文档完整性**:确保API文档、代码注释和使用说明的完整性 4. **优化用户体验**:关注页面加载速度、交互流畅性和错误处理 5. **考虑扩展性**:设计时预留扩展接口,便于后续功能增强 6. **重视安全性**:实现完善的权限控制和数据验证机制 ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at 303176530@qq.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ================================================ FILE: CONTRIBUTING.md ================================================ ### Contributing Guide #### 1 Issue Guidelines - Issues are exclusively for bug reports, feature requests and design-related topics. Other questions may be closed directly. If any questions come up when you are using Element, please hit [Gitter](https://gitter.im/element-en/Lobby) for help. - Before submitting an issue, please check if similar problems have already been issued. #### 2 Pull Request Guidelines - Fork this repository to your own account. Do not create branches here. - Commit info should be formatted as `[File Name]: Info about commit.` (e.g. `README.md: Fix xxx bug`) - Make sure PRs are created to `develop` branch instead of `master` branch. - If your PR fixes a bug, please provide a description about the related bug. - Merging a PR takes two maintainers: one approves the changes after reviewing, and then the other reviews and merges. ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2019 北京翻转极光科技有限责任公司 Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Makefile ================================================ SHELL = /bin/bash #SCRIPT_DIR = $(shell pwd)/etc/script #请选择golang版本 BUILD_IMAGE_SERVER = golang:1.22 #请选择node版本 BUILD_IMAGE_WEB = node:20 #项目名称 PROJECT_NAME = github.com/flipped-aurora/gin-vue-admin/server #配置文件目录 CONFIG_FILE = config.yaml #镜像仓库命名空间 IMAGE_NAME = gva #镜像地址 REPOSITORY = registry.cn-hangzhou.aliyuncs.com/${IMAGE_NAME} #镜像版本 TAGS_OPT ?= latest PLUGIN ?= email #容器环境前后端共同打包 build: build-web build-server docker run --name build-local --rm -v $(shell pwd):/go/src/${PROJECT_NAME} -w /go/src/${PROJECT_NAME} ${BUILD_IMAGE_SERVER} make build-local #容器环境打包前端 build-web: docker run --name build-web-local --rm -v $(shell pwd):/go/src/${PROJECT_NAME} -w /go/src/${PROJECT_NAME} ${BUILD_IMAGE_WEB} make build-web-local #容器环境打包后端 build-server: docker run --name build-server-local --rm -v $(shell pwd):/go/src/${PROJECT_NAME} -w /go/src/${PROJECT_NAME} ${BUILD_IMAGE_SERVER} make build-server-local #构建web镜像 build-image-web: @cd web/ && docker build -t ${REPOSITORY}/web:${TAGS_OPT} . #构建server镜像 build-image-server: @cd server/ && docker build -t ${REPOSITORY}/server:${TAGS_OPT} . #本地环境打包前后端 build-local: if [ -d "build" ];then rm -rf build; else echo "OK!"; fi \ && if [ -f "/.dockerenv" ];then echo "OK!"; else make build-web-local && make build-server-local; fi \ && mkdir build && cp -r web/dist build/ && cp server/server build/ && cp -r server/resource build/resource #本地环境打包前端 build-web-local: @cd web/ && if [ -d "dist" ];then rm -rf dist; else echo "OK!"; fi \ && yarn config set registry http://mirrors.cloud.tencent.com/npm/ && yarn install && yarn build #本地环境打包后端 build-server-local: @cd server/ && if [ -f "server" ];then rm -rf server; else echo "OK!"; fi \ && go env -w GO111MODULE=on && go env -w GOPROXY=https://goproxy.cn,direct \ && go env -w CGO_ENABLED=0 && go env && go mod tidy \ && go build -ldflags "-B 0x$(shell head -c20 /dev/urandom|od -An -tx1|tr -d ' \n') -X main.Version=${TAGS_OPT}" -v #打包前后端二合一镜像 image: build docker build -t ${REPOSITORY}/gin-vue-admin:${TAGS_OPT} -f deploy/docker/Dockerfile . #尝鲜版 images: build build-image-web build-image-server docker build -t ${REPOSITORY}/all:${TAGS_OPT} -f deploy/docker/Dockerfile . #swagger 文档生成 doc: @cd server && swag init #插件快捷打包: make plugin PLUGIN="这里是插件文件夹名称,默认为email" plugin: if [ -d ".plugin" ];then rm -rf .plugin ; else echo "OK!"; fi && mkdir -p .plugin/${PLUGIN}/{server/plugin,web/plugin} \ && if [ -d "server/plugin/${PLUGIN}" ];then cp -r server/plugin/${PLUGIN} .plugin/${PLUGIN}/server/plugin/ ; else echo "OK!"; fi \ && if [ -d "web/src/plugin/${PLUGIN}" ];then cp -r web/src/plugin/${PLUGIN} .plugin/${PLUGIN}/web/plugin/ ; else echo "OK!"; fi \ && cd .plugin && zip -r ${PLUGIN}.zip ${PLUGIN} && mv ${PLUGIN}.zip ../ && cd .. ================================================ FILE: README-en.md ================================================
English | [简体中文](./README.md) [gitee](https://gitee.com/pixelmax/gin-vue-admin): https://gitee.com/pixelmax/gin-vue-admin [github](https://github.com/flipped-aurora/gin-vue-admin): https://github.com/flipped-aurora/gin-vue-admin # Project Guidelines [Online Documentation](https://www.gin-vue-admin.com/) : https://www.gin-vue-admin.com/ [From the environment to the deployment of teaching videos](https://www.bilibili.com/video/BV1fV411y7dT) [Development Steps](https://www.gin-vue-admin.com/guide/start-quickly/env.html) (Contributor: LLemonGreen And Fann) ## 1. Basic Introduction ### 1.1 Project Introduction > Gin-vue-admin is a backstage management system based on [vue](https://vuejs.org) and [gin](https://gin-gonic.com), which separates the front and rear of the full stack. It integrates jwt authentication, dynamic routing, dynamic menu, casbin authentication, form generator, code generator and other functions. It provides a variety of sample files, allowing you to focus more time on business development. [Online Demo](http://demo.gin-vue-admin.com): http://demo.gin-vue-admin.com username:admin password:123456 ### 1.2 Contributing Guide Hi! Thank you for choosing gin-vue-admin. Gin-vue-admin is a full-stack (frontend and backend separation) framework for developers, designers and product managers. We are excited that you are interested in contributing to gin-vue-admin. Before submitting your contribution though, please make sure to take a moment and read through the following guidelines. #### 1.2.1 Issue Guidelines - Issues are exclusively for bug reports, feature requests and design-related topics. Other questions may be closed directly. If any questions come up when you are using Element, please hit [Gitter](https://gitter.im/element-en/Lobby) for help. - Before submitting an issue, please check if similar problems have already been issued. #### 1.2.2 Pull Request Guidelines - Fork this repository to your own account. Do not create branches here. - Commit info should be formatted as `[File Name]: Info about commit.` (e.g. `README.md: Fix xxx bug`) - Make sure PRs are created to `develop` branch instead of `master` branch. - If your PR fixes a bug, please provide a description about the related bug. - Merging a PR takes two maintainers: one approves the changes after reviewing, and then the other reviews and merges. ### 1.3 Version list - master: 2.0 code, for prod - develop: 2.0 dev code, for test - [gin-vue-admin_v2_dev](https://github.com/flipped-aurora/gin-vue-admin/tree/gin-vue-admin_v2_dev) (v2.0 [GormV1](https://v1.gorm.io) Stable branch) - [gva_gormv2_dev](https://github.com/flipped-aurora/gin-vue-admin/tree/gva_gormv2_dev) (v2.0 [GormV2](https://v2.gorm.io) Development branch) ## 2. Getting started ``` - node version > v8.6.0 - golang version >= v1.14 - IDE recommendation: Goland - initialization project: different versions of the database are not initialized. See synonyms at initialization https://www.gin-vue-admin.com/docs/first - Replace the Qiniuyun public key, private key, warehouse name and default url address in the project to avoid data confusion in the test file. ``` ### 2.1 server project use `Goland` And other editing tools,open server catalogue,You can't open it. `gin-vue-admin` root directory ```bash # clone the project git clone https://github.com/flipped-aurora/gin-vue-admin.git # open server catalogue cd server # use go mod And install the go dependency package go generate # Compile go build -o server main.go (windows the compile command is go build -o server.exe main.go ) # Run binary ./server (windows The run command is server.exe) ``` ### 2.1 web project ```bash # enter the project directory cd web # install dependency npm install # develop npm run serve ``` ### 2.2 Server ```bash # using go.mod # install go modules go list (go mod tidy) # build the server go build ``` ### 2.3 API docs auto-generation using swagger #### 2.3.1 install swagger ##### (1) Using VPN or outside mainland China ```` go get -u github.com/swaggo/swag/cmd/swag ```` ##### (2) In mainland China In mainland China, access to go.org/x is prohibited,we recommend [goproxy.io](https://goproxy.io/zh/) or [goproxy.cn](https://goproxy.cn) ````bash # If you are using a version of Go 1.13 - 1.15 Need to set up manually GO111MODULE=on, The opening mode is as follows, If your Go version is 1.16 ~ Latest edition You can ignore the following step one # Step one、Enable Go Modules Function go env -w GO111MODULE=on # Step two、Configuration GOPROXY Environment variable go env -w GOPROXY=https://goproxy.cn,https://goproxy.io,direct # If you dislike trouble,You can use the go generate Automatically execute code before compilation, But this can't be used command line terminal of `Goland` or `Vscode` cd server go generate -run "go env -w .*?" # Use the following command to download swag go get -u github.com/swaggo/swag/cmd/swag ```` #### 2.3.2 API docs generation ```` cd server swag init ```` > After executing the above command,server directory will appear in the docs folder `docs.go`, `swagger.json`, `swagger.yaml` Three file updates,After starting the go service, type in the browser [http://localhost:8888/swagger/index.html](http://localhost:8888/swagger/index.html) You can view swagger document ## 3. Technical selection - Frontend: using [Element](https://github.com/ElemeFE/element) based on [Vue](https://vuejs.org),to code the page. - Backend: using [Gin](https://gin-gonic.com/) to quickly build basic RESTful API. [Gin](https://gin-gonic.com/)is a web framework written in Go (Golang). - DB: `MySql`(5.6.44),using [gorm](http://gorm.io)` to implement data manipulation, added support for SQLite databases. - Cache: using `Redis` to implement the recording of the JWT token of the currently active user and implement the multi-login restriction. - API: using Swagger to auto generate APIs docs。 - Config: using [fsnotify](https://github.com/fsnotify/fsnotify) and [viper](https://github.com/spf13/viper) to implement `yaml` config file。 - Log: using [zap](https://github.com/uber-go/zap) record logs。 ## 4. Project Architecture ### 4.1 Architecture Diagram ![Architecture diagram](http://qmplusimg.henrongyi.top/gva/gin-vue-admin.png) ### 4.2 Front-end Detailed Design Diagram (Contributor: baobeisuper) ![Front-end Detailed Design Diagram](http://qmplusimg.henrongyi.top/naotu.png) ### 4.3 Project Layout ``` ├── server ├── api (api entrance) │ └── v1 (v1 version interface) ├── config (configuration package) ├── core (core document) ├── docs (swagger document directory) ├── global (global object) ├── initialize (initialization) │ └── internal (initialize internal function) ├── middleware (middleware layer) ├── model (model layer) │ ├── request (input parameter structure) │ └── response (out-of-parameter structure) ├── packfile (static file packaging) ├── resource (static resource folder) │ ├── excel (excel import and export default path) │ ├── page (form generator) │ └── template (template) ├── router (routing layer) ├── service (service layer) ├── source (source layer) └── utils (tool kit) ├── timer (timer interface encapsulation) └── upload (oss interface encapsulation) └─web (frontend) ├─public (deploy templates) └─src (source code) ├─api (frontend APIs) ├─assets (static files) ├─components(components) ├─router (frontend routers) ├─store (vuex state management) ├─style (common styles) ├─utils (frontend common utilitie) └─view (pages) ``` ## 5. Features - Authority management: Authority management based on `jwt` and `casbin`. - File upload and download: implement file upload operations based on `Qiniuyun', `Aliyun 'and `Tencent Cloud` (please develop your own application for each platform corresponding to `token` or `key` ). - Pagination Encapsulation:The frontend uses `mixins` to encapsulate paging, and the paging method can call `mixins` . - User management: The system administrator assigns user roles and role permissions. - Role management: Create the main object of permission control, and then assign different API permissions and menu permissions to the role. - Menu management: User dynamic menu configuration implementation, assigning different menus to different roles. - API management: Different users can call different API permissions. - Configuration management: the configuration file can be modified in the foreground (this feature is not available in the online experience site). - Conditional search: Add an example of conditional search. - Restful example: You can see sample APIs in user management module. - Front-end file reference: [web/src/view/superAdmin/api/api.vue](https://github.com/flipped-aurora/gin-vue-admin/blob/master/web/src/view/superAdmin/api/api.vue). - Stage reference: [server/router/sys_api.go](https://github.com/flipped-aurora/gin-vue-admin/blob/master/server/router/sys_api.go). - Multi-login restriction: Change `user-multipoint` to true in `system` in `config.yaml` (You need to configure redis and redis parameters yourself. During the test period, please report in time if there is a bug). - Upload file by chunk:Provides examples of file upload and large file upload by chunk. - Form Builder:With the help of [@form-generator](https://github.com/JakHuang/form-generator). - Code generator: Providing backend with basic logic and simple curd code generator. ## 6. Knowledge base ### 6.1 Team blog > https://www.yuque.com/flipped-aurora > >There are video courses about frontend framework in our blo. If you think the project is helpful to you, you can add my personal WeChat:shouzi_1994,your comments is welcomed。 ### 6.2 Video courses (1) Development environment course > Bilibili:https://www.bilibili.com/video/BV1Fg4y187Bw/ (2) Template course > Bilibili:https://www.bilibili.com/video/BV16K4y1r7BD/ (3) 2.0 version introduction and development experience > Bilibili:https://www.bilibili.com/video/BV1aV411d7Gm#reply2831798461 (4) Golang basic course > https://space.bilibili.com/322210472/channel/detail?cid=108884 (5) gin frame basic teaching > bilibili:https://space.bilibili.com/322210472/channel/detail?cid=126418&ctype=0 (6) gin-vue-admin version update introduction video > bilibili:https://space.bilibili.com/322210472/channel/detail?cid=126418&ctype=0 ## 7.Contacts ### 7.1 Groups #### QQ group: 622360840 | QQ group |d | :---: | | | #### Wechat group: comment "加入gin-vue-admin交流群" | Wechat | | :---: | | #### [About Us](https://www.gin-vue-admin.com/about/join.html) ## 8. Contributors Thank you for considering your contribution to gin-vue-admin! Contribution Leaderboard ## 9. Donate If you find this project useful, you can buy author a glass of juice :tropical_drink: [here](https://www.gin-vue-admin.com/coffee/index.html) ## 10. Commercial considerations If you use this project for commercial purposes, please comply with the Apache2.0 agreement and retain the author's technical support statement. ================================================ FILE: README.md ================================================
Calcium-Ion%2Fnew-api | Trendshift
[English](./README-en.md) | 简体中文 ## 支持claw生态 [🦞GvaClaw](https://plugin.gin-vue-admin.com/details/159) ## ✨一分钟生成前后端基础代码

⭐️ 高度适配AI编辑器的MCP

📄 创建基础模板

🤖 AI生成结构

⏰ 生成代码

🏷️ 分配权限

🎉 基础CURD生成完成

# 项目文档 [在线文档](https://www.gin-vue-admin.com) : https://www.gin-vue-admin.com [初始化](https://www.gin-vue-admin.com/guide/start-quickly/initialization.html) [从环境到部署教学视频](https://www.bilibili.com/video/BV1Rg411u7xH) [开发教学](https://www.gin-vue-admin.com/guide/start-quickly/env.html) (贡献者: LLemonGreen And Fann) [交流社区](https://support.qq.com/products/371961) [插件市场](https://plugin.gin-vue-admin.com/) [软件著作权证书](https://www.gin-vue-admin.com/copyright.pdf) # 重要提示 1.本项目从起步到开发到部署均有文档和详细视频教程 2.本项目需要您有一定的golang和vue基础 3.您完全可以通过我们的教程和文档完成一切操作,因此我们不再提供免费的技术服务,如需服务请进行[付费支持](https://www.gin-vue-admin.com/coffee/payment.html) 4.如果您将此项目用于商业用途,请遵守Apache2.0协议并保留作者技术支持声明。您需保留如下版权声明信息,以及日志和代码中所包含的版权声明信息。所需保留信息均为文案性质,不会影响任何业务内容,如决定商用【产生收益的商业行为均在商用行列】或者必须剔除请[购买授权](https://plugin.gin-vue-admin.com/licenseindex.html) \ ## 1. 基本介绍 ### 1.1 项目介绍 > Gin-vue-admin是一个基于 [vue](https://vuejs.org) 和 [gin](https://gin-gonic.com) 开发的全栈前后端分离的开发基础平台,集成jwt鉴权,动态路由,动态菜单,casbin鉴权,表单生成器,代码生成器等功能,提供多种示例文件,让您把更多时间专注在业务开发上。 [在线预览](http://demo.gin-vue-admin.com): http://demo.gin-vue-admin.com 测试用户名:admin 测试密码:123456 ### 1.2 贡献指南 Hi! 首先感谢你使用 gin-vue-admin。 Gin-vue-admin 是一套为快速研发准备的一整套前后端分离架构式的开源框架,旨在快速搭建中小型项目。 Gin-vue-admin 的成长离不开大家的支持,如果你愿意为 gin-vue-admin 贡献代码或提供建议,请阅读以下内容。 #### 1.2.1 Issue 规范 - issue 仅用于提交 Bug 或 Feature 以及设计相关的内容,其它内容可能会被直接关闭。 - 在提交 issue 之前,请搜索相关内容是否已被提出。 #### 1.2.2 Pull Request 规范 - 请先 fork 一份到自己的项目下,不要直接在仓库下建分支。 - commit 信息要以`[文件名]: 描述信息` 的形式填写,例如 `README.md: fix xxx bug`。 - 如果是修复 bug,请在 PR 中给出描述信息。 - 合并代码需要两名维护人员参与:一人进行 review 后 approve,另一人再次 review,通过后即可合并。 ## 2. 使用说明 ``` - node版本 > v18.16.0 - golang版本 >= v1.22 - IDE推荐:Goland ``` ### 2.1 server项目 使用 `Goland` 等编辑工具,打开server目录,不可以打开 gin-vue-admin 根目录 ```bash # 克隆项目 git clone https://github.com/flipped-aurora/gin-vue-admin.git # 进入server文件夹 cd server # 使用 go mod 并安装go依赖包 go generate # 运行 go run . ``` ### 2.2 web项目 ```bash # 进入web文件夹 cd web # 安装依赖 npm install # 启动web项目 npm run serve ``` ### 2.3 swagger自动化API文档 #### 2.3.1 安装 swagger ``` shell go install github.com/swaggo/swag/cmd/swag@latest ``` #### 2.3.2 生成API文档 ```` shell cd server swag init ```` > 执行上面的命令后,server目录下会出现docs文件夹里的 `docs.go`, `swagger.json`, `swagger.yaml` 三个文件更新,启动go服务之后, 在浏览器输入 [http://localhost:8888/swagger/index.html](http://localhost:8888/swagger/index.html) 即可查看swagger文档 ### 2.4 VSCode工作区 #### 2.4.1 开发 使用`VSCode`打开根目录下的工作区文件`gin-vue-admin.code-workspace`,在边栏可以看到三个虚拟目录:`backend`、`frontend`、`root`。 #### 2.4.2 运行/调试 在运行和调试中也可以看到三个task:`Backend`、`Frontend`、`Both (Backend & Frontend)`。运行`Both (Backend & Frontend)`可以同时启动前后端项目。 #### 2.4.3 settings 在工作区配置文件中有`go.toolsEnvVars`字段,是用于`VSCode`自身的go工具环境变量。此外在多go版本的系统中,可以通过`gopath`、`go.goroot`指定运行版本。 ```json "go.gopath": null, "go.goroot": null, ``` ## 3. 技术选型 - 前端:用基于 [Vue](https://vuejs.org) 的 [Element](https://github.com/ElemeFE/element) 构建基础页面。 - 后端:用 [Gin](https://gin-gonic.com/) 快速搭建基础restful风格API,[Gin](https://gin-gonic.com/) 是一个go语言编写的Web框架。 - 数据库:采用`MySql` > (5.7) 版本 数据库引擎 InnoDB,使用 [gorm](http://gorm.cn) 实现对数据库的基本操作。 - 缓存:使用`Redis`实现记录当前活跃用户的`jwt`令牌并实现多点登录限制。 - API文档:使用`Swagger`构建自动化文档。 - 配置文件:使用 [fsnotify](https://github.com/fsnotify/fsnotify) 和 [viper](https://github.com/spf13/viper) 实现`yaml`格式的配置文件。 - 日志:使用 [zap](https://github.com/uber-go/zap) 实现日志记录。 ## 4. 项目架构 ### 4.1 系统架构图 ![系统架构图](http://qmplusimg.henrongyi.top/gva/gin-vue-admin.png) ### 4.2 前端详细设计图 (提供者:baobeisuper) ![前端详细设计图](http://qmplusimg.henrongyi.top/naotu.png) ### 4.3 目录结构 ``` ├── server ├── api (api层) │ └── v1 (v1版本接口) ├── config (配置包) ├── core (核心文件) ├── docs (swagger文档目录) ├── global (全局对象) ├── initialize (初始化) │ └── internal (初始化内部函数) ├── middleware (中间件层) ├── model (模型层) │ ├── request (入参结构体) │ └── response (出参结构体) ├── packfile (静态文件打包) ├── resource (静态资源文件夹) │ ├── excel (excel导入导出默认路径) │ ├── page (表单生成器) │ └── template (模板) ├── router (路由层) ├── service (service层) ├── source (source层) └── utils (工具包) ├── timer (定时器接口封装) └── upload (oss接口封装) web ├── babel.config.js ├── Dockerfile ├── favicon.ico ├── index.html -- 主页面 ├── limit.js -- 助手代码 ├── package.json -- 包管理器代码 ├── src -- 源代码 │ ├── api -- api 组 │ ├── App.vue -- 主页面 │ ├── assets -- 静态资源 │ ├── components -- 全局组件 │ ├── core -- gva 组件包 │ │ ├── config.js -- gva网站配置文件 │ │ ├── gin-vue-admin.js -- 注册欢迎文件 │ │ └── global.js -- 统一导入文件 │ ├── directive -- v-auth 注册文件 │ ├── main.js -- 主文件 │ ├── permission.js -- 路由中间件 │ ├── pinia -- pinia 状态管理器,取代vuex │ │ ├── index.js -- 入口文件 │ │ └── modules -- modules │ │ ├── dictionary.js │ │ ├── router.js │ │ └── user.js │ ├── router -- 路由声明文件 │ │ └── index.js │ ├── style -- 全局样式 │ │ ├── base.scss │ │ ├── basics.scss │ │ ├── element_visiable.scss -- 此处可以全局覆盖 element-plus 样式 │ │ ├── iconfont.css -- 顶部几个icon的样式文件 │ │ ├── main.scss │ │ ├── mobile.scss │ │ └── newLogin.scss │ ├── utils -- 方法包库 │ │ ├── asyncRouter.js -- 动态路由相关 │ │ ├── btnAuth.js -- 动态权限按钮相关 │ │ ├── bus.js -- 全局mitt声明文件 │ │ ├── date.js -- 日期相关 │ │ ├── dictionary.js -- 获取字典方法 │ │ ├── downloadImg.js -- 下载图片方法 │ │ ├── format.js -- 格式整理相关 │ │ ├── image.js -- 图片相关方法 │ │ ├── page.js -- 设置页面标题 │ │ ├── request.js -- 请求 │ │ └── stringFun.js -- 字符串文件 | ├── view -- 主要view代码 | | ├── about -- 关于我们 | | ├── dashboard -- 面板 | | ├── error -- 错误 | | ├── example --上传案例 | | ├── iconList -- icon列表 | | ├── init -- 初始化数据 | | | ├── index -- 新版本 | | | ├── init -- 旧版本 | | ├── layout -- layout约束页面 | | | ├── aside | | | ├── bottomInfo -- bottomInfo | | | ├── screenfull -- 全屏设置 | | | ├── setting -- 系统设置 | | | └── index.vue -- base 约束 | | ├── login --登录 | | ├── person --个人中心 | | ├── superAdmin -- 超级管理员操作 | | ├── system -- 系统检测页面 | | ├── systemTools -- 系统配置相关页面 | | └── routerHolder.vue -- page 入口页面 ├── vite.config.js -- vite 配置文件 └── yarn.lock ``` ## 5. 主要功能 - 权限管理:基于`jwt`和`casbin`实现的权限管理。 - 文件上传下载:实现基于`七牛云`, `阿里云`, `腾讯云` 的文件上传操作(请开发自己去各个平台的申请对应 `token` 或者对应`key`)。 - 分页封装:前端使用 `mixins` 封装分页,分页方法调用 `mixins` 即可。 - 用户管理:系统管理员分配用户角色和角色权限。 - 角色管理:创建权限控制的主要对象,可以给角色分配不同api权限和菜单权限。 - 菜单管理:实现用户动态菜单配置,实现不同角色不同菜单。 - api管理:不同用户可调用的api接口的权限不同。 - 配置管理:配置文件可前台修改(在线体验站点不开放此功能)。 - 条件搜索:增加条件搜索示例。 - restful示例:可以参考用户管理模块中的示例API。 - 前端文件参考: [web/src/view/superAdmin/api/api.vue](https://github.com/flipped-aurora/gin-vue-admin/blob/master/web/src/view/superAdmin/api/api.vue) - 后台文件参考: [server/router/sys_api.go](https://github.com/flipped-aurora/gin-vue-admin/blob/master/server/router/sys_api.go) - 多点登录限制:需要在`config.yaml`中把`system`中的`use-multipoint`修改为true(需要自行配置Redis和Config中的Redis参数,测试阶段,有bug请及时反馈)。 - 分片上传:提供文件分片上传和大文件分片上传功能示例。 - 表单生成器:表单生成器借助 [@Variant Form](https://github.com/vform666/variant-form) 。 - 代码生成器:后台基础逻辑以及简单curd的代码生成器。 ## 6. 知识库 ## 6.1 团队博客 > https://www.yuque.com/flipped-aurora > >内有前端框架教学视频。如果觉得项目对您有所帮助可以添加我的个人微信:shouzi_1994,欢迎您提出宝贵的需求。 ## 6.2 教学视频 (1)手把手教学视频 > https://www.bilibili.com/video/BV1Rg411u7xH/ (2)后端目录结构调整介绍以及使用方法 > https://www.bilibili.com/video/BV1x44y117TT/ (3)golang基础教学视频 > bilibili:https://space.bilibili.com/322210472/channel/detail?cid=108884 (4)gin框架基础教学 > bilibili:https://space.bilibili.com/322210472/channel/detail?cid=126418&ctype=0 (5)gin-vue-admin 版本更新介绍视频 > bilibili:https://www.bilibili.com/video/BV1kv4y1g7nT ## 7. 联系方式 ### 7.1 技术群 ### QQ交流群:971857775 ### 微信交流群 | 微信 | | :---: | | 防止广告进群,添加微信,输入以下代码执行结果(请勿转码为string) ``` str := "5Yqg5YWlR1ZB5Lqk5rWB576k" decodeBytes, err := base64.StdEncoding.DecodeString(str) fmt.Println(decodeBytes, err) ``` ### [关于我们](https://www.gin-vue-admin.com/about/join.html) ## 8. 贡献者 感谢您对gin-vue-admin的贡献! Contribution Leaderboard ## 9. 捐赠 如果你觉得这个项目对你有帮助,你可以请作者喝饮料 :tropical_drink: [点我](https://www.gin-vue-admin.com/coffee/index.html) ## 10. 注意事项 请严格遵守Apache 2.0协议并保留作品声明,去除版权信息请务必[获取授权](https://plugin.gin-vue-admin.com/license) 未授权去除版权信息将依法追究法律责任 ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Reporting a Vulnerability Please report security issues to qimiaojiangjizhao@gmail.com ================================================ FILE: deploy/docker/Dockerfile ================================================ FROM centos:7 WORKDIR /opt ENV LANG=en_US.utf8 COPY deploy/docker/entrypoint.sh . COPY build/ /usr/share/nginx/html/ COPY server/config.yaml /usr/share/nginx/html/config.yaml COPY web/.docker-compose/nginx/conf.d/nginx.conf /etc/nginx/conf.d/nginx.conf RUN set -ex \ && echo "LANG=en_US.utf8" > /etc/locale.conf \ && echo "net.core.somaxconn = 1024" >> /etc/sysctl.conf \ && echo "vm.overcommit_memory = 1" >> /etc/sysctl.conf \ && yum -y install epel-release \ && yum -y localinstall http://mirrors.ustc.edu.cn/mysql-repo/mysql57-community-release-el7.rpm \ && yum -y install mysql-community-server git redis nginx go npm --nogpgcheck && chmod +x ./entrypoint.sh \ && npm install -g yarn && go env -w GO111MODULE=on && go env -w GOPROXY=https://goproxy.cn,direct \ && echo "start" > /dev/null EXPOSE 80 ENTRYPOINT ["./entrypoint.sh"] ================================================ FILE: deploy/docker/entrypoint.sh ================================================ #!/bin/bash if [ ! -d "/var/lib/mysql/gva" ]; then mysqld --initialize-insecure --user=mysql --datadir=/var/lib/mysql mysqld --daemonize --user=mysql sleep 5s mysql -uroot -e "create database gva default charset 'utf8' collate 'utf8_bin'; grant all on gva.* to 'root'@'127.0.0.1' identified by '123456'; flush privileges;" else mysqld --daemonize --user=mysql fi redis-server & if [ "$1" = "actions" ]; then cd /opt/gva/server && go run main.go & cd /opt/gva/web/ && yarn serve & else /usr/sbin/nginx & cd /usr/share/nginx/html/ && ./server & fi echo "gva ALL start!!!" tail -f /dev/null ================================================ FILE: deploy/docker-compose/docker-compose.yaml ================================================ version: "3" # 声明一个名为network的networks,subnet为network的子网地址,默认网关是177.7.0.1 networks: network: ipam: driver: default config: - subnet: '177.7.0.0/16' # 设置mysql,redis持久化保存 volumes: mysql: redis: services: web: build: context: ../../web dockerfile: ./Dockerfile container_name: gva-web restart: always ports: - '8080:8080' depends_on: - server command: [ 'nginx-debug', '-g', 'daemon off;' ] networks: network: ipv4_address: 177.7.0.11 server: build: context: ../../server dockerfile: ./Dockerfile container_name: gva-server restart: always ports: - '8888:8888' depends_on: mysql: condition: service_healthy redis: condition: service_healthy links: - mysql - redis networks: network: ipv4_address: 177.7.0.12 mysql: image: mysql:8.0.21 # 如果您是 arm64 架构:如 MacOS 的 M1,请修改镜像为 image: mysql/mysql-server:8.0.21 container_name: gva-mysql command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci #设置utf8字符集 restart: always ports: - "13306:3306" # host物理直接映射端口为13306 environment: #MYSQL_ROOT_PASSWORD: 'Aa@6447985' # root管理员用户密码 MYSQL_DATABASE: 'qmPlus' # 初始化启动时要创建的数据库的名称 MYSQL_USER: 'gva' MYSQL_PASSWORD: 'Aa@6447985' healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "gva", "-pAa@6447985"] interval: 10s timeout: 5s retries: 3 volumes: - mysql:/var/lib/mysql networks: network: ipv4_address: 177.7.0.13 redis: image: redis:6.0.6 container_name: gva-redis # 容器名 restart: always ports: - '16379:6379' healthcheck: test: ["CMD-SHELL", "redis-cli ping | grep PONG || exit 1"] interval: 10s timeout: 5s retries: 3 volumes: - redis:/data networks: network: ipv4_address: 177.7.0.14 ================================================ FILE: deploy/kubernetes/server/gva-server-configmap.yaml ================================================ apiVersion: v1 kind: ConfigMap metadata: name: config.yaml annotations: flipped-aurora/gin-vue-admin: backend github: "https://github.com/flipped-aurora/gin-vue-admin.git" app.kubernetes.io/version: 0.0.1 labels: app: gva-server version: gva-vue3 data: config.yaml: | # github.com/flipped-aurora/gin-vue-admin/server Global Configuration # jwt configuration jwt: signing-key: 'qmPlus' expires-time: 604800 buffer-time: 86400 # zap logger configuration zap: level: 'info' format: 'console' prefix: '[github.com/flipped-aurora/gin-vue-admin/server]' director: 'log' link-name: 'latest_log' show-line: true encode-level: 'LowercaseColorLevelEncoder' stacktrace-key: 'stacktrace' log-in-console: true # redis configuration redis: db: 0 addr: '127.0.0.1:6379' password: '' # email configuration email: to: 'xxx@qq.com' port: 465 from: 'xxx@163.com' host: 'smtp.163.com' is-ssl: true secret: 'xxx' nickname: 'test' # casbin configuration casbin: model-path: './resource/rbac_model.conf' # system configuration system: env: 'develop' # Change to "develop" to skip authentication for development mode addr: 8888 db-type: 'mysql' oss-type: 'local' # 控制oss选择走本期还是 七牛等其他仓 自行增加其他oss仓可以在 server/utils/upload/upload.go 中 NewOss函数配置 use-multipoint: false # captcha configuration captcha: key-long: 6 img-width: 240 img-height: 80 # mysql connect configuration # 未初始化之前请勿手动修改数据库信息!!!如果一定要手动初始化请看(https://www.github.com/flipped-aurora/gin-vue-admin/server.com/docs/first) mysql: path: '' config: '' db-name: '' username: '' password: '' max-idle-conns: 10 max-open-conns: 100 log-mode: false log-zap: "" # local configuration local: path: 'uploads/file' # autocode configuration autocode: transfer-restart: true root: "" server: /server server-api: /api/v1/autocode server-initialize: /initialize server-model: /model/autocode server-request: /model/autocode/request/ server-router: /router/autocode server-service: /service/autocode web: /web/src web-api: /api web-flow: /view web-form: /view web-table: /view # qiniu configuration (请自行七牛申请对应的 公钥 私钥 bucket 和 域名地址) qiniu: zone: 'ZoneHuaDong' bucket: '' img-path: '' use-https: false access-key: '' secret-key: '' use-cdn-domains: false # aliyun oss configuration aliyun-oss: endpoint: 'yourEndpoint' access-key-id: 'yourAccessKeyId' access-key-secret: 'yourAccessKeySecret' bucket-name: 'yourBucketName' bucket-url: 'yourBucketUrl' base-path: 'yourBasePath' # tencent cos configuration tencent-cos: bucket: 'xxxxx-10005608' region: 'ap-shanghai' secret-id: 'xxxxxxxx' secret-key: 'xxxxxxxx' base-url: 'https://gin.vue.admin' path-prefix: 'github.com/flipped-aurora/gin-vue-admin/server' # excel configuration excel: dir: './resource/excel/' # timer task db clear table Timer: start: true spec: "@daily" # 定时任务详细配置参考 https://pkg.go.dev/github.com/robfig/cron/v3 detail: [ # tableName: 需要清理的表名 # compareField: 需要比较时间的字段 # interval: 时间间隔, 具体配置详看 time.ParseDuration() 中字符串表示 且不能为负数 # 2160h = 24 * 30 * 3 -> 三个月 { tableName: "sys_operation_records" , compareField: "created_at", interval: "2160h" }, { tableName: "jwt_blacklists" , compareField: "created_at", interval: "168h" } #{ tableName: "log2" , compareField: "created_at", interval: "2160h" } ] ================================================ FILE: deploy/kubernetes/server/gva-server-deployment.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: name: gva-server annotations: flipped-aurora/gin-vue-admin: backend github: "https://github.com/flipped-aurora/gin-vue-admin.git" app.kubernetes.io/version: 0.0.1 labels: app: gva-server version: gva-vue3 spec: replicas: 1 selector: matchLabels: app: gva-server version: gva-vue3 template: metadata: labels: app: gva-server version: gva-vue3 spec: containers: - name: gin-vue-admin-container image: registry.cn-hangzhou.aliyuncs.com/gva/server:latest imagePullPolicy: Always ports: - containerPort: 8888 name: http volumeMounts: - mountPath: /go/src/github.com/flipped-aurora/gin-vue-admin/server/config.docker.yaml name: config subPath: config.yaml - mountPath: /etc/localtime name: localtime resources: limits: cpu: 1000m memory: 2000Mi requests: cpu: 100m memory: 200Mi livenessProbe: failureThreshold: 1 periodSeconds: 5 successThreshold: 1 tcpSocket: port: 8888 timeoutSeconds: 1 readinessProbe: failureThreshold: 3 initialDelaySeconds: 30 periodSeconds: 5 successThreshold: 1 tcpSocket: port: 8888 timeoutSeconds: 1 startupProbe: failureThreshold: 40 periodSeconds: 5 successThreshold: 1 tcpSocket: port: 8888 timeoutSeconds: 1 #imagePullSecrets: # - name: docker-registry volumes: - name: localtime hostPath: path: /etc/localtime - name: config configMap: name: config.yaml ================================================ FILE: deploy/kubernetes/server/gva-server-service.yaml ================================================ apiVersion: v1 kind: Service metadata: name: gva-server annotations: flipped-aurora/gin-vue-admin: backend github: "https://github.com/flipped-aurora/gin-vue-admin.git" app.kubernetes.io/version: 0.0.1 labels: app: gva-server version: gva-vue3 spec: selector: app: gva-server version: gva-vue3 ports: - port: 8888 name: http targetPort: 8888 type: ClusterIP # type: NodePort ================================================ FILE: deploy/kubernetes/web/gva-web-configmap.yaml ================================================ apiVersion: v1 kind: ConfigMap metadata: name: my.conf data: my.conf: | server { listen 8080; server_name localhost; #charset koi8-r; #access_log logs/host.access.log main; location / { root /usr/share/nginx/html; add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0'; try_files $uri $uri/ /index.html; } location /api { proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; rewrite ^/api/(.*)$ /$1 break; #重写 proxy_pass http://gva-server:8888; # 设置代理服务器的协议和地址 } location /api/swagger/index.html { proxy_pass http://gva-server:8888/swagger/index.html; } } ================================================ FILE: deploy/kubernetes/web/gva-web-deploymemt.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: name: gva-web annotations: flipped-aurora/gin-vue-admin: ui github: "https://github.com/flipped-aurora/gin-vue-admin.git" app.kubernetes.io/version: 0.0.1 labels: app: gva-web version: gva-vue3 spec: replicas: 1 selector: matchLabels: app: gva-web version: gva-vue3 template: metadata: labels: app: gva-web version: gva-vue3 spec: containers: - name: gin-vue-admin-nginx-container image: registry.cn-hangzhou.aliyuncs.com/gva/web:latest imagePullPolicy: Always ports: - containerPort: 8080 name: http readinessProbe: tcpSocket: port: 8080 initialDelaySeconds: 10 periodSeconds: 10 successThreshold: 1 failureThreshold: 3 resources: limits: cpu: 500m memory: 1000Mi requests: cpu: 100m memory: 100Mi volumeMounts: - mountPath: /etc/nginx/conf.d/ name: nginx-config volumes: - name: nginx-config configMap: name: my.conf ================================================ FILE: deploy/kubernetes/web/gva-web-ingress.yaml ================================================ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: gva-ingress annotations: kubernetes.io/ingress.class: "nginx" spec: rules: - host: demo.gin-vue-admin.com http: paths: - path: / pathType: Prefix backend: service: name: gva-web port: number: 8080 ================================================ FILE: deploy/kubernetes/web/gva-web-service.yaml ================================================ apiVersion: v1 kind: Service metadata: name: gva-web annotations: flipped-aurora/gin-vue-admin: ui github: "https://github.com/flipped-aurora/gin-vue-admin.git" app.kubernetes.io/version: 0.0.1 labels: app: gva-web version: gva-vue3 spec: # type: NodePort type: ClusterIP ports: - name: http port: 8080 targetPort: 8080 selector: app: gva-web version: gva-vue3 ================================================ FILE: gin-vue-admin.code-workspace ================================================ { "folders": [ { "path": "server", "name": "backend" }, { "path": "web", "name": "frontend" }, { "path": ".", "name": "root" } ], "settings": { "go.toolsEnvVars": { "GOPROXY": "https://goproxy.cn,direct", "GONOPROXY": "none;" } }, "launch": { "version": "0.2.0", "configurations": [ { "type": "go", "request": "launch", "name": "Backend", "cwd": "${workspaceFolder:backend}", "program": "${workspaceFolder:backend}/" }, { "type": "node", "request": "launch", "cwd": "${workspaceFolder:frontend}", "name": "Frontend", "runtimeExecutable": "npm", "runtimeArgs": ["run-script", "serve"] } ], "compounds": [ { "name": "Both (Backend & Frontend)", "configurations": ["Backend", "Frontend"], "stopAll": true } ] } } ================================================ FILE: server/Dockerfile ================================================ FROM golang:alpine as builder WORKDIR /go/src/github.com/flipped-aurora/gin-vue-admin/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/github.com/flipped-aurora/gin-vue-admin/server COPY --from=0 /go/src/github.com/flipped-aurora/gin-vue-admin/server/server ./ COPY --from=0 /go/src/github.com/flipped-aurora/gin-vue-admin/server/resource ./resource/ COPY --from=0 /go/src/github.com/flipped-aurora/gin-vue-admin/server/config.docker.yaml ./ # 挂载目录:如果使用了sqlite数据库,容器命令示例:docker run -d -v /宿主机路径/gva.db:/go/src/github.com/flipped-aurora/gin-vue-admin/server/gva.db -p 8888:8888 --name gva-server-v1 gva-server:1.0 # VOLUME ["/go/src/github.com/flipped-aurora/gin-vue-admin/server"] EXPOSE 8888 ENTRYPOINT ./server -c config.docker.yaml ================================================ FILE: server/README.md ================================================ ## 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接口封装 | ================================================ FILE: server/api/v1/enter.go ================================================ package v1 import ( "github.com/flipped-aurora/gin-vue-admin/server/api/v1/example" "github.com/flipped-aurora/gin-vue-admin/server/api/v1/system" ) var ApiGroupApp = new(ApiGroup) type ApiGroup struct { SystemApiGroup system.ApiGroup ExampleApiGroup example.ApiGroup } ================================================ FILE: server/api/v1/example/enter.go ================================================ package example import "github.com/flipped-aurora/gin-vue-admin/server/service" type ApiGroup struct { CustomerApi FileUploadAndDownloadApi AttachmentCategoryApi } var ( customerService = service.ServiceGroupApp.ExampleServiceGroup.CustomerService fileUploadAndDownloadService = service.ServiceGroupApp.ExampleServiceGroup.FileUploadAndDownloadService attachmentCategoryService = service.ServiceGroupApp.ExampleServiceGroup.AttachmentCategoryService ) ================================================ FILE: server/api/v1/example/exa_attachment_category.go ================================================ package example import ( "github.com/flipped-aurora/gin-vue-admin/server/global" common "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" "github.com/flipped-aurora/gin-vue-admin/server/model/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) } ================================================ FILE: server/api/v1/example/exa_breakpoint_continue.go ================================================ package example import ( "fmt" "io" "mime/multipart" "strconv" "strings" "github.com/flipped-aurora/gin-vue-admin/server/model/example" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" exampleRes "github.com/flipped-aurora/gin-vue-admin/server/model/example/response" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/gin-gonic/gin" "go.uber.org/zap" ) // BreakpointContinue // @Tags ExaFileUploadAndDownload // @Summary 断点续传到服务器 // @Security ApiKeyAuth // @accept multipart/form-data // @Produce application/json // @Param file formData file true "an example for breakpoint resume, 断点续传示例" // @Success 200 {object} response.Response{msg=string} "断点续传到服务器" // @Router /fileUploadAndDownload/breakpointContinue [post] func (b *FileUploadAndDownloadApi) BreakpointContinue(c *gin.Context) { fileMd5 := c.Request.FormValue("fileMd5") fileName := c.Request.FormValue("fileName") chunkMd5 := c.Request.FormValue("chunkMd5") chunkNumber, _ := strconv.Atoi(c.Request.FormValue("chunkNumber")) chunkTotal, _ := strconv.Atoi(c.Request.FormValue("chunkTotal")) _, FileHeader, err := c.Request.FormFile("file") if err != nil { global.GVA_LOG.Error("接收文件失败!", zap.Error(err)) response.FailWithMessage("接收文件失败", c) return } f, err := FileHeader.Open() if err != nil { global.GVA_LOG.Error("文件读取失败!", zap.Error(err)) response.FailWithMessage("文件读取失败", c) return } defer func(f multipart.File) { err := f.Close() if err != nil { fmt.Println(err) } }(f) cen, _ := io.ReadAll(f) if !utils.CheckMd5(cen, chunkMd5) { global.GVA_LOG.Error("检查md5失败!", zap.Error(err)) response.FailWithMessage("检查md5失败", c) return } file, err := fileUploadAndDownloadService.FindOrCreateFile(fileMd5, fileName, chunkTotal) if err != nil { global.GVA_LOG.Error("查找或创建记录失败!", zap.Error(err)) response.FailWithMessage("查找或创建记录失败", c) return } pathC, err := utils.BreakPointContinue(cen, fileName, chunkNumber, chunkTotal, fileMd5) if err != nil { global.GVA_LOG.Error("断点续传失败!", zap.Error(err)) response.FailWithMessage("断点续传失败", c) return } if err = fileUploadAndDownloadService.CreateFileChunk(file.ID, pathC, chunkNumber); err != nil { global.GVA_LOG.Error("创建文件记录失败!", zap.Error(err)) response.FailWithMessage("创建文件记录失败", c) return } response.OkWithMessage("切片创建成功", c) } // FindFile // @Tags ExaFileUploadAndDownload // @Summary 查找文件 // @Security ApiKeyAuth // @accept multipart/form-data // @Produce application/json // @Param file formData file true "Find the file, 查找文件" // @Success 200 {object} response.Response{data=exampleRes.FileResponse,msg=string} "查找文件,返回包括文件详情" // @Router /fileUploadAndDownload/findFile [get] func (b *FileUploadAndDownloadApi) FindFile(c *gin.Context) { fileMd5 := c.Query("fileMd5") fileName := c.Query("fileName") chunkTotal, _ := strconv.Atoi(c.Query("chunkTotal")) file, err := fileUploadAndDownloadService.FindOrCreateFile(fileMd5, fileName, chunkTotal) if err != nil { global.GVA_LOG.Error("查找失败!", zap.Error(err)) response.FailWithMessage("查找失败", c) } else { response.OkWithDetailed(exampleRes.FileResponse{File: file}, "查找成功", c) } } // BreakpointContinueFinish // @Tags ExaFileUploadAndDownload // @Summary 创建文件 // @Security ApiKeyAuth // @accept multipart/form-data // @Produce application/json // @Param file formData file true "上传文件完成" // @Success 200 {object} response.Response{data=exampleRes.FilePathResponse,msg=string} "创建文件,返回包括文件路径" // @Router /fileUploadAndDownload/findFile [post] func (b *FileUploadAndDownloadApi) BreakpointContinueFinish(c *gin.Context) { fileMd5 := c.Query("fileMd5") fileName := c.Query("fileName") filePath, err := utils.MakeFile(fileName, fileMd5) if err != nil { global.GVA_LOG.Error("文件创建失败!", zap.Error(err)) response.FailWithDetailed(exampleRes.FilePathResponse{FilePath: filePath}, "文件创建失败", c) } else { response.OkWithDetailed(exampleRes.FilePathResponse{FilePath: filePath}, "文件创建成功", c) } } // RemoveChunk // @Tags ExaFileUploadAndDownload // @Summary 删除切片 // @Security ApiKeyAuth // @accept multipart/form-data // @Produce application/json // @Param file formData file true "删除缓存切片" // @Success 200 {object} response.Response{msg=string} "删除切片" // @Router /fileUploadAndDownload/removeChunk [post] func (b *FileUploadAndDownloadApi) RemoveChunk(c *gin.Context) { var file example.ExaFile err := c.ShouldBindJSON(&file) if err != nil { response.FailWithMessage(err.Error(), c) return } // 路径穿越拦截 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) } ================================================ FILE: server/api/v1/example/exa_customer.go ================================================ package example import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" "github.com/flipped-aurora/gin-vue-admin/server/model/example" exampleRes "github.com/flipped-aurora/gin-vue-admin/server/model/example/response" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/gin-gonic/gin" "go.uber.org/zap" ) type CustomerApi struct{} // CreateExaCustomer // @Tags ExaCustomer // @Summary 创建客户 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body example.ExaCustomer true "客户用户名, 客户手机号码" // @Success 200 {object} response.Response{msg=string} "创建客户" // @Router /customer/customer [post] func (e *CustomerApi) CreateExaCustomer(c *gin.Context) { var customer example.ExaCustomer err := c.ShouldBindJSON(&customer) if err != nil { response.FailWithMessage(err.Error(), c) return } err = utils.Verify(customer, utils.CustomerVerify) if err != nil { response.FailWithMessage(err.Error(), c) return } customer.SysUserID = utils.GetUserID(c) customer.SysUserAuthorityID = utils.GetUserAuthorityId(c) err = customerService.CreateExaCustomer(customer) if err != nil { global.GVA_LOG.Error("创建失败!", zap.Error(err)) response.FailWithMessage("创建失败", c) return } response.OkWithMessage("创建成功", c) } // DeleteExaCustomer // @Tags ExaCustomer // @Summary 删除客户 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body example.ExaCustomer true "客户ID" // @Success 200 {object} response.Response{msg=string} "删除客户" // @Router /customer/customer [delete] func (e *CustomerApi) DeleteExaCustomer(c *gin.Context) { var customer example.ExaCustomer err := c.ShouldBindJSON(&customer) if err != nil { response.FailWithMessage(err.Error(), c) return } err = utils.Verify(customer.GVA_MODEL, utils.IdVerify) if err != nil { response.FailWithMessage(err.Error(), c) return } err = customerService.DeleteExaCustomer(customer) if err != nil { global.GVA_LOG.Error("删除失败!", zap.Error(err)) response.FailWithMessage("删除失败", c) return } response.OkWithMessage("删除成功", c) } // UpdateExaCustomer // @Tags ExaCustomer // @Summary 更新客户信息 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body example.ExaCustomer true "客户ID, 客户信息" // @Success 200 {object} response.Response{msg=string} "更新客户信息" // @Router /customer/customer [put] func (e *CustomerApi) UpdateExaCustomer(c *gin.Context) { var customer example.ExaCustomer err := c.ShouldBindJSON(&customer) if err != nil { response.FailWithMessage(err.Error(), c) return } err = utils.Verify(customer.GVA_MODEL, utils.IdVerify) if err != nil { response.FailWithMessage(err.Error(), c) return } err = utils.Verify(customer, utils.CustomerVerify) if err != nil { response.FailWithMessage(err.Error(), c) return } err = customerService.UpdateExaCustomer(&customer) if err != nil { global.GVA_LOG.Error("更新失败!", zap.Error(err)) response.FailWithMessage("更新失败", c) return } response.OkWithMessage("更新成功", c) } // GetExaCustomer // @Tags ExaCustomer // @Summary 获取单一客户信息 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data query example.ExaCustomer true "客户ID" // @Success 200 {object} response.Response{data=exampleRes.ExaCustomerResponse,msg=string} "获取单一客户信息,返回包括客户详情" // @Router /customer/customer [get] func (e *CustomerApi) GetExaCustomer(c *gin.Context) { var customer example.ExaCustomer err := c.ShouldBindQuery(&customer) if err != nil { response.FailWithMessage(err.Error(), c) return } err = utils.Verify(customer.GVA_MODEL, utils.IdVerify) if err != nil { response.FailWithMessage(err.Error(), c) return } data, err := customerService.GetExaCustomer(customer.ID) if err != nil { global.GVA_LOG.Error("获取失败!", zap.Error(err)) response.FailWithMessage("获取失败", c) return } response.OkWithDetailed(exampleRes.ExaCustomerResponse{Customer: data}, "获取成功", c) } // GetExaCustomerList // @Tags ExaCustomer // @Summary 分页获取权限客户列表 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data query request.PageInfo true "页码, 每页大小" // @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页获取权限客户列表,返回包括列表,总数,页码,每页数量" // @Router /customer/customerList [get] func (e *CustomerApi) GetExaCustomerList(c *gin.Context) { var pageInfo request.PageInfo err := c.ShouldBindQuery(&pageInfo) if err != nil { response.FailWithMessage(err.Error(), c) return } err = utils.Verify(pageInfo, utils.PageInfoVerify) if err != nil { response.FailWithMessage(err.Error(), c) return } customerList, total, err := customerService.GetCustomerInfoList(utils.GetUserAuthorityId(c), pageInfo) if err != nil { global.GVA_LOG.Error("获取失败!", zap.Error(err)) response.FailWithMessage("获取失败"+err.Error(), c) return } response.OkWithDetailed(response.PageResult{ List: customerList, Total: total, Page: pageInfo.Page, PageSize: pageInfo.PageSize, }, "获取成功", c) } ================================================ FILE: server/api/v1/example/exa_file_upload_download.go ================================================ package example import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" "github.com/flipped-aurora/gin-vue-admin/server/model/example" "github.com/flipped-aurora/gin-vue-admin/server/model/example/request" exampleRes "github.com/flipped-aurora/gin-vue-admin/server/model/example/response" "github.com/gin-gonic/gin" "go.uber.org/zap" "strconv" ) type FileUploadAndDownloadApi struct{} // UploadFile // @Tags ExaFileUploadAndDownload // @Summary 上传文件示例 // @Security ApiKeyAuth // @accept multipart/form-data // @Produce application/json // @Param file formData file true "上传文件示例" // @Success 200 {object} response.Response{data=exampleRes.ExaFileResponse,msg=string} "上传文件示例,返回包括文件详情" // @Router /fileUploadAndDownload/upload [post] func (b *FileUploadAndDownloadApi) UploadFile(c *gin.Context) { var file example.ExaFileUploadAndDownload noSave := c.DefaultQuery("noSave", "0") _, header, err := c.Request.FormFile("file") classId, _ := strconv.Atoi(c.DefaultPostForm("classId", "0")) if err != nil { global.GVA_LOG.Error("接收文件失败!", zap.Error(err)) response.FailWithMessage("接收文件失败", c) return } file, err = fileUploadAndDownloadService.UploadFile(header, noSave, classId) // 文件上传后拿到文件路径 if err != nil { global.GVA_LOG.Error("上传文件失败!", zap.Error(err)) response.FailWithMessage("上传文件失败", c) return } response.OkWithDetailed(exampleRes.ExaFileResponse{File: file}, "上传成功", c) } // EditFileName 编辑文件名或者备注 func (b *FileUploadAndDownloadApi) EditFileName(c *gin.Context) { var file example.ExaFileUploadAndDownload err := c.ShouldBindJSON(&file) if err != nil { response.FailWithMessage(err.Error(), c) return } err = fileUploadAndDownloadService.EditFileName(file) if err != nil { global.GVA_LOG.Error("编辑失败!", zap.Error(err)) response.FailWithMessage("编辑失败", c) return } response.OkWithMessage("编辑成功", c) } // DeleteFile // @Tags ExaFileUploadAndDownload // @Summary 删除文件 // @Security ApiKeyAuth // @Produce application/json // @Param data body example.ExaFileUploadAndDownload true "传入文件里面id即可" // @Success 200 {object} response.Response{msg=string} "删除文件" // @Router /fileUploadAndDownload/deleteFile [post] func (b *FileUploadAndDownloadApi) DeleteFile(c *gin.Context) { var file example.ExaFileUploadAndDownload err := c.ShouldBindJSON(&file) if err != nil { response.FailWithMessage(err.Error(), c) return } if err := fileUploadAndDownloadService.DeleteFile(file); err != nil { global.GVA_LOG.Error("删除失败!", zap.Error(err)) response.FailWithMessage("删除失败", c) return } response.OkWithMessage("删除成功", c) } // GetFileList // @Tags ExaFileUploadAndDownload // @Summary 分页文件列表 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body request.ExaAttachmentCategorySearch true "页码, 每页大小, 分类id" // @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页文件列表,返回包括列表,总数,页码,每页数量" // @Router /fileUploadAndDownload/getFileList [post] func (b *FileUploadAndDownloadApi) GetFileList(c *gin.Context) { var pageInfo request.ExaAttachmentCategorySearch err := c.ShouldBindJSON(&pageInfo) if err != nil { response.FailWithMessage(err.Error(), c) return } list, total, err := fileUploadAndDownloadService.GetFileRecordInfoList(pageInfo) if err != nil { global.GVA_LOG.Error("获取失败!", zap.Error(err)) response.FailWithMessage("获取失败", c) return } response.OkWithDetailed(response.PageResult{ List: list, Total: total, Page: pageInfo.Page, PageSize: pageInfo.PageSize, }, "获取成功", c) } // ImportURL // @Tags ExaFileUploadAndDownload // @Summary 导入URL // @Security ApiKeyAuth // @Produce application/json // @Param data body example.ExaFileUploadAndDownload true "对象" // @Success 200 {object} response.Response{msg=string} "导入URL" // @Router /fileUploadAndDownload/importURL [post] func (b *FileUploadAndDownloadApi) ImportURL(c *gin.Context) { var file []example.ExaFileUploadAndDownload err := c.ShouldBindJSON(&file) if err != nil { response.FailWithMessage(err.Error(), c) return } if err := fileUploadAndDownloadService.ImportURL(&file); err != nil { global.GVA_LOG.Error("导入URL失败!", zap.Error(err)) response.FailWithMessage("导入URL失败", c) return } response.OkWithMessage("导入URL成功", c) } ================================================ FILE: server/api/v1/system/auto_code_history.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/global" common "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" request "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "github.com/gin-gonic/gin" "go.uber.org/zap" ) type AutoCodeHistoryApi struct{} // First // @Tags AutoCode // @Summary 获取meta信息 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body request.GetById true "请求参数" // @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "获取meta信息" // @Router /autoCode/getMeta [post] func (a *AutoCodeHistoryApi) First(c *gin.Context) { var info common.GetById err := c.ShouldBindJSON(&info) if err != nil { response.FailWithMessage(err.Error(), c) return } data, err := autoCodeHistoryService.First(c.Request.Context(), info) if err != nil { response.FailWithMessage(err.Error(), c) return } response.OkWithDetailed(gin.H{"meta": data}, "获取成功", c) } // Delete // @Tags AutoCode // @Summary 删除回滚记录 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body request.GetById true "请求参数" // @Success 200 {object} response.Response{msg=string} "删除回滚记录" // @Router /autoCode/delSysHistory [post] func (a *AutoCodeHistoryApi) Delete(c *gin.Context) { var info common.GetById err := c.ShouldBindJSON(&info) if err != nil { response.FailWithMessage(err.Error(), c) return } err = autoCodeHistoryService.Delete(c.Request.Context(), info) if err != nil { global.GVA_LOG.Error("删除失败!", zap.Error(err)) response.FailWithMessage("删除失败", c) return } response.OkWithMessage("删除成功", c) } // RollBack // @Tags AutoCode // @Summary 回滚自动生成代码 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body request.SysAutoHistoryRollBack true "请求参数" // @Success 200 {object} response.Response{msg=string} "回滚自动生成代码" // @Router /autoCode/rollback [post] func (a *AutoCodeHistoryApi) RollBack(c *gin.Context) { var info request.SysAutoHistoryRollBack err := c.ShouldBindJSON(&info) if err != nil { response.FailWithMessage(err.Error(), c) return } err = autoCodeHistoryService.RollBack(c.Request.Context(), info) if err != nil { response.FailWithMessage(err.Error(), c) return } response.OkWithMessage("回滚成功", c) } // GetList // @Tags AutoCode // @Summary 查询回滚记录 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body common.PageInfo true "请求参数" // @Success 200 {object} response.Response{data=response.PageResult,msg=string} "查询回滚记录,返回包括列表,总数,页码,每页数量" // @Router /autoCode/getSysHistory [post] func (a *AutoCodeHistoryApi) GetList(c *gin.Context) { var info common.PageInfo err := c.ShouldBindJSON(&info) if err != nil { response.FailWithMessage(err.Error(), c) return } list, total, err := autoCodeHistoryService.GetList(c.Request.Context(), info) if err != nil { global.GVA_LOG.Error("获取失败!", zap.Error(err)) response.FailWithMessage("获取失败", c) return } response.OkWithDetailed(response.PageResult{ List: list, Total: total, Page: info.Page, PageSize: info.PageSize, }, "获取成功", c) } ================================================ FILE: server/api/v1/system/auto_code_mcp.go ================================================ package system import ( "fmt" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/mcp/client" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "github.com/gin-gonic/gin" "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) } ================================================ FILE: server/api/v1/system/auto_code_package.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/global" common "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/gin-gonic/gin" "go.uber.org/zap" "strings" ) type AutoCodePackageApi struct{} // Create // @Tags AutoCodePackage // @Summary 创建package // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body request.SysAutoCodePackageCreate true "创建package" // @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "创建package成功" // @Router /autoCode/createPackage [post] func (a *AutoCodePackageApi) Create(c *gin.Context) { var info request.SysAutoCodePackageCreate _ = c.ShouldBindJSON(&info) if err := utils.Verify(info, utils.AutoPackageVerify); err != nil { response.FailWithMessage(err.Error(), c) return } if strings.Contains(info.PackageName, "\\") || strings.Contains(info.PackageName, "/") || strings.Contains(info.PackageName, "..") { response.FailWithMessage("包名不合法", c) return } // PackageName可能导致路径穿越的问题 / 和 \ 都要防止 err := autoCodePackageService.Create(c.Request.Context(), &info) if err != nil { global.GVA_LOG.Error("创建失败!", zap.Error(err)) response.FailWithMessage("创建失败", c) return } response.OkWithMessage("创建成功", c) } // Delete // @Tags AutoCode // @Summary 删除package // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body common.GetById true "创建package" // @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "删除package成功" // @Router /autoCode/delPackage [post] func (a *AutoCodePackageApi) Delete(c *gin.Context) { var info common.GetById _ = c.ShouldBindJSON(&info) err := autoCodePackageService.Delete(c.Request.Context(), info) if err != nil { global.GVA_LOG.Error("删除失败!", zap.Error(err)) response.FailWithMessage("删除失败", c) return } response.OkWithMessage("删除成功", c) } // All // @Tags AutoCodePackage // @Summary 获取package // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "创建package成功" // @Router /autoCode/getPackage [post] func (a *AutoCodePackageApi) All(c *gin.Context) { data, err := autoCodePackageService.All(c.Request.Context()) if err != nil { global.GVA_LOG.Error("获取失败!", zap.Error(err)) response.FailWithMessage("获取失败", c) return } response.OkWithDetailed(gin.H{"pkgs": data}, "获取成功", c) } // Templates // @Tags AutoCodePackage // @Summary 获取package // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "创建package成功" // @Router /autoCode/getTemplates [get] func (a *AutoCodePackageApi) Templates(c *gin.Context) { data, err := autoCodePackageService.Templates(c.Request.Context()) if err != nil { global.GVA_LOG.Error("获取失败!", zap.Error(err)) response.FailWithMessage("获取失败", c) return } response.OkWithDetailed(data, "获取成功", c) } ================================================ FILE: server/api/v1/system/auto_code_plugin.go ================================================ package system import ( "fmt" "os" "path/filepath" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" "github.com/flipped-aurora/gin-vue-admin/server/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) } ================================================ FILE: server/api/v1/system/auto_code_template.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/gin-gonic/gin" "go.uber.org/zap" ) type AutoCodeTemplateApi struct{} // Preview // @Tags AutoCodeTemplate // @Summary 预览创建后的代码 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body request.AutoCode true "预览创建代码" // @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "预览创建后的代码" // @Router /autoCode/preview [post] func (a *AutoCodeTemplateApi) Preview(c *gin.Context) { var info request.AutoCode err := c.ShouldBindJSON(&info) if err != nil { response.FailWithMessage(err.Error(), c) return } err = utils.Verify(info, utils.AutoCodeVerify) if err != nil { response.FailWithMessage(err.Error(), c) return } err = info.Pretreatment() if err != nil { response.FailWithMessage(err.Error(), c) return } info.PackageT = utils.FirstUpper(info.Package) autoCode, err := autoCodeTemplateService.Preview(c.Request.Context(), info) if err != nil { global.GVA_LOG.Error(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) } } ================================================ FILE: server/api/v1/system/enter.go ================================================ package system import "github.com/flipped-aurora/gin-vue-admin/server/service" type ApiGroup struct { DBApi JwtApi BaseApi SystemApi CasbinApi AutoCodeApi SystemApiApi AuthorityApi DictionaryApi AuthorityMenuApi OperationRecordApi DictionaryDetailApi AuthorityBtnApi SysExportTemplateApi AutoCodePluginApi AutoCodePackageApi AutoCodeHistoryApi AutoCodeTemplateApi SysParamsApi 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 ) ================================================ FILE: server/api/v1/system/sys_api.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" "github.com/flipped-aurora/gin-vue-admin/server/model/system" systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/gin-gonic/gin" "go.uber.org/zap" ) type SystemApiApi struct{} // CreateApi // @Tags SysApi // @Summary 创建基础api // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body system.SysApi true "api路径, api中文描述, api组, 方法" // @Success 200 {object} response.Response{msg=string} "创建基础api" // @Router /api/createApi [post] func (s *SystemApiApi) CreateApi(c *gin.Context) { var api system.SysApi err := c.ShouldBindJSON(&api) if err != nil { response.FailWithMessage(err.Error(), c) return } err = utils.Verify(api, utils.ApiVerify) if err != nil { response.FailWithMessage(err.Error(), c) return } err = apiService.CreateApi(api) if err != nil { global.GVA_LOG.Error("创建失败!", zap.Error(err)) response.FailWithMessage("创建失败", c) return } response.OkWithMessage("创建成功", c) } // SyncApi // @Tags SysApi // @Summary 同步API // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Success 200 {object} response.Response{msg=string} "同步API" // @Router /api/syncApi [get] func (s *SystemApiApi) SyncApi(c *gin.Context) { newApis, deleteApis, ignoreApis, err := apiService.SyncApi() if err != nil { global.GVA_LOG.Error("同步失败!", zap.Error(err)) response.FailWithMessage("同步失败", c) return } response.OkWithData(gin.H{ "newApis": newApis, "deleteApis": deleteApis, "ignoreApis": ignoreApis, }, c) } // GetApiGroups // @Tags SysApi // @Summary 获取API分组 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Success 200 {object} response.Response{msg=string} "获取API分组" // @Router /api/getApiGroups [get] func (s *SystemApiApi) GetApiGroups(c *gin.Context) { groups, apiGroupMap, err := apiService.GetApiGroups() if err != nil { global.GVA_LOG.Error("获取失败!", zap.Error(err)) response.FailWithMessage("获取失败", c) return } response.OkWithData(gin.H{ "groups": groups, "apiGroupMap": apiGroupMap, }, c) } // IgnoreApi // @Tags IgnoreApi // @Summary 忽略API // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Success 200 {object} response.Response{msg=string} "同步API" // @Router /api/ignoreApi [post] func (s *SystemApiApi) IgnoreApi(c *gin.Context) { var ignoreApi system.SysIgnoreApi err := c.ShouldBindJSON(&ignoreApi) if err != nil { response.FailWithMessage(err.Error(), c) return } err = apiService.IgnoreApi(ignoreApi) if err != nil { global.GVA_LOG.Error("忽略失败!", zap.Error(err)) response.FailWithMessage("忽略失败", c) return } response.Ok(c) } // EnterSyncApi // @Tags SysApi // @Summary 确认同步API // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Success 200 {object} response.Response{msg=string} "确认同步API" // @Router /api/enterSyncApi [post] func (s *SystemApiApi) EnterSyncApi(c *gin.Context) { var syncApi systemRes.SysSyncApis err := c.ShouldBindJSON(&syncApi) if err != nil { response.FailWithMessage(err.Error(), c) return } err = apiService.EnterSyncApi(syncApi) if err != nil { global.GVA_LOG.Error("忽略失败!", zap.Error(err)) response.FailWithMessage("忽略失败", c) return } response.Ok(c) } // DeleteApi // @Tags SysApi // @Summary 删除api // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body system.SysApi true "ID" // @Success 200 {object} response.Response{msg=string} "删除api" // @Router /api/deleteApi [post] func (s *SystemApiApi) DeleteApi(c *gin.Context) { var api system.SysApi err := c.ShouldBindJSON(&api) if err != nil { response.FailWithMessage(err.Error(), c) return } err = utils.Verify(api.GVA_MODEL, utils.IdVerify) if err != nil { response.FailWithMessage(err.Error(), c) return } err = apiService.DeleteApi(api) if err != nil { global.GVA_LOG.Error("删除失败!", zap.Error(err)) response.FailWithMessage("删除失败", c) return } response.OkWithMessage("删除成功", c) } // GetApiList // @Tags SysApi // @Summary 分页获取API列表 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body systemReq.SearchApiParams true "分页获取API列表" // @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页获取API列表,返回包括列表,总数,页码,每页数量" // @Router /api/getApiList [post] func (s *SystemApiApi) GetApiList(c *gin.Context) { var pageInfo systemReq.SearchApiParams err := c.ShouldBindJSON(&pageInfo) if err != nil { response.FailWithMessage(err.Error(), c) return } err = utils.Verify(pageInfo.PageInfo, utils.PageInfoVerify) if err != nil { response.FailWithMessage(err.Error(), c) return } list, total, err := apiService.GetAPIInfoList(pageInfo.SysApi, pageInfo.PageInfo, pageInfo.OrderKey, pageInfo.Desc) if err != nil { global.GVA_LOG.Error("获取失败!", zap.Error(err)) response.FailWithMessage("获取失败", c) return } response.OkWithDetailed(response.PageResult{ List: list, Total: total, Page: pageInfo.Page, PageSize: pageInfo.PageSize, }, "获取成功", c) } // GetApiById // @Tags SysApi // @Summary 根据id获取api // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body request.GetById true "根据id获取api" // @Success 200 {object} response.Response{data=systemRes.SysAPIResponse} "根据id获取api,返回包括api详情" // @Router /api/getApiById [post] func (s *SystemApiApi) GetApiById(c *gin.Context) { var idInfo request.GetById err := c.ShouldBindJSON(&idInfo) if err != nil { response.FailWithMessage(err.Error(), c) return } err = utils.Verify(idInfo, utils.IdVerify) if err != nil { response.FailWithMessage(err.Error(), c) return } api, err := apiService.GetApiById(idInfo.ID) if err != nil { global.GVA_LOG.Error("获取失败!", zap.Error(err)) response.FailWithMessage("获取失败", c) return } response.OkWithDetailed(systemRes.SysAPIResponse{Api: api}, "获取成功", c) } // UpdateApi // @Tags SysApi // @Summary 修改基础api // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body system.SysApi true "api路径, api中文描述, api组, 方法" // @Success 200 {object} response.Response{msg=string} "修改基础api" // @Router /api/updateApi [post] func (s *SystemApiApi) UpdateApi(c *gin.Context) { var api system.SysApi err := c.ShouldBindJSON(&api) if err != nil { response.FailWithMessage(err.Error(), c) return } err = utils.Verify(api, utils.ApiVerify) if err != nil { response.FailWithMessage(err.Error(), c) return } err = apiService.UpdateApi(api) if err != nil { global.GVA_LOG.Error("修改失败!", zap.Error(err)) response.FailWithMessage("修改失败", c) return } response.OkWithMessage("修改成功", c) } // GetAllApis // @Tags SysApi // @Summary 获取所有的Api 不分页 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Success 200 {object} response.Response{data=systemRes.SysAPIListResponse,msg=string} "获取所有的Api 不分页,返回包括api列表" // @Router /api/getAllApis [post] func (s *SystemApiApi) GetAllApis(c *gin.Context) { authorityID := utils.GetUserAuthorityId(c) apis, err := apiService.GetAllApis(authorityID) if err != nil { global.GVA_LOG.Error("获取失败!", zap.Error(err)) response.FailWithMessage("获取失败", c) return } response.OkWithDetailed(systemRes.SysAPIListResponse{Apis: apis}, "获取成功", c) } // DeleteApisByIds // @Tags SysApi // @Summary 删除选中Api // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body request.IdsReq true "ID" // @Success 200 {object} response.Response{msg=string} "删除选中Api" // @Router /api/deleteApisByIds [delete] func (s *SystemApiApi) DeleteApisByIds(c *gin.Context) { var ids request.IdsReq err := c.ShouldBindJSON(&ids) if err != nil { response.FailWithMessage(err.Error(), c) return } err = apiService.DeleteApisByIds(ids) if err != nil { global.GVA_LOG.Error("删除失败!", zap.Error(err)) response.FailWithMessage("删除失败", c) return } response.OkWithMessage("删除成功", c) } // FreshCasbin // @Tags SysApi // @Summary 刷新casbin缓存 // @accept application/json // @Produce application/json // @Success 200 {object} response.Response{msg=string} "刷新成功" // @Router /api/freshCasbin [get] func (s *SystemApiApi) FreshCasbin(c *gin.Context) { err := casbinService.FreshCasbin() if err != nil { global.GVA_LOG.Error("刷新失败!", zap.Error(err)) response.FailWithMessage("刷新失败", c) return } response.OkWithMessage("刷新成功", c) } // GetApiRoles // @Tags SysApi // @Summary 获取拥有指定API权限的角色ID列表 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param path query string true "API路径" // @Param method query string true "请求方法" // @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "获取成功" // @Router /api/getApiRoles [get] func (s *SystemApiApi) GetApiRoles(c *gin.Context) { path := c.Query("path") method := c.Query("method") if path == "" || method == "" { response.FailWithMessage("API路径和请求方法不能为空", c) return } authorityIds, err := casbinService.GetAuthoritiesByApi(path, method) if err != nil { global.GVA_LOG.Error("获取失败!", zap.Error(err)) response.FailWithMessage("获取失败"+err.Error(), c) return } if authorityIds == nil { authorityIds = []uint{} } response.OkWithDetailed(authorityIds, "获取成功", c) } // SetApiRoles // @Tags SysApi // @Summary 全量覆盖某API关联的角色列表 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body systemReq.SetApiAuthorities true "API路径、请求方法和角色ID列表" // @Success 200 {object} response.Response{msg=string} "设置成功" // @Router /api/setApiRoles [post] func (s *SystemApiApi) SetApiRoles(c *gin.Context) { var req systemReq.SetApiAuthorities if err := c.ShouldBindJSON(&req); err != nil { response.FailWithMessage(err.Error(), c) return } if req.Path == "" || req.Method == "" { response.FailWithMessage("API路径和请求方法不能为空", c) return } if err := casbinService.SetApiAuthorities(req.Path, req.Method, req.AuthorityIds); err != nil { global.GVA_LOG.Error("设置失败!", zap.Error(err)) response.FailWithMessage("设置失败"+err.Error(), c) return } // 刷新casbin缓存使策略立即生效 _ = casbinService.FreshCasbin() response.OkWithMessage("设置成功", c) } ================================================ FILE: server/api/v1/system/sys_api_token.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" "github.com/flipped-aurora/gin-vue-admin/server/model/system" sysReq "github.com/flipped-aurora/gin-vue-admin/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) } ================================================ FILE: server/api/v1/system/sys_authority.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" "github.com/flipped-aurora/gin-vue-admin/server/model/system" systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/gin-gonic/gin" "go.uber.org/zap" ) type AuthorityApi struct{} // CreateAuthority // @Tags Authority // @Summary 创建角色 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body system.SysAuthority true "权限id, 权限名, 父角色id" // @Success 200 {object} response.Response{data=systemRes.SysAuthorityResponse,msg=string} "创建角色,返回包括系统角色详情" // @Router /authority/createAuthority [post] func (a *AuthorityApi) CreateAuthority(c *gin.Context) { var authority, authBack system.SysAuthority var err error if err = c.ShouldBindJSON(&authority); err != nil { response.FailWithMessage(err.Error(), c) return } if err = utils.Verify(authority, utils.AuthorityVerify); err != nil { response.FailWithMessage(err.Error(), c) return } if *authority.ParentId == 0 && global.GVA_CONFIG.System.UseStrictAuth { authority.ParentId = utils.Pointer(utils.GetUserAuthorityId(c)) } if authBack, err = authorityService.CreateAuthority(authority); err != nil { global.GVA_LOG.Error("创建失败!", zap.Error(err)) response.FailWithMessage("创建失败"+err.Error(), c) return } err = casbinService.FreshCasbin() if err != nil { global.GVA_LOG.Error("创建成功,权限刷新失败。", zap.Error(err)) response.FailWithMessage("创建成功,权限刷新失败。"+err.Error(), c) return } response.OkWithDetailed(systemRes.SysAuthorityResponse{Authority: authBack}, "创建成功", c) } // CopyAuthority // @Tags Authority // @Summary 拷贝角色 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body response.SysAuthorityCopyResponse true "旧角色id, 新权限id, 新权限名, 新父角色id" // @Success 200 {object} response.Response{data=systemRes.SysAuthorityResponse,msg=string} "拷贝角色,返回包括系统角色详情" // @Router /authority/copyAuthority [post] func (a *AuthorityApi) CopyAuthority(c *gin.Context) { var copyInfo systemRes.SysAuthorityCopyResponse err := c.ShouldBindJSON(©Info) if err != nil { response.FailWithMessage(err.Error(), c) return } err = utils.Verify(copyInfo, utils.OldAuthorityVerify) if err != nil { response.FailWithMessage(err.Error(), c) return } err = utils.Verify(copyInfo.Authority, utils.AuthorityVerify) if err != nil { response.FailWithMessage(err.Error(), c) return } adminAuthorityID := utils.GetUserAuthorityId(c) authBack, err := authorityService.CopyAuthority(adminAuthorityID, copyInfo) if err != nil { global.GVA_LOG.Error("拷贝失败!", zap.Error(err)) response.FailWithMessage("拷贝失败"+err.Error(), c) return } response.OkWithDetailed(systemRes.SysAuthorityResponse{Authority: authBack}, "拷贝成功", c) } // DeleteAuthority // @Tags Authority // @Summary 删除角色 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body system.SysAuthority true "删除角色" // @Success 200 {object} response.Response{msg=string} "删除角色" // @Router /authority/deleteAuthority [post] func (a *AuthorityApi) DeleteAuthority(c *gin.Context) { var authority system.SysAuthority var err error if err = c.ShouldBindJSON(&authority); err != nil { response.FailWithMessage(err.Error(), c) return } if err = utils.Verify(authority, utils.AuthorityIdVerify); err != nil { response.FailWithMessage(err.Error(), c) return } // 删除角色之前需要判断是否有用户正在使用此角色 if err = authorityService.DeleteAuthority(&authority); err != nil { global.GVA_LOG.Error("删除失败!", zap.Error(err)) response.FailWithMessage("删除失败"+err.Error(), c) return } _ = casbinService.FreshCasbin() response.OkWithMessage("删除成功", c) } // UpdateAuthority // @Tags Authority // @Summary 更新角色信息 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body system.SysAuthority true "权限id, 权限名, 父角色id" // @Success 200 {object} response.Response{data=systemRes.SysAuthorityResponse,msg=string} "更新角色信息,返回包括系统角色详情" // @Router /authority/updateAuthority [put] func (a *AuthorityApi) UpdateAuthority(c *gin.Context) { var auth system.SysAuthority err := c.ShouldBindJSON(&auth) if err != nil { response.FailWithMessage(err.Error(), c) return } err = utils.Verify(auth, utils.AuthorityVerify) if err != nil { response.FailWithMessage(err.Error(), c) return } authority, err := authorityService.UpdateAuthority(auth) if err != nil { global.GVA_LOG.Error("更新失败!", zap.Error(err)) response.FailWithMessage("更新失败"+err.Error(), c) return } response.OkWithDetailed(systemRes.SysAuthorityResponse{Authority: authority}, "更新成功", c) } // GetAuthorityList // @Tags Authority // @Summary 分页获取角色列表 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body request.PageInfo true "页码, 每页大小" // @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页获取角色列表,返回包括列表,总数,页码,每页数量" // @Router /authority/getAuthorityList [post] func (a *AuthorityApi) GetAuthorityList(c *gin.Context) { authorityID := utils.GetUserAuthorityId(c) list, err := authorityService.GetAuthorityInfoList(authorityID) if err != nil { global.GVA_LOG.Error("获取失败!", zap.Error(err)) response.FailWithMessage("获取失败"+err.Error(), c) return } response.OkWithDetailed(list, "获取成功", c) } // SetDataAuthority // @Tags Authority // @Summary 设置角色资源权限 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body system.SysAuthority true "设置角色资源权限" // @Success 200 {object} response.Response{msg=string} "设置角色资源权限" // @Router /authority/setDataAuthority [post] func (a *AuthorityApi) SetDataAuthority(c *gin.Context) { var auth system.SysAuthority err := c.ShouldBindJSON(&auth) if err != nil { response.FailWithMessage(err.Error(), c) return } err = utils.Verify(auth, utils.AuthorityIdVerify) if err != nil { response.FailWithMessage(err.Error(), c) return } adminAuthorityID := utils.GetUserAuthorityId(c) err = authorityService.SetDataAuthority(adminAuthorityID, auth) if err != nil { global.GVA_LOG.Error("设置失败!", zap.Error(err)) response.FailWithMessage("设置失败"+err.Error(), c) return } response.OkWithMessage("设置成功", c) } // GetUsersByAuthority // @Tags Authority // @Summary 获取拥有指定角色的用户ID列表 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param authorityId query uint true "角色ID" // @Success 200 {object} response.Response{data=[]uint,msg=string} "获取成功" // @Router /authority/getUsersByAuthority [get] func (a *AuthorityApi) GetUsersByAuthority(c *gin.Context) { var req systemReq.SetRoleUsers if err := c.ShouldBindQuery(&req); err != nil { response.FailWithMessage(err.Error(), c) return } userIds, err := authorityService.GetUserIdsByAuthorityId(req.AuthorityId) if err != nil { global.GVA_LOG.Error("获取失败!", zap.Error(err)) response.FailWithMessage("获取失败"+err.Error(), c) return } if userIds == nil { userIds = []uint{} } response.OkWithDetailed(userIds, "获取成功", c) } // SetRoleUsers // @Tags Authority // @Summary 全量覆盖某角色关联的用户列表 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body systemReq.SetRoleUsers true "角色ID和用户ID列表" // @Success 200 {object} response.Response{msg=string} "设置成功" // @Router /authority/setRoleUsers [post] func (a *AuthorityApi) SetRoleUsers(c *gin.Context) { var req systemReq.SetRoleUsers if err := c.ShouldBindJSON(&req); err != nil { response.FailWithMessage(err.Error(), c) return } if req.AuthorityId == 0 { response.FailWithMessage("角色ID不能为空", c) return } if err := authorityService.SetRoleUsers(req.AuthorityId, req.UserIds); err != nil { global.GVA_LOG.Error("设置失败!", zap.Error(err)) response.FailWithMessage("设置失败"+err.Error(), c) return } response.OkWithMessage("设置成功", c) } ================================================ FILE: server/api/v1/system/sys_authority_btn.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "github.com/gin-gonic/gin" "go.uber.org/zap" ) type AuthorityBtnApi struct{} // GetAuthorityBtn // @Tags AuthorityBtn // @Summary 获取权限按钮 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body request.SysAuthorityBtnReq true "菜单id, 角色id, 选中的按钮id" // @Success 200 {object} response.Response{data=response.SysAuthorityBtnRes,msg=string} "返回列表成功" // @Router /authorityBtn/getAuthorityBtn [post] func (a *AuthorityBtnApi) GetAuthorityBtn(c *gin.Context) { var req request.SysAuthorityBtnReq err := c.ShouldBindJSON(&req) if err != nil { response.FailWithMessage(err.Error(), c) return } res, err := authorityBtnService.GetAuthorityBtn(req) if err != nil { global.GVA_LOG.Error("查询失败!", zap.Error(err)) response.FailWithMessage("查询失败", c) return } response.OkWithDetailed(res, "查询成功", c) } // SetAuthorityBtn // @Tags AuthorityBtn // @Summary 设置权限按钮 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body request.SysAuthorityBtnReq true "菜单id, 角色id, 选中的按钮id" // @Success 200 {object} response.Response{msg=string} "返回列表成功" // @Router /authorityBtn/setAuthorityBtn [post] func (a *AuthorityBtnApi) SetAuthorityBtn(c *gin.Context) { var req request.SysAuthorityBtnReq err := c.ShouldBindJSON(&req) if err != nil { response.FailWithMessage(err.Error(), c) return } err = authorityBtnService.SetAuthorityBtn(req) if err != nil { global.GVA_LOG.Error("分配失败!", zap.Error(err)) response.FailWithMessage("分配失败", c) return } response.OkWithMessage("分配成功", c) } // CanRemoveAuthorityBtn // @Tags AuthorityBtn // @Summary 设置权限按钮 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Success 200 {object} response.Response{msg=string} "删除成功" // @Router /authorityBtn/canRemoveAuthorityBtn [post] func (a *AuthorityBtnApi) CanRemoveAuthorityBtn(c *gin.Context) { id := c.Query("id") err := authorityBtnService.CanRemoveAuthorityBtn(id) if err != nil { global.GVA_LOG.Error("删除失败!", zap.Error(err)) response.FailWithMessage(err.Error(), c) return } response.OkWithMessage("删除成功", c) } ================================================ FILE: server/api/v1/system/sys_auto_code.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/model/common" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" "github.com/gin-gonic/gin" "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) } ================================================ FILE: server/api/v1/system/sys_captcha.go ================================================ package system import ( "time" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" "github.com/gin-gonic/gin" "github.com/mojocn/base64Captcha" "go.uber.org/zap" ) // 当开启多服务器部署时,替换下面的配置,使用redis共享存储验证码 // var store = captcha.NewDefaultRedisStore() var store = base64Captcha.DefaultMemStore type BaseApi struct{} // Captcha // @Tags Base // @Summary 生成验证码 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Success 200 {object} response.Response{data=systemRes.SysCaptchaResponse,msg=string} "生成验证码,返回包括随机数id,base64,验证码长度,是否开启验证码" // @Router /base/captcha [post] func (b *BaseApi) Captcha(c *gin.Context) { // 判断验证码是否开启 openCaptcha := global.GVA_CONFIG.Captcha.OpenCaptcha // 是否开启防爆次数 openCaptchaTimeOut := global.GVA_CONFIG.Captcha.OpenCaptchaTimeOut // 缓存超时时间 key := c.ClientIP() v, ok := global.BlackCache.Get(key) if !ok { global.BlackCache.Set(key, 1, time.Second*time.Duration(openCaptchaTimeOut)) } var oc bool if openCaptcha == 0 || openCaptcha < interfaceToInt(v) { oc = true } // 字符,公式,验证码配置 // 生成默认数字的driver driver := base64Captcha.NewDriverDigit(global.GVA_CONFIG.Captcha.ImgHeight, global.GVA_CONFIG.Captcha.ImgWidth, global.GVA_CONFIG.Captcha.KeyLong, 0.7, 80) // cp := base64Captcha.NewCaptcha(driver, store.UseWithCtx(c)) // v8下使用redis cp := base64Captcha.NewCaptcha(driver, store) id, b64s, _, err := cp.Generate() if err != nil { global.GVA_LOG.Error("验证码获取失败!", zap.Error(err)) response.FailWithMessage("验证码获取失败", c) return } response.OkWithDetailed(systemRes.SysCaptchaResponse{ CaptchaId: id, PicPath: b64s, CaptchaLength: global.GVA_CONFIG.Captcha.KeyLong, OpenCaptcha: oc, }, "验证码获取成功", c) } // 类型转换 func interfaceToInt(v interface{}) (i int) { switch v := v.(type) { case int: i = v default: i = 0 } return } ================================================ FILE: server/api/v1/system/sys_casbin.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/gin-gonic/gin" "go.uber.org/zap" ) type CasbinApi struct{} // UpdateCasbin // @Tags Casbin // @Summary 更新角色api权限 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body request.CasbinInReceive true "权限id, 权限模型列表" // @Success 200 {object} response.Response{msg=string} "更新角色api权限" // @Router /casbin/UpdateCasbin [post] func (cas *CasbinApi) UpdateCasbin(c *gin.Context) { var cmr request.CasbinInReceive err := c.ShouldBindJSON(&cmr) if err != nil { response.FailWithMessage(err.Error(), c) return } err = utils.Verify(cmr, utils.AuthorityIdVerify) if err != nil { response.FailWithMessage(err.Error(), c) return } adminAuthorityID := utils.GetUserAuthorityId(c) err = casbinService.UpdateCasbin(adminAuthorityID, cmr.AuthorityId, cmr.CasbinInfos) if err != nil { global.GVA_LOG.Error("更新失败!", zap.Error(err)) response.FailWithMessage("更新失败", c) return } response.OkWithMessage("更新成功", c) } // GetPolicyPathByAuthorityId // @Tags Casbin // @Summary 获取权限列表 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body request.CasbinInReceive true "权限id, 权限模型列表" // @Success 200 {object} response.Response{data=systemRes.PolicyPathResponse,msg=string} "获取权限列表,返回包括casbin详情列表" // @Router /casbin/getPolicyPathByAuthorityId [post] func (cas *CasbinApi) GetPolicyPathByAuthorityId(c *gin.Context) { var casbin request.CasbinInReceive err := c.ShouldBindJSON(&casbin) if err != nil { response.FailWithMessage(err.Error(), c) return } err = utils.Verify(casbin, utils.AuthorityIdVerify) if err != nil { response.FailWithMessage(err.Error(), c) return } paths := casbinService.GetPolicyPathByAuthorityId(casbin.AuthorityId) response.OkWithDetailed(systemRes.PolicyPathResponse{Paths: paths}, "获取成功", c) } ================================================ FILE: server/api/v1/system/sys_dictionary.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "github.com/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) } ================================================ FILE: server/api/v1/system/sys_dictionary_detail.go ================================================ package system import ( "strconv" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/gin-gonic/gin" "go.uber.org/zap" ) type DictionaryDetailApi struct{} // CreateSysDictionaryDetail // @Tags SysDictionaryDetail // @Summary 创建SysDictionaryDetail // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body system.SysDictionaryDetail true "SysDictionaryDetail模型" // @Success 200 {object} response.Response{msg=string} "创建SysDictionaryDetail" // @Router /sysDictionaryDetail/createSysDictionaryDetail [post] func (s *DictionaryDetailApi) CreateSysDictionaryDetail(c *gin.Context) { var detail system.SysDictionaryDetail err := c.ShouldBindJSON(&detail) if err != nil { response.FailWithMessage(err.Error(), c) return } err = dictionaryDetailService.CreateSysDictionaryDetail(detail) if err != nil { global.GVA_LOG.Error("创建失败!", zap.Error(err)) response.FailWithMessage("创建失败", c) return } response.OkWithMessage("创建成功", c) } // DeleteSysDictionaryDetail // @Tags SysDictionaryDetail // @Summary 删除SysDictionaryDetail // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body system.SysDictionaryDetail true "SysDictionaryDetail模型" // @Success 200 {object} response.Response{msg=string} "删除SysDictionaryDetail" // @Router /sysDictionaryDetail/deleteSysDictionaryDetail [delete] func (s *DictionaryDetailApi) DeleteSysDictionaryDetail(c *gin.Context) { var detail system.SysDictionaryDetail err := c.ShouldBindJSON(&detail) if err != nil { response.FailWithMessage(err.Error(), c) return } err = dictionaryDetailService.DeleteSysDictionaryDetail(detail) if err != nil { global.GVA_LOG.Error("删除失败!", zap.Error(err)) response.FailWithMessage("删除失败", c) return } response.OkWithMessage("删除成功", c) } // UpdateSysDictionaryDetail // @Tags SysDictionaryDetail // @Summary 更新SysDictionaryDetail // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body system.SysDictionaryDetail true "更新SysDictionaryDetail" // @Success 200 {object} response.Response{msg=string} "更新SysDictionaryDetail" // @Router /sysDictionaryDetail/updateSysDictionaryDetail [put] func (s *DictionaryDetailApi) UpdateSysDictionaryDetail(c *gin.Context) { var detail system.SysDictionaryDetail err := c.ShouldBindJSON(&detail) if err != nil { response.FailWithMessage(err.Error(), c) return } err = dictionaryDetailService.UpdateSysDictionaryDetail(&detail) if err != nil { global.GVA_LOG.Error("更新失败!", zap.Error(err)) response.FailWithMessage("更新失败", c) return } response.OkWithMessage("更新成功", c) } // FindSysDictionaryDetail // @Tags SysDictionaryDetail // @Summary 用id查询SysDictionaryDetail // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data query system.SysDictionaryDetail true "用id查询SysDictionaryDetail" // @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "用id查询SysDictionaryDetail" // @Router /sysDictionaryDetail/findSysDictionaryDetail [get] func (s *DictionaryDetailApi) FindSysDictionaryDetail(c *gin.Context) { var detail system.SysDictionaryDetail err := c.ShouldBindQuery(&detail) if err != nil { response.FailWithMessage(err.Error(), c) return } err = utils.Verify(detail, utils.IdVerify) if err != nil { response.FailWithMessage(err.Error(), c) return } reSysDictionaryDetail, err := dictionaryDetailService.GetSysDictionaryDetail(detail.ID) if err != nil { global.GVA_LOG.Error("查询失败!", zap.Error(err)) response.FailWithMessage("查询失败", c) return } response.OkWithDetailed(gin.H{"reSysDictionaryDetail": reSysDictionaryDetail}, "查询成功", c) } // GetSysDictionaryDetailList // @Tags SysDictionaryDetail // @Summary 分页获取SysDictionaryDetail列表 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data query request.SysDictionaryDetailSearch true "页码, 每页大小, 搜索条件" // @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页获取SysDictionaryDetail列表,返回包括列表,总数,页码,每页数量" // @Router /sysDictionaryDetail/getSysDictionaryDetailList [get] func (s *DictionaryDetailApi) GetSysDictionaryDetailList(c *gin.Context) { var pageInfo request.SysDictionaryDetailSearch err := c.ShouldBindQuery(&pageInfo) if err != nil { response.FailWithMessage(err.Error(), c) return } list, total, err := dictionaryDetailService.GetSysDictionaryDetailInfoList(pageInfo) if err != nil { global.GVA_LOG.Error("获取失败!", zap.Error(err)) response.FailWithMessage("获取失败", c) return } response.OkWithDetailed(response.PageResult{ List: list, Total: total, Page: pageInfo.Page, PageSize: pageInfo.PageSize, }, "获取成功", c) } // 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) } ================================================ FILE: server/api/v1/system/sys_error.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" "github.com/flipped-aurora/gin-vue-admin/server/model/system" systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "github.com/gin-gonic/gin" "go.uber.org/zap" ) type 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) } ================================================ FILE: server/api/v1/system/sys_export_template.go ================================================ package system import ( "fmt" "net/http" "net/url" "sync" "time" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" "github.com/flipped-aurora/gin-vue-admin/server/model/system" systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "github.com/flipped-aurora/gin-vue-admin/server/service" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/gin-gonic/gin" "go.uber.org/zap" ) // 用于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) } } ================================================ FILE: server/api/v1/system/sys_initdb.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "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) } ================================================ FILE: server/api/v1/system/sys_jwt_blacklist.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/gin-gonic/gin" "go.uber.org/zap" ) type JwtApi struct{} // JsonInBlacklist // @Tags Jwt // @Summary jwt加入黑名单 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Success 200 {object} response.Response{msg=string} "jwt加入黑名单" // @Router /jwt/jsonInBlacklist [post] func (j *JwtApi) JsonInBlacklist(c *gin.Context) { token := utils.GetToken(c) jwt := system.JwtBlacklist{Jwt: token} err := jwtService.JsonInBlacklist(jwt) if err != nil { global.GVA_LOG.Error("jwt作废失败!", zap.Error(err)) response.FailWithMessage("jwt作废失败", c) return } utils.ClearToken(c) response.OkWithMessage("jwt作废成功", c) } ================================================ FILE: server/api/v1/system/sys_login_log.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" "github.com/flipped-aurora/gin-vue-admin/server/model/system" systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "github.com/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) } ================================================ FILE: server/api/v1/system/sys_menu.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" "github.com/flipped-aurora/gin-vue-admin/server/model/system" systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/gin-gonic/gin" "go.uber.org/zap" ) type AuthorityMenuApi struct{} // GetMenu // @Tags AuthorityMenu // @Summary 获取用户动态路由 // @Security ApiKeyAuth // @Produce application/json // @Param data body request.Empty true "空" // @Success 200 {object} response.Response{data=systemRes.SysMenusResponse,msg=string} "获取用户动态路由,返回包括系统菜单详情列表" // @Router /menu/getMenu [post] func (a *AuthorityMenuApi) GetMenu(c *gin.Context) { menus, err := menuService.GetMenuTree(utils.GetUserAuthorityId(c)) if err != nil { global.GVA_LOG.Error("获取失败!", zap.Error(err)) response.FailWithMessage("获取失败", c) return } if menus == nil { menus = []system.SysMenu{} } response.OkWithDetailed(systemRes.SysMenusResponse{Menus: menus}, "获取成功", c) } // GetBaseMenuTree // @Tags AuthorityMenu // @Summary 获取用户动态路由 // @Security ApiKeyAuth // @Produce application/json // @Param data body request.Empty true "空" // @Success 200 {object} response.Response{data=systemRes.SysBaseMenusResponse,msg=string} "获取用户动态路由,返回包括系统菜单列表" // @Router /menu/getBaseMenuTree [post] func (a *AuthorityMenuApi) GetBaseMenuTree(c *gin.Context) { authority := utils.GetUserAuthorityId(c) menus, err := menuService.GetBaseMenuTree(authority) if err != nil { global.GVA_LOG.Error("获取失败!", zap.Error(err)) response.FailWithMessage("获取失败", c) return } response.OkWithDetailed(systemRes.SysBaseMenusResponse{Menus: menus}, "获取成功", c) } // AddMenuAuthority // @Tags AuthorityMenu // @Summary 增加menu和角色关联关系 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body systemReq.AddMenuAuthorityInfo true "角色ID" // @Success 200 {object} response.Response{msg=string} "增加menu和角色关联关系" // @Router /menu/addMenuAuthority [post] func (a *AuthorityMenuApi) AddMenuAuthority(c *gin.Context) { var authorityMenu systemReq.AddMenuAuthorityInfo err := c.ShouldBindJSON(&authorityMenu) if err != nil { response.FailWithMessage(err.Error(), c) return } if err := utils.Verify(authorityMenu, utils.AuthorityIdVerify); err != nil { response.FailWithMessage(err.Error(), c) return } adminAuthorityID := utils.GetUserAuthorityId(c) if err := menuService.AddMenuAuthority(authorityMenu.Menus, adminAuthorityID, authorityMenu.AuthorityId); err != nil { global.GVA_LOG.Error("添加失败!", zap.Error(err)) response.FailWithMessage("添加失败", c) } else { response.OkWithMessage("添加成功", c) } } // GetMenuAuthority // @Tags AuthorityMenu // @Summary 获取指定角色menu // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body request.GetAuthorityId true "角色ID" // @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "获取指定角色menu" // @Router /menu/getMenuAuthority [post] func (a *AuthorityMenuApi) GetMenuAuthority(c *gin.Context) { var param request.GetAuthorityId err := c.ShouldBindJSON(¶m) if err != nil { response.FailWithMessage(err.Error(), c) return } err = utils.Verify(param, utils.AuthorityIdVerify) if err != nil { response.FailWithMessage(err.Error(), c) return } menus, err := menuService.GetMenuAuthority(¶m) if err != nil { global.GVA_LOG.Error("获取失败!", zap.Error(err)) response.FailWithDetailed(systemRes.SysMenusResponse{Menus: menus}, "获取失败", c) return } response.OkWithDetailed(gin.H{"menus": menus}, "获取成功", c) } // AddBaseMenu // @Tags Menu // @Summary 新增菜单 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body system.SysBaseMenu true "路由path, 父菜单ID, 路由name, 对应前端文件路径, 排序标记" // @Success 200 {object} response.Response{msg=string} "新增菜单" // @Router /menu/addBaseMenu [post] func (a *AuthorityMenuApi) AddBaseMenu(c *gin.Context) { var menu system.SysBaseMenu err := c.ShouldBindJSON(&menu) if err != nil { response.FailWithMessage(err.Error(), c) return } err = utils.Verify(menu, utils.MenuVerify) if err != nil { response.FailWithMessage(err.Error(), c) return } err = utils.Verify(menu.Meta, utils.MenuMetaVerify) if err != nil { response.FailWithMessage(err.Error(), c) return } err = menuService.AddBaseMenu(menu) if err != nil { global.GVA_LOG.Error("添加失败!", zap.Error(err)) response.FailWithMessage("添加失败:"+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) } // GetMenuRoles // @Tags AuthorityMenu // @Summary 获取拥有指定菜单的角色ID列表 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param menuId query uint true "菜单ID" // @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "获取成功" // @Router /menu/getMenuRoles [get] func (a *AuthorityMenuApi) GetMenuRoles(c *gin.Context) { var req systemReq.SetMenuAuthorities if err := c.ShouldBindQuery(&req); err != nil { response.FailWithMessage(err.Error(), c) return } if req.MenuId == 0 { response.FailWithMessage("菜单ID不能为空", c) return } authorityIds, err := menuService.GetAuthoritiesByMenuId(req.MenuId) if err != nil { global.GVA_LOG.Error("获取失败!", zap.Error(err)) response.FailWithMessage("获取失败"+err.Error(), c) return } if authorityIds == nil { authorityIds = []uint{} } defaultRouterAuthorityIds, err := menuService.GetDefaultRouterAuthorityIds(req.MenuId) if err != nil { global.GVA_LOG.Error("获取首页角色失败!", zap.Error(err)) response.FailWithMessage("获取失败"+err.Error(), c) return } if defaultRouterAuthorityIds == nil { defaultRouterAuthorityIds = []uint{} } response.OkWithDetailed(gin.H{ "authorityIds": authorityIds, "defaultRouterAuthorityIds": defaultRouterAuthorityIds, }, "获取成功", c) } // SetMenuRoles // @Tags AuthorityMenu // @Summary 全量覆盖某菜单关联的角色列表 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body systemReq.SetMenuAuthorities true "菜单ID和角色ID列表" // @Success 200 {object} response.Response{msg=string} "设置成功" // @Router /menu/setMenuRoles [post] func (a *AuthorityMenuApi) SetMenuRoles(c *gin.Context) { var req systemReq.SetMenuAuthorities if err := c.ShouldBindJSON(&req); err != nil { response.FailWithMessage(err.Error(), c) return } if req.MenuId == 0 { response.FailWithMessage("菜单ID不能为空", c) return } if err := menuService.SetMenuAuthorities(req.MenuId, req.AuthorityIds); err != nil { global.GVA_LOG.Error("设置失败!", zap.Error(err)) response.FailWithMessage("设置失败"+err.Error(), c) return } response.OkWithMessage("设置成功", 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) } ================================================ FILE: server/api/v1/system/sys_operation_record.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" "github.com/flipped-aurora/gin-vue-admin/server/model/system" systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/gin-gonic/gin" "go.uber.org/zap" ) type OperationRecordApi struct{} // 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) } ================================================ FILE: server/api/v1/system/sys_params.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" "github.com/flipped-aurora/gin-vue-admin/server/model/system" systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "github.com/gin-gonic/gin" "go.uber.org/zap" ) type SysParamsApi struct{} // CreateSysParams 创建参数 // @Tags SysParams // @Summary 创建参数 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body system.SysParams true "创建参数" // @Success 200 {object} response.Response{msg=string} "创建成功" // @Router /sysParams/createSysParams [post] func (sysParamsApi *SysParamsApi) CreateSysParams(c *gin.Context) { var sysParams system.SysParams err := c.ShouldBindJSON(&sysParams) if err != nil { response.FailWithMessage(err.Error(), c) return } err = sysParamsService.CreateSysParams(&sysParams) if err != nil { global.GVA_LOG.Error("创建失败!", zap.Error(err)) response.FailWithMessage("创建失败:"+err.Error(), c) return } response.OkWithMessage("创建成功", c) } // DeleteSysParams 删除参数 // @Tags SysParams // @Summary 删除参数 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body system.SysParams true "删除参数" // @Success 200 {object} response.Response{msg=string} "删除成功" // @Router /sysParams/deleteSysParams [delete] func (sysParamsApi *SysParamsApi) DeleteSysParams(c *gin.Context) { ID := c.Query("ID") err := sysParamsService.DeleteSysParams(ID) if err != nil { global.GVA_LOG.Error("删除失败!", zap.Error(err)) response.FailWithMessage("删除失败:"+err.Error(), c) return } response.OkWithMessage("删除成功", c) } // DeleteSysParamsByIds 批量删除参数 // @Tags SysParams // @Summary 批量删除参数 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Success 200 {object} response.Response{msg=string} "批量删除成功" // @Router /sysParams/deleteSysParamsByIds [delete] func (sysParamsApi *SysParamsApi) DeleteSysParamsByIds(c *gin.Context) { IDs := c.QueryArray("IDs[]") err := sysParamsService.DeleteSysParamsByIds(IDs) if err != nil { global.GVA_LOG.Error("批量删除失败!", zap.Error(err)) response.FailWithMessage("批量删除失败:"+err.Error(), c) return } response.OkWithMessage("批量删除成功", c) } // UpdateSysParams 更新参数 // @Tags SysParams // @Summary 更新参数 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body system.SysParams true "更新参数" // @Success 200 {object} response.Response{msg=string} "更新成功" // @Router /sysParams/updateSysParams [put] func (sysParamsApi *SysParamsApi) UpdateSysParams(c *gin.Context) { var sysParams system.SysParams err := c.ShouldBindJSON(&sysParams) if err != nil { response.FailWithMessage(err.Error(), c) return } err = sysParamsService.UpdateSysParams(sysParams) if err != nil { global.GVA_LOG.Error("更新失败!", zap.Error(err)) response.FailWithMessage("更新失败:"+err.Error(), c) return } response.OkWithMessage("更新成功", c) } // FindSysParams 用id查询参数 // @Tags SysParams // @Summary 用id查询参数 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data query system.SysParams true "用id查询参数" // @Success 200 {object} response.Response{data=system.SysParams,msg=string} "查询成功" // @Router /sysParams/findSysParams [get] func (sysParamsApi *SysParamsApi) FindSysParams(c *gin.Context) { ID := c.Query("ID") resysParams, err := sysParamsService.GetSysParams(ID) if err != nil { global.GVA_LOG.Error("查询失败!", zap.Error(err)) response.FailWithMessage("查询失败:"+err.Error(), c) return } response.OkWithData(resysParams, c) } // GetSysParamsList 分页获取参数列表 // @Tags SysParams // @Summary 分页获取参数列表 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data query systemReq.SysParamsSearch true "分页获取参数列表" // @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" // @Router /sysParams/getSysParamsList [get] func (sysParamsApi *SysParamsApi) GetSysParamsList(c *gin.Context) { var pageInfo systemReq.SysParamsSearch err := c.ShouldBindQuery(&pageInfo) if err != nil { response.FailWithMessage(err.Error(), c) return } list, total, err := sysParamsService.GetSysParamsInfoList(pageInfo) if err != nil { global.GVA_LOG.Error("获取失败!", zap.Error(err)) response.FailWithMessage("获取失败:"+err.Error(), c) return } response.OkWithDetailed(response.PageResult{ List: list, Total: total, Page: pageInfo.Page, PageSize: pageInfo.PageSize, }, "获取成功", c) } // GetSysParam 根据key获取参数value // @Tags SysParams // @Summary 根据key获取参数value // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param key query string true "key" // @Success 200 {object} response.Response{data=system.SysParams,msg=string} "获取成功" // @Router /sysParams/getSysParam [get] func (sysParamsApi *SysParamsApi) GetSysParam(c *gin.Context) { k := c.Query("key") params, err := sysParamsService.GetSysParam(k) if err != nil { global.GVA_LOG.Error("获取失败!", zap.Error(err)) response.FailWithMessage("获取失败:"+err.Error(), c) return } response.OkWithDetailed(params, "获取成功", c) } ================================================ FILE: server/api/v1/system/sys_skills.go ================================================ package system import ( "net/http" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "github.com/gin-gonic/gin" "go.uber.org/zap" ) type 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) DeleteSkill(c *gin.Context) { var req request.SkillDeleteRequest _ = c.ShouldBindJSON(&req) if err := skillsService.Delete(c.Request.Context(), req); err != nil { global.GVA_LOG.Error("删除技能失败", zap.Error(err)) response.FailWithMessage("删除技能失败: "+err.Error(), 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) } func (s *SkillsApi) PackageSkill(c *gin.Context) { var req request.SkillPackageRequest _ = c.ShouldBindJSON(&req) fileName, data, err := skillsService.Package(c.Request.Context(), req) if err != nil { global.GVA_LOG.Error("打包技能失败", zap.Error(err)) response.FailWithMessage("打包技能失败: "+err.Error(), c) return } c.Header("Content-Type", "application/zip") c.Header("Content-Disposition", "attachment; filename=\""+fileName+"\"") c.Data(http.StatusOK, "application/zip", data) } func (s *SkillsApi) DownloadOnlineSkill(c *gin.Context) { var req request.DownloadOnlineSkillReq if err := c.ShouldBindJSON(&req); err != nil { response.FailWithMessage("参数错误", c) return } if err := skillsService.DownloadOnlineSkill(c.Request.Context(), req); err != nil { global.GVA_LOG.Error("下载在线技能失败", zap.Error(err)) response.FailWithMessage("下载在线技能失败: "+err.Error(), c) return } response.OkWithMessage("下载成功", c) } ================================================ FILE: server/api/v1/system/sys_system.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" "github.com/flipped-aurora/gin-vue-admin/server/model/system" systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/gin-gonic/gin" "go.uber.org/zap" ) type SystemApi struct{} // GetSystemConfig // @Tags System // @Summary 获取配置文件内容 // @Security ApiKeyAuth // @Produce application/json // @Success 200 {object} response.Response{data=systemRes.SysConfigResponse,msg=string} "获取配置文件内容,返回包括系统配置" // @Router /system/getSystemConfig [post] func (s *SystemApi) GetSystemConfig(c *gin.Context) { config, err := systemConfigService.GetSystemConfig() if err != nil { global.GVA_LOG.Error("获取失败!", zap.Error(err)) response.FailWithMessage("获取失败", c) return } response.OkWithDetailed(systemRes.SysConfigResponse{Config: config}, "获取成功", c) } // SetSystemConfig // @Tags System // @Summary 设置配置文件内容 // @Security ApiKeyAuth // @Produce application/json // @Param data body system.System true "设置配置文件内容" // @Success 200 {object} response.Response{data=string} "设置配置文件内容" // @Router /system/setSystemConfig [post] func (s *SystemApi) SetSystemConfig(c *gin.Context) { var sys system.System err := c.ShouldBindJSON(&sys) if err != nil { response.FailWithMessage(err.Error(), c) return } err = systemConfigService.SetSystemConfig(sys) if err != nil { global.GVA_LOG.Error("设置失败!", zap.Error(err)) response.FailWithMessage("设置失败", c) return } response.OkWithMessage("设置成功", c) } // ReloadSystem // @Tags System // @Summary 重载系统 // @Security ApiKeyAuth // @Produce application/json // @Success 200 {object} response.Response{msg=string} "重载系统" // @Router /system/reloadSystem [post] func (s *SystemApi) ReloadSystem(c *gin.Context) { // 触发系统重载事件 err := utils.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) } ================================================ FILE: server/api/v1/system/sys_user.go ================================================ package system import ( "strconv" "time" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common" "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" "github.com/flipped-aurora/gin-vue-admin/server/model/system" systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/gin-gonic/gin" "github.com/redis/go-redis/v9" "go.uber.org/zap" ) // Login // @Tags Base // @Summary 用户登录 // @Produce application/json // @Param data body systemReq.Login true "用户名, 密码, 验证码" // @Success 200 {object} response.Response{data=systemRes.LoginResponse,msg=string} "返回包括用户信息,token,过期时间" // @Router /base/login [post] func (b *BaseApi) Login(c *gin.Context) { var l systemReq.Login err := c.ShouldBindJSON(&l) 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) } ================================================ FILE: server/api/v1/system/sys_version.go ================================================ package system import ( "encoding/json" "fmt" "net/http" "sort" "strconv" "time" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" "github.com/flipped-aurora/gin-vue-admin/server/model/system" systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/gin-gonic/gin" "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) } ================================================ FILE: server/config/auto_code.go ================================================ 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...) } ================================================ FILE: server/config/captcha.go ================================================ 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(秒) } ================================================ FILE: server/config/config.go ================================================ 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"` } ================================================ FILE: server/config/cors.go ================================================ 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"` } ================================================ FILE: server/config/db_list.go ================================================ 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"` } ================================================ FILE: server/config/disk.go ================================================ package config type Disk struct { MountPoint string `mapstructure:"mount-point" json:"mount-point" yaml:"mount-point"` } type DiskList struct { Disk `yaml:",inline" mapstructure:",squash"` } ================================================ FILE: server/config/email.go ================================================ 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、微软邮箱服务器等) } ================================================ FILE: server/config/excel.go ================================================ package config type Excel struct { Dir string `mapstructure:"dir" json:"dir" yaml:"dir"` } ================================================ FILE: server/config/gorm_mssql.go ================================================ 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" } ================================================ FILE: server/config/gorm_mysql.go ================================================ 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 } ================================================ FILE: server/config/gorm_oracle.go ================================================ 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 } ================================================ FILE: server/config/gorm_pgsql.go ================================================ 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 } ================================================ FILE: server/config/gorm_sqlite.go ================================================ 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") } ================================================ FILE: server/config/jwt.go ================================================ 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"` // 签发者 } ================================================ FILE: server/config/mcp.go ================================================ 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服务 } ================================================ FILE: server/config/mongo.go ================================================ 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) } ================================================ FILE: server/config/oss_aliyun.go ================================================ 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"` } ================================================ FILE: server/config/oss_aws.go ================================================ 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"` } ================================================ FILE: server/config/oss_cloudflare.go ================================================ 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"` } ================================================ FILE: server/config/oss_huawei.go ================================================ 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"` } ================================================ FILE: server/config/oss_local.go ================================================ package config type Local struct { Path string `mapstructure:"path" json:"path" yaml:"path"` // 本地文件访问路径 StorePath string `mapstructure:"store-path" json:"store-path" yaml:"store-path"` // 本地文件存储路径 } ================================================ FILE: server/config/oss_minio.go ================================================ 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"` } ================================================ FILE: server/config/oss_qiniu.go ================================================ 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上传加速 } ================================================ FILE: server/config/oss_tencent.go ================================================ 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"` } ================================================ FILE: server/config/redis.go ================================================ 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"` // 集群模式下的节点地址列表 } ================================================ FILE: server/config/system.go ================================================ 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,手动迁移 } ================================================ FILE: server/config/zap.go ================================================ package config import ( "go.uber.org/zap/zapcore" "time" ) type Zap struct { Level string `mapstructure:"level" json:"level" yaml:"level"` // 级别 Prefix string `mapstructure:"prefix" json:"prefix" yaml:"prefix"` // 日志前缀 Format string `mapstructure:"format" json:"format" yaml:"format"` // 输出 Director string `mapstructure:"director" json:"director" yaml:"director"` // 日志文件夹 EncodeLevel string `mapstructure:"encode-level" json:"encode-level" yaml:"encode-level"` // 编码级 StacktraceKey string `mapstructure:"stacktrace-key" json:"stacktrace-key" yaml:"stacktrace-key"` // 栈名 ShowLine bool `mapstructure:"show-line" json:"show-line" yaml:"show-line"` // 显示行 LogInConsole bool `mapstructure:"log-in-console" json:"log-in-console" yaml:"log-in-console"` // 输出控制台 RetentionDay int `mapstructure:"retention-day" json:"retention-day" yaml:"retention-day"` // 日志保留天数 } // Levels 根据字符串转化为 zapcore.Levels func (c *Zap) Levels() []zapcore.Level { levels := make([]zapcore.Level, 0, 7) level, err := zapcore.ParseLevel(c.Level) if err != nil { level = zapcore.DebugLevel } for ; level <= zapcore.FatalLevel; level++ { levels = append(levels, level) } return levels } func (c *Zap) Encoder() zapcore.Encoder { config := zapcore.EncoderConfig{ TimeKey: "time", NameKey: "name", LevelKey: "level", CallerKey: "caller", MessageKey: "message", StacktraceKey: c.StacktraceKey, LineEnding: zapcore.DefaultLineEnding, EncodeTime: func(t time.Time, encoder zapcore.PrimitiveArrayEncoder) { encoder.AppendString(c.Prefix + t.Format("2006-01-02 15:04:05.000")) }, EncodeLevel: c.LevelEncoder(), EncodeCaller: zapcore.FullCallerEncoder, EncodeDuration: zapcore.SecondsDurationEncoder, } if c.Format == "json" { return zapcore.NewJSONEncoder(config) } return zapcore.NewConsoleEncoder(config) } // LevelEncoder 根据 EncodeLevel 返回 zapcore.LevelEncoder // Author [SliverHorn](https://github.com/SliverHorn) func (c *Zap) LevelEncoder() zapcore.LevelEncoder { switch { case c.EncodeLevel == "LowercaseLevelEncoder": // 小写编码器(默认) return zapcore.LowercaseLevelEncoder case c.EncodeLevel == "LowercaseColorLevelEncoder": // 小写编码器带颜色 return zapcore.LowercaseColorLevelEncoder case c.EncodeLevel == "CapitalLevelEncoder": // 大写编码器 return zapcore.CapitalLevelEncoder case c.EncodeLevel == "CapitalColorLevelEncoder": // 大写编码器带颜色 return zapcore.CapitalColorLevelEncoder default: return zapcore.LowercaseLevelEncoder } } ================================================ FILE: server/config.docker.yaml ================================================ # github.com/flipped-aurora/gin-vue-admin/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: "[github.com/flipped-aurora/gin-vue-admin/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: 'github.com/flipped-aurora/gin-vue-admin/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: github.com/flipped-aurora/gin-vue-admin/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: github.com/flipped-aurora/gin-vue-admin/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 ================================================ FILE: server/config.yaml ================================================ # github.com/flipped-aurora/gin-vue-admin/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: "[github.com/flipped-aurora/gin-vue-admin/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: 127.0.0.1:6379 password: "" db: 0 clusterAddrs: - "172.21.0.3:7000" - "172.21.0.4:7001" - "172.21.0.2:7002" # redis-list configuration redis-list: - name: cache # 数据库的名称,注意: name 需要在 redis-list 中唯一 useCluster: false # 是否使用redis集群模式 addr: 127.0.0.1:6379 # 使用集群模式addr和db默认无效 password: "" db: 0 clusterAddrs: - "172.21.0.3:7000" - "172.21.0.4:7001" - "172.21.0.2: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 # 禁用自动迁移数据库表结构,生产环境建议设为true,手动迁移 disable-auto-migrate: 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: 'github.com/flipped-aurora/gin-vue-admin/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: github.com/flipped-aurora/gin-vue-admin/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: github.com/flipped-aurora/gin-vue-admin/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 ================================================ FILE: server/core/internal/constant.go ================================================ package internal const ( ConfigEnv = "GVA_CONFIG" ConfigDefaultFile = "config.yaml" ConfigTestFile = "config.test.yaml" ConfigDebugFile = "config.debug.yaml" ConfigReleaseFile = "config.release.yaml" ) ================================================ FILE: server/core/internal/cutter.go ================================================ 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 }) } ================================================ FILE: server/core/internal/zap_core.go ================================================ package internal import ( "context" "fmt" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/service" astutil "github.com/flipped-aurora/gin-vue-admin/server/utils/ast" "github.com/flipped-aurora/gin-vue-admin/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() } ================================================ FILE: server/core/server.go ================================================ package core import ( "fmt" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/initialize" "github.com/flipped-aurora/gin-vue-admin/server/service/system" "go.uber.org/zap" "time" ) 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(` 欢迎使用 gin-vue-admin 当前版本:%s 加群方式:微信号:shouzi_1994 QQ群:470239250 项目地址:https://github.com/flipped-aurora/gin-vue-admin 插件市场:https://plugin.gin-vue-admin.com GVA讨论社区:https://support.qq.com/products/371961 默认自动化文档地址: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 --------------------------------------版权声明-------------------------------------- ** 版权所有方:flipped-aurora开源团队 ** ** 版权持有公司:北京翻转极光科技有限责任公司 ** ** 剔除授权标识需购买商用授权:https://plugin.gin-vue-admin.com/license ** ** 感谢您对Gin-Vue-Admin的支持与关注 合法授权使用更有利于项目的长久发展** `, global.Version, address, address, global.GVA_CONFIG.MCP.SSEPath, address, global.GVA_CONFIG.MCP.MessagePath) initServer(address, Router, 10*time.Minute, 10*time.Minute) } ================================================ FILE: server/core/server_run.go ================================================ 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服务已关闭") } ================================================ FILE: server/core/viper.go ================================================ package core import ( "flag" "fmt" "os" "path/filepath" "github.com/flipped-aurora/gin-vue-admin/server/core/internal" "github.com/flipped-aurora/gin-vue-admin/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 } ================================================ FILE: server/core/zap.go ================================================ package core import ( "fmt" "github.com/flipped-aurora/gin-vue-admin/server/core/internal" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/utils" "go.uber.org/zap" "go.uber.org/zap/zapcore" "os" ) // Zap 获取 zap.Logger // Author [SliverHorn](https://github.com/SliverHorn) func Zap() (logger *zap.Logger) { if ok, _ := utils.PathExists(global.GVA_CONFIG.Zap.Director); !ok { // 判断是否有Director文件夹 fmt.Printf("create %v directory\n", global.GVA_CONFIG.Zap.Director) _ = os.Mkdir(global.GVA_CONFIG.Zap.Director, os.ModePerm) } levels := global.GVA_CONFIG.Zap.Levels() length := len(levels) cores := make([]zapcore.Core, 0, length) for i := 0; i < length; i++ { core := internal.NewZapCore(levels[i]) cores = append(cores, core) } // 构建基础 logger(错误级别的入库逻辑已在自定义 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 } ================================================ FILE: server/docs/docs.go ================================================ // Code generated by swaggo/swag. DO NOT EDIT. package docs import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/swaggo/swag" ) const docTemplate = `{ "schemes": {{ marshal .Schemes }}, "swagger": "2.0", "info": { "description": "{{escape .Description}}", "title": "{{.Title}}", "contact": {}, "version": "{{.Version}}" }, "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { "/api/createApi": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysApi" ], "summary": "创建基础api", "parameters": [ { "description": "api路径, api中文描述, api组, 方法", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysApi" } } ], "responses": { "200": { "description": "创建基础api", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/api/deleteApi": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysApi" ], "summary": "删除api", "parameters": [ { "description": "ID", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysApi" } } ], "responses": { "200": { "description": "删除api", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/api/deleteApisByIds": { "delete": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysApi" ], "summary": "删除选中Api", "parameters": [ { "description": "ID", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.IdsReq" } } ], "responses": { "200": { "description": "删除选中Api", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/api/enterSyncApi": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysApi" ], "summary": "确认同步API", "responses": { "200": { "description": "确认同步API", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/api/freshCasbin": { "get": { "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysApi" ], "summary": "刷新casbin缓存", "responses": { "200": { "description": "刷新成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/api/getAllApis": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysApi" ], "summary": "获取所有的Api 不分页", "responses": { "200": { "description": "获取所有的Api 不分页,返回包括api列表", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.SysAPIListResponse" }, "msg": { "type": "string" } } } ] } } } } }, "/api/getApiById": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysApi" ], "summary": "根据id获取api", "parameters": [ { "description": "根据id获取api", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.GetById" } } ], "responses": { "200": { "description": "根据id获取api,返回包括api详情", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.SysAPIResponse" } } } ] } } } } }, "/api/getApiGroups": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysApi" ], "summary": "获取API分组", "responses": { "200": { "description": "获取API分组", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/api/getApiList": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysApi" ], "summary": "分页获取API列表", "parameters": [ { "description": "分页获取API列表", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.SearchApiParams" } } ], "responses": { "200": { "description": "分页获取API列表,返回包括列表,总数,页码,每页数量", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.PageResult" }, "msg": { "type": "string" } } } ] } } } } }, "/api/ignoreApi": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "IgnoreApi" ], "summary": "忽略API", "responses": { "200": { "description": "同步API", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/api/syncApi": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysApi" ], "summary": "同步API", "responses": { "200": { "description": "同步API", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/api/updateApi": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysApi" ], "summary": "修改基础api", "parameters": [ { "description": "api路径, api中文描述, api组, 方法", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysApi" } } ], "responses": { "200": { "description": "修改基础api", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/attachmentCategory/addCategory": { "post": { "security": [ { "AttachmentCategory": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AddCategory" ], "summary": "添加媒体库分类", "parameters": [ { "description": "媒体库分类数据", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/example.ExaAttachmentCategory" } } ], "responses": {} } }, "/attachmentCategory/deleteCategory": { "post": { "security": [ { "AttachmentCategory": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "DeleteCategory" ], "summary": "删除分类", "parameters": [ { "description": "分类id", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.GetById" } } ], "responses": { "200": { "description": "删除分类", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/attachmentCategory/getCategoryList": { "get": { "security": [ { "AttachmentCategory": [] } ], "produces": [ "application/json" ], "tags": [ "GetCategoryList" ], "summary": "媒体库分类列表", "responses": { "200": { "description": "媒体库分类列表", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/example.ExaAttachmentCategory" }, "msg": { "type": "string" } } } ] } } } } }, "/authority/copyAuthority": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Authority" ], "summary": "拷贝角色", "parameters": [ { "description": "旧角色id, 新权限id, 新权限名, 新父角色id", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/response.SysAuthorityCopyResponse" } } ], "responses": { "200": { "description": "拷贝角色,返回包括系统角色详情", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.SysAuthorityResponse" }, "msg": { "type": "string" } } } ] } } } } }, "/authority/createAuthority": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Authority" ], "summary": "创建角色", "parameters": [ { "description": "权限id, 权限名, 父角色id", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysAuthority" } } ], "responses": { "200": { "description": "创建角色,返回包括系统角色详情", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.SysAuthorityResponse" }, "msg": { "type": "string" } } } ] } } } } }, "/authority/deleteAuthority": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Authority" ], "summary": "删除角色", "parameters": [ { "description": "删除角色", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysAuthority" } } ], "responses": { "200": { "description": "删除角色", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/authority/getAuthorityList": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Authority" ], "summary": "分页获取角色列表", "parameters": [ { "description": "页码, 每页大小", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.PageInfo" } } ], "responses": { "200": { "description": "分页获取角色列表,返回包括列表,总数,页码,每页数量", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.PageResult" }, "msg": { "type": "string" } } } ] } } } } }, "/authority/setDataAuthority": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Authority" ], "summary": "设置角色资源权限", "parameters": [ { "description": "设置角色资源权限", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysAuthority" } } ], "responses": { "200": { "description": "设置角色资源权限", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/authority/updateAuthority": { "put": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Authority" ], "summary": "更新角色信息", "parameters": [ { "description": "权限id, 权限名, 父角色id", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysAuthority" } } ], "responses": { "200": { "description": "更新角色信息,返回包括系统角色详情", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.SysAuthorityResponse" }, "msg": { "type": "string" } } } ] } } } } }, "/authorityBtn/canRemoveAuthorityBtn": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AuthorityBtn" ], "summary": "设置权限按钮", "responses": { "200": { "description": "删除成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/authorityBtn/getAuthorityBtn": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AuthorityBtn" ], "summary": "获取权限按钮", "parameters": [ { "description": "菜单id, 角色id, 选中的按钮id", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.SysAuthorityBtnReq" } } ], "responses": { "200": { "description": "返回列表成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.SysAuthorityBtnRes" }, "msg": { "type": "string" } } } ] } } } } }, "/authorityBtn/setAuthorityBtn": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AuthorityBtn" ], "summary": "设置权限按钮", "parameters": [ { "description": "菜单id, 角色id, 选中的按钮id", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.SysAuthorityBtnReq" } } ], "responses": { "200": { "description": "返回列表成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/autoCode/addFunc": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AddFunc" ], "summary": "增加方法", "parameters": [ { "description": "增加方法", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.AutoCode" } } ], "responses": { "200": { "description": "{\"success\":true,\"data\":{},\"msg\":\"创建成功\"}", "schema": { "type": "string" } } } } }, "/autoCode/createPackage": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AutoCodePackage" ], "summary": "创建package", "parameters": [ { "description": "创建package", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.SysAutoCodePackageCreate" } } ], "responses": { "200": { "description": "创建package成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/autoCode/createTemp": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AutoCodeTemplate" ], "summary": "自动代码模板", "parameters": [ { "description": "创建自动代码", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.AutoCode" } } ], "responses": { "200": { "description": "{\"success\":true,\"data\":{},\"msg\":\"创建成功\"}", "schema": { "type": "string" } } } } }, "/autoCode/delPackage": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AutoCode" ], "summary": "删除package", "parameters": [ { "description": "创建package", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.GetById" } } ], "responses": { "200": { "description": "删除package成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/autoCode/delSysHistory": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AutoCode" ], "summary": "删除回滚记录", "parameters": [ { "description": "请求参数", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.GetById" } } ], "responses": { "200": { "description": "删除回滚记录", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/autoCode/getColumn": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AutoCode" ], "summary": "获取当前表所有字段", "responses": { "200": { "description": "获取当前表所有字段", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/autoCode/getDB": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AutoCode" ], "summary": "获取当前所有数据库", "responses": { "200": { "description": "获取当前所有数据库", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/autoCode/getMeta": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AutoCode" ], "summary": "获取meta信息", "parameters": [ { "description": "请求参数", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.GetById" } } ], "responses": { "200": { "description": "获取meta信息", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/autoCode/getPackage": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AutoCodePackage" ], "summary": "获取package", "responses": { "200": { "description": "创建package成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/autoCode/getSysHistory": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AutoCode" ], "summary": "查询回滚记录", "parameters": [ { "description": "请求参数", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.PageInfo" } } ], "responses": { "200": { "description": "查询回滚记录,返回包括列表,总数,页码,每页数量", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.PageResult" }, "msg": { "type": "string" } } } ] } } } } }, "/autoCode/getTables": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AutoCode" ], "summary": "获取当前数据库所有表", "responses": { "200": { "description": "获取当前数据库所有表", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/autoCode/getTemplates": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AutoCodePackage" ], "summary": "获取package", "responses": { "200": { "description": "创建package成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/autoCode/initAPI": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AutoCodePlugin" ], "summary": "打包插件", "responses": { "200": { "description": "打包插件成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/autoCode/initMenu": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AutoCodePlugin" ], "summary": "打包插件", "responses": { "200": { "description": "打包插件成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/autoCode/installPlugin": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "multipart/form-data" ], "produces": [ "application/json" ], "tags": [ "AutoCodePlugin" ], "summary": "安装插件", "parameters": [ { "type": "file", "description": "this is a test file", "name": "plug", "in": "formData", "required": true } ], "responses": { "200": { "description": "安装插件成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "object" } }, "msg": { "type": "string" } } } ] } } } } }, "/autoCode/preview": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AutoCodeTemplate" ], "summary": "预览创建后的代码", "parameters": [ { "description": "预览创建代码", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.AutoCode" } } ], "responses": { "200": { "description": "预览创建后的代码", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/autoCode/pubPlug": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AutoCodePlugin" ], "summary": "打包插件", "parameters": [ { "type": "string", "description": "插件名称", "name": "plugName", "in": "query", "required": true } ], "responses": { "200": { "description": "打包插件成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/autoCode/rollback": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AutoCode" ], "summary": "回滚自动生成代码", "parameters": [ { "description": "请求参数", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.SysAutoHistoryRollBack" } } ], "responses": { "200": { "description": "回滚自动生成代码", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/base/captcha": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Base" ], "summary": "生成验证码", "responses": { "200": { "description": "生成验证码,返回包括随机数id,base64,验证码长度,是否开启验证码", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.SysCaptchaResponse" }, "msg": { "type": "string" } } } ] } } } } }, "/base/login": { "post": { "produces": [ "application/json" ], "tags": [ "Base" ], "summary": "用户登录", "parameters": [ { "description": "用户名, 密码, 验证码", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.Login" } } ], "responses": { "200": { "description": "返回包括用户信息,token,过期时间", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.LoginResponse" }, "msg": { "type": "string" } } } ] } } } } }, "/casbin/UpdateCasbin": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Casbin" ], "summary": "更新角色api权限", "parameters": [ { "description": "权限id, 权限模型列表", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.CasbinInReceive" } } ], "responses": { "200": { "description": "更新角色api权限", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/casbin/getPolicyPathByAuthorityId": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Casbin" ], "summary": "获取权限列表", "parameters": [ { "description": "权限id, 权限模型列表", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.CasbinInReceive" } } ], "responses": { "200": { "description": "获取权限列表,返回包括casbin详情列表", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.PolicyPathResponse" }, "msg": { "type": "string" } } } ] } } } } }, "/customer/customer": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "ExaCustomer" ], "summary": "获取单一客户信息", "parameters": [ { "type": "integer", "description": "主键ID", "name": "ID", "in": "query" }, { "type": "string", "description": "创建时间", "name": "createdAt", "in": "query" }, { "type": "string", "description": "客户名", "name": "customerName", "in": "query" }, { "type": "string", "description": "客户手机号", "name": "customerPhoneData", "in": "query" }, { "type": "integer", "description": "管理角色ID", "name": "sysUserAuthorityID", "in": "query" }, { "type": "integer", "description": "管理ID", "name": "sysUserId", "in": "query" }, { "type": "string", "description": "更新时间", "name": "updatedAt", "in": "query" } ], "responses": { "200": { "description": "获取单一客户信息,返回包括客户详情", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.ExaCustomerResponse" }, "msg": { "type": "string" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "ExaCustomer" ], "summary": "更新客户信息", "parameters": [ { "description": "客户ID, 客户信息", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/example.ExaCustomer" } } ], "responses": { "200": { "description": "更新客户信息", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } }, "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "ExaCustomer" ], "summary": "创建客户", "parameters": [ { "description": "客户用户名, 客户手机号码", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/example.ExaCustomer" } } ], "responses": { "200": { "description": "创建客户", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } }, "delete": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "ExaCustomer" ], "summary": "删除客户", "parameters": [ { "description": "客户ID", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/example.ExaCustomer" } } ], "responses": { "200": { "description": "删除客户", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/customer/customerList": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "ExaCustomer" ], "summary": "分页获取权限客户列表", "parameters": [ { "type": "string", "description": "关键字", "name": "keyword", "in": "query" }, { "type": "integer", "description": "页码", "name": "page", "in": "query" }, { "type": "integer", "description": "每页大小", "name": "pageSize", "in": "query" } ], "responses": { "200": { "description": "分页获取权限客户列表,返回包括列表,总数,页码,每页数量", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.PageResult" }, "msg": { "type": "string" } } } ] } } } } }, "/email/emailTest": { "post": { "security": [ { "ApiKeyAuth": [] } ], "produces": [ "application/json" ], "tags": [ "System" ], "summary": "发送测试邮件", "responses": { "200": { "description": "{\"success\":true,\"data\":{},\"msg\":\"发送成功\"}", "schema": { "type": "string" } } } } }, "/email/sendEmail": { "post": { "security": [ { "ApiKeyAuth": [] } ], "produces": [ "application/json" ], "tags": [ "System" ], "summary": "发送邮件", "parameters": [ { "description": "发送邮件必须的参数", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/response.Email" } } ], "responses": { "200": { "description": "{\"success\":true,\"data\":{},\"msg\":\"发送成功\"}", "schema": { "type": "string" } } } } }, "/fileUploadAndDownload/breakpointContinue": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "multipart/form-data" ], "produces": [ "application/json" ], "tags": [ "ExaFileUploadAndDownload" ], "summary": "断点续传到服务器", "parameters": [ { "type": "file", "description": "an example for breakpoint resume, 断点续传示例", "name": "file", "in": "formData", "required": true } ], "responses": { "200": { "description": "断点续传到服务器", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/fileUploadAndDownload/deleteFile": { "post": { "security": [ { "ApiKeyAuth": [] } ], "produces": [ "application/json" ], "tags": [ "ExaFileUploadAndDownload" ], "summary": "删除文件", "parameters": [ { "description": "传入文件里面id即可", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/example.ExaFileUploadAndDownload" } } ], "responses": { "200": { "description": "删除文件", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/fileUploadAndDownload/findFile": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "multipart/form-data" ], "produces": [ "application/json" ], "tags": [ "ExaFileUploadAndDownload" ], "summary": "查找文件", "parameters": [ { "type": "file", "description": "Find the file, 查找文件", "name": "file", "in": "formData", "required": true } ], "responses": { "200": { "description": "查找文件,返回包括文件详情", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.FileResponse" }, "msg": { "type": "string" } } } ] } } } }, "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "multipart/form-data" ], "produces": [ "application/json" ], "tags": [ "ExaFileUploadAndDownload" ], "summary": "创建文件", "parameters": [ { "type": "file", "description": "上传文件完成", "name": "file", "in": "formData", "required": true } ], "responses": { "200": { "description": "创建文件,返回包括文件路径", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.FilePathResponse" }, "msg": { "type": "string" } } } ] } } } } }, "/fileUploadAndDownload/getFileList": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "ExaFileUploadAndDownload" ], "summary": "分页文件列表", "parameters": [ { "description": "页码, 每页大小, 分类id", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.ExaAttachmentCategorySearch" } } ], "responses": { "200": { "description": "分页文件列表,返回包括列表,总数,页码,每页数量", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.PageResult" }, "msg": { "type": "string" } } } ] } } } } }, "/fileUploadAndDownload/importURL": { "post": { "security": [ { "ApiKeyAuth": [] } ], "produces": [ "application/json" ], "tags": [ "ExaFileUploadAndDownload" ], "summary": "导入URL", "parameters": [ { "description": "对象", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/example.ExaFileUploadAndDownload" } } ], "responses": { "200": { "description": "导入URL", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/fileUploadAndDownload/removeChunk": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "multipart/form-data" ], "produces": [ "application/json" ], "tags": [ "ExaFileUploadAndDownload" ], "summary": "删除切片", "parameters": [ { "type": "file", "description": "删除缓存切片", "name": "file", "in": "formData", "required": true } ], "responses": { "200": { "description": "删除切片", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/fileUploadAndDownload/upload": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "multipart/form-data" ], "produces": [ "application/json" ], "tags": [ "ExaFileUploadAndDownload" ], "summary": "上传文件示例", "parameters": [ { "type": "file", "description": "上传文件示例", "name": "file", "in": "formData", "required": true } ], "responses": { "200": { "description": "上传文件示例,返回包括文件详情", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.ExaFileResponse" }, "msg": { "type": "string" } } } ] } } } } }, "/info/createInfo": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Info" ], "summary": "创建公告", "parameters": [ { "description": "创建公告", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/model.Info" } } ], "responses": { "200": { "description": "创建成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/info/deleteInfo": { "delete": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Info" ], "summary": "删除公告", "parameters": [ { "description": "删除公告", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/model.Info" } } ], "responses": { "200": { "description": "删除成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/info/deleteInfoByIds": { "delete": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Info" ], "summary": "批量删除公告", "responses": { "200": { "description": "批量删除成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/info/findInfo": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Info" ], "summary": "用id查询公告", "parameters": [ { "type": "integer", "description": "主键ID", "name": "ID", "in": "query" }, { "type": "string", "description": "内容", "name": "content", "in": "query" }, { "type": "string", "description": "创建时间", "name": "createdAt", "in": "query" }, { "type": "string", "description": "标题", "name": "title", "in": "query" }, { "type": "string", "description": "更新时间", "name": "updatedAt", "in": "query" }, { "type": "integer", "description": "作者", "name": "userID", "in": "query" } ], "responses": { "200": { "description": "查询成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/model.Info" }, "msg": { "type": "string" } } } ] } } } } }, "/info/getInfoDataSource": { "get": { "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Info" ], "summary": "获取Info的数据源", "responses": { "200": { "description": "查询成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object" }, "msg": { "type": "string" } } } ] } } } } }, "/info/getInfoList": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Info" ], "summary": "分页获取公告列表", "parameters": [ { "type": "string", "name": "endCreatedAt", "in": "query" }, { "type": "string", "description": "关键字", "name": "keyword", "in": "query" }, { "type": "integer", "description": "页码", "name": "page", "in": "query" }, { "type": "integer", "description": "每页大小", "name": "pageSize", "in": "query" }, { "type": "string", "name": "startCreatedAt", "in": "query" } ], "responses": { "200": { "description": "获取成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.PageResult" }, "msg": { "type": "string" } } } ] } } } } }, "/info/getInfoPublic": { "get": { "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Info" ], "summary": "不需要鉴权的公告接口", "parameters": [ { "type": "string", "name": "endCreatedAt", "in": "query" }, { "type": "string", "description": "关键字", "name": "keyword", "in": "query" }, { "type": "integer", "description": "页码", "name": "page", "in": "query" }, { "type": "integer", "description": "每页大小", "name": "pageSize", "in": "query" }, { "type": "string", "name": "startCreatedAt", "in": "query" } ], "responses": { "200": { "description": "获取成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object" }, "msg": { "type": "string" } } } ] } } } } }, "/info/updateInfo": { "put": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Info" ], "summary": "更新公告", "parameters": [ { "description": "更新公告", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/model.Info" } } ], "responses": { "200": { "description": "更新成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/init/checkdb": { "post": { "produces": [ "application/json" ], "tags": [ "CheckDB" ], "summary": "初始化用户数据库", "responses": { "200": { "description": "初始化用户数据库", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/init/initdb": { "post": { "produces": [ "application/json" ], "tags": [ "InitDB" ], "summary": "初始化用户数据库", "parameters": [ { "description": "初始化数据库参数", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.InitDB" } } ], "responses": { "200": { "description": "初始化用户数据库", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "string" } } } ] } } } } }, "/jwt/jsonInBlacklist": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Jwt" ], "summary": "jwt加入黑名单", "responses": { "200": { "description": "jwt加入黑名单", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/menu/addBaseMenu": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Menu" ], "summary": "新增菜单", "parameters": [ { "description": "路由path, 父菜单ID, 路由name, 对应前端文件路径, 排序标记", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysBaseMenu" } } ], "responses": { "200": { "description": "新增菜单", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/menu/addMenuAuthority": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AuthorityMenu" ], "summary": "增加menu和角色关联关系", "parameters": [ { "description": "角色ID", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.AddMenuAuthorityInfo" } } ], "responses": { "200": { "description": "增加menu和角色关联关系", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/menu/deleteBaseMenu": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Menu" ], "summary": "删除菜单", "parameters": [ { "description": "菜单id", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.GetById" } } ], "responses": { "200": { "description": "删除菜单", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/menu/getBaseMenuById": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Menu" ], "summary": "根据id获取菜单", "parameters": [ { "description": "菜单id", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.GetById" } } ], "responses": { "200": { "description": "根据id获取菜单,返回包括系统菜单列表", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.SysBaseMenuResponse" }, "msg": { "type": "string" } } } ] } } } } }, "/menu/getBaseMenuTree": { "post": { "security": [ { "ApiKeyAuth": [] } ], "produces": [ "application/json" ], "tags": [ "AuthorityMenu" ], "summary": "获取用户动态路由", "parameters": [ { "description": "空", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.Empty" } } ], "responses": { "200": { "description": "获取用户动态路由,返回包括系统菜单列表", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.SysBaseMenusResponse" }, "msg": { "type": "string" } } } ] } } } } }, "/menu/getMenu": { "post": { "security": [ { "ApiKeyAuth": [] } ], "produces": [ "application/json" ], "tags": [ "AuthorityMenu" ], "summary": "获取用户动态路由", "parameters": [ { "description": "空", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.Empty" } } ], "responses": { "200": { "description": "获取用户动态路由,返回包括系统菜单详情列表", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.SysMenusResponse" }, "msg": { "type": "string" } } } ] } } } } }, "/menu/getMenuAuthority": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AuthorityMenu" ], "summary": "获取指定角色menu", "parameters": [ { "description": "角色ID", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.GetAuthorityId" } } ], "responses": { "200": { "description": "获取指定角色menu", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/menu/getMenuList": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Menu" ], "summary": "分页获取基础menu列表", "parameters": [ { "description": "页码, 每页大小", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.PageInfo" } } ], "responses": { "200": { "description": "分页获取基础menu列表,返回包括列表,总数,页码,每页数量", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.PageResult" }, "msg": { "type": "string" } } } ] } } } } }, "/menu/updateBaseMenu": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Menu" ], "summary": "更新菜单", "parameters": [ { "description": "路由path, 父菜单ID, 路由name, 对应前端文件路径, 排序标记", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysBaseMenu" } } ], "responses": { "200": { "description": "更新菜单", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/sysDictionary/createSysDictionary": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysDictionary" ], "summary": "创建SysDictionary", "parameters": [ { "description": "SysDictionary模型", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysDictionary" } } ], "responses": { "200": { "description": "创建SysDictionary", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/sysDictionary/deleteSysDictionary": { "delete": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysDictionary" ], "summary": "删除SysDictionary", "parameters": [ { "description": "SysDictionary模型", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysDictionary" } } ], "responses": { "200": { "description": "删除SysDictionary", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/sysDictionary/findSysDictionary": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysDictionary" ], "summary": "用id查询SysDictionary", "parameters": [ { "type": "integer", "description": "主键ID", "name": "ID", "in": "query" }, { "type": "string", "description": "创建时间", "name": "createdAt", "in": "query" }, { "type": "string", "description": "描述", "name": "desc", "in": "query" }, { "type": "string", "description": "字典名(中)", "name": "name", "in": "query" }, { "type": "boolean", "description": "状态", "name": "status", "in": "query" }, { "type": "string", "description": "字典名(英)", "name": "type", "in": "query" }, { "type": "string", "description": "更新时间", "name": "updatedAt", "in": "query" } ], "responses": { "200": { "description": "用id查询SysDictionary", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/sysDictionary/getSysDictionaryList": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysDictionary" ], "summary": "分页获取SysDictionary列表", "responses": { "200": { "description": "分页获取SysDictionary列表,返回包括列表,总数,页码,每页数量", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.PageResult" }, "msg": { "type": "string" } } } ] } } } } }, "/sysDictionary/updateSysDictionary": { "put": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysDictionary" ], "summary": "更新SysDictionary", "parameters": [ { "description": "SysDictionary模型", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysDictionary" } } ], "responses": { "200": { "description": "更新SysDictionary", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/sysDictionaryDetail/createSysDictionaryDetail": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysDictionaryDetail" ], "summary": "创建SysDictionaryDetail", "parameters": [ { "description": "SysDictionaryDetail模型", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysDictionaryDetail" } } ], "responses": { "200": { "description": "创建SysDictionaryDetail", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/sysDictionaryDetail/deleteSysDictionaryDetail": { "delete": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysDictionaryDetail" ], "summary": "删除SysDictionaryDetail", "parameters": [ { "description": "SysDictionaryDetail模型", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysDictionaryDetail" } } ], "responses": { "200": { "description": "删除SysDictionaryDetail", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/sysDictionaryDetail/findSysDictionaryDetail": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysDictionaryDetail" ], "summary": "用id查询SysDictionaryDetail", "parameters": [ { "type": "integer", "description": "主键ID", "name": "ID", "in": "query" }, { "type": "string", "description": "创建时间", "name": "createdAt", "in": "query" }, { "type": "string", "description": "扩展值", "name": "extend", "in": "query" }, { "type": "string", "description": "展示值", "name": "label", "in": "query" }, { "type": "integer", "description": "排序标记", "name": "sort", "in": "query" }, { "type": "boolean", "description": "启用状态", "name": "status", "in": "query" }, { "type": "integer", "description": "关联标记", "name": "sysDictionaryID", "in": "query" }, { "type": "string", "description": "更新时间", "name": "updatedAt", "in": "query" }, { "type": "string", "description": "字典值", "name": "value", "in": "query" } ], "responses": { "200": { "description": "用id查询SysDictionaryDetail", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/sysDictionaryDetail/getSysDictionaryDetailList": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysDictionaryDetail" ], "summary": "分页获取SysDictionaryDetail列表", "parameters": [ { "type": "integer", "description": "主键ID", "name": "ID", "in": "query" }, { "type": "string", "description": "创建时间", "name": "createdAt", "in": "query" }, { "type": "string", "description": "扩展值", "name": "extend", "in": "query" }, { "type": "string", "description": "关键字", "name": "keyword", "in": "query" }, { "type": "string", "description": "展示值", "name": "label", "in": "query" }, { "type": "integer", "description": "页码", "name": "page", "in": "query" }, { "type": "integer", "description": "每页大小", "name": "pageSize", "in": "query" }, { "type": "integer", "description": "排序标记", "name": "sort", "in": "query" }, { "type": "boolean", "description": "启用状态", "name": "status", "in": "query" }, { "type": "integer", "description": "关联标记", "name": "sysDictionaryID", "in": "query" }, { "type": "string", "description": "更新时间", "name": "updatedAt", "in": "query" }, { "type": "string", "description": "字典值", "name": "value", "in": "query" } ], "responses": { "200": { "description": "分页获取SysDictionaryDetail列表,返回包括列表,总数,页码,每页数量", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.PageResult" }, "msg": { "type": "string" } } } ] } } } } }, "/sysDictionaryDetail/updateSysDictionaryDetail": { "put": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysDictionaryDetail" ], "summary": "更新SysDictionaryDetail", "parameters": [ { "description": "更新SysDictionaryDetail", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysDictionaryDetail" } } ], "responses": { "200": { "description": "更新SysDictionaryDetail", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/sysExportTemplate/ExportTemplate": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysExportTemplate" ], "summary": "导出表格模板", "responses": {} } }, "/sysExportTemplate/createSysExportTemplate": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysExportTemplate" ], "summary": "创建导出模板", "parameters": [ { "description": "创建导出模板", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysExportTemplate" } } ], "responses": { "200": { "description": "{\"success\":true,\"data\":{},\"msg\":\"创建成功\"}", "schema": { "type": "string" } } } } }, "/sysExportTemplate/deleteSysExportTemplate": { "delete": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysExportTemplate" ], "summary": "删除导出模板", "parameters": [ { "description": "删除导出模板", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysExportTemplate" } } ], "responses": { "200": { "description": "{\"success\":true,\"data\":{},\"msg\":\"删除成功\"}", "schema": { "type": "string" } } } } }, "/sysExportTemplate/deleteSysExportTemplateByIds": { "delete": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysExportTemplate" ], "summary": "批量删除导出模板", "parameters": [ { "description": "批量删除导出模板", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.IdsReq" } } ], "responses": { "200": { "description": "{\"success\":true,\"data\":{},\"msg\":\"批量删除成功\"}", "schema": { "type": "string" } } } } }, "/sysExportTemplate/exportExcel": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysExportTemplate" ], "summary": "导出表格", "responses": {} } }, "/sysExportTemplate/findSysExportTemplate": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysExportTemplate" ], "summary": "用id查询导出模板", "parameters": [ { "type": "integer", "description": "主键ID", "name": "ID", "in": "query" }, { "type": "string", "description": "创建时间", "name": "createdAt", "in": "query" }, { "type": "string", "description": "数据库名称", "name": "dbName", "in": "query" }, { "type": "integer", "name": "limit", "in": "query" }, { "type": "string", "description": "模板名称", "name": "name", "in": "query" }, { "type": "string", "name": "order", "in": "query" }, { "type": "string", "description": "表名称", "name": "tableName", "in": "query" }, { "type": "string", "description": "模板标识", "name": "templateID", "in": "query" }, { "type": "string", "description": "模板信息", "name": "templateInfo", "in": "query" }, { "type": "string", "description": "更新时间", "name": "updatedAt", "in": "query" } ], "responses": { "200": { "description": "{\"success\":true,\"data\":{},\"msg\":\"查询成功\"}", "schema": { "type": "string" } } } } }, "/sysExportTemplate/getSysExportTemplateList": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysExportTemplate" ], "summary": "分页获取导出模板列表", "parameters": [ { "type": "integer", "description": "主键ID", "name": "ID", "in": "query" }, { "type": "string", "description": "创建时间", "name": "createdAt", "in": "query" }, { "type": "string", "description": "数据库名称", "name": "dbName", "in": "query" }, { "type": "string", "name": "endCreatedAt", "in": "query" }, { "type": "string", "description": "关键字", "name": "keyword", "in": "query" }, { "type": "integer", "name": "limit", "in": "query" }, { "type": "string", "description": "模板名称", "name": "name", "in": "query" }, { "type": "string", "name": "order", "in": "query" }, { "type": "integer", "description": "页码", "name": "page", "in": "query" }, { "type": "integer", "description": "每页大小", "name": "pageSize", "in": "query" }, { "type": "string", "name": "startCreatedAt", "in": "query" }, { "type": "string", "description": "表名称", "name": "tableName", "in": "query" }, { "type": "string", "description": "模板标识", "name": "templateID", "in": "query" }, { "type": "string", "description": "模板信息", "name": "templateInfo", "in": "query" }, { "type": "string", "description": "更新时间", "name": "updatedAt", "in": "query" } ], "responses": { "200": { "description": "{\"success\":true,\"data\":{},\"msg\":\"获取成功\"}", "schema": { "type": "string" } } } } }, "/sysExportTemplate/importExcel": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysImportTemplate" ], "summary": "导入表格", "responses": {} } }, "/sysExportTemplate/updateSysExportTemplate": { "put": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysExportTemplate" ], "summary": "更新导出模板", "parameters": [ { "description": "更新导出模板", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysExportTemplate" } } ], "responses": { "200": { "description": "{\"success\":true,\"data\":{},\"msg\":\"更新成功\"}", "schema": { "type": "string" } } } } }, "/sysOperationRecord/createSysOperationRecord": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysOperationRecord" ], "summary": "创建SysOperationRecord", "parameters": [ { "description": "创建SysOperationRecord", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysOperationRecord" } } ], "responses": { "200": { "description": "创建SysOperationRecord", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/sysOperationRecord/deleteSysOperationRecord": { "delete": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysOperationRecord" ], "summary": "删除SysOperationRecord", "parameters": [ { "description": "SysOperationRecord模型", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysOperationRecord" } } ], "responses": { "200": { "description": "删除SysOperationRecord", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/sysOperationRecord/deleteSysOperationRecordByIds": { "delete": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysOperationRecord" ], "summary": "批量删除SysOperationRecord", "parameters": [ { "description": "批量删除SysOperationRecord", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.IdsReq" } } ], "responses": { "200": { "description": "批量删除SysOperationRecord", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/sysOperationRecord/findSysOperationRecord": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysOperationRecord" ], "summary": "用id查询SysOperationRecord", "parameters": [ { "type": "integer", "description": "主键ID", "name": "ID", "in": "query" }, { "type": "string", "description": "代理", "name": "agent", "in": "query" }, { "type": "string", "description": "请求Body", "name": "body", "in": "query" }, { "type": "string", "description": "创建时间", "name": "createdAt", "in": "query" }, { "type": "string", "description": "错误信息", "name": "error_message", "in": "query" }, { "type": "string", "description": "请求ip", "name": "ip", "in": "query" }, { "type": "string", "description": "延迟", "name": "latency", "in": "query" }, { "type": "string", "description": "请求方法", "name": "method", "in": "query" }, { "type": "string", "description": "请求路径", "name": "path", "in": "query" }, { "type": "string", "description": "响应Body", "name": "resp", "in": "query" }, { "type": "integer", "description": "请求状态", "name": "status", "in": "query" }, { "type": "string", "description": "更新时间", "name": "updatedAt", "in": "query" }, { "type": "integer", "description": "用户id", "name": "user_id", "in": "query" } ], "responses": { "200": { "description": "用id查询SysOperationRecord", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/sysOperationRecord/getSysOperationRecordList": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysOperationRecord" ], "summary": "分页获取SysOperationRecord列表", "parameters": [ { "type": "integer", "description": "主键ID", "name": "ID", "in": "query" }, { "type": "string", "description": "代理", "name": "agent", "in": "query" }, { "type": "string", "description": "请求Body", "name": "body", "in": "query" }, { "type": "string", "description": "创建时间", "name": "createdAt", "in": "query" }, { "type": "string", "description": "错误信息", "name": "error_message", "in": "query" }, { "type": "string", "description": "请求ip", "name": "ip", "in": "query" }, { "type": "string", "description": "关键字", "name": "keyword", "in": "query" }, { "type": "string", "description": "延迟", "name": "latency", "in": "query" }, { "type": "string", "description": "请求方法", "name": "method", "in": "query" }, { "type": "integer", "description": "页码", "name": "page", "in": "query" }, { "type": "integer", "description": "每页大小", "name": "pageSize", "in": "query" }, { "type": "string", "description": "请求路径", "name": "path", "in": "query" }, { "type": "string", "description": "响应Body", "name": "resp", "in": "query" }, { "type": "integer", "description": "请求状态", "name": "status", "in": "query" }, { "type": "string", "description": "更新时间", "name": "updatedAt", "in": "query" }, { "type": "integer", "description": "用户id", "name": "user_id", "in": "query" } ], "responses": { "200": { "description": "分页获取SysOperationRecord列表,返回包括列表,总数,页码,每页数量", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.PageResult" }, "msg": { "type": "string" } } } ] } } } } }, "/sysParams/createSysParams": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysParams" ], "summary": "创建参数", "parameters": [ { "description": "创建参数", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysParams" } } ], "responses": { "200": { "description": "创建成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/sysParams/deleteSysParams": { "delete": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysParams" ], "summary": "删除参数", "parameters": [ { "description": "删除参数", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysParams" } } ], "responses": { "200": { "description": "删除成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/sysParams/deleteSysParamsByIds": { "delete": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysParams" ], "summary": "批量删除参数", "responses": { "200": { "description": "批量删除成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/sysParams/findSysParams": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysParams" ], "summary": "用id查询参数", "parameters": [ { "type": "integer", "description": "主键ID", "name": "ID", "in": "query" }, { "type": "string", "description": "创建时间", "name": "createdAt", "in": "query" }, { "type": "string", "description": "参数说明", "name": "desc", "in": "query" }, { "type": "string", "description": "参数键", "name": "key", "in": "query", "required": true }, { "type": "string", "description": "参数名称", "name": "name", "in": "query", "required": true }, { "type": "string", "description": "更新时间", "name": "updatedAt", "in": "query" }, { "type": "string", "description": "参数值", "name": "value", "in": "query", "required": true } ], "responses": { "200": { "description": "查询成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/system.SysParams" }, "msg": { "type": "string" } } } ] } } } } }, "/sysParams/getSysParam": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysParams" ], "summary": "根据key获取参数value", "parameters": [ { "type": "string", "description": "key", "name": "key", "in": "query", "required": true } ], "responses": { "200": { "description": "获取成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/system.SysParams" }, "msg": { "type": "string" } } } ] } } } } }, "/sysParams/getSysParamsList": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysParams" ], "summary": "分页获取参数列表", "parameters": [ { "type": "string", "name": "endCreatedAt", "in": "query" }, { "type": "string", "name": "key", "in": "query" }, { "type": "string", "description": "关键字", "name": "keyword", "in": "query" }, { "type": "string", "name": "name", "in": "query" }, { "type": "integer", "description": "页码", "name": "page", "in": "query" }, { "type": "integer", "description": "每页大小", "name": "pageSize", "in": "query" }, { "type": "string", "name": "startCreatedAt", "in": "query" } ], "responses": { "200": { "description": "获取成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.PageResult" }, "msg": { "type": "string" } } } ] } } } } }, "/sysParams/updateSysParams": { "put": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysParams" ], "summary": "更新参数", "parameters": [ { "description": "更新参数", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysParams" } } ], "responses": { "200": { "description": "更新成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/system/getServerInfo": { "post": { "security": [ { "ApiKeyAuth": [] } ], "produces": [ "application/json" ], "tags": [ "System" ], "summary": "获取服务器信息", "responses": { "200": { "description": "获取服务器信息", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/system/getSystemConfig": { "post": { "security": [ { "ApiKeyAuth": [] } ], "produces": [ "application/json" ], "tags": [ "System" ], "summary": "获取配置文件内容", "responses": { "200": { "description": "获取配置文件内容,返回包括系统配置", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.SysConfigResponse" }, "msg": { "type": "string" } } } ] } } } } }, "/system/reloadSystem": { "post": { "security": [ { "ApiKeyAuth": [] } ], "produces": [ "application/json" ], "tags": [ "System" ], "summary": "重启系统", "responses": { "200": { "description": "重启系统", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/system/setSystemConfig": { "post": { "security": [ { "ApiKeyAuth": [] } ], "produces": [ "application/json" ], "tags": [ "System" ], "summary": "设置配置文件内容", "parameters": [ { "description": "设置配置文件内容", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.System" } } ], "responses": { "200": { "description": "设置配置文件内容", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "string" } } } ] } } } } }, "/user/SetSelfInfo": { "put": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysUser" ], "summary": "设置用户信息", "parameters": [ { "description": "ID, 用户名, 昵称, 头像链接", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysUser" } } ], "responses": { "200": { "description": "设置用户信息", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/user/SetSelfSetting": { "put": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysUser" ], "summary": "设置用户配置", "parameters": [ { "description": "用户配置数据", "name": "data", "in": "body", "required": true, "schema": { "type": "object", "additionalProperties": true } } ], "responses": { "200": { "description": "设置用户配置", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/user/admin_register": { "post": { "produces": [ "application/json" ], "tags": [ "SysUser" ], "summary": "用户注册账号", "parameters": [ { "description": "用户名, 昵称, 密码, 角色ID", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.Register" } } ], "responses": { "200": { "description": "用户注册账号,返回包括用户信息", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.SysUserResponse" }, "msg": { "type": "string" } } } ] } } } } }, "/user/changePassword": { "post": { "security": [ { "ApiKeyAuth": [] } ], "produces": [ "application/json" ], "tags": [ "SysUser" ], "summary": "用户修改密码", "parameters": [ { "description": "用户名, 原密码, 新密码", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.ChangePasswordReq" } } ], "responses": { "200": { "description": "用户修改密码", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/user/deleteUser": { "delete": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysUser" ], "summary": "删除用户", "parameters": [ { "description": "用户ID", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.GetById" } } ], "responses": { "200": { "description": "删除用户", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/user/getUserInfo": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysUser" ], "summary": "获取用户信息", "responses": { "200": { "description": "获取用户信息", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/user/getUserList": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysUser" ], "summary": "分页获取用户列表", "parameters": [ { "description": "页码, 每页大小", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.GetUserList" } } ], "responses": { "200": { "description": "分页获取用户列表,返回包括列表,总数,页码,每页数量", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.PageResult" }, "msg": { "type": "string" } } } ] } } } } }, "/user/resetPassword": { "post": { "security": [ { "ApiKeyAuth": [] } ], "produces": [ "application/json" ], "tags": [ "SysUser" ], "summary": "重置用户密码", "parameters": [ { "description": "ID", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysUser" } } ], "responses": { "200": { "description": "重置用户密码", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/user/setUserAuthorities": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysUser" ], "summary": "设置用户权限", "parameters": [ { "description": "用户UUID, 角色ID", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.SetUserAuthorities" } } ], "responses": { "200": { "description": "设置用户权限", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/user/setUserAuthority": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysUser" ], "summary": "更改用户权限", "parameters": [ { "description": "用户UUID, 角色ID", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.SetUserAuth" } } ], "responses": { "200": { "description": "设置用户权限", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/user/setUserInfo": { "put": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysUser" ], "summary": "设置用户信息", "parameters": [ { "description": "ID, 用户名, 昵称, 头像链接", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysUser" } } ], "responses": { "200": { "description": "设置用户信息", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } } }, "definitions": { "common.JSONMap": { "type": "object", "additionalProperties": true }, "config.AliyunOSS": { "type": "object", "properties": { "access-key-id": { "type": "string" }, "access-key-secret": { "type": "string" }, "base-path": { "type": "string" }, "bucket-name": { "type": "string" }, "bucket-url": { "type": "string" }, "endpoint": { "type": "string" } } }, "config.Autocode": { "type": "object", "properties": { "ai-path": { "type": "string" }, "module": { "type": "string" }, "root": { "type": "string" }, "server": { "type": "string" }, "web": { "type": "string" } } }, "config.AwsS3": { "type": "object", "properties": { "base-url": { "type": "string" }, "bucket": { "type": "string" }, "disable-ssl": { "type": "boolean" }, "endpoint": { "type": "string" }, "path-prefix": { "type": "string" }, "region": { "type": "string" }, "s3-force-path-style": { "type": "boolean" }, "secret-id": { "type": "string" }, "secret-key": { "type": "string" } } }, "config.CORS": { "type": "object", "properties": { "mode": { "type": "string" }, "whitelist": { "type": "array", "items": { "$ref": "#/definitions/config.CORSWhitelist" } } } }, "config.CORSWhitelist": { "type": "object", "properties": { "allow-credentials": { "type": "boolean" }, "allow-headers": { "type": "string" }, "allow-methods": { "type": "string" }, "allow-origin": { "type": "string" }, "expose-headers": { "type": "string" } } }, "config.Captcha": { "type": "object", "properties": { "img-height": { "description": "验证码高度", "type": "integer" }, "img-width": { "description": "验证码宽度", "type": "integer" }, "key-long": { "description": "验证码长度", "type": "integer" }, "open-captcha": { "description": "防爆破验证码开启此数,0代表每次登录都需要验证码,其他数字代表错误密码此数,如3代表错误三次后出现验证码", "type": "integer" }, "open-captcha-timeout": { "description": "防爆破验证码超时时间,单位:s(秒)", "type": "integer" } } }, "config.CloudflareR2": { "type": "object", "properties": { "access-key-id": { "type": "string" }, "account-id": { "type": "string" }, "base-url": { "type": "string" }, "bucket": { "type": "string" }, "path": { "type": "string" }, "secret-access-key": { "type": "string" } } }, "config.DiskList": { "type": "object", "properties": { "mount-point": { "type": "string" } } }, "config.Excel": { "type": "object", "properties": { "dir": { "type": "string" } } }, "config.HuaWeiObs": { "type": "object", "properties": { "access-key": { "type": "string" }, "bucket": { "type": "string" }, "endpoint": { "type": "string" }, "path": { "type": "string" }, "secret-key": { "type": "string" } } }, "config.JWT": { "type": "object", "properties": { "buffer-time": { "description": "缓冲时间", "type": "string" }, "expires-time": { "description": "过期时间", "type": "string" }, "issuer": { "description": "签发者", "type": "string" }, "signing-key": { "description": "jwt签名", "type": "string" } } }, "config.Local": { "type": "object", "properties": { "path": { "description": "本地文件访问路径", "type": "string" }, "store-path": { "description": "本地文件存储路径", "type": "string" } } }, "config.Minio": { "type": "object", "properties": { "access-key-id": { "type": "string" }, "access-key-secret": { "type": "string" }, "base-path": { "type": "string" }, "bucket-name": { "type": "string" }, "bucket-url": { "type": "string" }, "endpoint": { "type": "string" }, "use-ssl": { "type": "boolean" } } }, "config.Mongo": { "type": "object", "properties": { "auth-source": { "description": "验证数据库", "type": "string" }, "coll": { "description": "collection name", "type": "string" }, "connect-timeout-ms": { "description": "连接超时时间", "type": "integer" }, "database": { "description": "database name", "type": "string" }, "hosts": { "description": "主机列表", "type": "array", "items": { "$ref": "#/definitions/config.MongoHost" } }, "is-zap": { "description": "是否开启zap日志", "type": "boolean" }, "max-pool-size": { "description": "最大连接池", "type": "integer" }, "min-pool-size": { "description": "最小连接池", "type": "integer" }, "options": { "description": "mongodb options", "type": "string" }, "password": { "description": "密码", "type": "string" }, "socket-timeout-ms": { "description": "socket超时时间", "type": "integer" }, "username": { "description": "用户名", "type": "string" } } }, "config.MongoHost": { "type": "object", "properties": { "host": { "description": "ip地址", "type": "string" }, "port": { "description": "端口", "type": "string" } } }, "config.Mssql": { "type": "object", "properties": { "config": { "description": "高级配置", "type": "string" }, "db-name": { "description": "数据库名", "type": "string" }, "engine": { "description": "数据库引擎,默认InnoDB", "type": "string", "default": "InnoDB" }, "log-mode": { "description": "是否开启Gorm全局日志", "type": "string" }, "log-zap": { "description": "是否通过zap写入日志文件", "type": "boolean" }, "max-idle-conns": { "description": "空闲中的最大连接数", "type": "integer" }, "max-open-conns": { "description": "打开到数据库的最大连接数", "type": "integer" }, "password": { "description": "数据库密码", "type": "string" }, "path": { "description": "数据库地址", "type": "string" }, "port": { "description": "数据库端口", "type": "string" }, "prefix": { "description": "数据库前缀", "type": "string" }, "singular": { "description": "是否开启全局禁用复数,true表示开启", "type": "boolean" }, "username": { "description": "数据库账号", "type": "string" } } }, "config.Mysql": { "type": "object", "properties": { "config": { "description": "高级配置", "type": "string" }, "db-name": { "description": "数据库名", "type": "string" }, "engine": { "description": "数据库引擎,默认InnoDB", "type": "string", "default": "InnoDB" }, "log-mode": { "description": "是否开启Gorm全局日志", "type": "string" }, "log-zap": { "description": "是否通过zap写入日志文件", "type": "boolean" }, "max-idle-conns": { "description": "空闲中的最大连接数", "type": "integer" }, "max-open-conns": { "description": "打开到数据库的最大连接数", "type": "integer" }, "password": { "description": "数据库密码", "type": "string" }, "path": { "description": "数据库地址", "type": "string" }, "port": { "description": "数据库端口", "type": "string" }, "prefix": { "description": "数据库前缀", "type": "string" }, "singular": { "description": "是否开启全局禁用复数,true表示开启", "type": "boolean" }, "username": { "description": "数据库账号", "type": "string" } } }, "config.Oracle": { "type": "object", "properties": { "config": { "description": "高级配置", "type": "string" }, "db-name": { "description": "数据库名", "type": "string" }, "engine": { "description": "数据库引擎,默认InnoDB", "type": "string", "default": "InnoDB" }, "log-mode": { "description": "是否开启Gorm全局日志", "type": "string" }, "log-zap": { "description": "是否通过zap写入日志文件", "type": "boolean" }, "max-idle-conns": { "description": "空闲中的最大连接数", "type": "integer" }, "max-open-conns": { "description": "打开到数据库的最大连接数", "type": "integer" }, "password": { "description": "数据库密码", "type": "string" }, "path": { "description": "数据库地址", "type": "string" }, "port": { "description": "数据库端口", "type": "string" }, "prefix": { "description": "数据库前缀", "type": "string" }, "singular": { "description": "是否开启全局禁用复数,true表示开启", "type": "boolean" }, "username": { "description": "数据库账号", "type": "string" } } }, "config.Pgsql": { "type": "object", "properties": { "config": { "description": "高级配置", "type": "string" }, "db-name": { "description": "数据库名", "type": "string" }, "engine": { "description": "数据库引擎,默认InnoDB", "type": "string", "default": "InnoDB" }, "log-mode": { "description": "是否开启Gorm全局日志", "type": "string" }, "log-zap": { "description": "是否通过zap写入日志文件", "type": "boolean" }, "max-idle-conns": { "description": "空闲中的最大连接数", "type": "integer" }, "max-open-conns": { "description": "打开到数据库的最大连接数", "type": "integer" }, "password": { "description": "数据库密码", "type": "string" }, "path": { "description": "数据库地址", "type": "string" }, "port": { "description": "数据库端口", "type": "string" }, "prefix": { "description": "数据库前缀", "type": "string" }, "singular": { "description": "是否开启全局禁用复数,true表示开启", "type": "boolean" }, "username": { "description": "数据库账号", "type": "string" } } }, "config.Qiniu": { "type": "object", "properties": { "access-key": { "description": "秘钥AK", "type": "string" }, "bucket": { "description": "空间名称", "type": "string" }, "img-path": { "description": "CDN加速域名", "type": "string" }, "secret-key": { "description": "秘钥SK", "type": "string" }, "use-cdn-domains": { "description": "上传是否使用CDN上传加速", "type": "boolean" }, "use-https": { "description": "是否使用https", "type": "boolean" }, "zone": { "description": "存储区域", "type": "string" } } }, "config.Redis": { "type": "object", "properties": { "addr": { "description": "服务器地址:端口", "type": "string" }, "clusterAddrs": { "description": "集群模式下的节点地址列表", "type": "array", "items": { "type": "string" } }, "db": { "description": "单实例模式下redis的哪个数据库", "type": "integer" }, "name": { "description": "代表当前实例的名字", "type": "string" }, "password": { "description": "密码", "type": "string" }, "useCluster": { "description": "是否使用集群模式", "type": "boolean" } } }, "config.Server": { "type": "object", "properties": { "aliyun-oss": { "$ref": "#/definitions/config.AliyunOSS" }, "autocode": { "description": "auto", "allOf": [ { "$ref": "#/definitions/config.Autocode" } ] }, "aws-s3": { "$ref": "#/definitions/config.AwsS3" }, "captcha": { "$ref": "#/definitions/config.Captcha" }, "cloudflare-r2": { "$ref": "#/definitions/config.CloudflareR2" }, "cors": { "description": "跨域配置", "allOf": [ { "$ref": "#/definitions/config.CORS" } ] }, "db-list": { "type": "array", "items": { "$ref": "#/definitions/config.SpecializedDB" } }, "disk-list": { "type": "array", "items": { "$ref": "#/definitions/config.DiskList" } }, "email": { "$ref": "#/definitions/github_com_flipped-aurora_gin-vue-admin_server_config.Email" }, "excel": { "$ref": "#/definitions/config.Excel" }, "hua-wei-obs": { "$ref": "#/definitions/config.HuaWeiObs" }, "jwt": { "$ref": "#/definitions/config.JWT" }, "local": { "description": "oss", "allOf": [ { "$ref": "#/definitions/config.Local" } ] }, "minio": { "$ref": "#/definitions/config.Minio" }, "mongo": { "$ref": "#/definitions/config.Mongo" }, "mssql": { "$ref": "#/definitions/config.Mssql" }, "mysql": { "description": "gorm", "allOf": [ { "$ref": "#/definitions/config.Mysql" } ] }, "oracle": { "$ref": "#/definitions/config.Oracle" }, "pgsql": { "$ref": "#/definitions/config.Pgsql" }, "qiniu": { "$ref": "#/definitions/config.Qiniu" }, "redis": { "$ref": "#/definitions/config.Redis" }, "redis-list": { "type": "array", "items": { "$ref": "#/definitions/config.Redis" } }, "sqlite": { "$ref": "#/definitions/config.Sqlite" }, "system": { "$ref": "#/definitions/config.System" }, "tencent-cos": { "$ref": "#/definitions/config.TencentCOS" }, "zap": { "$ref": "#/definitions/config.Zap" } } }, "config.SpecializedDB": { "type": "object", "properties": { "alias-name": { "type": "string" }, "config": { "description": "高级配置", "type": "string" }, "db-name": { "description": "数据库名", "type": "string" }, "disable": { "type": "boolean" }, "engine": { "description": "数据库引擎,默认InnoDB", "type": "string", "default": "InnoDB" }, "log-mode": { "description": "是否开启Gorm全局日志", "type": "string" }, "log-zap": { "description": "是否通过zap写入日志文件", "type": "boolean" }, "max-idle-conns": { "description": "空闲中的最大连接数", "type": "integer" }, "max-open-conns": { "description": "打开到数据库的最大连接数", "type": "integer" }, "password": { "description": "数据库密码", "type": "string" }, "path": { "description": "数据库地址", "type": "string" }, "port": { "description": "数据库端口", "type": "string" }, "prefix": { "description": "数据库前缀", "type": "string" }, "singular": { "description": "是否开启全局禁用复数,true表示开启", "type": "boolean" }, "type": { "type": "string" }, "username": { "description": "数据库账号", "type": "string" } } }, "config.Sqlite": { "type": "object", "properties": { "config": { "description": "高级配置", "type": "string" }, "db-name": { "description": "数据库名", "type": "string" }, "engine": { "description": "数据库引擎,默认InnoDB", "type": "string", "default": "InnoDB" }, "log-mode": { "description": "是否开启Gorm全局日志", "type": "string" }, "log-zap": { "description": "是否通过zap写入日志文件", "type": "boolean" }, "max-idle-conns": { "description": "空闲中的最大连接数", "type": "integer" }, "max-open-conns": { "description": "打开到数据库的最大连接数", "type": "integer" }, "password": { "description": "数据库密码", "type": "string" }, "path": { "description": "数据库地址", "type": "string" }, "port": { "description": "数据库端口", "type": "string" }, "prefix": { "description": "数据库前缀", "type": "string" }, "singular": { "description": "是否开启全局禁用复数,true表示开启", "type": "boolean" }, "username": { "description": "数据库账号", "type": "string" } } }, "config.System": { "type": "object", "properties": { "addr": { "description": "端口值", "type": "integer" }, "db-type": { "description": "数据库类型:mysql(默认)|sqlite|sqlserver|postgresql", "type": "string" }, "iplimit-count": { "type": "integer" }, "iplimit-time": { "type": "integer" }, "oss-type": { "description": "Oss类型", "type": "string" }, "router-prefix": { "type": "string" }, "use-mongo": { "description": "使用mongo", "type": "boolean" }, "use-multipoint": { "description": "多点登录拦截", "type": "boolean" }, "use-redis": { "description": "使用redis", "type": "boolean" }, "use-strict-auth": { "description": "使用树形角色分配模式", "type": "boolean" } } }, "config.TencentCOS": { "type": "object", "properties": { "base-url": { "type": "string" }, "bucket": { "type": "string" }, "path-prefix": { "type": "string" }, "region": { "type": "string" }, "secret-id": { "type": "string" }, "secret-key": { "type": "string" } } }, "config.Zap": { "type": "object", "properties": { "director": { "description": "日志文件夹", "type": "string" }, "encode-level": { "description": "编码级", "type": "string" }, "format": { "description": "输出", "type": "string" }, "level": { "description": "级别", "type": "string" }, "log-in-console": { "description": "输出控制台", "type": "boolean" }, "prefix": { "description": "日志前缀", "type": "string" }, "retention-day": { "description": "日志保留天数", "type": "integer" }, "show-line": { "description": "显示行", "type": "boolean" }, "stacktrace-key": { "description": "栈名", "type": "string" } } }, "example.ExaAttachmentCategory": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "children": { "type": "array", "items": { "$ref": "#/definitions/example.ExaAttachmentCategory" } }, "createdAt": { "description": "创建时间", "type": "string" }, "name": { "type": "string" }, "pid": { "type": "integer" }, "updatedAt": { "description": "更新时间", "type": "string" } } }, "example.ExaCustomer": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "createdAt": { "description": "创建时间", "type": "string" }, "customerName": { "description": "客户名", "type": "string" }, "customerPhoneData": { "description": "客户手机号", "type": "string" }, "sysUser": { "description": "管理详情", "allOf": [ { "$ref": "#/definitions/system.SysUser" } ] }, "sysUserAuthorityID": { "description": "管理角色ID", "type": "integer" }, "sysUserId": { "description": "管理ID", "type": "integer" }, "updatedAt": { "description": "更新时间", "type": "string" } } }, "example.ExaFile": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "chunkTotal": { "type": "integer" }, "createdAt": { "description": "创建时间", "type": "string" }, "exaFileChunk": { "type": "array", "items": { "$ref": "#/definitions/example.ExaFileChunk" } }, "fileMd5": { "type": "string" }, "fileName": { "type": "string" }, "filePath": { "type": "string" }, "isFinish": { "type": "boolean" }, "updatedAt": { "description": "更新时间", "type": "string" } } }, "example.ExaFileChunk": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "createdAt": { "description": "创建时间", "type": "string" }, "exaFileID": { "type": "integer" }, "fileChunkNumber": { "type": "integer" }, "fileChunkPath": { "type": "string" }, "updatedAt": { "description": "更新时间", "type": "string" } } }, "example.ExaFileUploadAndDownload": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "classId": { "description": "分类id", "type": "integer" }, "createdAt": { "description": "创建时间", "type": "string" }, "key": { "description": "编号", "type": "string" }, "name": { "description": "文件名", "type": "string" }, "tag": { "description": "文件标签", "type": "string" }, "updatedAt": { "description": "更新时间", "type": "string" }, "url": { "description": "文件地址", "type": "string" } } }, "github_com_flipped-aurora_gin-vue-admin_server_config.Email": { "type": "object", "properties": { "from": { "description": "发件人 你自己要发邮件的邮箱", "type": "string" }, "host": { "description": "服务器地址 例如 smtp.qq.com 请前往QQ或者你要发邮件的邮箱查看其smtp协议", "type": "string" }, "is-ssl": { "description": "是否SSL 是否开启SSL", "type": "boolean" }, "nickname": { "description": "昵称 发件人昵称 通常为自己的邮箱", "type": "string" }, "port": { "description": "端口 请前往QQ或者你要发邮件的邮箱查看其smtp协议 大多为 465", "type": "integer" }, "secret": { "description": "密钥 用于登录的密钥 最好不要用邮箱密码 去邮箱smtp申请一个用于登录的密钥", "type": "string" }, "to": { "description": "收件人:多个以英文逗号分隔 例:a@qq.com b@qq.com 正式开发中请把此项目作为参数使用", "type": "string" } } }, "model.Info": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "attachments": { "description": "附件", "type": "array", "items": { "type": "object" } }, "content": { "description": "内容", "type": "string" }, "createdAt": { "description": "创建时间", "type": "string" }, "title": { "description": "标题", "type": "string" }, "updatedAt": { "description": "更新时间", "type": "string" }, "userID": { "description": "作者", "type": "integer" } } }, "request.AddMenuAuthorityInfo": { "type": "object", "properties": { "authorityId": { "description": "角色ID", "type": "integer" }, "menus": { "type": "array", "items": { "$ref": "#/definitions/system.SysBaseMenu" } } } }, "request.AutoCode": { "type": "object", "properties": { "abbreviation": { "description": "Struct简称", "type": "string", "example": "Struct简称" }, "autoCreateApiToSql": { "description": "是否自动创建api", "type": "boolean", "example": false }, "autoCreateBtnAuth": { "description": "是否自动创建按钮权限", "type": "boolean", "example": false }, "autoCreateMenuToSql": { "description": "是否自动创建menu", "type": "boolean", "example": false }, "autoCreateResource": { "description": "是否自动创建资源标识", "type": "boolean", "example": false }, "autoMigrate": { "description": "是否自动迁移表结构", "type": "boolean", "example": false }, "businessDB": { "description": "业务数据库", "type": "string", "example": "业务数据库" }, "description": { "description": "Struct中文名称", "type": "string", "example": "Struct中文名称" }, "fields": { "type": "array", "items": { "$ref": "#/definitions/request.AutoCodeField" } }, "generateServer": { "description": "是否生成server", "type": "boolean", "example": true }, "generateWeb": { "description": "是否生成web", "type": "boolean", "example": true }, "gvaModel": { "description": "是否使用gva默认Model", "type": "boolean", "example": false }, "humpPackageName": { "description": "go文件名称", "type": "string", "example": "go文件名称" }, "isAdd": { "description": "是否新增", "type": "boolean", "example": false }, "isTree": { "description": "是否树形结构", "type": "boolean", "example": false }, "onlyTemplate": { "description": "是否只生成模板", "type": "boolean", "example": false }, "package": { "type": "string" }, "packageName": { "description": "文件名称", "type": "string", "example": "文件名称" }, "primaryField": { "$ref": "#/definitions/request.AutoCodeField" }, "structName": { "description": "Struct名称", "type": "string", "example": "Struct名称" }, "tableName": { "description": "表名", "type": "string", "example": "表名" }, "treeJson": { "description": "展示的树json字段", "type": "string", "example": "展示的树json字段" } } }, "request.AutoCodeField": { "type": "object", "properties": { "checkDataSource": { "description": "是否检查数据源", "type": "boolean" }, "clearable": { "description": "是否可清空", "type": "boolean" }, "columnName": { "description": "数据库字段", "type": "string" }, "comment": { "description": "数据库字段描述", "type": "string" }, "dataSource": { "description": "数据源", "allOf": [ { "$ref": "#/definitions/request.DataSource" } ] }, "dataTypeLong": { "description": "数据库字段长度", "type": "string" }, "defaultValue": { "description": "是否必填", "type": "string" }, "desc": { "description": "是否前端详情", "type": "boolean" }, "dictType": { "description": "字典", "type": "string" }, "errorText": { "description": "校验失败文字", "type": "string" }, "excel": { "description": "是否导入/导出", "type": "boolean" }, "fieldDesc": { "description": "中文名", "type": "string" }, "fieldIndexType": { "description": "索引类型", "type": "string" }, "fieldJson": { "description": "FieldJson", "type": "string" }, "fieldName": { "description": "Field名", "type": "string" }, "fieldSearchHide": { "description": "是否隐藏查询条件", "type": "boolean" }, "fieldSearchType": { "description": "搜索条件", "type": "string" }, "fieldType": { "description": "Field数据类型", "type": "string" }, "form": { "description": "Front bool ` + "`" + `json:\"front\"` + "`" + ` // 是否前端可见", "type": "boolean" }, "primaryKey": { "description": "是否主键", "type": "boolean" }, "require": { "description": "是否必填", "type": "boolean" }, "sort": { "description": "是否增加排序", "type": "boolean" }, "table": { "description": "是否前端表格列", "type": "boolean" } } }, "request.CasbinInReceive": { "type": "object", "properties": { "authorityId": { "description": "权限id", "type": "integer" }, "casbinInfos": { "type": "array", "items": { "$ref": "#/definitions/request.CasbinInfo" } } } }, "request.CasbinInfo": { "type": "object", "properties": { "method": { "description": "方法", "type": "string" }, "path": { "description": "路径", "type": "string" } } }, "request.ChangePasswordReq": { "type": "object", "properties": { "newPassword": { "description": "新密码", "type": "string" }, "password": { "description": "密码", "type": "string" } } }, "request.DataSource": { "type": "object", "properties": { "association": { "description": "关联关系 1 一对一 2 一对多", "type": "integer" }, "dbName": { "type": "string" }, "hasDeletedAt": { "type": "boolean" }, "label": { "type": "string" }, "table": { "type": "string" }, "value": { "type": "string" } } }, "request.Empty": { "type": "object" }, "request.ExaAttachmentCategorySearch": { "type": "object", "properties": { "classId": { "type": "integer" }, "keyword": { "description": "关键字", "type": "string" }, "page": { "description": "页码", "type": "integer" }, "pageSize": { "description": "每页大小", "type": "integer" } } }, "request.GetAuthorityId": { "type": "object", "properties": { "authorityId": { "description": "角色ID", "type": "integer" } } }, "request.GetById": { "type": "object", "properties": { "id": { "description": "主键ID", "type": "integer" } } }, "request.GetUserList": { "type": "object", "properties": { "email": { "type": "string" }, "keyword": { "description": "关键字", "type": "string" }, "nickName": { "type": "string" }, "page": { "description": "页码", "type": "integer" }, "pageSize": { "description": "每页大小", "type": "integer" }, "phone": { "type": "string" }, "username": { "type": "string" } } }, "request.IdsReq": { "type": "object", "properties": { "ids": { "type": "array", "items": { "type": "integer" } } } }, "request.InitDB": { "type": "object", "required": [ "adminPassword", "dbName" ], "properties": { "adminPassword": { "type": "string" }, "dbName": { "description": "数据库名", "type": "string" }, "dbPath": { "description": "sqlite数据库文件路径", "type": "string" }, "dbType": { "description": "数据库类型", "type": "string" }, "host": { "description": "服务器地址", "type": "string" }, "password": { "description": "数据库密码", "type": "string" }, "port": { "description": "数据库连接端口", "type": "string" }, "template": { "description": "postgresql指定template", "type": "string" }, "userName": { "description": "数据库用户名", "type": "string" } } }, "request.Login": { "type": "object", "properties": { "captcha": { "description": "验证码", "type": "string" }, "captchaId": { "description": "验证码ID", "type": "string" }, "password": { "description": "密码", "type": "string" }, "username": { "description": "用户名", "type": "string" } } }, "request.PageInfo": { "type": "object", "properties": { "keyword": { "description": "关键字", "type": "string" }, "page": { "description": "页码", "type": "integer" }, "pageSize": { "description": "每页大小", "type": "integer" } } }, "request.Register": { "type": "object", "properties": { "authorityId": { "type": "string", "example": "int 角色id" }, "authorityIds": { "type": "string", "example": "[]uint 角色id" }, "email": { "type": "string", "example": "电子邮箱" }, "enable": { "type": "string", "example": "int 是否启用" }, "headerImg": { "type": "string", "example": "头像链接" }, "nickName": { "type": "string", "example": "昵称" }, "passWord": { "type": "string", "example": "密码" }, "phone": { "type": "string", "example": "电话号码" }, "userName": { "type": "string", "example": "用户名" } } }, "request.SearchApiParams": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "apiGroup": { "description": "api组", "type": "string" }, "createdAt": { "description": "创建时间", "type": "string" }, "desc": { "description": "排序方式:升序false(默认)|降序true", "type": "boolean" }, "description": { "description": "api中文描述", "type": "string" }, "keyword": { "description": "关键字", "type": "string" }, "method": { "description": "方法:创建POST(默认)|查看GET|更新PUT|删除DELETE", "type": "string" }, "orderKey": { "description": "排序", "type": "string" }, "page": { "description": "页码", "type": "integer" }, "pageSize": { "description": "每页大小", "type": "integer" }, "path": { "description": "api路径", "type": "string" }, "updatedAt": { "description": "更新时间", "type": "string" } } }, "request.SetUserAuth": { "type": "object", "properties": { "authorityId": { "description": "角色ID", "type": "integer" } } }, "request.SetUserAuthorities": { "type": "object", "properties": { "authorityIds": { "description": "角色ID", "type": "array", "items": { "type": "integer" } }, "id": { "type": "integer" } } }, "request.SysAuthorityBtnReq": { "type": "object", "properties": { "authorityId": { "type": "integer" }, "menuID": { "type": "integer" }, "selected": { "type": "array", "items": { "type": "integer" } } } }, "request.SysAutoCodePackageCreate": { "type": "object", "properties": { "desc": { "type": "string", "example": "描述" }, "label": { "type": "string", "example": "展示名" }, "packageName": { "type": "string", "example": "包名" }, "template": { "type": "string", "example": "模版" } } }, "request.SysAutoHistoryRollBack": { "type": "object", "properties": { "deleteApi": { "description": "是否删除接口", "type": "boolean" }, "deleteMenu": { "description": "是否删除菜单", "type": "boolean" }, "deleteTable": { "description": "是否删除表", "type": "boolean" }, "id": { "description": "主键ID", "type": "integer" } } }, "response.Email": { "type": "object", "properties": { "body": { "description": "邮件内容", "type": "string" }, "subject": { "description": "邮件标题", "type": "string" }, "to": { "description": "邮件发送给谁", "type": "string" } } }, "response.ExaCustomerResponse": { "type": "object", "properties": { "customer": { "$ref": "#/definitions/example.ExaCustomer" } } }, "response.ExaFileResponse": { "type": "object", "properties": { "file": { "$ref": "#/definitions/example.ExaFileUploadAndDownload" } } }, "response.FilePathResponse": { "type": "object", "properties": { "filePath": { "type": "string" } } }, "response.FileResponse": { "type": "object", "properties": { "file": { "$ref": "#/definitions/example.ExaFile" } } }, "response.LoginResponse": { "type": "object", "properties": { "expiresAt": { "type": "integer" }, "token": { "type": "string" }, "user": { "$ref": "#/definitions/system.SysUser" } } }, "response.PageResult": { "type": "object", "properties": { "list": {}, "page": { "type": "integer" }, "pageSize": { "type": "integer" }, "total": { "type": "integer" } } }, "response.PolicyPathResponse": { "type": "object", "properties": { "paths": { "type": "array", "items": { "$ref": "#/definitions/request.CasbinInfo" } } } }, "response.Response": { "type": "object", "properties": { "code": { "type": "integer" }, "data": {}, "msg": { "type": "string" } } }, "response.SysAPIListResponse": { "type": "object", "properties": { "apis": { "type": "array", "items": { "$ref": "#/definitions/system.SysApi" } } } }, "response.SysAPIResponse": { "type": "object", "properties": { "api": { "$ref": "#/definitions/system.SysApi" } } }, "response.SysAuthorityBtnRes": { "type": "object", "properties": { "selected": { "type": "array", "items": { "type": "integer" } } } }, "response.SysAuthorityCopyResponse": { "type": "object", "properties": { "authority": { "$ref": "#/definitions/system.SysAuthority" }, "oldAuthorityId": { "description": "旧角色ID", "type": "integer" } } }, "response.SysAuthorityResponse": { "type": "object", "properties": { "authority": { "$ref": "#/definitions/system.SysAuthority" } } }, "response.SysBaseMenuResponse": { "type": "object", "properties": { "menu": { "$ref": "#/definitions/system.SysBaseMenu" } } }, "response.SysBaseMenusResponse": { "type": "object", "properties": { "menus": { "type": "array", "items": { "$ref": "#/definitions/system.SysBaseMenu" } } } }, "response.SysCaptchaResponse": { "type": "object", "properties": { "captchaId": { "type": "string" }, "captchaLength": { "type": "integer" }, "openCaptcha": { "type": "boolean" }, "picPath": { "type": "string" } } }, "response.SysConfigResponse": { "type": "object", "properties": { "config": { "$ref": "#/definitions/config.Server" } } }, "response.SysMenusResponse": { "type": "object", "properties": { "menus": { "type": "array", "items": { "$ref": "#/definitions/system.SysMenu" } } } }, "response.SysUserResponse": { "type": "object", "properties": { "user": { "$ref": "#/definitions/system.SysUser" } } }, "system.Condition": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "column": { "type": "string" }, "createdAt": { "description": "创建时间", "type": "string" }, "from": { "type": "string" }, "operator": { "type": "string" }, "templateID": { "type": "string" }, "updatedAt": { "description": "更新时间", "type": "string" } } }, "system.JoinTemplate": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "createdAt": { "description": "创建时间", "type": "string" }, "joins": { "type": "string" }, "on": { "type": "string" }, "table": { "type": "string" }, "templateID": { "type": "string" }, "updatedAt": { "description": "更新时间", "type": "string" } } }, "system.Meta": { "type": "object", "properties": { "activeName": { "type": "string" }, "closeTab": { "description": "自动关闭tab", "type": "boolean" }, "defaultMenu": { "description": "是否是基础路由(开发中)", "type": "boolean" }, "icon": { "description": "菜单图标", "type": "string" }, "keepAlive": { "description": "是否缓存", "type": "boolean" }, "title": { "description": "菜单名", "type": "string" } } }, "system.SysApi": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "apiGroup": { "description": "api组", "type": "string" }, "createdAt": { "description": "创建时间", "type": "string" }, "description": { "description": "api中文描述", "type": "string" }, "method": { "description": "方法:创建POST(默认)|查看GET|更新PUT|删除DELETE", "type": "string" }, "path": { "description": "api路径", "type": "string" }, "updatedAt": { "description": "更新时间", "type": "string" } } }, "system.SysAuthority": { "type": "object", "properties": { "authorityId": { "description": "角色ID", "type": "integer" }, "authorityName": { "description": "角色名", "type": "string" }, "children": { "type": "array", "items": { "$ref": "#/definitions/system.SysAuthority" } }, "createdAt": { "description": "创建时间", "type": "string" }, "dataAuthorityId": { "type": "array", "items": { "$ref": "#/definitions/system.SysAuthority" } }, "defaultRouter": { "description": "默认菜单(默认dashboard)", "type": "string" }, "deletedAt": { "type": "string" }, "menus": { "type": "array", "items": { "$ref": "#/definitions/system.SysBaseMenu" } }, "parentId": { "description": "父角色ID", "type": "integer" }, "updatedAt": { "description": "更新时间", "type": "string" } } }, "system.SysBaseMenu": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "authoritys": { "type": "array", "items": { "$ref": "#/definitions/system.SysAuthority" } }, "children": { "type": "array", "items": { "$ref": "#/definitions/system.SysBaseMenu" } }, "component": { "description": "对应前端文件路径", "type": "string" }, "createdAt": { "description": "创建时间", "type": "string" }, "hidden": { "description": "是否在列表隐藏", "type": "boolean" }, "menuBtn": { "type": "array", "items": { "$ref": "#/definitions/system.SysBaseMenuBtn" } }, "meta": { "description": "附加属性", "allOf": [ { "$ref": "#/definitions/system.Meta" } ] }, "name": { "description": "路由name", "type": "string" }, "parameters": { "type": "array", "items": { "$ref": "#/definitions/system.SysBaseMenuParameter" } }, "parentId": { "description": "父菜单ID", "type": "integer" }, "path": { "description": "路由path", "type": "string" }, "sort": { "description": "排序标记", "type": "integer" }, "updatedAt": { "description": "更新时间", "type": "string" } } }, "system.SysBaseMenuBtn": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "createdAt": { "description": "创建时间", "type": "string" }, "desc": { "type": "string" }, "name": { "type": "string" }, "sysBaseMenuID": { "type": "integer" }, "updatedAt": { "description": "更新时间", "type": "string" } } }, "system.SysBaseMenuParameter": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "createdAt": { "description": "创建时间", "type": "string" }, "key": { "description": "地址栏携带参数的key", "type": "string" }, "sysBaseMenuID": { "type": "integer" }, "type": { "description": "地址栏携带参数为params还是query", "type": "string" }, "updatedAt": { "description": "更新时间", "type": "string" }, "value": { "description": "地址栏携带参数的值", "type": "string" } } }, "system.SysDictionary": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "createdAt": { "description": "创建时间", "type": "string" }, "desc": { "description": "描述", "type": "string" }, "name": { "description": "字典名(中)", "type": "string" }, "status": { "description": "状态", "type": "boolean" }, "sysDictionaryDetails": { "type": "array", "items": { "$ref": "#/definitions/system.SysDictionaryDetail" } }, "type": { "description": "字典名(英)", "type": "string" }, "updatedAt": { "description": "更新时间", "type": "string" } } }, "system.SysDictionaryDetail": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "createdAt": { "description": "创建时间", "type": "string" }, "extend": { "description": "扩展值", "type": "string" }, "label": { "description": "展示值", "type": "string" }, "sort": { "description": "排序标记", "type": "integer" }, "status": { "description": "启用状态", "type": "boolean" }, "sysDictionaryID": { "description": "关联标记", "type": "integer" }, "updatedAt": { "description": "更新时间", "type": "string" }, "value": { "description": "字典值", "type": "string" } } }, "system.SysExportTemplate": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "conditions": { "type": "array", "items": { "$ref": "#/definitions/system.Condition" } }, "createdAt": { "description": "创建时间", "type": "string" }, "dbName": { "description": "数据库名称", "type": "string" }, "joinTemplate": { "type": "array", "items": { "$ref": "#/definitions/system.JoinTemplate" } }, "limit": { "type": "integer" }, "name": { "description": "模板名称", "type": "string" }, "order": { "type": "string" }, "tableName": { "description": "表名称", "type": "string" }, "templateID": { "description": "模板标识", "type": "string" }, "templateInfo": { "description": "模板信息", "type": "string" }, "updatedAt": { "description": "更新时间", "type": "string" } } }, "system.SysMenu": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "authoritys": { "type": "array", "items": { "$ref": "#/definitions/system.SysAuthority" } }, "btns": { "type": "object", "additionalProperties": { "type": "integer" } }, "children": { "type": "array", "items": { "$ref": "#/definitions/system.SysMenu" } }, "component": { "description": "对应前端文件路径", "type": "string" }, "createdAt": { "description": "创建时间", "type": "string" }, "hidden": { "description": "是否在列表隐藏", "type": "boolean" }, "menuBtn": { "type": "array", "items": { "$ref": "#/definitions/system.SysBaseMenuBtn" } }, "menuId": { "type": "integer" }, "meta": { "description": "附加属性", "allOf": [ { "$ref": "#/definitions/system.Meta" } ] }, "name": { "description": "路由name", "type": "string" }, "parameters": { "type": "array", "items": { "$ref": "#/definitions/system.SysBaseMenuParameter" } }, "parentId": { "description": "父菜单ID", "type": "integer" }, "path": { "description": "路由path", "type": "string" }, "sort": { "description": "排序标记", "type": "integer" }, "updatedAt": { "description": "更新时间", "type": "string" } } }, "system.SysOperationRecord": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "agent": { "description": "代理", "type": "string" }, "body": { "description": "请求Body", "type": "string" }, "createdAt": { "description": "创建时间", "type": "string" }, "error_message": { "description": "错误信息", "type": "string" }, "ip": { "description": "请求ip", "type": "string" }, "latency": { "description": "延迟", "type": "string" }, "method": { "description": "请求方法", "type": "string" }, "path": { "description": "请求路径", "type": "string" }, "resp": { "description": "响应Body", "type": "string" }, "status": { "description": "请求状态", "type": "integer" }, "updatedAt": { "description": "更新时间", "type": "string" }, "user": { "$ref": "#/definitions/system.SysUser" }, "user_id": { "description": "用户id", "type": "integer" } } }, "system.SysParams": { "type": "object", "required": [ "key", "name", "value" ], "properties": { "ID": { "description": "主键ID", "type": "integer" }, "createdAt": { "description": "创建时间", "type": "string" }, "desc": { "description": "参数说明", "type": "string" }, "key": { "description": "参数键", "type": "string" }, "name": { "description": "参数名称", "type": "string" }, "updatedAt": { "description": "更新时间", "type": "string" }, "value": { "description": "参数值", "type": "string" } } }, "system.SysUser": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "authorities": { "description": "多用户角色", "type": "array", "items": { "$ref": "#/definitions/system.SysAuthority" } }, "authority": { "description": "用户角色", "allOf": [ { "$ref": "#/definitions/system.SysAuthority" } ] }, "authorityId": { "description": "用户角色ID", "type": "integer" }, "createdAt": { "description": "创建时间", "type": "string" }, "email": { "description": "用户邮箱", "type": "string" }, "enable": { "description": "用户是否被冻结 1正常 2冻结", "type": "integer" }, "headerImg": { "description": "用户头像", "type": "string" }, "nickName": { "description": "用户昵称", "type": "string" }, "originSetting": { "description": "配置", "allOf": [ { "$ref": "#/definitions/common.JSONMap" } ] }, "phone": { "description": "用户手机号", "type": "string" }, "updatedAt": { "description": "更新时间", "type": "string" }, "userName": { "description": "用户登录名", "type": "string" }, "uuid": { "description": "用户UUID", "type": "string" } } }, "system.System": { "type": "object", "properties": { "config": { "$ref": "#/definitions/config.Server" } } } }, "securityDefinitions": { "ApiKeyAuth": { "type": "apiKey", "name": "x-token", "in": "header" } }, "tags": [ { "name": "Base" }, { "description": "用户", "name": "SysUser" } ] }` // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ Version: global.Version, Host: "", BasePath: "", Schemes: []string{}, Title: "Gin-Vue-Admin Swagger API接口文档", Description: "使用gin+vue进行极速开发的全栈开发基础平台", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, } func init() { swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) } ================================================ FILE: server/docs/swagger.json ================================================ { "swagger": "2.0", "info": { "description": "使用gin+vue进行极速开发的全栈开发基础平台", "title": "Gin-Vue-Admin Swagger API接口文档", "contact": {}, "version": "v2.7.9-beta" }, "paths": { "/api/createApi": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysApi" ], "summary": "创建基础api", "parameters": [ { "description": "api路径, api中文描述, api组, 方法", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysApi" } } ], "responses": { "200": { "description": "创建基础api", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/api/deleteApi": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysApi" ], "summary": "删除api", "parameters": [ { "description": "ID", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysApi" } } ], "responses": { "200": { "description": "删除api", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/api/deleteApisByIds": { "delete": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysApi" ], "summary": "删除选中Api", "parameters": [ { "description": "ID", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.IdsReq" } } ], "responses": { "200": { "description": "删除选中Api", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/api/enterSyncApi": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysApi" ], "summary": "确认同步API", "responses": { "200": { "description": "确认同步API", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/api/freshCasbin": { "get": { "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysApi" ], "summary": "刷新casbin缓存", "responses": { "200": { "description": "刷新成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/api/getAllApis": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysApi" ], "summary": "获取所有的Api 不分页", "responses": { "200": { "description": "获取所有的Api 不分页,返回包括api列表", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.SysAPIListResponse" }, "msg": { "type": "string" } } } ] } } } } }, "/api/getApiById": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysApi" ], "summary": "根据id获取api", "parameters": [ { "description": "根据id获取api", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.GetById" } } ], "responses": { "200": { "description": "根据id获取api,返回包括api详情", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.SysAPIResponse" } } } ] } } } } }, "/api/getApiGroups": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysApi" ], "summary": "获取API分组", "responses": { "200": { "description": "获取API分组", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/api/getApiList": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysApi" ], "summary": "分页获取API列表", "parameters": [ { "description": "分页获取API列表", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.SearchApiParams" } } ], "responses": { "200": { "description": "分页获取API列表,返回包括列表,总数,页码,每页数量", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.PageResult" }, "msg": { "type": "string" } } } ] } } } } }, "/api/ignoreApi": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "IgnoreApi" ], "summary": "忽略API", "responses": { "200": { "description": "同步API", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/api/syncApi": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysApi" ], "summary": "同步API", "responses": { "200": { "description": "同步API", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/api/updateApi": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysApi" ], "summary": "修改基础api", "parameters": [ { "description": "api路径, api中文描述, api组, 方法", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysApi" } } ], "responses": { "200": { "description": "修改基础api", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/attachmentCategory/addCategory": { "post": { "security": [ { "AttachmentCategory": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AddCategory" ], "summary": "添加媒体库分类", "parameters": [ { "description": "媒体库分类数据", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/example.ExaAttachmentCategory" } } ], "responses": {} } }, "/attachmentCategory/deleteCategory": { "post": { "security": [ { "AttachmentCategory": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "DeleteCategory" ], "summary": "删除分类", "parameters": [ { "description": "分类id", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.GetById" } } ], "responses": { "200": { "description": "删除分类", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/attachmentCategory/getCategoryList": { "get": { "security": [ { "AttachmentCategory": [] } ], "produces": [ "application/json" ], "tags": [ "GetCategoryList" ], "summary": "媒体库分类列表", "responses": { "200": { "description": "媒体库分类列表", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/example.ExaAttachmentCategory" }, "msg": { "type": "string" } } } ] } } } } }, "/authority/copyAuthority": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Authority" ], "summary": "拷贝角色", "parameters": [ { "description": "旧角色id, 新权限id, 新权限名, 新父角色id", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/response.SysAuthorityCopyResponse" } } ], "responses": { "200": { "description": "拷贝角色,返回包括系统角色详情", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.SysAuthorityResponse" }, "msg": { "type": "string" } } } ] } } } } }, "/authority/createAuthority": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Authority" ], "summary": "创建角色", "parameters": [ { "description": "权限id, 权限名, 父角色id", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysAuthority" } } ], "responses": { "200": { "description": "创建角色,返回包括系统角色详情", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.SysAuthorityResponse" }, "msg": { "type": "string" } } } ] } } } } }, "/authority/deleteAuthority": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Authority" ], "summary": "删除角色", "parameters": [ { "description": "删除角色", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysAuthority" } } ], "responses": { "200": { "description": "删除角色", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/authority/getAuthorityList": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Authority" ], "summary": "分页获取角色列表", "parameters": [ { "description": "页码, 每页大小", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.PageInfo" } } ], "responses": { "200": { "description": "分页获取角色列表,返回包括列表,总数,页码,每页数量", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.PageResult" }, "msg": { "type": "string" } } } ] } } } } }, "/authority/setDataAuthority": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Authority" ], "summary": "设置角色资源权限", "parameters": [ { "description": "设置角色资源权限", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysAuthority" } } ], "responses": { "200": { "description": "设置角色资源权限", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/authority/updateAuthority": { "put": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Authority" ], "summary": "更新角色信息", "parameters": [ { "description": "权限id, 权限名, 父角色id", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysAuthority" } } ], "responses": { "200": { "description": "更新角色信息,返回包括系统角色详情", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.SysAuthorityResponse" }, "msg": { "type": "string" } } } ] } } } } }, "/authorityBtn/canRemoveAuthorityBtn": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AuthorityBtn" ], "summary": "设置权限按钮", "responses": { "200": { "description": "删除成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/authorityBtn/getAuthorityBtn": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AuthorityBtn" ], "summary": "获取权限按钮", "parameters": [ { "description": "菜单id, 角色id, 选中的按钮id", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.SysAuthorityBtnReq" } } ], "responses": { "200": { "description": "返回列表成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.SysAuthorityBtnRes" }, "msg": { "type": "string" } } } ] } } } } }, "/authorityBtn/setAuthorityBtn": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AuthorityBtn" ], "summary": "设置权限按钮", "parameters": [ { "description": "菜单id, 角色id, 选中的按钮id", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.SysAuthorityBtnReq" } } ], "responses": { "200": { "description": "返回列表成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/autoCode/addFunc": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AddFunc" ], "summary": "增加方法", "parameters": [ { "description": "增加方法", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.AutoCode" } } ], "responses": { "200": { "description": "{\"success\":true,\"data\":{},\"msg\":\"创建成功\"}", "schema": { "type": "string" } } } } }, "/autoCode/createPackage": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AutoCodePackage" ], "summary": "创建package", "parameters": [ { "description": "创建package", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.SysAutoCodePackageCreate" } } ], "responses": { "200": { "description": "创建package成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/autoCode/createTemp": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AutoCodeTemplate" ], "summary": "自动代码模板", "parameters": [ { "description": "创建自动代码", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.AutoCode" } } ], "responses": { "200": { "description": "{\"success\":true,\"data\":{},\"msg\":\"创建成功\"}", "schema": { "type": "string" } } } } }, "/autoCode/delPackage": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AutoCode" ], "summary": "删除package", "parameters": [ { "description": "创建package", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.GetById" } } ], "responses": { "200": { "description": "删除package成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/autoCode/delSysHistory": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AutoCode" ], "summary": "删除回滚记录", "parameters": [ { "description": "请求参数", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.GetById" } } ], "responses": { "200": { "description": "删除回滚记录", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/autoCode/getColumn": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AutoCode" ], "summary": "获取当前表所有字段", "responses": { "200": { "description": "获取当前表所有字段", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/autoCode/getDB": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AutoCode" ], "summary": "获取当前所有数据库", "responses": { "200": { "description": "获取当前所有数据库", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/autoCode/getMeta": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AutoCode" ], "summary": "获取meta信息", "parameters": [ { "description": "请求参数", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.GetById" } } ], "responses": { "200": { "description": "获取meta信息", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/autoCode/getPackage": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AutoCodePackage" ], "summary": "获取package", "responses": { "200": { "description": "创建package成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/autoCode/getSysHistory": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AutoCode" ], "summary": "查询回滚记录", "parameters": [ { "description": "请求参数", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.PageInfo" } } ], "responses": { "200": { "description": "查询回滚记录,返回包括列表,总数,页码,每页数量", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.PageResult" }, "msg": { "type": "string" } } } ] } } } } }, "/autoCode/getTables": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AutoCode" ], "summary": "获取当前数据库所有表", "responses": { "200": { "description": "获取当前数据库所有表", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/autoCode/getTemplates": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AutoCodePackage" ], "summary": "获取package", "responses": { "200": { "description": "创建package成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/autoCode/initAPI": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AutoCodePlugin" ], "summary": "打包插件", "responses": { "200": { "description": "打包插件成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/autoCode/initMenu": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AutoCodePlugin" ], "summary": "打包插件", "responses": { "200": { "description": "打包插件成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/autoCode/installPlugin": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "multipart/form-data" ], "produces": [ "application/json" ], "tags": [ "AutoCodePlugin" ], "summary": "安装插件", "parameters": [ { "type": "file", "description": "this is a test file", "name": "plug", "in": "formData", "required": true } ], "responses": { "200": { "description": "安装插件成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "object" } }, "msg": { "type": "string" } } } ] } } } } }, "/autoCode/preview": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AutoCodeTemplate" ], "summary": "预览创建后的代码", "parameters": [ { "description": "预览创建代码", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.AutoCode" } } ], "responses": { "200": { "description": "预览创建后的代码", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/autoCode/pubPlug": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AutoCodePlugin" ], "summary": "打包插件", "parameters": [ { "type": "string", "description": "插件名称", "name": "plugName", "in": "query", "required": true } ], "responses": { "200": { "description": "打包插件成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/autoCode/rollback": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AutoCode" ], "summary": "回滚自动生成代码", "parameters": [ { "description": "请求参数", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.SysAutoHistoryRollBack" } } ], "responses": { "200": { "description": "回滚自动生成代码", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/base/captcha": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Base" ], "summary": "生成验证码", "responses": { "200": { "description": "生成验证码,返回包括随机数id,base64,验证码长度,是否开启验证码", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.SysCaptchaResponse" }, "msg": { "type": "string" } } } ] } } } } }, "/base/login": { "post": { "produces": [ "application/json" ], "tags": [ "Base" ], "summary": "用户登录", "parameters": [ { "description": "用户名, 密码, 验证码", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.Login" } } ], "responses": { "200": { "description": "返回包括用户信息,token,过期时间", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.LoginResponse" }, "msg": { "type": "string" } } } ] } } } } }, "/casbin/UpdateCasbin": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Casbin" ], "summary": "更新角色api权限", "parameters": [ { "description": "权限id, 权限模型列表", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.CasbinInReceive" } } ], "responses": { "200": { "description": "更新角色api权限", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/casbin/getPolicyPathByAuthorityId": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Casbin" ], "summary": "获取权限列表", "parameters": [ { "description": "权限id, 权限模型列表", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.CasbinInReceive" } } ], "responses": { "200": { "description": "获取权限列表,返回包括casbin详情列表", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.PolicyPathResponse" }, "msg": { "type": "string" } } } ] } } } } }, "/customer/customer": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "ExaCustomer" ], "summary": "获取单一客户信息", "parameters": [ { "type": "integer", "description": "主键ID", "name": "ID", "in": "query" }, { "type": "string", "description": "创建时间", "name": "createdAt", "in": "query" }, { "type": "string", "description": "客户名", "name": "customerName", "in": "query" }, { "type": "string", "description": "客户手机号", "name": "customerPhoneData", "in": "query" }, { "type": "integer", "description": "管理角色ID", "name": "sysUserAuthorityID", "in": "query" }, { "type": "integer", "description": "管理ID", "name": "sysUserId", "in": "query" }, { "type": "string", "description": "更新时间", "name": "updatedAt", "in": "query" } ], "responses": { "200": { "description": "获取单一客户信息,返回包括客户详情", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.ExaCustomerResponse" }, "msg": { "type": "string" } } } ] } } } }, "put": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "ExaCustomer" ], "summary": "更新客户信息", "parameters": [ { "description": "客户ID, 客户信息", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/example.ExaCustomer" } } ], "responses": { "200": { "description": "更新客户信息", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } }, "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "ExaCustomer" ], "summary": "创建客户", "parameters": [ { "description": "客户用户名, 客户手机号码", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/example.ExaCustomer" } } ], "responses": { "200": { "description": "创建客户", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } }, "delete": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "ExaCustomer" ], "summary": "删除客户", "parameters": [ { "description": "客户ID", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/example.ExaCustomer" } } ], "responses": { "200": { "description": "删除客户", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/customer/customerList": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "ExaCustomer" ], "summary": "分页获取权限客户列表", "parameters": [ { "type": "string", "description": "关键字", "name": "keyword", "in": "query" }, { "type": "integer", "description": "页码", "name": "page", "in": "query" }, { "type": "integer", "description": "每页大小", "name": "pageSize", "in": "query" } ], "responses": { "200": { "description": "分页获取权限客户列表,返回包括列表,总数,页码,每页数量", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.PageResult" }, "msg": { "type": "string" } } } ] } } } } }, "/email/emailTest": { "post": { "security": [ { "ApiKeyAuth": [] } ], "produces": [ "application/json" ], "tags": [ "System" ], "summary": "发送测试邮件", "responses": { "200": { "description": "{\"success\":true,\"data\":{},\"msg\":\"发送成功\"}", "schema": { "type": "string" } } } } }, "/email/sendEmail": { "post": { "security": [ { "ApiKeyAuth": [] } ], "produces": [ "application/json" ], "tags": [ "System" ], "summary": "发送邮件", "parameters": [ { "description": "发送邮件必须的参数", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/response.Email" } } ], "responses": { "200": { "description": "{\"success\":true,\"data\":{},\"msg\":\"发送成功\"}", "schema": { "type": "string" } } } } }, "/fileUploadAndDownload/breakpointContinue": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "multipart/form-data" ], "produces": [ "application/json" ], "tags": [ "ExaFileUploadAndDownload" ], "summary": "断点续传到服务器", "parameters": [ { "type": "file", "description": "an example for breakpoint resume, 断点续传示例", "name": "file", "in": "formData", "required": true } ], "responses": { "200": { "description": "断点续传到服务器", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/fileUploadAndDownload/deleteFile": { "post": { "security": [ { "ApiKeyAuth": [] } ], "produces": [ "application/json" ], "tags": [ "ExaFileUploadAndDownload" ], "summary": "删除文件", "parameters": [ { "description": "传入文件里面id即可", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/example.ExaFileUploadAndDownload" } } ], "responses": { "200": { "description": "删除文件", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/fileUploadAndDownload/findFile": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "multipart/form-data" ], "produces": [ "application/json" ], "tags": [ "ExaFileUploadAndDownload" ], "summary": "查找文件", "parameters": [ { "type": "file", "description": "Find the file, 查找文件", "name": "file", "in": "formData", "required": true } ], "responses": { "200": { "description": "查找文件,返回包括文件详情", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.FileResponse" }, "msg": { "type": "string" } } } ] } } } }, "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "multipart/form-data" ], "produces": [ "application/json" ], "tags": [ "ExaFileUploadAndDownload" ], "summary": "创建文件", "parameters": [ { "type": "file", "description": "上传文件完成", "name": "file", "in": "formData", "required": true } ], "responses": { "200": { "description": "创建文件,返回包括文件路径", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.FilePathResponse" }, "msg": { "type": "string" } } } ] } } } } }, "/fileUploadAndDownload/getFileList": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "ExaFileUploadAndDownload" ], "summary": "分页文件列表", "parameters": [ { "description": "页码, 每页大小, 分类id", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.ExaAttachmentCategorySearch" } } ], "responses": { "200": { "description": "分页文件列表,返回包括列表,总数,页码,每页数量", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.PageResult" }, "msg": { "type": "string" } } } ] } } } } }, "/fileUploadAndDownload/importURL": { "post": { "security": [ { "ApiKeyAuth": [] } ], "produces": [ "application/json" ], "tags": [ "ExaFileUploadAndDownload" ], "summary": "导入URL", "parameters": [ { "description": "对象", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/example.ExaFileUploadAndDownload" } } ], "responses": { "200": { "description": "导入URL", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/fileUploadAndDownload/removeChunk": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "multipart/form-data" ], "produces": [ "application/json" ], "tags": [ "ExaFileUploadAndDownload" ], "summary": "删除切片", "parameters": [ { "type": "file", "description": "删除缓存切片", "name": "file", "in": "formData", "required": true } ], "responses": { "200": { "description": "删除切片", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/fileUploadAndDownload/upload": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "multipart/form-data" ], "produces": [ "application/json" ], "tags": [ "ExaFileUploadAndDownload" ], "summary": "上传文件示例", "parameters": [ { "type": "file", "description": "上传文件示例", "name": "file", "in": "formData", "required": true } ], "responses": { "200": { "description": "上传文件示例,返回包括文件详情", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.ExaFileResponse" }, "msg": { "type": "string" } } } ] } } } } }, "/info/createInfo": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Info" ], "summary": "创建公告", "parameters": [ { "description": "创建公告", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/model.Info" } } ], "responses": { "200": { "description": "创建成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/info/deleteInfo": { "delete": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Info" ], "summary": "删除公告", "parameters": [ { "description": "删除公告", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/model.Info" } } ], "responses": { "200": { "description": "删除成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/info/deleteInfoByIds": { "delete": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Info" ], "summary": "批量删除公告", "responses": { "200": { "description": "批量删除成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/info/findInfo": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Info" ], "summary": "用id查询公告", "parameters": [ { "type": "integer", "description": "主键ID", "name": "ID", "in": "query" }, { "type": "string", "description": "内容", "name": "content", "in": "query" }, { "type": "string", "description": "创建时间", "name": "createdAt", "in": "query" }, { "type": "string", "description": "标题", "name": "title", "in": "query" }, { "type": "string", "description": "更新时间", "name": "updatedAt", "in": "query" }, { "type": "integer", "description": "作者", "name": "userID", "in": "query" } ], "responses": { "200": { "description": "查询成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/model.Info" }, "msg": { "type": "string" } } } ] } } } } }, "/info/getInfoDataSource": { "get": { "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Info" ], "summary": "获取Info的数据源", "responses": { "200": { "description": "查询成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object" }, "msg": { "type": "string" } } } ] } } } } }, "/info/getInfoList": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Info" ], "summary": "分页获取公告列表", "parameters": [ { "type": "string", "name": "endCreatedAt", "in": "query" }, { "type": "string", "description": "关键字", "name": "keyword", "in": "query" }, { "type": "integer", "description": "页码", "name": "page", "in": "query" }, { "type": "integer", "description": "每页大小", "name": "pageSize", "in": "query" }, { "type": "string", "name": "startCreatedAt", "in": "query" } ], "responses": { "200": { "description": "获取成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.PageResult" }, "msg": { "type": "string" } } } ] } } } } }, "/info/getInfoPublic": { "get": { "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Info" ], "summary": "不需要鉴权的公告接口", "parameters": [ { "type": "string", "name": "endCreatedAt", "in": "query" }, { "type": "string", "description": "关键字", "name": "keyword", "in": "query" }, { "type": "integer", "description": "页码", "name": "page", "in": "query" }, { "type": "integer", "description": "每页大小", "name": "pageSize", "in": "query" }, { "type": "string", "name": "startCreatedAt", "in": "query" } ], "responses": { "200": { "description": "获取成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object" }, "msg": { "type": "string" } } } ] } } } } }, "/info/updateInfo": { "put": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Info" ], "summary": "更新公告", "parameters": [ { "description": "更新公告", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/model.Info" } } ], "responses": { "200": { "description": "更新成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/init/checkdb": { "post": { "produces": [ "application/json" ], "tags": [ "CheckDB" ], "summary": "初始化用户数据库", "responses": { "200": { "description": "初始化用户数据库", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/init/initdb": { "post": { "produces": [ "application/json" ], "tags": [ "InitDB" ], "summary": "初始化用户数据库", "parameters": [ { "description": "初始化数据库参数", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.InitDB" } } ], "responses": { "200": { "description": "初始化用户数据库", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "string" } } } ] } } } } }, "/jwt/jsonInBlacklist": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Jwt" ], "summary": "jwt加入黑名单", "responses": { "200": { "description": "jwt加入黑名单", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/menu/addBaseMenu": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Menu" ], "summary": "新增菜单", "parameters": [ { "description": "路由path, 父菜单ID, 路由name, 对应前端文件路径, 排序标记", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysBaseMenu" } } ], "responses": { "200": { "description": "新增菜单", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/menu/addMenuAuthority": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AuthorityMenu" ], "summary": "增加menu和角色关联关系", "parameters": [ { "description": "角色ID", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.AddMenuAuthorityInfo" } } ], "responses": { "200": { "description": "增加menu和角色关联关系", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/menu/deleteBaseMenu": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Menu" ], "summary": "删除菜单", "parameters": [ { "description": "菜单id", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.GetById" } } ], "responses": { "200": { "description": "删除菜单", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/menu/getBaseMenuById": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Menu" ], "summary": "根据id获取菜单", "parameters": [ { "description": "菜单id", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.GetById" } } ], "responses": { "200": { "description": "根据id获取菜单,返回包括系统菜单列表", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.SysBaseMenuResponse" }, "msg": { "type": "string" } } } ] } } } } }, "/menu/getBaseMenuTree": { "post": { "security": [ { "ApiKeyAuth": [] } ], "produces": [ "application/json" ], "tags": [ "AuthorityMenu" ], "summary": "获取用户动态路由", "parameters": [ { "description": "空", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.Empty" } } ], "responses": { "200": { "description": "获取用户动态路由,返回包括系统菜单列表", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.SysBaseMenusResponse" }, "msg": { "type": "string" } } } ] } } } } }, "/menu/getMenu": { "post": { "security": [ { "ApiKeyAuth": [] } ], "produces": [ "application/json" ], "tags": [ "AuthorityMenu" ], "summary": "获取用户动态路由", "parameters": [ { "description": "空", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.Empty" } } ], "responses": { "200": { "description": "获取用户动态路由,返回包括系统菜单详情列表", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.SysMenusResponse" }, "msg": { "type": "string" } } } ] } } } } }, "/menu/getMenuAuthority": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "AuthorityMenu" ], "summary": "获取指定角色menu", "parameters": [ { "description": "角色ID", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.GetAuthorityId" } } ], "responses": { "200": { "description": "获取指定角色menu", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/menu/getMenuList": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Menu" ], "summary": "分页获取基础menu列表", "parameters": [ { "description": "页码, 每页大小", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.PageInfo" } } ], "responses": { "200": { "description": "分页获取基础menu列表,返回包括列表,总数,页码,每页数量", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.PageResult" }, "msg": { "type": "string" } } } ] } } } } }, "/menu/updateBaseMenu": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Menu" ], "summary": "更新菜单", "parameters": [ { "description": "路由path, 父菜单ID, 路由name, 对应前端文件路径, 排序标记", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysBaseMenu" } } ], "responses": { "200": { "description": "更新菜单", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/sysDictionary/createSysDictionary": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysDictionary" ], "summary": "创建SysDictionary", "parameters": [ { "description": "SysDictionary模型", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysDictionary" } } ], "responses": { "200": { "description": "创建SysDictionary", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/sysDictionary/deleteSysDictionary": { "delete": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysDictionary" ], "summary": "删除SysDictionary", "parameters": [ { "description": "SysDictionary模型", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysDictionary" } } ], "responses": { "200": { "description": "删除SysDictionary", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/sysDictionary/findSysDictionary": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysDictionary" ], "summary": "用id查询SysDictionary", "parameters": [ { "type": "integer", "description": "主键ID", "name": "ID", "in": "query" }, { "type": "string", "description": "创建时间", "name": "createdAt", "in": "query" }, { "type": "string", "description": "描述", "name": "desc", "in": "query" }, { "type": "string", "description": "字典名(中)", "name": "name", "in": "query" }, { "type": "boolean", "description": "状态", "name": "status", "in": "query" }, { "type": "string", "description": "字典名(英)", "name": "type", "in": "query" }, { "type": "string", "description": "更新时间", "name": "updatedAt", "in": "query" } ], "responses": { "200": { "description": "用id查询SysDictionary", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/sysDictionary/getSysDictionaryList": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysDictionary" ], "summary": "分页获取SysDictionary列表", "responses": { "200": { "description": "分页获取SysDictionary列表,返回包括列表,总数,页码,每页数量", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.PageResult" }, "msg": { "type": "string" } } } ] } } } } }, "/sysDictionary/updateSysDictionary": { "put": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysDictionary" ], "summary": "更新SysDictionary", "parameters": [ { "description": "SysDictionary模型", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysDictionary" } } ], "responses": { "200": { "description": "更新SysDictionary", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/sysDictionaryDetail/createSysDictionaryDetail": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysDictionaryDetail" ], "summary": "创建SysDictionaryDetail", "parameters": [ { "description": "SysDictionaryDetail模型", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysDictionaryDetail" } } ], "responses": { "200": { "description": "创建SysDictionaryDetail", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/sysDictionaryDetail/deleteSysDictionaryDetail": { "delete": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysDictionaryDetail" ], "summary": "删除SysDictionaryDetail", "parameters": [ { "description": "SysDictionaryDetail模型", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysDictionaryDetail" } } ], "responses": { "200": { "description": "删除SysDictionaryDetail", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/sysDictionaryDetail/findSysDictionaryDetail": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysDictionaryDetail" ], "summary": "用id查询SysDictionaryDetail", "parameters": [ { "type": "integer", "description": "主键ID", "name": "ID", "in": "query" }, { "type": "string", "description": "创建时间", "name": "createdAt", "in": "query" }, { "type": "string", "description": "扩展值", "name": "extend", "in": "query" }, { "type": "string", "description": "展示值", "name": "label", "in": "query" }, { "type": "integer", "description": "排序标记", "name": "sort", "in": "query" }, { "type": "boolean", "description": "启用状态", "name": "status", "in": "query" }, { "type": "integer", "description": "关联标记", "name": "sysDictionaryID", "in": "query" }, { "type": "string", "description": "更新时间", "name": "updatedAt", "in": "query" }, { "type": "string", "description": "字典值", "name": "value", "in": "query" } ], "responses": { "200": { "description": "用id查询SysDictionaryDetail", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/sysDictionaryDetail/getSysDictionaryDetailList": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysDictionaryDetail" ], "summary": "分页获取SysDictionaryDetail列表", "parameters": [ { "type": "integer", "description": "主键ID", "name": "ID", "in": "query" }, { "type": "string", "description": "创建时间", "name": "createdAt", "in": "query" }, { "type": "string", "description": "扩展值", "name": "extend", "in": "query" }, { "type": "string", "description": "关键字", "name": "keyword", "in": "query" }, { "type": "string", "description": "展示值", "name": "label", "in": "query" }, { "type": "integer", "description": "页码", "name": "page", "in": "query" }, { "type": "integer", "description": "每页大小", "name": "pageSize", "in": "query" }, { "type": "integer", "description": "排序标记", "name": "sort", "in": "query" }, { "type": "boolean", "description": "启用状态", "name": "status", "in": "query" }, { "type": "integer", "description": "关联标记", "name": "sysDictionaryID", "in": "query" }, { "type": "string", "description": "更新时间", "name": "updatedAt", "in": "query" }, { "type": "string", "description": "字典值", "name": "value", "in": "query" } ], "responses": { "200": { "description": "分页获取SysDictionaryDetail列表,返回包括列表,总数,页码,每页数量", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.PageResult" }, "msg": { "type": "string" } } } ] } } } } }, "/sysDictionaryDetail/updateSysDictionaryDetail": { "put": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysDictionaryDetail" ], "summary": "更新SysDictionaryDetail", "parameters": [ { "description": "更新SysDictionaryDetail", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysDictionaryDetail" } } ], "responses": { "200": { "description": "更新SysDictionaryDetail", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/sysExportTemplate/ExportTemplate": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysExportTemplate" ], "summary": "导出表格模板", "responses": {} } }, "/sysExportTemplate/createSysExportTemplate": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysExportTemplate" ], "summary": "创建导出模板", "parameters": [ { "description": "创建导出模板", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysExportTemplate" } } ], "responses": { "200": { "description": "{\"success\":true,\"data\":{},\"msg\":\"创建成功\"}", "schema": { "type": "string" } } } } }, "/sysExportTemplate/deleteSysExportTemplate": { "delete": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysExportTemplate" ], "summary": "删除导出模板", "parameters": [ { "description": "删除导出模板", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysExportTemplate" } } ], "responses": { "200": { "description": "{\"success\":true,\"data\":{},\"msg\":\"删除成功\"}", "schema": { "type": "string" } } } } }, "/sysExportTemplate/deleteSysExportTemplateByIds": { "delete": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysExportTemplate" ], "summary": "批量删除导出模板", "parameters": [ { "description": "批量删除导出模板", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.IdsReq" } } ], "responses": { "200": { "description": "{\"success\":true,\"data\":{},\"msg\":\"批量删除成功\"}", "schema": { "type": "string" } } } } }, "/sysExportTemplate/exportExcel": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysExportTemplate" ], "summary": "导出表格", "responses": {} } }, "/sysExportTemplate/findSysExportTemplate": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysExportTemplate" ], "summary": "用id查询导出模板", "parameters": [ { "type": "integer", "description": "主键ID", "name": "ID", "in": "query" }, { "type": "string", "description": "创建时间", "name": "createdAt", "in": "query" }, { "type": "string", "description": "数据库名称", "name": "dbName", "in": "query" }, { "type": "integer", "name": "limit", "in": "query" }, { "type": "string", "description": "模板名称", "name": "name", "in": "query" }, { "type": "string", "name": "order", "in": "query" }, { "type": "string", "description": "表名称", "name": "tableName", "in": "query" }, { "type": "string", "description": "模板标识", "name": "templateID", "in": "query" }, { "type": "string", "description": "模板信息", "name": "templateInfo", "in": "query" }, { "type": "string", "description": "更新时间", "name": "updatedAt", "in": "query" } ], "responses": { "200": { "description": "{\"success\":true,\"data\":{},\"msg\":\"查询成功\"}", "schema": { "type": "string" } } } } }, "/sysExportTemplate/getSysExportTemplateList": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysExportTemplate" ], "summary": "分页获取导出模板列表", "parameters": [ { "type": "integer", "description": "主键ID", "name": "ID", "in": "query" }, { "type": "string", "description": "创建时间", "name": "createdAt", "in": "query" }, { "type": "string", "description": "数据库名称", "name": "dbName", "in": "query" }, { "type": "string", "name": "endCreatedAt", "in": "query" }, { "type": "string", "description": "关键字", "name": "keyword", "in": "query" }, { "type": "integer", "name": "limit", "in": "query" }, { "type": "string", "description": "模板名称", "name": "name", "in": "query" }, { "type": "string", "name": "order", "in": "query" }, { "type": "integer", "description": "页码", "name": "page", "in": "query" }, { "type": "integer", "description": "每页大小", "name": "pageSize", "in": "query" }, { "type": "string", "name": "startCreatedAt", "in": "query" }, { "type": "string", "description": "表名称", "name": "tableName", "in": "query" }, { "type": "string", "description": "模板标识", "name": "templateID", "in": "query" }, { "type": "string", "description": "模板信息", "name": "templateInfo", "in": "query" }, { "type": "string", "description": "更新时间", "name": "updatedAt", "in": "query" } ], "responses": { "200": { "description": "{\"success\":true,\"data\":{},\"msg\":\"获取成功\"}", "schema": { "type": "string" } } } } }, "/sysExportTemplate/importExcel": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysImportTemplate" ], "summary": "导入表格", "responses": {} } }, "/sysExportTemplate/updateSysExportTemplate": { "put": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysExportTemplate" ], "summary": "更新导出模板", "parameters": [ { "description": "更新导出模板", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysExportTemplate" } } ], "responses": { "200": { "description": "{\"success\":true,\"data\":{},\"msg\":\"更新成功\"}", "schema": { "type": "string" } } } } }, "/sysOperationRecord/createSysOperationRecord": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysOperationRecord" ], "summary": "创建SysOperationRecord", "parameters": [ { "description": "创建SysOperationRecord", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysOperationRecord" } } ], "responses": { "200": { "description": "创建SysOperationRecord", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/sysOperationRecord/deleteSysOperationRecord": { "delete": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysOperationRecord" ], "summary": "删除SysOperationRecord", "parameters": [ { "description": "SysOperationRecord模型", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysOperationRecord" } } ], "responses": { "200": { "description": "删除SysOperationRecord", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/sysOperationRecord/deleteSysOperationRecordByIds": { "delete": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysOperationRecord" ], "summary": "批量删除SysOperationRecord", "parameters": [ { "description": "批量删除SysOperationRecord", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.IdsReq" } } ], "responses": { "200": { "description": "批量删除SysOperationRecord", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/sysOperationRecord/findSysOperationRecord": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysOperationRecord" ], "summary": "用id查询SysOperationRecord", "parameters": [ { "type": "integer", "description": "主键ID", "name": "ID", "in": "query" }, { "type": "string", "description": "代理", "name": "agent", "in": "query" }, { "type": "string", "description": "请求Body", "name": "body", "in": "query" }, { "type": "string", "description": "创建时间", "name": "createdAt", "in": "query" }, { "type": "string", "description": "错误信息", "name": "error_message", "in": "query" }, { "type": "string", "description": "请求ip", "name": "ip", "in": "query" }, { "type": "string", "description": "延迟", "name": "latency", "in": "query" }, { "type": "string", "description": "请求方法", "name": "method", "in": "query" }, { "type": "string", "description": "请求路径", "name": "path", "in": "query" }, { "type": "string", "description": "响应Body", "name": "resp", "in": "query" }, { "type": "integer", "description": "请求状态", "name": "status", "in": "query" }, { "type": "string", "description": "更新时间", "name": "updatedAt", "in": "query" }, { "type": "integer", "description": "用户id", "name": "user_id", "in": "query" } ], "responses": { "200": { "description": "用id查询SysOperationRecord", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/sysOperationRecord/getSysOperationRecordList": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysOperationRecord" ], "summary": "分页获取SysOperationRecord列表", "parameters": [ { "type": "integer", "description": "主键ID", "name": "ID", "in": "query" }, { "type": "string", "description": "代理", "name": "agent", "in": "query" }, { "type": "string", "description": "请求Body", "name": "body", "in": "query" }, { "type": "string", "description": "创建时间", "name": "createdAt", "in": "query" }, { "type": "string", "description": "错误信息", "name": "error_message", "in": "query" }, { "type": "string", "description": "请求ip", "name": "ip", "in": "query" }, { "type": "string", "description": "关键字", "name": "keyword", "in": "query" }, { "type": "string", "description": "延迟", "name": "latency", "in": "query" }, { "type": "string", "description": "请求方法", "name": "method", "in": "query" }, { "type": "integer", "description": "页码", "name": "page", "in": "query" }, { "type": "integer", "description": "每页大小", "name": "pageSize", "in": "query" }, { "type": "string", "description": "请求路径", "name": "path", "in": "query" }, { "type": "string", "description": "响应Body", "name": "resp", "in": "query" }, { "type": "integer", "description": "请求状态", "name": "status", "in": "query" }, { "type": "string", "description": "更新时间", "name": "updatedAt", "in": "query" }, { "type": "integer", "description": "用户id", "name": "user_id", "in": "query" } ], "responses": { "200": { "description": "分页获取SysOperationRecord列表,返回包括列表,总数,页码,每页数量", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.PageResult" }, "msg": { "type": "string" } } } ] } } } } }, "/sysParams/createSysParams": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysParams" ], "summary": "创建参数", "parameters": [ { "description": "创建参数", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysParams" } } ], "responses": { "200": { "description": "创建成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/sysParams/deleteSysParams": { "delete": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysParams" ], "summary": "删除参数", "parameters": [ { "description": "删除参数", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysParams" } } ], "responses": { "200": { "description": "删除成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/sysParams/deleteSysParamsByIds": { "delete": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysParams" ], "summary": "批量删除参数", "responses": { "200": { "description": "批量删除成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/sysParams/findSysParams": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysParams" ], "summary": "用id查询参数", "parameters": [ { "type": "integer", "description": "主键ID", "name": "ID", "in": "query" }, { "type": "string", "description": "创建时间", "name": "createdAt", "in": "query" }, { "type": "string", "description": "参数说明", "name": "desc", "in": "query" }, { "type": "string", "description": "参数键", "name": "key", "in": "query", "required": true }, { "type": "string", "description": "参数名称", "name": "name", "in": "query", "required": true }, { "type": "string", "description": "更新时间", "name": "updatedAt", "in": "query" }, { "type": "string", "description": "参数值", "name": "value", "in": "query", "required": true } ], "responses": { "200": { "description": "查询成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/system.SysParams" }, "msg": { "type": "string" } } } ] } } } } }, "/sysParams/getSysParam": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysParams" ], "summary": "根据key获取参数value", "parameters": [ { "type": "string", "description": "key", "name": "key", "in": "query", "required": true } ], "responses": { "200": { "description": "获取成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/system.SysParams" }, "msg": { "type": "string" } } } ] } } } } }, "/sysParams/getSysParamsList": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysParams" ], "summary": "分页获取参数列表", "parameters": [ { "type": "string", "name": "endCreatedAt", "in": "query" }, { "type": "string", "name": "key", "in": "query" }, { "type": "string", "description": "关键字", "name": "keyword", "in": "query" }, { "type": "string", "name": "name", "in": "query" }, { "type": "integer", "description": "页码", "name": "page", "in": "query" }, { "type": "integer", "description": "每页大小", "name": "pageSize", "in": "query" }, { "type": "string", "name": "startCreatedAt", "in": "query" } ], "responses": { "200": { "description": "获取成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.PageResult" }, "msg": { "type": "string" } } } ] } } } } }, "/sysParams/updateSysParams": { "put": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysParams" ], "summary": "更新参数", "parameters": [ { "description": "更新参数", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysParams" } } ], "responses": { "200": { "description": "更新成功", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/system/getServerInfo": { "post": { "security": [ { "ApiKeyAuth": [] } ], "produces": [ "application/json" ], "tags": [ "System" ], "summary": "获取服务器信息", "responses": { "200": { "description": "获取服务器信息", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/system/getSystemConfig": { "post": { "security": [ { "ApiKeyAuth": [] } ], "produces": [ "application/json" ], "tags": [ "System" ], "summary": "获取配置文件内容", "responses": { "200": { "description": "获取配置文件内容,返回包括系统配置", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.SysConfigResponse" }, "msg": { "type": "string" } } } ] } } } } }, "/system/reloadSystem": { "post": { "security": [ { "ApiKeyAuth": [] } ], "produces": [ "application/json" ], "tags": [ "System" ], "summary": "重启系统", "responses": { "200": { "description": "重启系统", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/system/setSystemConfig": { "post": { "security": [ { "ApiKeyAuth": [] } ], "produces": [ "application/json" ], "tags": [ "System" ], "summary": "设置配置文件内容", "parameters": [ { "description": "设置配置文件内容", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.System" } } ], "responses": { "200": { "description": "设置配置文件内容", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "string" } } } ] } } } } }, "/user/SetSelfInfo": { "put": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysUser" ], "summary": "设置用户信息", "parameters": [ { "description": "ID, 用户名, 昵称, 头像链接", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysUser" } } ], "responses": { "200": { "description": "设置用户信息", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/user/SetSelfSetting": { "put": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysUser" ], "summary": "设置用户配置", "parameters": [ { "description": "用户配置数据", "name": "data", "in": "body", "required": true, "schema": { "type": "object", "additionalProperties": true } } ], "responses": { "200": { "description": "设置用户配置", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/user/admin_register": { "post": { "produces": [ "application/json" ], "tags": [ "SysUser" ], "summary": "用户注册账号", "parameters": [ { "description": "用户名, 昵称, 密码, 角色ID", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.Register" } } ], "responses": { "200": { "description": "用户注册账号,返回包括用户信息", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.SysUserResponse" }, "msg": { "type": "string" } } } ] } } } } }, "/user/changePassword": { "post": { "security": [ { "ApiKeyAuth": [] } ], "produces": [ "application/json" ], "tags": [ "SysUser" ], "summary": "用户修改密码", "parameters": [ { "description": "用户名, 原密码, 新密码", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.ChangePasswordReq" } } ], "responses": { "200": { "description": "用户修改密码", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/user/deleteUser": { "delete": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysUser" ], "summary": "删除用户", "parameters": [ { "description": "用户ID", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.GetById" } } ], "responses": { "200": { "description": "删除用户", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/user/getUserInfo": { "get": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysUser" ], "summary": "获取用户信息", "responses": { "200": { "description": "获取用户信息", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } }, "/user/getUserList": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysUser" ], "summary": "分页获取用户列表", "parameters": [ { "description": "页码, 每页大小", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.GetUserList" } } ], "responses": { "200": { "description": "分页获取用户列表,返回包括列表,总数,页码,每页数量", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/response.PageResult" }, "msg": { "type": "string" } } } ] } } } } }, "/user/resetPassword": { "post": { "security": [ { "ApiKeyAuth": [] } ], "produces": [ "application/json" ], "tags": [ "SysUser" ], "summary": "重置用户密码", "parameters": [ { "description": "ID", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysUser" } } ], "responses": { "200": { "description": "重置用户密码", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/user/setUserAuthorities": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysUser" ], "summary": "设置用户权限", "parameters": [ { "description": "用户UUID, 角色ID", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.SetUserAuthorities" } } ], "responses": { "200": { "description": "设置用户权限", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/user/setUserAuthority": { "post": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysUser" ], "summary": "更改用户权限", "parameters": [ { "description": "用户UUID, 角色ID", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/request.SetUserAuth" } } ], "responses": { "200": { "description": "设置用户权限", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "msg": { "type": "string" } } } ] } } } } }, "/user/setUserInfo": { "put": { "security": [ { "ApiKeyAuth": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "SysUser" ], "summary": "设置用户信息", "parameters": [ { "description": "ID, 用户名, 昵称, 头像链接", "name": "data", "in": "body", "required": true, "schema": { "$ref": "#/definitions/system.SysUser" } } ], "responses": { "200": { "description": "设置用户信息", "schema": { "allOf": [ { "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "msg": { "type": "string" } } } ] } } } } } }, "definitions": { "common.JSONMap": { "type": "object", "additionalProperties": true }, "config.AliyunOSS": { "type": "object", "properties": { "access-key-id": { "type": "string" }, "access-key-secret": { "type": "string" }, "base-path": { "type": "string" }, "bucket-name": { "type": "string" }, "bucket-url": { "type": "string" }, "endpoint": { "type": "string" } } }, "config.Autocode": { "type": "object", "properties": { "ai-path": { "type": "string" }, "module": { "type": "string" }, "root": { "type": "string" }, "server": { "type": "string" }, "web": { "type": "string" } } }, "config.AwsS3": { "type": "object", "properties": { "base-url": { "type": "string" }, "bucket": { "type": "string" }, "disable-ssl": { "type": "boolean" }, "endpoint": { "type": "string" }, "path-prefix": { "type": "string" }, "region": { "type": "string" }, "s3-force-path-style": { "type": "boolean" }, "secret-id": { "type": "string" }, "secret-key": { "type": "string" } } }, "config.CORS": { "type": "object", "properties": { "mode": { "type": "string" }, "whitelist": { "type": "array", "items": { "$ref": "#/definitions/config.CORSWhitelist" } } } }, "config.CORSWhitelist": { "type": "object", "properties": { "allow-credentials": { "type": "boolean" }, "allow-headers": { "type": "string" }, "allow-methods": { "type": "string" }, "allow-origin": { "type": "string" }, "expose-headers": { "type": "string" } } }, "config.Captcha": { "type": "object", "properties": { "img-height": { "description": "验证码高度", "type": "integer" }, "img-width": { "description": "验证码宽度", "type": "integer" }, "key-long": { "description": "验证码长度", "type": "integer" }, "open-captcha": { "description": "防爆破验证码开启此数,0代表每次登录都需要验证码,其他数字代表错误密码此数,如3代表错误三次后出现验证码", "type": "integer" }, "open-captcha-timeout": { "description": "防爆破验证码超时时间,单位:s(秒)", "type": "integer" } } }, "config.CloudflareR2": { "type": "object", "properties": { "access-key-id": { "type": "string" }, "account-id": { "type": "string" }, "base-url": { "type": "string" }, "bucket": { "type": "string" }, "path": { "type": "string" }, "secret-access-key": { "type": "string" } } }, "config.DiskList": { "type": "object", "properties": { "mount-point": { "type": "string" } } }, "config.Excel": { "type": "object", "properties": { "dir": { "type": "string" } } }, "config.HuaWeiObs": { "type": "object", "properties": { "access-key": { "type": "string" }, "bucket": { "type": "string" }, "endpoint": { "type": "string" }, "path": { "type": "string" }, "secret-key": { "type": "string" } } }, "config.JWT": { "type": "object", "properties": { "buffer-time": { "description": "缓冲时间", "type": "string" }, "expires-time": { "description": "过期时间", "type": "string" }, "issuer": { "description": "签发者", "type": "string" }, "signing-key": { "description": "jwt签名", "type": "string" } } }, "config.Local": { "type": "object", "properties": { "path": { "description": "本地文件访问路径", "type": "string" }, "store-path": { "description": "本地文件存储路径", "type": "string" } } }, "config.Minio": { "type": "object", "properties": { "access-key-id": { "type": "string" }, "access-key-secret": { "type": "string" }, "base-path": { "type": "string" }, "bucket-name": { "type": "string" }, "bucket-url": { "type": "string" }, "endpoint": { "type": "string" }, "use-ssl": { "type": "boolean" } } }, "config.Mongo": { "type": "object", "properties": { "auth-source": { "description": "验证数据库", "type": "string" }, "coll": { "description": "collection name", "type": "string" }, "connect-timeout-ms": { "description": "连接超时时间", "type": "integer" }, "database": { "description": "database name", "type": "string" }, "hosts": { "description": "主机列表", "type": "array", "items": { "$ref": "#/definitions/config.MongoHost" } }, "is-zap": { "description": "是否开启zap日志", "type": "boolean" }, "max-pool-size": { "description": "最大连接池", "type": "integer" }, "min-pool-size": { "description": "最小连接池", "type": "integer" }, "options": { "description": "mongodb options", "type": "string" }, "password": { "description": "密码", "type": "string" }, "socket-timeout-ms": { "description": "socket超时时间", "type": "integer" }, "username": { "description": "用户名", "type": "string" } } }, "config.MongoHost": { "type": "object", "properties": { "host": { "description": "ip地址", "type": "string" }, "port": { "description": "端口", "type": "string" } } }, "config.Mssql": { "type": "object", "properties": { "config": { "description": "高级配置", "type": "string" }, "db-name": { "description": "数据库名", "type": "string" }, "engine": { "description": "数据库引擎,默认InnoDB", "type": "string", "default": "InnoDB" }, "log-mode": { "description": "是否开启Gorm全局日志", "type": "string" }, "log-zap": { "description": "是否通过zap写入日志文件", "type": "boolean" }, "max-idle-conns": { "description": "空闲中的最大连接数", "type": "integer" }, "max-open-conns": { "description": "打开到数据库的最大连接数", "type": "integer" }, "password": { "description": "数据库密码", "type": "string" }, "path": { "description": "数据库地址", "type": "string" }, "port": { "description": "数据库端口", "type": "string" }, "prefix": { "description": "数据库前缀", "type": "string" }, "singular": { "description": "是否开启全局禁用复数,true表示开启", "type": "boolean" }, "username": { "description": "数据库账号", "type": "string" } } }, "config.Mysql": { "type": "object", "properties": { "config": { "description": "高级配置", "type": "string" }, "db-name": { "description": "数据库名", "type": "string" }, "engine": { "description": "数据库引擎,默认InnoDB", "type": "string", "default": "InnoDB" }, "log-mode": { "description": "是否开启Gorm全局日志", "type": "string" }, "log-zap": { "description": "是否通过zap写入日志文件", "type": "boolean" }, "max-idle-conns": { "description": "空闲中的最大连接数", "type": "integer" }, "max-open-conns": { "description": "打开到数据库的最大连接数", "type": "integer" }, "password": { "description": "数据库密码", "type": "string" }, "path": { "description": "数据库地址", "type": "string" }, "port": { "description": "数据库端口", "type": "string" }, "prefix": { "description": "数据库前缀", "type": "string" }, "singular": { "description": "是否开启全局禁用复数,true表示开启", "type": "boolean" }, "username": { "description": "数据库账号", "type": "string" } } }, "config.Oracle": { "type": "object", "properties": { "config": { "description": "高级配置", "type": "string" }, "db-name": { "description": "数据库名", "type": "string" }, "engine": { "description": "数据库引擎,默认InnoDB", "type": "string", "default": "InnoDB" }, "log-mode": { "description": "是否开启Gorm全局日志", "type": "string" }, "log-zap": { "description": "是否通过zap写入日志文件", "type": "boolean" }, "max-idle-conns": { "description": "空闲中的最大连接数", "type": "integer" }, "max-open-conns": { "description": "打开到数据库的最大连接数", "type": "integer" }, "password": { "description": "数据库密码", "type": "string" }, "path": { "description": "数据库地址", "type": "string" }, "port": { "description": "数据库端口", "type": "string" }, "prefix": { "description": "数据库前缀", "type": "string" }, "singular": { "description": "是否开启全局禁用复数,true表示开启", "type": "boolean" }, "username": { "description": "数据库账号", "type": "string" } } }, "config.Pgsql": { "type": "object", "properties": { "config": { "description": "高级配置", "type": "string" }, "db-name": { "description": "数据库名", "type": "string" }, "engine": { "description": "数据库引擎,默认InnoDB", "type": "string", "default": "InnoDB" }, "log-mode": { "description": "是否开启Gorm全局日志", "type": "string" }, "log-zap": { "description": "是否通过zap写入日志文件", "type": "boolean" }, "max-idle-conns": { "description": "空闲中的最大连接数", "type": "integer" }, "max-open-conns": { "description": "打开到数据库的最大连接数", "type": "integer" }, "password": { "description": "数据库密码", "type": "string" }, "path": { "description": "数据库地址", "type": "string" }, "port": { "description": "数据库端口", "type": "string" }, "prefix": { "description": "数据库前缀", "type": "string" }, "singular": { "description": "是否开启全局禁用复数,true表示开启", "type": "boolean" }, "username": { "description": "数据库账号", "type": "string" } } }, "config.Qiniu": { "type": "object", "properties": { "access-key": { "description": "秘钥AK", "type": "string" }, "bucket": { "description": "空间名称", "type": "string" }, "img-path": { "description": "CDN加速域名", "type": "string" }, "secret-key": { "description": "秘钥SK", "type": "string" }, "use-cdn-domains": { "description": "上传是否使用CDN上传加速", "type": "boolean" }, "use-https": { "description": "是否使用https", "type": "boolean" }, "zone": { "description": "存储区域", "type": "string" } } }, "config.Redis": { "type": "object", "properties": { "addr": { "description": "服务器地址:端口", "type": "string" }, "clusterAddrs": { "description": "集群模式下的节点地址列表", "type": "array", "items": { "type": "string" } }, "db": { "description": "单实例模式下redis的哪个数据库", "type": "integer" }, "name": { "description": "代表当前实例的名字", "type": "string" }, "password": { "description": "密码", "type": "string" }, "useCluster": { "description": "是否使用集群模式", "type": "boolean" } } }, "config.Server": { "type": "object", "properties": { "aliyun-oss": { "$ref": "#/definitions/config.AliyunOSS" }, "autocode": { "description": "auto", "allOf": [ { "$ref": "#/definitions/config.Autocode" } ] }, "aws-s3": { "$ref": "#/definitions/config.AwsS3" }, "captcha": { "$ref": "#/definitions/config.Captcha" }, "cloudflare-r2": { "$ref": "#/definitions/config.CloudflareR2" }, "cors": { "description": "跨域配置", "allOf": [ { "$ref": "#/definitions/config.CORS" } ] }, "db-list": { "type": "array", "items": { "$ref": "#/definitions/config.SpecializedDB" } }, "disk-list": { "type": "array", "items": { "$ref": "#/definitions/config.DiskList" } }, "email": { "$ref": "#/definitions/github_com_flipped-aurora_gin-vue-admin_server_config.Email" }, "excel": { "$ref": "#/definitions/config.Excel" }, "hua-wei-obs": { "$ref": "#/definitions/config.HuaWeiObs" }, "jwt": { "$ref": "#/definitions/config.JWT" }, "local": { "description": "oss", "allOf": [ { "$ref": "#/definitions/config.Local" } ] }, "minio": { "$ref": "#/definitions/config.Minio" }, "mongo": { "$ref": "#/definitions/config.Mongo" }, "mssql": { "$ref": "#/definitions/config.Mssql" }, "mysql": { "description": "gorm", "allOf": [ { "$ref": "#/definitions/config.Mysql" } ] }, "oracle": { "$ref": "#/definitions/config.Oracle" }, "pgsql": { "$ref": "#/definitions/config.Pgsql" }, "qiniu": { "$ref": "#/definitions/config.Qiniu" }, "redis": { "$ref": "#/definitions/config.Redis" }, "redis-list": { "type": "array", "items": { "$ref": "#/definitions/config.Redis" } }, "sqlite": { "$ref": "#/definitions/config.Sqlite" }, "system": { "$ref": "#/definitions/config.System" }, "tencent-cos": { "$ref": "#/definitions/config.TencentCOS" }, "zap": { "$ref": "#/definitions/config.Zap" } } }, "config.SpecializedDB": { "type": "object", "properties": { "alias-name": { "type": "string" }, "config": { "description": "高级配置", "type": "string" }, "db-name": { "description": "数据库名", "type": "string" }, "disable": { "type": "boolean" }, "engine": { "description": "数据库引擎,默认InnoDB", "type": "string", "default": "InnoDB" }, "log-mode": { "description": "是否开启Gorm全局日志", "type": "string" }, "log-zap": { "description": "是否通过zap写入日志文件", "type": "boolean" }, "max-idle-conns": { "description": "空闲中的最大连接数", "type": "integer" }, "max-open-conns": { "description": "打开到数据库的最大连接数", "type": "integer" }, "password": { "description": "数据库密码", "type": "string" }, "path": { "description": "数据库地址", "type": "string" }, "port": { "description": "数据库端口", "type": "string" }, "prefix": { "description": "数据库前缀", "type": "string" }, "singular": { "description": "是否开启全局禁用复数,true表示开启", "type": "boolean" }, "type": { "type": "string" }, "username": { "description": "数据库账号", "type": "string" } } }, "config.Sqlite": { "type": "object", "properties": { "config": { "description": "高级配置", "type": "string" }, "db-name": { "description": "数据库名", "type": "string" }, "engine": { "description": "数据库引擎,默认InnoDB", "type": "string", "default": "InnoDB" }, "log-mode": { "description": "是否开启Gorm全局日志", "type": "string" }, "log-zap": { "description": "是否通过zap写入日志文件", "type": "boolean" }, "max-idle-conns": { "description": "空闲中的最大连接数", "type": "integer" }, "max-open-conns": { "description": "打开到数据库的最大连接数", "type": "integer" }, "password": { "description": "数据库密码", "type": "string" }, "path": { "description": "数据库地址", "type": "string" }, "port": { "description": "数据库端口", "type": "string" }, "prefix": { "description": "数据库前缀", "type": "string" }, "singular": { "description": "是否开启全局禁用复数,true表示开启", "type": "boolean" }, "username": { "description": "数据库账号", "type": "string" } } }, "config.System": { "type": "object", "properties": { "addr": { "description": "端口值", "type": "integer" }, "db-type": { "description": "数据库类型:mysql(默认)|sqlite|sqlserver|postgresql", "type": "string" }, "iplimit-count": { "type": "integer" }, "iplimit-time": { "type": "integer" }, "oss-type": { "description": "Oss类型", "type": "string" }, "router-prefix": { "type": "string" }, "use-mongo": { "description": "使用mongo", "type": "boolean" }, "use-multipoint": { "description": "多点登录拦截", "type": "boolean" }, "use-redis": { "description": "使用redis", "type": "boolean" }, "use-strict-auth": { "description": "使用树形角色分配模式", "type": "boolean" } } }, "config.TencentCOS": { "type": "object", "properties": { "base-url": { "type": "string" }, "bucket": { "type": "string" }, "path-prefix": { "type": "string" }, "region": { "type": "string" }, "secret-id": { "type": "string" }, "secret-key": { "type": "string" } } }, "config.Zap": { "type": "object", "properties": { "director": { "description": "日志文件夹", "type": "string" }, "encode-level": { "description": "编码级", "type": "string" }, "format": { "description": "输出", "type": "string" }, "level": { "description": "级别", "type": "string" }, "log-in-console": { "description": "输出控制台", "type": "boolean" }, "prefix": { "description": "日志前缀", "type": "string" }, "retention-day": { "description": "日志保留天数", "type": "integer" }, "show-line": { "description": "显示行", "type": "boolean" }, "stacktrace-key": { "description": "栈名", "type": "string" } } }, "example.ExaAttachmentCategory": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "children": { "type": "array", "items": { "$ref": "#/definitions/example.ExaAttachmentCategory" } }, "createdAt": { "description": "创建时间", "type": "string" }, "name": { "type": "string" }, "pid": { "type": "integer" }, "updatedAt": { "description": "更新时间", "type": "string" } } }, "example.ExaCustomer": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "createdAt": { "description": "创建时间", "type": "string" }, "customerName": { "description": "客户名", "type": "string" }, "customerPhoneData": { "description": "客户手机号", "type": "string" }, "sysUser": { "description": "管理详情", "allOf": [ { "$ref": "#/definitions/system.SysUser" } ] }, "sysUserAuthorityID": { "description": "管理角色ID", "type": "integer" }, "sysUserId": { "description": "管理ID", "type": "integer" }, "updatedAt": { "description": "更新时间", "type": "string" } } }, "example.ExaFile": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "chunkTotal": { "type": "integer" }, "createdAt": { "description": "创建时间", "type": "string" }, "exaFileChunk": { "type": "array", "items": { "$ref": "#/definitions/example.ExaFileChunk" } }, "fileMd5": { "type": "string" }, "fileName": { "type": "string" }, "filePath": { "type": "string" }, "isFinish": { "type": "boolean" }, "updatedAt": { "description": "更新时间", "type": "string" } } }, "example.ExaFileChunk": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "createdAt": { "description": "创建时间", "type": "string" }, "exaFileID": { "type": "integer" }, "fileChunkNumber": { "type": "integer" }, "fileChunkPath": { "type": "string" }, "updatedAt": { "description": "更新时间", "type": "string" } } }, "example.ExaFileUploadAndDownload": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "classId": { "description": "分类id", "type": "integer" }, "createdAt": { "description": "创建时间", "type": "string" }, "key": { "description": "编号", "type": "string" }, "name": { "description": "文件名", "type": "string" }, "tag": { "description": "文件标签", "type": "string" }, "updatedAt": { "description": "更新时间", "type": "string" }, "url": { "description": "文件地址", "type": "string" } } }, "github_com_flipped-aurora_gin-vue-admin_server_config.Email": { "type": "object", "properties": { "from": { "description": "发件人 你自己要发邮件的邮箱", "type": "string" }, "host": { "description": "服务器地址 例如 smtp.qq.com 请前往QQ或者你要发邮件的邮箱查看其smtp协议", "type": "string" }, "is-ssl": { "description": "是否SSL 是否开启SSL", "type": "boolean" }, "nickname": { "description": "昵称 发件人昵称 通常为自己的邮箱", "type": "string" }, "port": { "description": "端口 请前往QQ或者你要发邮件的邮箱查看其smtp协议 大多为 465", "type": "integer" }, "secret": { "description": "密钥 用于登录的密钥 最好不要用邮箱密码 去邮箱smtp申请一个用于登录的密钥", "type": "string" }, "to": { "description": "收件人:多个以英文逗号分隔 例:a@qq.com b@qq.com 正式开发中请把此项目作为参数使用", "type": "string" } } }, "model.Info": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "attachments": { "description": "附件", "type": "array", "items": { "type": "object" } }, "content": { "description": "内容", "type": "string" }, "createdAt": { "description": "创建时间", "type": "string" }, "title": { "description": "标题", "type": "string" }, "updatedAt": { "description": "更新时间", "type": "string" }, "userID": { "description": "作者", "type": "integer" } } }, "request.AddMenuAuthorityInfo": { "type": "object", "properties": { "authorityId": { "description": "角色ID", "type": "integer" }, "menus": { "type": "array", "items": { "$ref": "#/definitions/system.SysBaseMenu" } } } }, "request.AutoCode": { "type": "object", "properties": { "abbreviation": { "description": "Struct简称", "type": "string", "example": "Struct简称" }, "autoCreateApiToSql": { "description": "是否自动创建api", "type": "boolean", "example": false }, "autoCreateBtnAuth": { "description": "是否自动创建按钮权限", "type": "boolean", "example": false }, "autoCreateMenuToSql": { "description": "是否自动创建menu", "type": "boolean", "example": false }, "autoCreateResource": { "description": "是否自动创建资源标识", "type": "boolean", "example": false }, "autoMigrate": { "description": "是否自动迁移表结构", "type": "boolean", "example": false }, "businessDB": { "description": "业务数据库", "type": "string", "example": "业务数据库" }, "description": { "description": "Struct中文名称", "type": "string", "example": "Struct中文名称" }, "fields": { "type": "array", "items": { "$ref": "#/definitions/request.AutoCodeField" } }, "generateServer": { "description": "是否生成server", "type": "boolean", "example": true }, "generateWeb": { "description": "是否生成web", "type": "boolean", "example": true }, "gvaModel": { "description": "是否使用gva默认Model", "type": "boolean", "example": false }, "humpPackageName": { "description": "go文件名称", "type": "string", "example": "go文件名称" }, "isAdd": { "description": "是否新增", "type": "boolean", "example": false }, "isTree": { "description": "是否树形结构", "type": "boolean", "example": false }, "onlyTemplate": { "description": "是否只生成模板", "type": "boolean", "example": false }, "package": { "type": "string" }, "packageName": { "description": "文件名称", "type": "string", "example": "文件名称" }, "primaryField": { "$ref": "#/definitions/request.AutoCodeField" }, "structName": { "description": "Struct名称", "type": "string", "example": "Struct名称" }, "tableName": { "description": "表名", "type": "string", "example": "表名" }, "treeJson": { "description": "展示的树json字段", "type": "string", "example": "展示的树json字段" } } }, "request.AutoCodeField": { "type": "object", "properties": { "checkDataSource": { "description": "是否检查数据源", "type": "boolean" }, "clearable": { "description": "是否可清空", "type": "boolean" }, "columnName": { "description": "数据库字段", "type": "string" }, "comment": { "description": "数据库字段描述", "type": "string" }, "dataSource": { "description": "数据源", "allOf": [ { "$ref": "#/definitions/request.DataSource" } ] }, "dataTypeLong": { "description": "数据库字段长度", "type": "string" }, "defaultValue": { "description": "是否必填", "type": "string" }, "desc": { "description": "是否前端详情", "type": "boolean" }, "dictType": { "description": "字典", "type": "string" }, "errorText": { "description": "校验失败文字", "type": "string" }, "excel": { "description": "是否导入/导出", "type": "boolean" }, "fieldDesc": { "description": "中文名", "type": "string" }, "fieldIndexType": { "description": "索引类型", "type": "string" }, "fieldJson": { "description": "FieldJson", "type": "string" }, "fieldName": { "description": "Field名", "type": "string" }, "fieldSearchHide": { "description": "是否隐藏查询条件", "type": "boolean" }, "fieldSearchType": { "description": "搜索条件", "type": "string" }, "fieldType": { "description": "Field数据类型", "type": "string" }, "form": { "description": "Front bool `json:\"front\"` // 是否前端可见", "type": "boolean" }, "primaryKey": { "description": "是否主键", "type": "boolean" }, "require": { "description": "是否必填", "type": "boolean" }, "sort": { "description": "是否增加排序", "type": "boolean" }, "table": { "description": "是否前端表格列", "type": "boolean" } } }, "request.CasbinInReceive": { "type": "object", "properties": { "authorityId": { "description": "权限id", "type": "integer" }, "casbinInfos": { "type": "array", "items": { "$ref": "#/definitions/request.CasbinInfo" } } } }, "request.CasbinInfo": { "type": "object", "properties": { "method": { "description": "方法", "type": "string" }, "path": { "description": "路径", "type": "string" } } }, "request.ChangePasswordReq": { "type": "object", "properties": { "newPassword": { "description": "新密码", "type": "string" }, "password": { "description": "密码", "type": "string" } } }, "request.DataSource": { "type": "object", "properties": { "association": { "description": "关联关系 1 一对一 2 一对多", "type": "integer" }, "dbName": { "type": "string" }, "hasDeletedAt": { "type": "boolean" }, "label": { "type": "string" }, "table": { "type": "string" }, "value": { "type": "string" } } }, "request.Empty": { "type": "object" }, "request.ExaAttachmentCategorySearch": { "type": "object", "properties": { "classId": { "type": "integer" }, "keyword": { "description": "关键字", "type": "string" }, "page": { "description": "页码", "type": "integer" }, "pageSize": { "description": "每页大小", "type": "integer" } } }, "request.GetAuthorityId": { "type": "object", "properties": { "authorityId": { "description": "角色ID", "type": "integer" } } }, "request.GetById": { "type": "object", "properties": { "id": { "description": "主键ID", "type": "integer" } } }, "request.GetUserList": { "type": "object", "properties": { "email": { "type": "string" }, "keyword": { "description": "关键字", "type": "string" }, "nickName": { "type": "string" }, "page": { "description": "页码", "type": "integer" }, "pageSize": { "description": "每页大小", "type": "integer" }, "phone": { "type": "string" }, "username": { "type": "string" } } }, "request.IdsReq": { "type": "object", "properties": { "ids": { "type": "array", "items": { "type": "integer" } } } }, "request.InitDB": { "type": "object", "required": [ "adminPassword", "dbName" ], "properties": { "adminPassword": { "type": "string" }, "dbName": { "description": "数据库名", "type": "string" }, "dbPath": { "description": "sqlite数据库文件路径", "type": "string" }, "dbType": { "description": "数据库类型", "type": "string" }, "host": { "description": "服务器地址", "type": "string" }, "password": { "description": "数据库密码", "type": "string" }, "port": { "description": "数据库连接端口", "type": "string" }, "template": { "description": "postgresql指定template", "type": "string" }, "userName": { "description": "数据库用户名", "type": "string" } } }, "request.Login": { "type": "object", "properties": { "captcha": { "description": "验证码", "type": "string" }, "captchaId": { "description": "验证码ID", "type": "string" }, "password": { "description": "密码", "type": "string" }, "username": { "description": "用户名", "type": "string" } } }, "request.PageInfo": { "type": "object", "properties": { "keyword": { "description": "关键字", "type": "string" }, "page": { "description": "页码", "type": "integer" }, "pageSize": { "description": "每页大小", "type": "integer" } } }, "request.Register": { "type": "object", "properties": { "authorityId": { "type": "string", "example": "int 角色id" }, "authorityIds": { "type": "string", "example": "[]uint 角色id" }, "email": { "type": "string", "example": "电子邮箱" }, "enable": { "type": "string", "example": "int 是否启用" }, "headerImg": { "type": "string", "example": "头像链接" }, "nickName": { "type": "string", "example": "昵称" }, "passWord": { "type": "string", "example": "密码" }, "phone": { "type": "string", "example": "电话号码" }, "userName": { "type": "string", "example": "用户名" } } }, "request.SearchApiParams": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "apiGroup": { "description": "api组", "type": "string" }, "createdAt": { "description": "创建时间", "type": "string" }, "desc": { "description": "排序方式:升序false(默认)|降序true", "type": "boolean" }, "description": { "description": "api中文描述", "type": "string" }, "keyword": { "description": "关键字", "type": "string" }, "method": { "description": "方法:创建POST(默认)|查看GET|更新PUT|删除DELETE", "type": "string" }, "orderKey": { "description": "排序", "type": "string" }, "page": { "description": "页码", "type": "integer" }, "pageSize": { "description": "每页大小", "type": "integer" }, "path": { "description": "api路径", "type": "string" }, "updatedAt": { "description": "更新时间", "type": "string" } } }, "request.SetUserAuth": { "type": "object", "properties": { "authorityId": { "description": "角色ID", "type": "integer" } } }, "request.SetUserAuthorities": { "type": "object", "properties": { "authorityIds": { "description": "角色ID", "type": "array", "items": { "type": "integer" } }, "id": { "type": "integer" } } }, "request.SysAuthorityBtnReq": { "type": "object", "properties": { "authorityId": { "type": "integer" }, "menuID": { "type": "integer" }, "selected": { "type": "array", "items": { "type": "integer" } } } }, "request.SysAutoCodePackageCreate": { "type": "object", "properties": { "desc": { "type": "string", "example": "描述" }, "label": { "type": "string", "example": "展示名" }, "packageName": { "type": "string", "example": "包名" }, "template": { "type": "string", "example": "模版" } } }, "request.SysAutoHistoryRollBack": { "type": "object", "properties": { "deleteApi": { "description": "是否删除接口", "type": "boolean" }, "deleteMenu": { "description": "是否删除菜单", "type": "boolean" }, "deleteTable": { "description": "是否删除表", "type": "boolean" }, "id": { "description": "主键ID", "type": "integer" } } }, "response.Email": { "type": "object", "properties": { "body": { "description": "邮件内容", "type": "string" }, "subject": { "description": "邮件标题", "type": "string" }, "to": { "description": "邮件发送给谁", "type": "string" } } }, "response.ExaCustomerResponse": { "type": "object", "properties": { "customer": { "$ref": "#/definitions/example.ExaCustomer" } } }, "response.ExaFileResponse": { "type": "object", "properties": { "file": { "$ref": "#/definitions/example.ExaFileUploadAndDownload" } } }, "response.FilePathResponse": { "type": "object", "properties": { "filePath": { "type": "string" } } }, "response.FileResponse": { "type": "object", "properties": { "file": { "$ref": "#/definitions/example.ExaFile" } } }, "response.LoginResponse": { "type": "object", "properties": { "expiresAt": { "type": "integer" }, "token": { "type": "string" }, "user": { "$ref": "#/definitions/system.SysUser" } } }, "response.PageResult": { "type": "object", "properties": { "list": {}, "page": { "type": "integer" }, "pageSize": { "type": "integer" }, "total": { "type": "integer" } } }, "response.PolicyPathResponse": { "type": "object", "properties": { "paths": { "type": "array", "items": { "$ref": "#/definitions/request.CasbinInfo" } } } }, "response.Response": { "type": "object", "properties": { "code": { "type": "integer" }, "data": {}, "msg": { "type": "string" } } }, "response.SysAPIListResponse": { "type": "object", "properties": { "apis": { "type": "array", "items": { "$ref": "#/definitions/system.SysApi" } } } }, "response.SysAPIResponse": { "type": "object", "properties": { "api": { "$ref": "#/definitions/system.SysApi" } } }, "response.SysAuthorityBtnRes": { "type": "object", "properties": { "selected": { "type": "array", "items": { "type": "integer" } } } }, "response.SysAuthorityCopyResponse": { "type": "object", "properties": { "authority": { "$ref": "#/definitions/system.SysAuthority" }, "oldAuthorityId": { "description": "旧角色ID", "type": "integer" } } }, "response.SysAuthorityResponse": { "type": "object", "properties": { "authority": { "$ref": "#/definitions/system.SysAuthority" } } }, "response.SysBaseMenuResponse": { "type": "object", "properties": { "menu": { "$ref": "#/definitions/system.SysBaseMenu" } } }, "response.SysBaseMenusResponse": { "type": "object", "properties": { "menus": { "type": "array", "items": { "$ref": "#/definitions/system.SysBaseMenu" } } } }, "response.SysCaptchaResponse": { "type": "object", "properties": { "captchaId": { "type": "string" }, "captchaLength": { "type": "integer" }, "openCaptcha": { "type": "boolean" }, "picPath": { "type": "string" } } }, "response.SysConfigResponse": { "type": "object", "properties": { "config": { "$ref": "#/definitions/config.Server" } } }, "response.SysMenusResponse": { "type": "object", "properties": { "menus": { "type": "array", "items": { "$ref": "#/definitions/system.SysMenu" } } } }, "response.SysUserResponse": { "type": "object", "properties": { "user": { "$ref": "#/definitions/system.SysUser" } } }, "system.Condition": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "column": { "type": "string" }, "createdAt": { "description": "创建时间", "type": "string" }, "from": { "type": "string" }, "operator": { "type": "string" }, "templateID": { "type": "string" }, "updatedAt": { "description": "更新时间", "type": "string" } } }, "system.JoinTemplate": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "createdAt": { "description": "创建时间", "type": "string" }, "joins": { "type": "string" }, "on": { "type": "string" }, "table": { "type": "string" }, "templateID": { "type": "string" }, "updatedAt": { "description": "更新时间", "type": "string" } } }, "system.Meta": { "type": "object", "properties": { "activeName": { "type": "string" }, "closeTab": { "description": "自动关闭tab", "type": "boolean" }, "defaultMenu": { "description": "是否是基础路由(开发中)", "type": "boolean" }, "icon": { "description": "菜单图标", "type": "string" }, "keepAlive": { "description": "是否缓存", "type": "boolean" }, "title": { "description": "菜单名", "type": "string" } } }, "system.SysApi": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "apiGroup": { "description": "api组", "type": "string" }, "createdAt": { "description": "创建时间", "type": "string" }, "description": { "description": "api中文描述", "type": "string" }, "method": { "description": "方法:创建POST(默认)|查看GET|更新PUT|删除DELETE", "type": "string" }, "path": { "description": "api路径", "type": "string" }, "updatedAt": { "description": "更新时间", "type": "string" } } }, "system.SysAuthority": { "type": "object", "properties": { "authorityId": { "description": "角色ID", "type": "integer" }, "authorityName": { "description": "角色名", "type": "string" }, "children": { "type": "array", "items": { "$ref": "#/definitions/system.SysAuthority" } }, "createdAt": { "description": "创建时间", "type": "string" }, "dataAuthorityId": { "type": "array", "items": { "$ref": "#/definitions/system.SysAuthority" } }, "defaultRouter": { "description": "默认菜单(默认dashboard)", "type": "string" }, "deletedAt": { "type": "string" }, "menus": { "type": "array", "items": { "$ref": "#/definitions/system.SysBaseMenu" } }, "parentId": { "description": "父角色ID", "type": "integer" }, "updatedAt": { "description": "更新时间", "type": "string" } } }, "system.SysBaseMenu": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "authoritys": { "type": "array", "items": { "$ref": "#/definitions/system.SysAuthority" } }, "children": { "type": "array", "items": { "$ref": "#/definitions/system.SysBaseMenu" } }, "component": { "description": "对应前端文件路径", "type": "string" }, "createdAt": { "description": "创建时间", "type": "string" }, "hidden": { "description": "是否在列表隐藏", "type": "boolean" }, "menuBtn": { "type": "array", "items": { "$ref": "#/definitions/system.SysBaseMenuBtn" } }, "meta": { "description": "附加属性", "allOf": [ { "$ref": "#/definitions/system.Meta" } ] }, "name": { "description": "路由name", "type": "string" }, "parameters": { "type": "array", "items": { "$ref": "#/definitions/system.SysBaseMenuParameter" } }, "parentId": { "description": "父菜单ID", "type": "integer" }, "path": { "description": "路由path", "type": "string" }, "sort": { "description": "排序标记", "type": "integer" }, "updatedAt": { "description": "更新时间", "type": "string" } } }, "system.SysBaseMenuBtn": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "createdAt": { "description": "创建时间", "type": "string" }, "desc": { "type": "string" }, "name": { "type": "string" }, "sysBaseMenuID": { "type": "integer" }, "updatedAt": { "description": "更新时间", "type": "string" } } }, "system.SysBaseMenuParameter": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "createdAt": { "description": "创建时间", "type": "string" }, "key": { "description": "地址栏携带参数的key", "type": "string" }, "sysBaseMenuID": { "type": "integer" }, "type": { "description": "地址栏携带参数为params还是query", "type": "string" }, "updatedAt": { "description": "更新时间", "type": "string" }, "value": { "description": "地址栏携带参数的值", "type": "string" } } }, "system.SysDictionary": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "createdAt": { "description": "创建时间", "type": "string" }, "desc": { "description": "描述", "type": "string" }, "name": { "description": "字典名(中)", "type": "string" }, "status": { "description": "状态", "type": "boolean" }, "sysDictionaryDetails": { "type": "array", "items": { "$ref": "#/definitions/system.SysDictionaryDetail" } }, "type": { "description": "字典名(英)", "type": "string" }, "updatedAt": { "description": "更新时间", "type": "string" } } }, "system.SysDictionaryDetail": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "createdAt": { "description": "创建时间", "type": "string" }, "extend": { "description": "扩展值", "type": "string" }, "label": { "description": "展示值", "type": "string" }, "sort": { "description": "排序标记", "type": "integer" }, "status": { "description": "启用状态", "type": "boolean" }, "sysDictionaryID": { "description": "关联标记", "type": "integer" }, "updatedAt": { "description": "更新时间", "type": "string" }, "value": { "description": "字典值", "type": "string" } } }, "system.SysExportTemplate": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "conditions": { "type": "array", "items": { "$ref": "#/definitions/system.Condition" } }, "createdAt": { "description": "创建时间", "type": "string" }, "dbName": { "description": "数据库名称", "type": "string" }, "joinTemplate": { "type": "array", "items": { "$ref": "#/definitions/system.JoinTemplate" } }, "limit": { "type": "integer" }, "name": { "description": "模板名称", "type": "string" }, "order": { "type": "string" }, "tableName": { "description": "表名称", "type": "string" }, "templateID": { "description": "模板标识", "type": "string" }, "templateInfo": { "description": "模板信息", "type": "string" }, "updatedAt": { "description": "更新时间", "type": "string" } } }, "system.SysMenu": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "authoritys": { "type": "array", "items": { "$ref": "#/definitions/system.SysAuthority" } }, "btns": { "type": "object", "additionalProperties": { "type": "integer" } }, "children": { "type": "array", "items": { "$ref": "#/definitions/system.SysMenu" } }, "component": { "description": "对应前端文件路径", "type": "string" }, "createdAt": { "description": "创建时间", "type": "string" }, "hidden": { "description": "是否在列表隐藏", "type": "boolean" }, "menuBtn": { "type": "array", "items": { "$ref": "#/definitions/system.SysBaseMenuBtn" } }, "menuId": { "type": "integer" }, "meta": { "description": "附加属性", "allOf": [ { "$ref": "#/definitions/system.Meta" } ] }, "name": { "description": "路由name", "type": "string" }, "parameters": { "type": "array", "items": { "$ref": "#/definitions/system.SysBaseMenuParameter" } }, "parentId": { "description": "父菜单ID", "type": "integer" }, "path": { "description": "路由path", "type": "string" }, "sort": { "description": "排序标记", "type": "integer" }, "updatedAt": { "description": "更新时间", "type": "string" } } }, "system.SysOperationRecord": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "agent": { "description": "代理", "type": "string" }, "body": { "description": "请求Body", "type": "string" }, "createdAt": { "description": "创建时间", "type": "string" }, "error_message": { "description": "错误信息", "type": "string" }, "ip": { "description": "请求ip", "type": "string" }, "latency": { "description": "延迟", "type": "string" }, "method": { "description": "请求方法", "type": "string" }, "path": { "description": "请求路径", "type": "string" }, "resp": { "description": "响应Body", "type": "string" }, "status": { "description": "请求状态", "type": "integer" }, "updatedAt": { "description": "更新时间", "type": "string" }, "user": { "$ref": "#/definitions/system.SysUser" }, "user_id": { "description": "用户id", "type": "integer" } } }, "system.SysParams": { "type": "object", "required": [ "key", "name", "value" ], "properties": { "ID": { "description": "主键ID", "type": "integer" }, "createdAt": { "description": "创建时间", "type": "string" }, "desc": { "description": "参数说明", "type": "string" }, "key": { "description": "参数键", "type": "string" }, "name": { "description": "参数名称", "type": "string" }, "updatedAt": { "description": "更新时间", "type": "string" }, "value": { "description": "参数值", "type": "string" } } }, "system.SysUser": { "type": "object", "properties": { "ID": { "description": "主键ID", "type": "integer" }, "authorities": { "description": "多用户角色", "type": "array", "items": { "$ref": "#/definitions/system.SysAuthority" } }, "authority": { "description": "用户角色", "allOf": [ { "$ref": "#/definitions/system.SysAuthority" } ] }, "authorityId": { "description": "用户角色ID", "type": "integer" }, "createdAt": { "description": "创建时间", "type": "string" }, "email": { "description": "用户邮箱", "type": "string" }, "enable": { "description": "用户是否被冻结 1正常 2冻结", "type": "integer" }, "headerImg": { "description": "用户头像", "type": "string" }, "nickName": { "description": "用户昵称", "type": "string" }, "originSetting": { "description": "配置", "allOf": [ { "$ref": "#/definitions/common.JSONMap" } ] }, "phone": { "description": "用户手机号", "type": "string" }, "updatedAt": { "description": "更新时间", "type": "string" }, "userName": { "description": "用户登录名", "type": "string" }, "uuid": { "description": "用户UUID", "type": "string" } } }, "system.System": { "type": "object", "properties": { "config": { "$ref": "#/definitions/config.Server" } } } }, "securityDefinitions": { "ApiKeyAuth": { "type": "apiKey", "name": "x-token", "in": "header" } }, "tags": [ { "name": "Base" }, { "description": "用户", "name": "SysUser" } ] } ================================================ FILE: server/docs/swagger.yaml ================================================ definitions: common.JSONMap: additionalProperties: true type: object config.AliyunOSS: properties: access-key-id: type: string access-key-secret: type: string base-path: type: string bucket-name: type: string bucket-url: type: string endpoint: type: string type: object config.Autocode: properties: ai-path: type: string module: type: string root: type: string server: type: string web: type: string type: object config.AwsS3: properties: base-url: type: string bucket: type: string disable-ssl: type: boolean endpoint: type: string path-prefix: type: string region: type: string s3-force-path-style: type: boolean secret-id: type: string secret-key: type: string type: object config.CORS: properties: mode: type: string whitelist: items: $ref: '#/definitions/config.CORSWhitelist' type: array type: object config.CORSWhitelist: properties: allow-credentials: type: boolean allow-headers: type: string allow-methods: type: string allow-origin: type: string expose-headers: type: string type: object config.Captcha: properties: img-height: description: 验证码高度 type: integer img-width: description: 验证码宽度 type: integer key-long: description: 验证码长度 type: integer open-captcha: description: 防爆破验证码开启此数,0代表每次登录都需要验证码,其他数字代表错误密码此数,如3代表错误三次后出现验证码 type: integer open-captcha-timeout: description: 防爆破验证码超时时间,单位:s(秒) type: integer type: object config.CloudflareR2: properties: access-key-id: type: string account-id: type: string base-url: type: string bucket: type: string path: type: string secret-access-key: type: string type: object config.DiskList: properties: mount-point: type: string type: object config.Excel: properties: dir: type: string type: object config.HuaWeiObs: properties: access-key: type: string bucket: type: string endpoint: type: string path: type: string secret-key: type: string type: object config.JWT: properties: buffer-time: description: 缓冲时间 type: string expires-time: description: 过期时间 type: string issuer: description: 签发者 type: string signing-key: description: jwt签名 type: string type: object config.Local: properties: path: description: 本地文件访问路径 type: string store-path: description: 本地文件存储路径 type: string type: object config.Minio: properties: access-key-id: type: string access-key-secret: type: string base-path: type: string bucket-name: type: string bucket-url: type: string endpoint: type: string use-ssl: type: boolean type: object config.Mongo: properties: auth-source: description: 验证数据库 type: string coll: description: collection name type: string connect-timeout-ms: description: 连接超时时间 type: integer database: description: database name type: string hosts: description: 主机列表 items: $ref: '#/definitions/config.MongoHost' type: array is-zap: description: 是否开启zap日志 type: boolean max-pool-size: description: 最大连接池 type: integer min-pool-size: description: 最小连接池 type: integer options: description: mongodb options type: string password: description: 密码 type: string socket-timeout-ms: description: socket超时时间 type: integer username: description: 用户名 type: string type: object config.MongoHost: properties: host: description: ip地址 type: string port: description: 端口 type: string type: object config.Mssql: properties: config: description: 高级配置 type: string db-name: description: 数据库名 type: string engine: default: InnoDB description: 数据库引擎,默认InnoDB type: string log-mode: description: 是否开启Gorm全局日志 type: string log-zap: description: 是否通过zap写入日志文件 type: boolean max-idle-conns: description: 空闲中的最大连接数 type: integer max-open-conns: description: 打开到数据库的最大连接数 type: integer password: description: 数据库密码 type: string path: description: 数据库地址 type: string port: description: 数据库端口 type: string prefix: description: 数据库前缀 type: string singular: description: 是否开启全局禁用复数,true表示开启 type: boolean username: description: 数据库账号 type: string type: object config.Mysql: properties: config: description: 高级配置 type: string db-name: description: 数据库名 type: string engine: default: InnoDB description: 数据库引擎,默认InnoDB type: string log-mode: description: 是否开启Gorm全局日志 type: string log-zap: description: 是否通过zap写入日志文件 type: boolean max-idle-conns: description: 空闲中的最大连接数 type: integer max-open-conns: description: 打开到数据库的最大连接数 type: integer password: description: 数据库密码 type: string path: description: 数据库地址 type: string port: description: 数据库端口 type: string prefix: description: 数据库前缀 type: string singular: description: 是否开启全局禁用复数,true表示开启 type: boolean username: description: 数据库账号 type: string type: object config.Oracle: properties: config: description: 高级配置 type: string db-name: description: 数据库名 type: string engine: default: InnoDB description: 数据库引擎,默认InnoDB type: string log-mode: description: 是否开启Gorm全局日志 type: string log-zap: description: 是否通过zap写入日志文件 type: boolean max-idle-conns: description: 空闲中的最大连接数 type: integer max-open-conns: description: 打开到数据库的最大连接数 type: integer password: description: 数据库密码 type: string path: description: 数据库地址 type: string port: description: 数据库端口 type: string prefix: description: 数据库前缀 type: string singular: description: 是否开启全局禁用复数,true表示开启 type: boolean username: description: 数据库账号 type: string type: object config.Pgsql: properties: config: description: 高级配置 type: string db-name: description: 数据库名 type: string engine: default: InnoDB description: 数据库引擎,默认InnoDB type: string log-mode: description: 是否开启Gorm全局日志 type: string log-zap: description: 是否通过zap写入日志文件 type: boolean max-idle-conns: description: 空闲中的最大连接数 type: integer max-open-conns: description: 打开到数据库的最大连接数 type: integer password: description: 数据库密码 type: string path: description: 数据库地址 type: string port: description: 数据库端口 type: string prefix: description: 数据库前缀 type: string singular: description: 是否开启全局禁用复数,true表示开启 type: boolean username: description: 数据库账号 type: string type: object config.Qiniu: properties: access-key: description: 秘钥AK type: string bucket: description: 空间名称 type: string img-path: description: CDN加速域名 type: string secret-key: description: 秘钥SK type: string use-cdn-domains: description: 上传是否使用CDN上传加速 type: boolean use-https: description: 是否使用https type: boolean zone: description: 存储区域 type: string type: object config.Redis: properties: addr: description: 服务器地址:端口 type: string clusterAddrs: description: 集群模式下的节点地址列表 items: type: string type: array db: description: 单实例模式下redis的哪个数据库 type: integer name: description: 代表当前实例的名字 type: string password: description: 密码 type: string useCluster: description: 是否使用集群模式 type: boolean type: object config.Server: properties: aliyun-oss: $ref: '#/definitions/config.AliyunOSS' autocode: allOf: - $ref: '#/definitions/config.Autocode' description: auto aws-s3: $ref: '#/definitions/config.AwsS3' captcha: $ref: '#/definitions/config.Captcha' cloudflare-r2: $ref: '#/definitions/config.CloudflareR2' cors: allOf: - $ref: '#/definitions/config.CORS' description: 跨域配置 db-list: items: $ref: '#/definitions/config.SpecializedDB' type: array disk-list: items: $ref: '#/definitions/config.DiskList' type: array email: $ref: '#/definitions/github_com_flipped-aurora_gin-vue-admin_server_config.Email' excel: $ref: '#/definitions/config.Excel' hua-wei-obs: $ref: '#/definitions/config.HuaWeiObs' jwt: $ref: '#/definitions/config.JWT' local: allOf: - $ref: '#/definitions/config.Local' description: oss minio: $ref: '#/definitions/config.Minio' mongo: $ref: '#/definitions/config.Mongo' mssql: $ref: '#/definitions/config.Mssql' mysql: allOf: - $ref: '#/definitions/config.Mysql' description: gorm oracle: $ref: '#/definitions/config.Oracle' pgsql: $ref: '#/definitions/config.Pgsql' qiniu: $ref: '#/definitions/config.Qiniu' redis: $ref: '#/definitions/config.Redis' redis-list: items: $ref: '#/definitions/config.Redis' type: array sqlite: $ref: '#/definitions/config.Sqlite' system: $ref: '#/definitions/config.System' tencent-cos: $ref: '#/definitions/config.TencentCOS' zap: $ref: '#/definitions/config.Zap' type: object config.SpecializedDB: properties: alias-name: type: string config: description: 高级配置 type: string db-name: description: 数据库名 type: string disable: type: boolean engine: default: InnoDB description: 数据库引擎,默认InnoDB type: string log-mode: description: 是否开启Gorm全局日志 type: string log-zap: description: 是否通过zap写入日志文件 type: boolean max-idle-conns: description: 空闲中的最大连接数 type: integer max-open-conns: description: 打开到数据库的最大连接数 type: integer password: description: 数据库密码 type: string path: description: 数据库地址 type: string port: description: 数据库端口 type: string prefix: description: 数据库前缀 type: string singular: description: 是否开启全局禁用复数,true表示开启 type: boolean type: type: string username: description: 数据库账号 type: string type: object config.Sqlite: properties: config: description: 高级配置 type: string db-name: description: 数据库名 type: string engine: default: InnoDB description: 数据库引擎,默认InnoDB type: string log-mode: description: 是否开启Gorm全局日志 type: string log-zap: description: 是否通过zap写入日志文件 type: boolean max-idle-conns: description: 空闲中的最大连接数 type: integer max-open-conns: description: 打开到数据库的最大连接数 type: integer password: description: 数据库密码 type: string path: description: 数据库地址 type: string port: description: 数据库端口 type: string prefix: description: 数据库前缀 type: string singular: description: 是否开启全局禁用复数,true表示开启 type: boolean username: description: 数据库账号 type: string type: object config.System: properties: addr: description: 端口值 type: integer db-type: description: 数据库类型:mysql(默认)|sqlite|sqlserver|postgresql type: string iplimit-count: type: integer iplimit-time: type: integer oss-type: description: Oss类型 type: string router-prefix: type: string use-mongo: description: 使用mongo type: boolean use-multipoint: description: 多点登录拦截 type: boolean use-redis: description: 使用redis type: boolean use-strict-auth: description: 使用树形角色分配模式 type: boolean type: object config.TencentCOS: properties: base-url: type: string bucket: type: string path-prefix: type: string region: type: string secret-id: type: string secret-key: type: string type: object config.Zap: properties: director: description: 日志文件夹 type: string encode-level: description: 编码级 type: string format: description: 输出 type: string level: description: 级别 type: string log-in-console: description: 输出控制台 type: boolean prefix: description: 日志前缀 type: string retention-day: description: 日志保留天数 type: integer show-line: description: 显示行 type: boolean stacktrace-key: description: 栈名 type: string type: object example.ExaAttachmentCategory: properties: ID: description: 主键ID type: integer children: items: $ref: '#/definitions/example.ExaAttachmentCategory' type: array createdAt: description: 创建时间 type: string name: type: string pid: type: integer updatedAt: description: 更新时间 type: string type: object example.ExaCustomer: properties: ID: description: 主键ID type: integer createdAt: description: 创建时间 type: string customerName: description: 客户名 type: string customerPhoneData: description: 客户手机号 type: string sysUser: allOf: - $ref: '#/definitions/system.SysUser' description: 管理详情 sysUserAuthorityID: description: 管理角色ID type: integer sysUserId: description: 管理ID type: integer updatedAt: description: 更新时间 type: string type: object example.ExaFile: properties: ID: description: 主键ID type: integer chunkTotal: type: integer createdAt: description: 创建时间 type: string exaFileChunk: items: $ref: '#/definitions/example.ExaFileChunk' type: array fileMd5: type: string fileName: type: string filePath: type: string isFinish: type: boolean updatedAt: description: 更新时间 type: string type: object example.ExaFileChunk: properties: ID: description: 主键ID type: integer createdAt: description: 创建时间 type: string exaFileID: type: integer fileChunkNumber: type: integer fileChunkPath: type: string updatedAt: description: 更新时间 type: string type: object example.ExaFileUploadAndDownload: properties: ID: description: 主键ID type: integer classId: description: 分类id type: integer createdAt: description: 创建时间 type: string key: description: 编号 type: string name: description: 文件名 type: string tag: description: 文件标签 type: string updatedAt: description: 更新时间 type: string url: description: 文件地址 type: string type: object github_com_flipped-aurora_gin-vue-admin_server_config.Email: properties: from: description: 发件人 你自己要发邮件的邮箱 type: string host: description: 服务器地址 例如 smtp.qq.com 请前往QQ或者你要发邮件的邮箱查看其smtp协议 type: string is-ssl: description: 是否SSL 是否开启SSL type: boolean nickname: description: 昵称 发件人昵称 通常为自己的邮箱 type: string port: description: 端口 请前往QQ或者你要发邮件的邮箱查看其smtp协议 大多为 465 type: integer secret: description: 密钥 用于登录的密钥 最好不要用邮箱密码 去邮箱smtp申请一个用于登录的密钥 type: string to: description: 收件人:多个以英文逗号分隔 例:a@qq.com b@qq.com 正式开发中请把此项目作为参数使用 type: string type: object model.Info: properties: ID: description: 主键ID type: integer attachments: description: 附件 items: type: object type: array content: description: 内容 type: string createdAt: description: 创建时间 type: string title: description: 标题 type: string updatedAt: description: 更新时间 type: string userID: description: 作者 type: integer type: object request.AddMenuAuthorityInfo: properties: authorityId: description: 角色ID type: integer menus: items: $ref: '#/definitions/system.SysBaseMenu' type: array type: object request.AutoCode: properties: abbreviation: description: Struct简称 example: Struct简称 type: string autoCreateApiToSql: description: 是否自动创建api example: false type: boolean autoCreateBtnAuth: description: 是否自动创建按钮权限 example: false type: boolean autoCreateMenuToSql: description: 是否自动创建menu example: false type: boolean autoCreateResource: description: 是否自动创建资源标识 example: false type: boolean autoMigrate: description: 是否自动迁移表结构 example: false type: boolean businessDB: description: 业务数据库 example: 业务数据库 type: string description: description: Struct中文名称 example: Struct中文名称 type: string fields: items: $ref: '#/definitions/request.AutoCodeField' type: array generateServer: description: 是否生成server example: true type: boolean generateWeb: description: 是否生成web example: true type: boolean gvaModel: description: 是否使用gva默认Model example: false type: boolean humpPackageName: description: go文件名称 example: go文件名称 type: string isAdd: description: 是否新增 example: false type: boolean isTree: description: 是否树形结构 example: false type: boolean onlyTemplate: description: 是否只生成模板 example: false type: boolean package: type: string packageName: description: 文件名称 example: 文件名称 type: string primaryField: $ref: '#/definitions/request.AutoCodeField' structName: description: Struct名称 example: Struct名称 type: string tableName: description: 表名 example: 表名 type: string treeJson: description: 展示的树json字段 example: 展示的树json字段 type: string type: object request.AutoCodeField: properties: checkDataSource: description: 是否检查数据源 type: boolean clearable: description: 是否可清空 type: boolean columnName: description: 数据库字段 type: string comment: description: 数据库字段描述 type: string dataSource: allOf: - $ref: '#/definitions/request.DataSource' description: 数据源 dataTypeLong: description: 数据库字段长度 type: string defaultValue: description: 是否必填 type: string desc: description: 是否前端详情 type: boolean dictType: description: 字典 type: string errorText: description: 校验失败文字 type: string excel: description: 是否导入/导出 type: boolean fieldDesc: description: 中文名 type: string fieldIndexType: description: 索引类型 type: string fieldJson: description: FieldJson type: string fieldName: description: Field名 type: string fieldSearchHide: description: 是否隐藏查询条件 type: boolean fieldSearchType: description: 搜索条件 type: string fieldType: description: Field数据类型 type: string form: description: Front bool `json:"front"` // 是否前端可见 type: boolean primaryKey: description: 是否主键 type: boolean require: description: 是否必填 type: boolean sort: description: 是否增加排序 type: boolean table: description: 是否前端表格列 type: boolean type: object request.CasbinInReceive: properties: authorityId: description: 权限id type: integer casbinInfos: items: $ref: '#/definitions/request.CasbinInfo' type: array type: object request.CasbinInfo: properties: method: description: 方法 type: string path: description: 路径 type: string type: object request.ChangePasswordReq: properties: newPassword: description: 新密码 type: string password: description: 密码 type: string type: object request.DataSource: properties: association: description: 关联关系 1 一对一 2 一对多 type: integer dbName: type: string hasDeletedAt: type: boolean label: type: string table: type: string value: type: string type: object request.Empty: type: object request.ExaAttachmentCategorySearch: properties: classId: type: integer keyword: description: 关键字 type: string page: description: 页码 type: integer pageSize: description: 每页大小 type: integer type: object request.GetAuthorityId: properties: authorityId: description: 角色ID type: integer type: object request.GetById: properties: id: description: 主键ID type: integer type: object request.GetUserList: properties: email: type: string keyword: description: 关键字 type: string nickName: type: string page: description: 页码 type: integer pageSize: description: 每页大小 type: integer phone: type: string username: type: string type: object request.IdsReq: properties: ids: items: type: integer type: array type: object request.InitDB: properties: adminPassword: type: string dbName: description: 数据库名 type: string dbPath: description: sqlite数据库文件路径 type: string dbType: description: 数据库类型 type: string host: description: 服务器地址 type: string password: description: 数据库密码 type: string port: description: 数据库连接端口 type: string template: description: postgresql指定template type: string userName: description: 数据库用户名 type: string required: - adminPassword - dbName type: object request.Login: properties: captcha: description: 验证码 type: string captchaId: description: 验证码ID type: string password: description: 密码 type: string username: description: 用户名 type: string type: object request.PageInfo: properties: keyword: description: 关键字 type: string page: description: 页码 type: integer pageSize: description: 每页大小 type: integer type: object request.Register: properties: authorityId: example: int 角色id type: string authorityIds: example: '[]uint 角色id' type: string email: example: 电子邮箱 type: string enable: example: int 是否启用 type: string headerImg: example: 头像链接 type: string nickName: example: 昵称 type: string passWord: example: 密码 type: string phone: example: 电话号码 type: string userName: example: 用户名 type: string type: object request.SearchApiParams: properties: ID: description: 主键ID type: integer apiGroup: description: api组 type: string createdAt: description: 创建时间 type: string desc: description: 排序方式:升序false(默认)|降序true type: boolean description: description: api中文描述 type: string keyword: description: 关键字 type: string method: description: 方法:创建POST(默认)|查看GET|更新PUT|删除DELETE type: string orderKey: description: 排序 type: string page: description: 页码 type: integer pageSize: description: 每页大小 type: integer path: description: api路径 type: string updatedAt: description: 更新时间 type: string type: object request.SetUserAuth: properties: authorityId: description: 角色ID type: integer type: object request.SetUserAuthorities: properties: authorityIds: description: 角色ID items: type: integer type: array id: type: integer type: object request.SysAuthorityBtnReq: properties: authorityId: type: integer menuID: type: integer selected: items: type: integer type: array type: object request.SysAutoCodePackageCreate: properties: desc: example: 描述 type: string label: example: 展示名 type: string packageName: example: 包名 type: string template: example: 模版 type: string type: object request.SysAutoHistoryRollBack: properties: deleteApi: description: 是否删除接口 type: boolean deleteMenu: description: 是否删除菜单 type: boolean deleteTable: description: 是否删除表 type: boolean id: description: 主键ID type: integer type: object response.Email: properties: body: description: 邮件内容 type: string subject: description: 邮件标题 type: string to: description: 邮件发送给谁 type: string type: object response.ExaCustomerResponse: properties: customer: $ref: '#/definitions/example.ExaCustomer' type: object response.ExaFileResponse: properties: file: $ref: '#/definitions/example.ExaFileUploadAndDownload' type: object response.FilePathResponse: properties: filePath: type: string type: object response.FileResponse: properties: file: $ref: '#/definitions/example.ExaFile' type: object response.LoginResponse: properties: expiresAt: type: integer token: type: string user: $ref: '#/definitions/system.SysUser' type: object response.PageResult: properties: list: {} page: type: integer pageSize: type: integer total: type: integer type: object response.PolicyPathResponse: properties: paths: items: $ref: '#/definitions/request.CasbinInfo' type: array type: object response.Response: properties: code: type: integer data: {} msg: type: string type: object response.SysAPIListResponse: properties: apis: items: $ref: '#/definitions/system.SysApi' type: array type: object response.SysAPIResponse: properties: api: $ref: '#/definitions/system.SysApi' type: object response.SysAuthorityBtnRes: properties: selected: items: type: integer type: array type: object response.SysAuthorityCopyResponse: properties: authority: $ref: '#/definitions/system.SysAuthority' oldAuthorityId: description: 旧角色ID type: integer type: object response.SysAuthorityResponse: properties: authority: $ref: '#/definitions/system.SysAuthority' type: object response.SysBaseMenuResponse: properties: menu: $ref: '#/definitions/system.SysBaseMenu' type: object response.SysBaseMenusResponse: properties: menus: items: $ref: '#/definitions/system.SysBaseMenu' type: array type: object response.SysCaptchaResponse: properties: captchaId: type: string captchaLength: type: integer openCaptcha: type: boolean picPath: type: string type: object response.SysConfigResponse: properties: config: $ref: '#/definitions/config.Server' type: object response.SysMenusResponse: properties: menus: items: $ref: '#/definitions/system.SysMenu' type: array type: object response.SysUserResponse: properties: user: $ref: '#/definitions/system.SysUser' type: object system.Condition: properties: ID: description: 主键ID type: integer column: type: string createdAt: description: 创建时间 type: string from: type: string operator: type: string templateID: type: string updatedAt: description: 更新时间 type: string type: object system.JoinTemplate: properties: ID: description: 主键ID type: integer createdAt: description: 创建时间 type: string joins: type: string "on": type: string table: type: string templateID: type: string updatedAt: description: 更新时间 type: string type: object system.Meta: properties: activeName: type: string closeTab: description: 自动关闭tab type: boolean defaultMenu: description: 是否是基础路由(开发中) type: boolean icon: description: 菜单图标 type: string keepAlive: description: 是否缓存 type: boolean title: description: 菜单名 type: string type: object system.SysApi: properties: ID: description: 主键ID type: integer apiGroup: description: api组 type: string createdAt: description: 创建时间 type: string description: description: api中文描述 type: string method: description: 方法:创建POST(默认)|查看GET|更新PUT|删除DELETE type: string path: description: api路径 type: string updatedAt: description: 更新时间 type: string type: object system.SysAuthority: properties: authorityId: description: 角色ID type: integer authorityName: description: 角色名 type: string children: items: $ref: '#/definitions/system.SysAuthority' type: array createdAt: description: 创建时间 type: string dataAuthorityId: items: $ref: '#/definitions/system.SysAuthority' type: array defaultRouter: description: 默认菜单(默认dashboard) type: string deletedAt: type: string menus: items: $ref: '#/definitions/system.SysBaseMenu' type: array parentId: description: 父角色ID type: integer updatedAt: description: 更新时间 type: string type: object system.SysBaseMenu: properties: ID: description: 主键ID type: integer authoritys: items: $ref: '#/definitions/system.SysAuthority' type: array children: items: $ref: '#/definitions/system.SysBaseMenu' type: array component: description: 对应前端文件路径 type: string createdAt: description: 创建时间 type: string hidden: description: 是否在列表隐藏 type: boolean menuBtn: items: $ref: '#/definitions/system.SysBaseMenuBtn' type: array meta: allOf: - $ref: '#/definitions/system.Meta' description: 附加属性 name: description: 路由name type: string parameters: items: $ref: '#/definitions/system.SysBaseMenuParameter' type: array parentId: description: 父菜单ID type: integer path: description: 路由path type: string sort: description: 排序标记 type: integer updatedAt: description: 更新时间 type: string type: object system.SysBaseMenuBtn: properties: ID: description: 主键ID type: integer createdAt: description: 创建时间 type: string desc: type: string name: type: string sysBaseMenuID: type: integer updatedAt: description: 更新时间 type: string type: object system.SysBaseMenuParameter: properties: ID: description: 主键ID type: integer createdAt: description: 创建时间 type: string key: description: 地址栏携带参数的key type: string sysBaseMenuID: type: integer type: description: 地址栏携带参数为params还是query type: string updatedAt: description: 更新时间 type: string value: description: 地址栏携带参数的值 type: string type: object system.SysDictionary: properties: ID: description: 主键ID type: integer createdAt: description: 创建时间 type: string desc: description: 描述 type: string name: description: 字典名(中) type: string status: description: 状态 type: boolean sysDictionaryDetails: items: $ref: '#/definitions/system.SysDictionaryDetail' type: array type: description: 字典名(英) type: string updatedAt: description: 更新时间 type: string type: object system.SysDictionaryDetail: properties: ID: description: 主键ID type: integer createdAt: description: 创建时间 type: string extend: description: 扩展值 type: string label: description: 展示值 type: string sort: description: 排序标记 type: integer status: description: 启用状态 type: boolean sysDictionaryID: description: 关联标记 type: integer updatedAt: description: 更新时间 type: string value: description: 字典值 type: string type: object system.SysExportTemplate: properties: ID: description: 主键ID type: integer conditions: items: $ref: '#/definitions/system.Condition' type: array createdAt: description: 创建时间 type: string dbName: description: 数据库名称 type: string joinTemplate: items: $ref: '#/definitions/system.JoinTemplate' type: array limit: type: integer name: description: 模板名称 type: string order: type: string tableName: description: 表名称 type: string templateID: description: 模板标识 type: string templateInfo: description: 模板信息 type: string updatedAt: description: 更新时间 type: string type: object system.SysMenu: properties: ID: description: 主键ID type: integer authoritys: items: $ref: '#/definitions/system.SysAuthority' type: array btns: additionalProperties: type: integer type: object children: items: $ref: '#/definitions/system.SysMenu' type: array component: description: 对应前端文件路径 type: string createdAt: description: 创建时间 type: string hidden: description: 是否在列表隐藏 type: boolean menuBtn: items: $ref: '#/definitions/system.SysBaseMenuBtn' type: array menuId: type: integer meta: allOf: - $ref: '#/definitions/system.Meta' description: 附加属性 name: description: 路由name type: string parameters: items: $ref: '#/definitions/system.SysBaseMenuParameter' type: array parentId: description: 父菜单ID type: integer path: description: 路由path type: string sort: description: 排序标记 type: integer updatedAt: description: 更新时间 type: string type: object system.SysOperationRecord: properties: ID: description: 主键ID type: integer agent: description: 代理 type: string body: description: 请求Body type: string createdAt: description: 创建时间 type: string error_message: description: 错误信息 type: string ip: description: 请求ip type: string latency: description: 延迟 type: string method: description: 请求方法 type: string path: description: 请求路径 type: string resp: description: 响应Body type: string status: description: 请求状态 type: integer updatedAt: description: 更新时间 type: string user: $ref: '#/definitions/system.SysUser' user_id: description: 用户id type: integer type: object system.SysParams: properties: ID: description: 主键ID type: integer createdAt: description: 创建时间 type: string desc: description: 参数说明 type: string key: description: 参数键 type: string name: description: 参数名称 type: string updatedAt: description: 更新时间 type: string value: description: 参数值 type: string required: - key - name - value type: object system.SysUser: properties: ID: description: 主键ID type: integer authorities: description: 多用户角色 items: $ref: '#/definitions/system.SysAuthority' type: array authority: allOf: - $ref: '#/definitions/system.SysAuthority' description: 用户角色 authorityId: description: 用户角色ID type: integer createdAt: description: 创建时间 type: string email: description: 用户邮箱 type: string enable: description: 用户是否被冻结 1正常 2冻结 type: integer headerImg: description: 用户头像 type: string nickName: description: 用户昵称 type: string originSetting: allOf: - $ref: '#/definitions/common.JSONMap' description: 配置 phone: description: 用户手机号 type: string updatedAt: description: 更新时间 type: string userName: description: 用户登录名 type: string uuid: description: 用户UUID type: string type: object system.System: properties: config: $ref: '#/definitions/config.Server' type: object info: contact: {} description: 使用gin+vue进行极速开发的全栈开发基础平台 title: Gin-Vue-Admin Swagger API接口文档 version: v2.7.9-beta paths: /api/createApi: post: consumes: - application/json parameters: - description: api路径, api中文描述, api组, 方法 in: body name: data required: true schema: $ref: '#/definitions/system.SysApi' produces: - application/json responses: "200": description: 创建基础api schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 创建基础api tags: - SysApi /api/deleteApi: post: consumes: - application/json parameters: - description: ID in: body name: data required: true schema: $ref: '#/definitions/system.SysApi' produces: - application/json responses: "200": description: 删除api schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 删除api tags: - SysApi /api/deleteApisByIds: delete: consumes: - application/json parameters: - description: ID in: body name: data required: true schema: $ref: '#/definitions/request.IdsReq' produces: - application/json responses: "200": description: 删除选中Api schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 删除选中Api tags: - SysApi /api/enterSyncApi: post: consumes: - application/json produces: - application/json responses: "200": description: 确认同步API schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 确认同步API tags: - SysApi /api/freshCasbin: get: consumes: - application/json produces: - application/json responses: "200": description: 刷新成功 schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object summary: 刷新casbin缓存 tags: - SysApi /api/getAllApis: post: consumes: - application/json produces: - application/json responses: "200": description: 获取所有的Api 不分页,返回包括api列表 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: $ref: '#/definitions/response.SysAPIListResponse' msg: type: string type: object security: - ApiKeyAuth: [] summary: 获取所有的Api 不分页 tags: - SysApi /api/getApiById: post: consumes: - application/json parameters: - description: 根据id获取api in: body name: data required: true schema: $ref: '#/definitions/request.GetById' produces: - application/json responses: "200": description: 根据id获取api,返回包括api详情 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: $ref: '#/definitions/response.SysAPIResponse' type: object security: - ApiKeyAuth: [] summary: 根据id获取api tags: - SysApi /api/getApiGroups: get: consumes: - application/json produces: - application/json responses: "200": description: 获取API分组 schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 获取API分组 tags: - SysApi /api/getApiList: post: consumes: - application/json parameters: - description: 分页获取API列表 in: body name: data required: true schema: $ref: '#/definitions/request.SearchApiParams' produces: - application/json responses: "200": description: 分页获取API列表,返回包括列表,总数,页码,每页数量 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: $ref: '#/definitions/response.PageResult' msg: type: string type: object security: - ApiKeyAuth: [] summary: 分页获取API列表 tags: - SysApi /api/ignoreApi: post: consumes: - application/json produces: - application/json responses: "200": description: 同步API schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 忽略API tags: - IgnoreApi /api/syncApi: get: consumes: - application/json produces: - application/json responses: "200": description: 同步API schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 同步API tags: - SysApi /api/updateApi: post: consumes: - application/json parameters: - description: api路径, api中文描述, api组, 方法 in: body name: data required: true schema: $ref: '#/definitions/system.SysApi' produces: - application/json responses: "200": description: 修改基础api schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 修改基础api tags: - SysApi /attachmentCategory/addCategory: post: consumes: - application/json parameters: - description: 媒体库分类数据 in: body name: data required: true schema: $ref: '#/definitions/example.ExaAttachmentCategory' produces: - application/json responses: {} security: - AttachmentCategory: [] summary: 添加媒体库分类 tags: - AddCategory /attachmentCategory/deleteCategory: post: consumes: - application/json parameters: - description: 分类id in: body name: data required: true schema: $ref: '#/definitions/request.GetById' produces: - application/json responses: "200": description: 删除分类 schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - AttachmentCategory: [] summary: 删除分类 tags: - DeleteCategory /attachmentCategory/getCategoryList: get: produces: - application/json responses: "200": description: 媒体库分类列表 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: $ref: '#/definitions/example.ExaAttachmentCategory' msg: type: string type: object security: - AttachmentCategory: [] summary: 媒体库分类列表 tags: - GetCategoryList /authority/copyAuthority: post: consumes: - application/json parameters: - description: 旧角色id, 新权限id, 新权限名, 新父角色id in: body name: data required: true schema: $ref: '#/definitions/response.SysAuthorityCopyResponse' produces: - application/json responses: "200": description: 拷贝角色,返回包括系统角色详情 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: $ref: '#/definitions/response.SysAuthorityResponse' msg: type: string type: object security: - ApiKeyAuth: [] summary: 拷贝角色 tags: - Authority /authority/createAuthority: post: consumes: - application/json parameters: - description: 权限id, 权限名, 父角色id in: body name: data required: true schema: $ref: '#/definitions/system.SysAuthority' produces: - application/json responses: "200": description: 创建角色,返回包括系统角色详情 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: $ref: '#/definitions/response.SysAuthorityResponse' msg: type: string type: object security: - ApiKeyAuth: [] summary: 创建角色 tags: - Authority /authority/deleteAuthority: post: consumes: - application/json parameters: - description: 删除角色 in: body name: data required: true schema: $ref: '#/definitions/system.SysAuthority' produces: - application/json responses: "200": description: 删除角色 schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 删除角色 tags: - Authority /authority/getAuthorityList: post: consumes: - application/json parameters: - description: 页码, 每页大小 in: body name: data required: true schema: $ref: '#/definitions/request.PageInfo' produces: - application/json responses: "200": description: 分页获取角色列表,返回包括列表,总数,页码,每页数量 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: $ref: '#/definitions/response.PageResult' msg: type: string type: object security: - ApiKeyAuth: [] summary: 分页获取角色列表 tags: - Authority /authority/setDataAuthority: post: consumes: - application/json parameters: - description: 设置角色资源权限 in: body name: data required: true schema: $ref: '#/definitions/system.SysAuthority' produces: - application/json responses: "200": description: 设置角色资源权限 schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 设置角色资源权限 tags: - Authority /authority/updateAuthority: put: consumes: - application/json parameters: - description: 权限id, 权限名, 父角色id in: body name: data required: true schema: $ref: '#/definitions/system.SysAuthority' produces: - application/json responses: "200": description: 更新角色信息,返回包括系统角色详情 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: $ref: '#/definitions/response.SysAuthorityResponse' msg: type: string type: object security: - ApiKeyAuth: [] summary: 更新角色信息 tags: - Authority /authorityBtn/canRemoveAuthorityBtn: post: consumes: - application/json produces: - application/json responses: "200": description: 删除成功 schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 设置权限按钮 tags: - AuthorityBtn /authorityBtn/getAuthorityBtn: post: consumes: - application/json parameters: - description: 菜单id, 角色id, 选中的按钮id in: body name: data required: true schema: $ref: '#/definitions/request.SysAuthorityBtnReq' produces: - application/json responses: "200": description: 返回列表成功 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: $ref: '#/definitions/response.SysAuthorityBtnRes' msg: type: string type: object security: - ApiKeyAuth: [] summary: 获取权限按钮 tags: - AuthorityBtn /authorityBtn/setAuthorityBtn: post: consumes: - application/json parameters: - description: 菜单id, 角色id, 选中的按钮id in: body name: data required: true schema: $ref: '#/definitions/request.SysAuthorityBtnReq' produces: - application/json responses: "200": description: 返回列表成功 schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 设置权限按钮 tags: - AuthorityBtn /autoCode/addFunc: post: consumes: - application/json parameters: - description: 增加方法 in: body name: data required: true schema: $ref: '#/definitions/request.AutoCode' produces: - application/json responses: "200": description: '{"success":true,"data":{},"msg":"创建成功"}' schema: type: string security: - ApiKeyAuth: [] summary: 增加方法 tags: - AddFunc /autoCode/createPackage: post: consumes: - application/json parameters: - description: 创建package in: body name: data required: true schema: $ref: '#/definitions/request.SysAutoCodePackageCreate' produces: - application/json responses: "200": description: 创建package成功 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: additionalProperties: true type: object msg: type: string type: object security: - ApiKeyAuth: [] summary: 创建package tags: - AutoCodePackage /autoCode/createTemp: post: consumes: - application/json parameters: - description: 创建自动代码 in: body name: data required: true schema: $ref: '#/definitions/request.AutoCode' produces: - application/json responses: "200": description: '{"success":true,"data":{},"msg":"创建成功"}' schema: type: string security: - ApiKeyAuth: [] summary: 自动代码模板 tags: - AutoCodeTemplate /autoCode/delPackage: post: consumes: - application/json parameters: - description: 创建package in: body name: data required: true schema: $ref: '#/definitions/request.GetById' produces: - application/json responses: "200": description: 删除package成功 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: additionalProperties: true type: object msg: type: string type: object security: - ApiKeyAuth: [] summary: 删除package tags: - AutoCode /autoCode/delSysHistory: post: consumes: - application/json parameters: - description: 请求参数 in: body name: data required: true schema: $ref: '#/definitions/request.GetById' produces: - application/json responses: "200": description: 删除回滚记录 schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 删除回滚记录 tags: - AutoCode /autoCode/getColumn: get: consumes: - application/json produces: - application/json responses: "200": description: 获取当前表所有字段 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: additionalProperties: true type: object msg: type: string type: object security: - ApiKeyAuth: [] summary: 获取当前表所有字段 tags: - AutoCode /autoCode/getDB: get: consumes: - application/json produces: - application/json responses: "200": description: 获取当前所有数据库 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: additionalProperties: true type: object msg: type: string type: object security: - ApiKeyAuth: [] summary: 获取当前所有数据库 tags: - AutoCode /autoCode/getMeta: post: consumes: - application/json parameters: - description: 请求参数 in: body name: data required: true schema: $ref: '#/definitions/request.GetById' produces: - application/json responses: "200": description: 获取meta信息 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: additionalProperties: true type: object msg: type: string type: object security: - ApiKeyAuth: [] summary: 获取meta信息 tags: - AutoCode /autoCode/getPackage: post: consumes: - application/json produces: - application/json responses: "200": description: 创建package成功 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: additionalProperties: true type: object msg: type: string type: object security: - ApiKeyAuth: [] summary: 获取package tags: - AutoCodePackage /autoCode/getSysHistory: post: consumes: - application/json parameters: - description: 请求参数 in: body name: data required: true schema: $ref: '#/definitions/request.PageInfo' produces: - application/json responses: "200": description: 查询回滚记录,返回包括列表,总数,页码,每页数量 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: $ref: '#/definitions/response.PageResult' msg: type: string type: object security: - ApiKeyAuth: [] summary: 查询回滚记录 tags: - AutoCode /autoCode/getTables: get: consumes: - application/json produces: - application/json responses: "200": description: 获取当前数据库所有表 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: additionalProperties: true type: object msg: type: string type: object security: - ApiKeyAuth: [] summary: 获取当前数据库所有表 tags: - AutoCode /autoCode/getTemplates: get: consumes: - application/json produces: - application/json responses: "200": description: 创建package成功 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: additionalProperties: true type: object msg: type: string type: object security: - ApiKeyAuth: [] summary: 获取package tags: - AutoCodePackage /autoCode/initAPI: post: consumes: - application/json produces: - application/json responses: "200": description: 打包插件成功 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: additionalProperties: true type: object msg: type: string type: object security: - ApiKeyAuth: [] summary: 打包插件 tags: - AutoCodePlugin /autoCode/initMenu: post: consumes: - application/json produces: - application/json responses: "200": description: 打包插件成功 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: additionalProperties: true type: object msg: type: string type: object security: - ApiKeyAuth: [] summary: 打包插件 tags: - AutoCodePlugin /autoCode/installPlugin: post: consumes: - multipart/form-data parameters: - description: this is a test file in: formData name: plug required: true type: file produces: - application/json responses: "200": description: 安装插件成功 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: items: type: object type: array msg: type: string type: object security: - ApiKeyAuth: [] summary: 安装插件 tags: - AutoCodePlugin /autoCode/preview: post: consumes: - application/json parameters: - description: 预览创建代码 in: body name: data required: true schema: $ref: '#/definitions/request.AutoCode' produces: - application/json responses: "200": description: 预览创建后的代码 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: additionalProperties: true type: object msg: type: string type: object security: - ApiKeyAuth: [] summary: 预览创建后的代码 tags: - AutoCodeTemplate /autoCode/pubPlug: post: consumes: - application/json parameters: - description: 插件名称 in: query name: plugName required: true type: string produces: - application/json responses: "200": description: 打包插件成功 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: additionalProperties: true type: object msg: type: string type: object security: - ApiKeyAuth: [] summary: 打包插件 tags: - AutoCodePlugin /autoCode/rollback: post: consumes: - application/json parameters: - description: 请求参数 in: body name: data required: true schema: $ref: '#/definitions/request.SysAutoHistoryRollBack' produces: - application/json responses: "200": description: 回滚自动生成代码 schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 回滚自动生成代码 tags: - AutoCode /base/captcha: post: consumes: - application/json produces: - application/json responses: "200": description: 生成验证码,返回包括随机数id,base64,验证码长度,是否开启验证码 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: $ref: '#/definitions/response.SysCaptchaResponse' msg: type: string type: object security: - ApiKeyAuth: [] summary: 生成验证码 tags: - Base /base/login: post: parameters: - description: 用户名, 密码, 验证码 in: body name: data required: true schema: $ref: '#/definitions/request.Login' produces: - application/json responses: "200": description: 返回包括用户信息,token,过期时间 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: $ref: '#/definitions/response.LoginResponse' msg: type: string type: object summary: 用户登录 tags: - Base /casbin/UpdateCasbin: post: consumes: - application/json parameters: - description: 权限id, 权限模型列表 in: body name: data required: true schema: $ref: '#/definitions/request.CasbinInReceive' produces: - application/json responses: "200": description: 更新角色api权限 schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 更新角色api权限 tags: - Casbin /casbin/getPolicyPathByAuthorityId: post: consumes: - application/json parameters: - description: 权限id, 权限模型列表 in: body name: data required: true schema: $ref: '#/definitions/request.CasbinInReceive' produces: - application/json responses: "200": description: 获取权限列表,返回包括casbin详情列表 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: $ref: '#/definitions/response.PolicyPathResponse' msg: type: string type: object security: - ApiKeyAuth: [] summary: 获取权限列表 tags: - Casbin /customer/customer: delete: consumes: - application/json parameters: - description: 客户ID in: body name: data required: true schema: $ref: '#/definitions/example.ExaCustomer' produces: - application/json responses: "200": description: 删除客户 schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 删除客户 tags: - ExaCustomer get: consumes: - application/json parameters: - description: 主键ID in: query name: ID type: integer - description: 创建时间 in: query name: createdAt type: string - description: 客户名 in: query name: customerName type: string - description: 客户手机号 in: query name: customerPhoneData type: string - description: 管理角色ID in: query name: sysUserAuthorityID type: integer - description: 管理ID in: query name: sysUserId type: integer - description: 更新时间 in: query name: updatedAt type: string produces: - application/json responses: "200": description: 获取单一客户信息,返回包括客户详情 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: $ref: '#/definitions/response.ExaCustomerResponse' msg: type: string type: object security: - ApiKeyAuth: [] summary: 获取单一客户信息 tags: - ExaCustomer post: consumes: - application/json parameters: - description: 客户用户名, 客户手机号码 in: body name: data required: true schema: $ref: '#/definitions/example.ExaCustomer' produces: - application/json responses: "200": description: 创建客户 schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 创建客户 tags: - ExaCustomer put: consumes: - application/json parameters: - description: 客户ID, 客户信息 in: body name: data required: true schema: $ref: '#/definitions/example.ExaCustomer' produces: - application/json responses: "200": description: 更新客户信息 schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 更新客户信息 tags: - ExaCustomer /customer/customerList: get: consumes: - application/json parameters: - description: 关键字 in: query name: keyword type: string - description: 页码 in: query name: page type: integer - description: 每页大小 in: query name: pageSize type: integer produces: - application/json responses: "200": description: 分页获取权限客户列表,返回包括列表,总数,页码,每页数量 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: $ref: '#/definitions/response.PageResult' msg: type: string type: object security: - ApiKeyAuth: [] summary: 分页获取权限客户列表 tags: - ExaCustomer /email/emailTest: post: produces: - application/json responses: "200": description: '{"success":true,"data":{},"msg":"发送成功"}' schema: type: string security: - ApiKeyAuth: [] summary: 发送测试邮件 tags: - System /email/sendEmail: post: parameters: - description: 发送邮件必须的参数 in: body name: data required: true schema: $ref: '#/definitions/response.Email' produces: - application/json responses: "200": description: '{"success":true,"data":{},"msg":"发送成功"}' schema: type: string security: - ApiKeyAuth: [] summary: 发送邮件 tags: - System /fileUploadAndDownload/breakpointContinue: post: consumes: - multipart/form-data parameters: - description: an example for breakpoint resume, 断点续传示例 in: formData name: file required: true type: file produces: - application/json responses: "200": description: 断点续传到服务器 schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 断点续传到服务器 tags: - ExaFileUploadAndDownload /fileUploadAndDownload/deleteFile: post: parameters: - description: 传入文件里面id即可 in: body name: data required: true schema: $ref: '#/definitions/example.ExaFileUploadAndDownload' produces: - application/json responses: "200": description: 删除文件 schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 删除文件 tags: - ExaFileUploadAndDownload /fileUploadAndDownload/findFile: get: consumes: - multipart/form-data parameters: - description: Find the file, 查找文件 in: formData name: file required: true type: file produces: - application/json responses: "200": description: 查找文件,返回包括文件详情 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: $ref: '#/definitions/response.FileResponse' msg: type: string type: object security: - ApiKeyAuth: [] summary: 查找文件 tags: - ExaFileUploadAndDownload post: consumes: - multipart/form-data parameters: - description: 上传文件完成 in: formData name: file required: true type: file produces: - application/json responses: "200": description: 创建文件,返回包括文件路径 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: $ref: '#/definitions/response.FilePathResponse' msg: type: string type: object security: - ApiKeyAuth: [] summary: 创建文件 tags: - ExaFileUploadAndDownload /fileUploadAndDownload/getFileList: post: consumes: - application/json parameters: - description: 页码, 每页大小, 分类id in: body name: data required: true schema: $ref: '#/definitions/request.ExaAttachmentCategorySearch' produces: - application/json responses: "200": description: 分页文件列表,返回包括列表,总数,页码,每页数量 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: $ref: '#/definitions/response.PageResult' msg: type: string type: object security: - ApiKeyAuth: [] summary: 分页文件列表 tags: - ExaFileUploadAndDownload /fileUploadAndDownload/importURL: post: parameters: - description: 对象 in: body name: data required: true schema: $ref: '#/definitions/example.ExaFileUploadAndDownload' produces: - application/json responses: "200": description: 导入URL schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 导入URL tags: - ExaFileUploadAndDownload /fileUploadAndDownload/removeChunk: post: consumes: - multipart/form-data parameters: - description: 删除缓存切片 in: formData name: file required: true type: file produces: - application/json responses: "200": description: 删除切片 schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 删除切片 tags: - ExaFileUploadAndDownload /fileUploadAndDownload/upload: post: consumes: - multipart/form-data parameters: - description: 上传文件示例 in: formData name: file required: true type: file produces: - application/json responses: "200": description: 上传文件示例,返回包括文件详情 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: $ref: '#/definitions/response.ExaFileResponse' msg: type: string type: object security: - ApiKeyAuth: [] summary: 上传文件示例 tags: - ExaFileUploadAndDownload /info/createInfo: post: consumes: - application/json parameters: - description: 创建公告 in: body name: data required: true schema: $ref: '#/definitions/model.Info' produces: - application/json responses: "200": description: 创建成功 schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 创建公告 tags: - Info /info/deleteInfo: delete: consumes: - application/json parameters: - description: 删除公告 in: body name: data required: true schema: $ref: '#/definitions/model.Info' produces: - application/json responses: "200": description: 删除成功 schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 删除公告 tags: - Info /info/deleteInfoByIds: delete: consumes: - application/json produces: - application/json responses: "200": description: 批量删除成功 schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 批量删除公告 tags: - Info /info/findInfo: get: consumes: - application/json parameters: - description: 主键ID in: query name: ID type: integer - description: 内容 in: query name: content type: string - description: 创建时间 in: query name: createdAt type: string - description: 标题 in: query name: title type: string - description: 更新时间 in: query name: updatedAt type: string - description: 作者 in: query name: userID type: integer produces: - application/json responses: "200": description: 查询成功 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: $ref: '#/definitions/model.Info' msg: type: string type: object security: - ApiKeyAuth: [] summary: 用id查询公告 tags: - Info /info/getInfoDataSource: get: consumes: - application/json produces: - application/json responses: "200": description: 查询成功 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: type: object msg: type: string type: object summary: 获取Info的数据源 tags: - Info /info/getInfoList: get: consumes: - application/json parameters: - in: query name: endCreatedAt type: string - description: 关键字 in: query name: keyword type: string - description: 页码 in: query name: page type: integer - description: 每页大小 in: query name: pageSize type: integer - in: query name: startCreatedAt type: string produces: - application/json responses: "200": description: 获取成功 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: $ref: '#/definitions/response.PageResult' msg: type: string type: object security: - ApiKeyAuth: [] summary: 分页获取公告列表 tags: - Info /info/getInfoPublic: get: consumes: - application/json parameters: - in: query name: endCreatedAt type: string - description: 关键字 in: query name: keyword type: string - description: 页码 in: query name: page type: integer - description: 每页大小 in: query name: pageSize type: integer - in: query name: startCreatedAt type: string produces: - application/json responses: "200": description: 获取成功 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: type: object msg: type: string type: object summary: 不需要鉴权的公告接口 tags: - Info /info/updateInfo: put: consumes: - application/json parameters: - description: 更新公告 in: body name: data required: true schema: $ref: '#/definitions/model.Info' produces: - application/json responses: "200": description: 更新成功 schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 更新公告 tags: - Info /init/checkdb: post: produces: - application/json responses: "200": description: 初始化用户数据库 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: additionalProperties: true type: object msg: type: string type: object summary: 初始化用户数据库 tags: - CheckDB /init/initdb: post: parameters: - description: 初始化数据库参数 in: body name: data required: true schema: $ref: '#/definitions/request.InitDB' produces: - application/json responses: "200": description: 初始化用户数据库 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: type: string type: object summary: 初始化用户数据库 tags: - InitDB /jwt/jsonInBlacklist: post: consumes: - application/json produces: - application/json responses: "200": description: jwt加入黑名单 schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: jwt加入黑名单 tags: - Jwt /menu/addBaseMenu: post: consumes: - application/json parameters: - description: 路由path, 父菜单ID, 路由name, 对应前端文件路径, 排序标记 in: body name: data required: true schema: $ref: '#/definitions/system.SysBaseMenu' produces: - application/json responses: "200": description: 新增菜单 schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 新增菜单 tags: - Menu /menu/addMenuAuthority: post: consumes: - application/json parameters: - description: 角色ID in: body name: data required: true schema: $ref: '#/definitions/request.AddMenuAuthorityInfo' produces: - application/json responses: "200": description: 增加menu和角色关联关系 schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 增加menu和角色关联关系 tags: - AuthorityMenu /menu/deleteBaseMenu: post: consumes: - application/json parameters: - description: 菜单id in: body name: data required: true schema: $ref: '#/definitions/request.GetById' produces: - application/json responses: "200": description: 删除菜单 schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 删除菜单 tags: - Menu /menu/getBaseMenuById: post: consumes: - application/json parameters: - description: 菜单id in: body name: data required: true schema: $ref: '#/definitions/request.GetById' produces: - application/json responses: "200": description: 根据id获取菜单,返回包括系统菜单列表 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: $ref: '#/definitions/response.SysBaseMenuResponse' msg: type: string type: object security: - ApiKeyAuth: [] summary: 根据id获取菜单 tags: - Menu /menu/getBaseMenuTree: post: parameters: - description: 空 in: body name: data required: true schema: $ref: '#/definitions/request.Empty' produces: - application/json responses: "200": description: 获取用户动态路由,返回包括系统菜单列表 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: $ref: '#/definitions/response.SysBaseMenusResponse' msg: type: string type: object security: - ApiKeyAuth: [] summary: 获取用户动态路由 tags: - AuthorityMenu /menu/getMenu: post: parameters: - description: 空 in: body name: data required: true schema: $ref: '#/definitions/request.Empty' produces: - application/json responses: "200": description: 获取用户动态路由,返回包括系统菜单详情列表 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: $ref: '#/definitions/response.SysMenusResponse' msg: type: string type: object security: - ApiKeyAuth: [] summary: 获取用户动态路由 tags: - AuthorityMenu /menu/getMenuAuthority: post: consumes: - application/json parameters: - description: 角色ID in: body name: data required: true schema: $ref: '#/definitions/request.GetAuthorityId' produces: - application/json responses: "200": description: 获取指定角色menu schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: additionalProperties: true type: object msg: type: string type: object security: - ApiKeyAuth: [] summary: 获取指定角色menu tags: - AuthorityMenu /menu/getMenuList: post: consumes: - application/json parameters: - description: 页码, 每页大小 in: body name: data required: true schema: $ref: '#/definitions/request.PageInfo' produces: - application/json responses: "200": description: 分页获取基础menu列表,返回包括列表,总数,页码,每页数量 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: $ref: '#/definitions/response.PageResult' msg: type: string type: object security: - ApiKeyAuth: [] summary: 分页获取基础menu列表 tags: - Menu /menu/updateBaseMenu: post: consumes: - application/json parameters: - description: 路由path, 父菜单ID, 路由name, 对应前端文件路径, 排序标记 in: body name: data required: true schema: $ref: '#/definitions/system.SysBaseMenu' produces: - application/json responses: "200": description: 更新菜单 schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 更新菜单 tags: - Menu /sysDictionary/createSysDictionary: post: consumes: - application/json parameters: - description: SysDictionary模型 in: body name: data required: true schema: $ref: '#/definitions/system.SysDictionary' produces: - application/json responses: "200": description: 创建SysDictionary schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 创建SysDictionary tags: - SysDictionary /sysDictionary/deleteSysDictionary: delete: consumes: - application/json parameters: - description: SysDictionary模型 in: body name: data required: true schema: $ref: '#/definitions/system.SysDictionary' produces: - application/json responses: "200": description: 删除SysDictionary schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 删除SysDictionary tags: - SysDictionary /sysDictionary/findSysDictionary: get: consumes: - application/json parameters: - description: 主键ID in: query name: ID type: integer - description: 创建时间 in: query name: createdAt type: string - description: 描述 in: query name: desc type: string - description: 字典名(中) in: query name: name type: string - description: 状态 in: query name: status type: boolean - description: 字典名(英) in: query name: type type: string - description: 更新时间 in: query name: updatedAt type: string produces: - application/json responses: "200": description: 用id查询SysDictionary schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: additionalProperties: true type: object msg: type: string type: object security: - ApiKeyAuth: [] summary: 用id查询SysDictionary tags: - SysDictionary /sysDictionary/getSysDictionaryList: get: consumes: - application/json produces: - application/json responses: "200": description: 分页获取SysDictionary列表,返回包括列表,总数,页码,每页数量 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: $ref: '#/definitions/response.PageResult' msg: type: string type: object security: - ApiKeyAuth: [] summary: 分页获取SysDictionary列表 tags: - SysDictionary /sysDictionary/updateSysDictionary: put: consumes: - application/json parameters: - description: SysDictionary模型 in: body name: data required: true schema: $ref: '#/definitions/system.SysDictionary' produces: - application/json responses: "200": description: 更新SysDictionary schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 更新SysDictionary tags: - SysDictionary /sysDictionaryDetail/createSysDictionaryDetail: post: consumes: - application/json parameters: - description: SysDictionaryDetail模型 in: body name: data required: true schema: $ref: '#/definitions/system.SysDictionaryDetail' produces: - application/json responses: "200": description: 创建SysDictionaryDetail schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 创建SysDictionaryDetail tags: - SysDictionaryDetail /sysDictionaryDetail/deleteSysDictionaryDetail: delete: consumes: - application/json parameters: - description: SysDictionaryDetail模型 in: body name: data required: true schema: $ref: '#/definitions/system.SysDictionaryDetail' produces: - application/json responses: "200": description: 删除SysDictionaryDetail schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 删除SysDictionaryDetail tags: - SysDictionaryDetail /sysDictionaryDetail/findSysDictionaryDetail: get: consumes: - application/json parameters: - description: 主键ID in: query name: ID type: integer - description: 创建时间 in: query name: createdAt type: string - description: 扩展值 in: query name: extend type: string - description: 展示值 in: query name: label type: string - description: 排序标记 in: query name: sort type: integer - description: 启用状态 in: query name: status type: boolean - description: 关联标记 in: query name: sysDictionaryID type: integer - description: 更新时间 in: query name: updatedAt type: string - description: 字典值 in: query name: value type: string produces: - application/json responses: "200": description: 用id查询SysDictionaryDetail schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: additionalProperties: true type: object msg: type: string type: object security: - ApiKeyAuth: [] summary: 用id查询SysDictionaryDetail tags: - SysDictionaryDetail /sysDictionaryDetail/getSysDictionaryDetailList: get: consumes: - application/json parameters: - description: 主键ID in: query name: ID type: integer - description: 创建时间 in: query name: createdAt type: string - description: 扩展值 in: query name: extend type: string - description: 关键字 in: query name: keyword type: string - description: 展示值 in: query name: label type: string - description: 页码 in: query name: page type: integer - description: 每页大小 in: query name: pageSize type: integer - description: 排序标记 in: query name: sort type: integer - description: 启用状态 in: query name: status type: boolean - description: 关联标记 in: query name: sysDictionaryID type: integer - description: 更新时间 in: query name: updatedAt type: string - description: 字典值 in: query name: value type: string produces: - application/json responses: "200": description: 分页获取SysDictionaryDetail列表,返回包括列表,总数,页码,每页数量 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: $ref: '#/definitions/response.PageResult' msg: type: string type: object security: - ApiKeyAuth: [] summary: 分页获取SysDictionaryDetail列表 tags: - SysDictionaryDetail /sysDictionaryDetail/updateSysDictionaryDetail: put: consumes: - application/json parameters: - description: 更新SysDictionaryDetail in: body name: data required: true schema: $ref: '#/definitions/system.SysDictionaryDetail' produces: - application/json responses: "200": description: 更新SysDictionaryDetail schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 更新SysDictionaryDetail tags: - SysDictionaryDetail /sysExportTemplate/ExportTemplate: get: consumes: - application/json produces: - application/json responses: {} security: - ApiKeyAuth: [] summary: 导出表格模板 tags: - SysExportTemplate /sysExportTemplate/createSysExportTemplate: post: consumes: - application/json parameters: - description: 创建导出模板 in: body name: data required: true schema: $ref: '#/definitions/system.SysExportTemplate' produces: - application/json responses: "200": description: '{"success":true,"data":{},"msg":"创建成功"}' schema: type: string security: - ApiKeyAuth: [] summary: 创建导出模板 tags: - SysExportTemplate /sysExportTemplate/deleteSysExportTemplate: delete: consumes: - application/json parameters: - description: 删除导出模板 in: body name: data required: true schema: $ref: '#/definitions/system.SysExportTemplate' produces: - application/json responses: "200": description: '{"success":true,"data":{},"msg":"删除成功"}' schema: type: string security: - ApiKeyAuth: [] summary: 删除导出模板 tags: - SysExportTemplate /sysExportTemplate/deleteSysExportTemplateByIds: delete: consumes: - application/json parameters: - description: 批量删除导出模板 in: body name: data required: true schema: $ref: '#/definitions/request.IdsReq' produces: - application/json responses: "200": description: '{"success":true,"data":{},"msg":"批量删除成功"}' schema: type: string security: - ApiKeyAuth: [] summary: 批量删除导出模板 tags: - SysExportTemplate /sysExportTemplate/exportExcel: get: consumes: - application/json produces: - application/json responses: {} security: - ApiKeyAuth: [] summary: 导出表格 tags: - SysExportTemplate /sysExportTemplate/findSysExportTemplate: get: consumes: - application/json parameters: - description: 主键ID in: query name: ID type: integer - description: 创建时间 in: query name: createdAt type: string - description: 数据库名称 in: query name: dbName type: string - in: query name: limit type: integer - description: 模板名称 in: query name: name type: string - in: query name: order type: string - description: 表名称 in: query name: tableName type: string - description: 模板标识 in: query name: templateID type: string - description: 模板信息 in: query name: templateInfo type: string - description: 更新时间 in: query name: updatedAt type: string produces: - application/json responses: "200": description: '{"success":true,"data":{},"msg":"查询成功"}' schema: type: string security: - ApiKeyAuth: [] summary: 用id查询导出模板 tags: - SysExportTemplate /sysExportTemplate/getSysExportTemplateList: get: consumes: - application/json parameters: - description: 主键ID in: query name: ID type: integer - description: 创建时间 in: query name: createdAt type: string - description: 数据库名称 in: query name: dbName type: string - in: query name: endCreatedAt type: string - description: 关键字 in: query name: keyword type: string - in: query name: limit type: integer - description: 模板名称 in: query name: name type: string - in: query name: order type: string - description: 页码 in: query name: page type: integer - description: 每页大小 in: query name: pageSize type: integer - in: query name: startCreatedAt type: string - description: 表名称 in: query name: tableName type: string - description: 模板标识 in: query name: templateID type: string - description: 模板信息 in: query name: templateInfo type: string - description: 更新时间 in: query name: updatedAt type: string produces: - application/json responses: "200": description: '{"success":true,"data":{},"msg":"获取成功"}' schema: type: string security: - ApiKeyAuth: [] summary: 分页获取导出模板列表 tags: - SysExportTemplate /sysExportTemplate/importExcel: post: consumes: - application/json produces: - application/json responses: {} security: - ApiKeyAuth: [] summary: 导入表格 tags: - SysImportTemplate /sysExportTemplate/updateSysExportTemplate: put: consumes: - application/json parameters: - description: 更新导出模板 in: body name: data required: true schema: $ref: '#/definitions/system.SysExportTemplate' produces: - application/json responses: "200": description: '{"success":true,"data":{},"msg":"更新成功"}' schema: type: string security: - ApiKeyAuth: [] summary: 更新导出模板 tags: - SysExportTemplate /sysOperationRecord/createSysOperationRecord: post: consumes: - application/json parameters: - description: 创建SysOperationRecord in: body name: data required: true schema: $ref: '#/definitions/system.SysOperationRecord' produces: - application/json responses: "200": description: 创建SysOperationRecord schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 创建SysOperationRecord tags: - SysOperationRecord /sysOperationRecord/deleteSysOperationRecord: delete: consumes: - application/json parameters: - description: SysOperationRecord模型 in: body name: data required: true schema: $ref: '#/definitions/system.SysOperationRecord' produces: - application/json responses: "200": description: 删除SysOperationRecord schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 删除SysOperationRecord tags: - SysOperationRecord /sysOperationRecord/deleteSysOperationRecordByIds: delete: consumes: - application/json parameters: - description: 批量删除SysOperationRecord in: body name: data required: true schema: $ref: '#/definitions/request.IdsReq' produces: - application/json responses: "200": description: 批量删除SysOperationRecord schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 批量删除SysOperationRecord tags: - SysOperationRecord /sysOperationRecord/findSysOperationRecord: get: consumes: - application/json parameters: - description: 主键ID in: query name: ID type: integer - description: 代理 in: query name: agent type: string - description: 请求Body in: query name: body type: string - description: 创建时间 in: query name: createdAt type: string - description: 错误信息 in: query name: error_message type: string - description: 请求ip in: query name: ip type: string - description: 延迟 in: query name: latency type: string - description: 请求方法 in: query name: method type: string - description: 请求路径 in: query name: path type: string - description: 响应Body in: query name: resp type: string - description: 请求状态 in: query name: status type: integer - description: 更新时间 in: query name: updatedAt type: string - description: 用户id in: query name: user_id type: integer produces: - application/json responses: "200": description: 用id查询SysOperationRecord schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: additionalProperties: true type: object msg: type: string type: object security: - ApiKeyAuth: [] summary: 用id查询SysOperationRecord tags: - SysOperationRecord /sysOperationRecord/getSysOperationRecordList: get: consumes: - application/json parameters: - description: 主键ID in: query name: ID type: integer - description: 代理 in: query name: agent type: string - description: 请求Body in: query name: body type: string - description: 创建时间 in: query name: createdAt type: string - description: 错误信息 in: query name: error_message type: string - description: 请求ip in: query name: ip type: string - description: 关键字 in: query name: keyword type: string - description: 延迟 in: query name: latency type: string - description: 请求方法 in: query name: method type: string - description: 页码 in: query name: page type: integer - description: 每页大小 in: query name: pageSize type: integer - description: 请求路径 in: query name: path type: string - description: 响应Body in: query name: resp type: string - description: 请求状态 in: query name: status type: integer - description: 更新时间 in: query name: updatedAt type: string - description: 用户id in: query name: user_id type: integer produces: - application/json responses: "200": description: 分页获取SysOperationRecord列表,返回包括列表,总数,页码,每页数量 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: $ref: '#/definitions/response.PageResult' msg: type: string type: object security: - ApiKeyAuth: [] summary: 分页获取SysOperationRecord列表 tags: - SysOperationRecord /sysParams/createSysParams: post: consumes: - application/json parameters: - description: 创建参数 in: body name: data required: true schema: $ref: '#/definitions/system.SysParams' produces: - application/json responses: "200": description: 创建成功 schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 创建参数 tags: - SysParams /sysParams/deleteSysParams: delete: consumes: - application/json parameters: - description: 删除参数 in: body name: data required: true schema: $ref: '#/definitions/system.SysParams' produces: - application/json responses: "200": description: 删除成功 schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 删除参数 tags: - SysParams /sysParams/deleteSysParamsByIds: delete: consumes: - application/json produces: - application/json responses: "200": description: 批量删除成功 schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 批量删除参数 tags: - SysParams /sysParams/findSysParams: get: consumes: - application/json parameters: - description: 主键ID in: query name: ID type: integer - description: 创建时间 in: query name: createdAt type: string - description: 参数说明 in: query name: desc type: string - description: 参数键 in: query name: key required: true type: string - description: 参数名称 in: query name: name required: true type: string - description: 更新时间 in: query name: updatedAt type: string - description: 参数值 in: query name: value required: true type: string produces: - application/json responses: "200": description: 查询成功 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: $ref: '#/definitions/system.SysParams' msg: type: string type: object security: - ApiKeyAuth: [] summary: 用id查询参数 tags: - SysParams /sysParams/getSysParam: get: consumes: - application/json parameters: - description: key in: query name: key required: true type: string produces: - application/json responses: "200": description: 获取成功 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: $ref: '#/definitions/system.SysParams' msg: type: string type: object security: - ApiKeyAuth: [] summary: 根据key获取参数value tags: - SysParams /sysParams/getSysParamsList: get: consumes: - application/json parameters: - in: query name: endCreatedAt type: string - in: query name: key type: string - description: 关键字 in: query name: keyword type: string - in: query name: name type: string - description: 页码 in: query name: page type: integer - description: 每页大小 in: query name: pageSize type: integer - in: query name: startCreatedAt type: string produces: - application/json responses: "200": description: 获取成功 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: $ref: '#/definitions/response.PageResult' msg: type: string type: object security: - ApiKeyAuth: [] summary: 分页获取参数列表 tags: - SysParams /sysParams/updateSysParams: put: consumes: - application/json parameters: - description: 更新参数 in: body name: data required: true schema: $ref: '#/definitions/system.SysParams' produces: - application/json responses: "200": description: 更新成功 schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 更新参数 tags: - SysParams /system/getServerInfo: post: produces: - application/json responses: "200": description: 获取服务器信息 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: additionalProperties: true type: object msg: type: string type: object security: - ApiKeyAuth: [] summary: 获取服务器信息 tags: - System /system/getSystemConfig: post: produces: - application/json responses: "200": description: 获取配置文件内容,返回包括系统配置 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: $ref: '#/definitions/response.SysConfigResponse' msg: type: string type: object security: - ApiKeyAuth: [] summary: 获取配置文件内容 tags: - System /system/reloadSystem: post: produces: - application/json responses: "200": description: 重启系统 schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 重启系统 tags: - System /system/setSystemConfig: post: parameters: - description: 设置配置文件内容 in: body name: data required: true schema: $ref: '#/definitions/system.System' produces: - application/json responses: "200": description: 设置配置文件内容 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: type: string type: object security: - ApiKeyAuth: [] summary: 设置配置文件内容 tags: - System /user/SetSelfInfo: put: consumes: - application/json parameters: - description: ID, 用户名, 昵称, 头像链接 in: body name: data required: true schema: $ref: '#/definitions/system.SysUser' produces: - application/json responses: "200": description: 设置用户信息 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: additionalProperties: true type: object msg: type: string type: object security: - ApiKeyAuth: [] summary: 设置用户信息 tags: - SysUser /user/SetSelfSetting: put: consumes: - application/json parameters: - description: 用户配置数据 in: body name: data required: true schema: additionalProperties: true type: object produces: - application/json responses: "200": description: 设置用户配置 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: additionalProperties: true type: object msg: type: string type: object security: - ApiKeyAuth: [] summary: 设置用户配置 tags: - SysUser /user/admin_register: post: parameters: - description: 用户名, 昵称, 密码, 角色ID in: body name: data required: true schema: $ref: '#/definitions/request.Register' produces: - application/json responses: "200": description: 用户注册账号,返回包括用户信息 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: $ref: '#/definitions/response.SysUserResponse' msg: type: string type: object summary: 用户注册账号 tags: - SysUser /user/changePassword: post: parameters: - description: 用户名, 原密码, 新密码 in: body name: data required: true schema: $ref: '#/definitions/request.ChangePasswordReq' produces: - application/json responses: "200": description: 用户修改密码 schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 用户修改密码 tags: - SysUser /user/deleteUser: delete: consumes: - application/json parameters: - description: 用户ID in: body name: data required: true schema: $ref: '#/definitions/request.GetById' produces: - application/json responses: "200": description: 删除用户 schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 删除用户 tags: - SysUser /user/getUserInfo: get: consumes: - application/json produces: - application/json responses: "200": description: 获取用户信息 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: additionalProperties: true type: object msg: type: string type: object security: - ApiKeyAuth: [] summary: 获取用户信息 tags: - SysUser /user/getUserList: post: consumes: - application/json parameters: - description: 页码, 每页大小 in: body name: data required: true schema: $ref: '#/definitions/request.GetUserList' produces: - application/json responses: "200": description: 分页获取用户列表,返回包括列表,总数,页码,每页数量 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: $ref: '#/definitions/response.PageResult' msg: type: string type: object security: - ApiKeyAuth: [] summary: 分页获取用户列表 tags: - SysUser /user/resetPassword: post: parameters: - description: ID in: body name: data required: true schema: $ref: '#/definitions/system.SysUser' produces: - application/json responses: "200": description: 重置用户密码 schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 重置用户密码 tags: - SysUser /user/setUserAuthorities: post: consumes: - application/json parameters: - description: 用户UUID, 角色ID in: body name: data required: true schema: $ref: '#/definitions/request.SetUserAuthorities' produces: - application/json responses: "200": description: 设置用户权限 schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 设置用户权限 tags: - SysUser /user/setUserAuthority: post: consumes: - application/json parameters: - description: 用户UUID, 角色ID in: body name: data required: true schema: $ref: '#/definitions/request.SetUserAuth' produces: - application/json responses: "200": description: 设置用户权限 schema: allOf: - $ref: '#/definitions/response.Response' - properties: msg: type: string type: object security: - ApiKeyAuth: [] summary: 更改用户权限 tags: - SysUser /user/setUserInfo: put: consumes: - application/json parameters: - description: ID, 用户名, 昵称, 头像链接 in: body name: data required: true schema: $ref: '#/definitions/system.SysUser' produces: - application/json responses: "200": description: 设置用户信息 schema: allOf: - $ref: '#/definitions/response.Response' - properties: data: additionalProperties: true type: object msg: type: string type: object security: - ApiKeyAuth: [] summary: 设置用户信息 tags: - SysUser securityDefinitions: ApiKeyAuth: in: header name: x-token type: apiKey swagger: "2.0" tags: - name: Base - description: 用户 name: SysUser ================================================ FILE: server/global/global.go ================================================ package global import ( "fmt" "github.com/mark3labs/mcp-go/server" "sync" "github.com/gin-gonic/gin" "github.com/qiniu/qmgo" "github.com/flipped-aurora/gin-vue-admin/server/utils/timer" "github.com/songzhibin97/gkit/cache/local_cache" "golang.org/x/sync/singleflight" "go.uber.org/zap" "github.com/flipped-aurora/gin-vue-admin/server/config" "github.com/redis/go-redis/v9" "github.com/spf13/viper" "gorm.io/gorm" ) var ( GVA_DB *gorm.DB GVA_DBList map[string]*gorm.DB GVA_REDIS redis.UniversalClient GVA_REDISList map[string]redis.UniversalClient GVA_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 } ================================================ FILE: server/global/model.go ================================================ 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:"-"` // 删除时间 } ================================================ FILE: server/global/version.go ================================================ package global // Version 版本信息 // 目前只有Version正式使用 其余为预留 const ( // Version 当前版本号 Version = "v2.9.0" // AppName 应用名称 AppName = "Gin-Vue-Admin" // Description 应用描述 Description = "使用gin+vue进行极速开发的全栈开发基础平台" ) ================================================ FILE: server/go.mod ================================================ module github.com/flipped-aurora/gin-vue-admin/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-v2 v1.41.2 github.com/aws/aws-sdk-go-v2/config v1.32.10 github.com/aws/aws-sdk-go-v2/credentials v1.19.10 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.3 github.com/aws/aws-sdk-go-v2/service/s3 v1.96.1 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/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/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/gen v0.3.26 gorm.io/gorm v1.25.12 ) require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/BurntSushi/toml v1.4.0 // indirect github.com/KyleBanks/depth v1.2.1 // indirect github.com/STARRY-S/zip v0.2.1 // indirect github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 // indirect github.com/andybalholm/brotli v1.1.1 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.9 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 // indirect github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 // indirect github.com/aws/smithy-go v1.24.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/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/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/mod v0.22.0 // indirect golang.org/x/net v0.35.0 // indirect golang.org/x/sys v0.32.0 // indirect golang.org/x/time v0.9.0 // indirect golang.org/x/tools v0.29.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gorm.io/hints v1.1.2 // indirect gorm.io/plugin/dbresolver v1.5.3 // indirect modernc.org/fileutil v1.3.0 // indirect modernc.org/libc v1.61.9 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.8.2 // indirect modernc.org/sqlite v1.34.5 // indirect ) ================================================ FILE: server/go.sum ================================================ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 h1:U2rTu3Ef+7w9FHKIAXM6ZyqF3UOWJZ12zIm8zECAFfg= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI= github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 h1:jBQA3cKT4L2rWMpgE7Yt3Hwh2aUj8KXjIGLxjHeYNNo= github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI= github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409/go.mod h1:1pk82RBxDY/JZnPQrtqHlUFfCctgdorsd9M06fMynOM= github.com/STARRY-S/zip v0.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-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls= github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c= github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI= github.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw= github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8= github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 h1:Ii4s+Sq3yDfaMLpjrJsqD6SmG/Wq/P5L/hw2qa78UAY= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18/go.mod h1:6x81qnY++ovptLE6nWQeWrpXxbnlIex+4H4eYYGcqfc= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.3 h1:+mQ8NQBh7B7c2FBtppRnwkrmuwFON1XQQ+5yblomZKk= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.3/go.mod h1:u67RKh3BRmS4FYLH+rN3N4T5fqpd9m2ttAwBJYEdosU= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 h1:eZioDaZGJ0tMM4gzmkNIO2aAoQd+je7Ug7TkvAzlmkU= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18/go.mod h1:CCXwUKAJdoWr6/NcxZ+zsiPr6oH/Q5aTooRGYieAyj4= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5/go.mod h1:AZLZf2fMaahW5s/wMRciu1sYbdsikT/UHwbUjOdEVTc= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.9 h1:IJRzQTvdpjHRPItx9gzNcz7Y1F+xqAR+xiy9rr5ZYl8= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.9/go.mod h1:Kzm5e6OmNH8VMkgK9t+ry5jEih4Y8whqs+1hrkxim1I= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 h1:LTRCYFlnnKFlKsyIQxKhJuDuA3ZkrDQMRYm6rXiHlLY= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18/go.mod h1:XhwkgGG6bHSd00nO/mexWTcTjgd6PjuvWQMqSn2UaEk= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 h1:/A/xDuZAVD2BpsS2fftFRo/NoEKQJ8YTnJDEHBy2Gtg= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18/go.mod h1:hWe9b4f+djUQGmyiGEeOnZv69dtMSgpDRIvNMvuvzvY= github.com/aws/aws-sdk-go-v2/service/s3 v1.96.1 h1:giB30dEeoar5bgDnkE0q+z7cFjcHaCjulpmPVmuKR84= github.com/aws/aws-sdk-go-v2/service/s3 v1.96.1/go.mod h1:071TH4M3botFLWDbzQLfBR7tXYi7Fs2RsXSiH7nlUlY= github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 h1:MzORe+J94I+hYu2a6XmV5yC9huoTv8NRcCrUNedDypQ= github.com/aws/aws-sdk-go-v2/service/signin v1.0.6/go.mod h1:hXzcHLARD7GeWnifd8j9RWqtfIgxj4/cAtIVIK7hg8g= github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 h1:7oGD8KPfBOJGXiCoRKrrrQkbvCp8N++u36hrLMPey6o= github.com/aws/aws-sdk-go-v2/service/sso v1.30.11/go.mod h1:0DO9B5EUJQlIDif+XJRWCljZRKsAFKh3gpFz7UnDtOo= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 h1:edCcNp9eGIUDUCrzoCu1jWAXLGFIizeqkdkKgRlJwWc= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15/go.mod h1:lyRQKED9xWfgkYC/wmmYfv7iVIM68Z5OQ88ZdcV1QbU= github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb83BbyggcUBVksN7c= github.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs= github.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0= github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= 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-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 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/huaweicloud/huaweicloud-sdk-go-obs v3.24.9+incompatible h1:XQVXdk+WAJ4fSNB6mMRuYNvFWou7BZs6SZB925hPrnk= github.com/huaweicloud/huaweicloud-sdk-go-obs v3.24.9+incompatible/go.mod h1:l7VUhRbTKCzdOacdT4oWCwATKyvZqUOlOqr0Ous3k4s= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/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/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.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0= github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/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/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/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/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/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.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= 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.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/protobuf v1.36.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/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/datatypes v1.2.5 h1:9UogU3jkydFVW1bIVVeoYsTpLRgwDVW3rHfJG6/Ek9I= gorm.io/datatypes v1.2.5/go.mod h1:I5FUdlKpLb5PMqeMQhm30CQ6jXP8Rj89xkTeCSAaAD4= gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= gorm.io/driver/sqlite v1.5.0 h1:zKYbzRCpBrT1bNijRnxLDJWPjVfImGEn0lSnUY5gZ+c= gorm.io/driver/sqlite v1.5.0/go.mod h1:kDMDfntV9u/vuMmz8APHtHF0b4nyBB7sfCieC6G8k8I= gorm.io/driver/sqlserver v1.5.4 h1:xA+Y1KDNspv79q43bPyjDMUgHoYHLhXYmdFcYPobg8g= gorm.io/driver/sqlserver v1.5.4/go.mod h1:+frZ/qYmuna11zHPlh5oc2O6ZA/lS88Keb0XSH1Zh/g= gorm.io/gen v0.3.26 h1:sFf1j7vNStimPRRAtH4zz5NiHM+1dr6eA9aaRdplyhY= gorm.io/gen v0.3.26/go.mod h1:a5lq5y3w4g5LMxBcw0wnO6tYUCdNutWODq5LrIt75LE= gorm.io/gorm v1.24.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.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= gorm.io/hints v1.1.2 h1:b5j0kwk5p4+3BtDtYqqfY+ATSxjj+6ptPgVveuynn9o= gorm.io/hints v1.1.2/go.mod h1:/ARdpUHAtyEMCh5NNi3tI7FsGh+Cj/MIUlvNxCNCFWg= gorm.io/plugin/dbresolver v1.5.3 h1:wFwINGZZmttuu9h7XpvbDHd8Lf9bb8GNzp/NpAMV2wU= gorm.io/plugin/dbresolver v1.5.3/go.mod h1:TSrVhaUg2DZAWP3PrHlDlITEJmNOkL0tFTjvTEsQ4XE= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0= modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/ccgo/v4 v4.23.13 h1:PFiaemQwE/jdwi8XEHyEV+qYWoIuikLP3T4rvDeJb00= modernc.org/ccgo/v4 v4.23.13/go.mod h1:vdN4h2WR5aEoNondUx26K7G8X+nuBscYnAEWSRmN2/0= modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8= modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= modernc.org/gc/v2 v2.6.1 h1:+Qf6xdG8l7B27TQ8D8lw/iFMUj1RXRBOuMUWziJOsk8= modernc.org/gc/v2 v2.6.1/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/libc v1.61.9 h1:PLSBXVkifXGELtJ5BOnBUyAHr7lsatNwFU/RRo4kfJM= modernc.org/libc v1.61.9/go.mod h1:61xrnzk/aR8gr5bR7Uj/lLFLuXu2/zMpIjcry63Eumk= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI= modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU= modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g= modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= ================================================ FILE: server/initialize/db_list.go ================================================ package initialize import ( "github.com/flipped-aurora/gin-vue-admin/server/config" "github.com/flipped-aurora/gin-vue-admin/server/global" "gorm.io/gorm" ) const sys = "system" func DBList() { dbMap := make(map[string]*gorm.DB) for _, info := range global.GVA_CONFIG.DBList { if info.Disable { continue } switch info.Type { case "mysql": dbMap[info.AliasName] = GormMysqlByConfig(config.Mysql{GeneralDB: info.GeneralDB}) case "mssql": dbMap[info.AliasName] = GormMssqlByConfig(config.Mssql{GeneralDB: info.GeneralDB}) case "pgsql": dbMap[info.AliasName] = GormPgSqlByConfig(config.Pgsql{GeneralDB: info.GeneralDB}) case "oracle": dbMap[info.AliasName] = GormOracleByConfig(config.Oracle{GeneralDB: info.GeneralDB}) default: continue } } // 做特殊判断,是否有迁移 // 适配低版本迁移多数据库版本 if sysDB, ok := dbMap[sys]; ok { global.GVA_DB = sysDB } global.GVA_DBList = dbMap } ================================================ FILE: server/initialize/ensure_tables.go ================================================ package initialize import ( "context" adapter "github.com/casbin/gorm-adapter/v3" "github.com/flipped-aurora/gin-vue-admin/server/model/example" sysModel "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/model" "github.com/flipped-aurora/gin-vue-admin/server/service/system" "gorm.io/gorm" ) const initOrderEnsureTables = system.InitOrderExternal - 1 type ensureTables struct{} // auto run func init() { system.RegisterInit(initOrderEnsureTables, &ensureTables{}) } func (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{}, model.Info{}, } for _, t := range tables { _ = db.AutoMigrate(&t) // 视图 authority_menu 会被当成表来创建,引发冲突错误(更新版本的gorm似乎不会) // 由于 AutoMigrate() 基本无需考虑错误,因此显式忽略 } return ctx, nil } func (e *ensureTables) TableCreated(ctx context.Context) bool { db, ok := ctx.Value("db").(*gorm.DB) if !ok { return false } tables := []interface{}{ sysModel.SysApi{}, sysModel.SysUser{}, sysModel.SysBaseMenu{}, sysModel.SysAuthority{}, sysModel.JwtBlacklist{}, sysModel.SysDictionary{}, sysModel.SysAutoCodeHistory{}, sysModel.SysOperationRecord{}, sysModel.SysDictionaryDetail{}, sysModel.SysBaseMenuParameter{}, sysModel.SysBaseMenuBtn{}, sysModel.SysAuthorityBtn{}, sysModel.SysAutoCodePackage{}, sysModel.SysExportTemplate{}, sysModel.Condition{}, sysModel.JoinTemplate{}, adapter.CasbinRule{}, example.ExaFile{}, example.ExaCustomer{}, example.ExaFileChunk{}, example.ExaFileUploadAndDownload{}, example.ExaAttachmentCategory{}, model.Info{}, } yes := true for _, t := range tables { yes = yes && db.Migrator().HasTable(t) } return yes } ================================================ FILE: server/initialize/gorm.go ================================================ package initialize import ( "os" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/example" "github.com/flipped-aurora/gin-vue-admin/server/model/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 } db := global.GVA_DB err := db.AutoMigrate( system.SysApi{}, system.SysIgnoreApi{}, system.SysUser{}, system.SysBaseMenu{}, system.JwtBlacklist{}, system.SysAuthority{}, system.SysDictionary{}, system.SysOperationRecord{}, system.SysAutoCodeHistory{}, system.SysDictionaryDetail{}, system.SysBaseMenuParameter{}, system.SysBaseMenuBtn{}, system.SysAuthorityBtn{}, system.SysAutoCodePackage{}, system.SysExportTemplate{}, system.Condition{}, system.JoinTemplate{}, system.SysParams{}, system.SysVersion{}, system.SysError{}, system.SysApiToken{}, system.SysLoginLog{}, example.ExaFile{}, example.ExaCustomer{}, example.ExaFileChunk{}, example.ExaFileUploadAndDownload{}, example.ExaAttachmentCategory{}, ) if err != nil { global.GVA_LOG.Error("register table failed", zap.Error(err)) os.Exit(0) } err = bizModel() if err != nil { global.GVA_LOG.Error("register biz_table failed", zap.Error(err)) os.Exit(0) } global.GVA_LOG.Info("register table success") } ================================================ FILE: server/initialize/gorm_biz.go ================================================ package initialize import ( "github.com/flipped-aurora/gin-vue-admin/server/global" ) func bizModel() error { db := global.GVA_DB err := db.AutoMigrate() if err != nil { return err } return nil } ================================================ FILE: server/initialize/gorm_mssql.go ================================================ 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 ( "github.com/flipped-aurora/gin-vue-admin/server/config" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/initialize/internal" "gorm.io/driver/sqlserver" "gorm.io/gorm" ) // GormMssql 初始化Mssql数据库 // Author [LouisZhang](191180776@qq.com) func GormMssql() *gorm.DB { m := global.GVA_CONFIG.Mssql if m.Dbname == "" { return nil } mssqlConfig := sqlserver.Config{ DSN: m.Dsn(), // DSN data source name DefaultStringSize: 191, // string 类型字段的默认长度 } // 数据库配置 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 } } ================================================ FILE: server/initialize/gorm_mysql.go ================================================ package initialize import ( "github.com/flipped-aurora/gin-vue-admin/server/config" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/initialize/internal" _ "github.com/go-sql-driver/mysql" "gorm.io/driver/mysql" "gorm.io/gorm" ) // GormMysql 初始化Mysql数据库 // Author [piexlmax](https://github.com/piexlmax) // Author [SliverHorn](https://github.com/SliverHorn) // 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 } } ================================================ FILE: server/initialize/gorm_oracle.go ================================================ package initialize import ( oracle "github.com/dzwvip/gorm-oracle" "github.com/flipped-aurora/gin-vue-admin/server/config" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/initialize/internal" "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 } } ================================================ FILE: server/initialize/gorm_pgsql.go ================================================ package initialize import ( "github.com/flipped-aurora/gin-vue-admin/server/config" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/initialize/internal" "gorm.io/driver/postgres" "gorm.io/gorm" ) // GormPgSql 初始化 Postgresql 数据库 // Author [piexlmax](https://github.com/piexlmax) // Author [SliverHorn](https://github.com/SliverHorn) func GormPgSql() *gorm.DB { p := global.GVA_CONFIG.Pgsql 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 } } ================================================ FILE: server/initialize/gorm_sqlite.go ================================================ package initialize import ( "github.com/flipped-aurora/gin-vue-admin/server/config" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/initialize/internal" "github.com/glebarez/sqlite" "gorm.io/gorm" ) // GormSqlite 初始化Sqlite数据库 func GormSqlite() *gorm.DB { s := global.GVA_CONFIG.Sqlite 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 } } ================================================ FILE: server/initialize/init.go ================================================ // 假设这是初始化逻辑的一部分 package initialize import ( "github.com/flipped-aurora/gin-vue-admin/server/utils" ) // 初始化全局函数 func SetupHandlers() { // 注册系统重载处理函数 utils.GlobalSystemEvents.RegisterReloadHandler(func() error { return Reload() }) } ================================================ FILE: server/initialize/internal/gorm.go ================================================ package internal import ( "time" "github.com/flipped-aurora/gin-vue-admin/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, } } ================================================ FILE: server/initialize/internal/gorm_logger_writer.go ================================================ package internal import ( "fmt" "github.com/flipped-aurora/gin-vue-admin/server/config" "github.com/flipped-aurora/gin-vue-admin/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 } } ================================================ FILE: server/initialize/internal/mongo.go ================================================ 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}}} } ================================================ FILE: server/initialize/mcp.go ================================================ package initialize import ( "github.com/flipped-aurora/gin-vue-admin/server/global" mcpTool "github.com/flipped-aurora/gin-vue-admin/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)) } ================================================ FILE: server/initialize/mongo.go ================================================ package initialize import ( "context" "fmt" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/initialize/internal" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/pkg/errors" "github.com/qiniu/qmgo" "github.com/qiniu/qmgo/options" "go.mongodb.org/mongo-driver/bson" option "go.mongodb.org/mongo-driver/mongo/options" "sort" "strings" ) var Mongo = new(mongo) type ( mongo struct{} Index struct { V any `bson:"v"` Ns any `bson:"ns"` Key []bson.E `bson:"key"` Name string `bson:"name"` } ) func (m *mongo) Indexes(ctx context.Context) error { // 表名:索引列表 列: "表名": [][]string{{"index1", "index2"}} indexMap := map[string][][]string{} for collection, indexes := range indexMap { err := m.CreateIndexes(ctx, collection, indexes) if err != nil { return err } } return nil } func (m *mongo) Initialization() error { var opts []options.ClientOptions if global.GVA_CONFIG.Mongo.IsZap { opts = internal.Mongo.GetClientOptions() } ctx := context.Background() 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 } ================================================ FILE: server/initialize/other.go ================================================ package initialize import ( "bufio" "github.com/songzhibin97/gkit/cache/local_cache" "os" "strings" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/utils" ) func OtherInit() { dr, err := utils.ParseDuration(global.GVA_CONFIG.JWT.ExpiresTime) if err != nil { panic(err) } _, err = utils.ParseDuration(global.GVA_CONFIG.JWT.BufferTime) if err != nil { panic(err) } global.BlackCache = local_cache.NewCache( local_cache.SetDefaultExpire(dr), ) file, err := os.Open("go.mod") if err == nil && global.GVA_CONFIG.AutoCode.Module == "" { scanner := bufio.NewScanner(file) scanner.Scan() global.GVA_CONFIG.AutoCode.Module = strings.TrimPrefix(scanner.Text(), "module ") } } ================================================ FILE: server/initialize/plugin.go ================================================ package initialize import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/gin-gonic/gin" ) func InstallPlugin(PrivateGroup *gin.RouterGroup, PublicRouter *gin.RouterGroup, engine *gin.Engine) { if global.GVA_DB == nil { global.GVA_LOG.Info("项目暂未初始化,无法安装插件,初始化后重启项目即可完成插件安装") return } bizPluginV1(PrivateGroup, PublicRouter) bizPluginV2(engine) } ================================================ FILE: server/initialize/plugin_biz_v1.go ================================================ package initialize import ( "fmt" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/plugin/email" "github.com/flipped-aurora/gin-vue-admin/server/utils/plugin" "github.com/gin-gonic/gin" ) func PluginInit(group *gin.RouterGroup, Plugin ...plugin.Plugin) { for i := range Plugin { fmt.Println(Plugin[i].RouterPath(), "注册开始!") PluginGroup := group.Group(Plugin[i].RouterPath()) Plugin[i].Register(PluginGroup) fmt.Println(Plugin[i].RouterPath(), "注册成功!") } } func bizPluginV1(group ...*gin.RouterGroup) { private := group[0] public := group[1] // 添加跟角色挂钩权限的插件 示例 本地示例模式于在线仓库模式注意上方的import 可以自行切换 效果相同 PluginInit(private, email.CreateEmailPlug( global.GVA_CONFIG.Email.To, global.GVA_CONFIG.Email.From, global.GVA_CONFIG.Email.Host, global.GVA_CONFIG.Email.Secret, global.GVA_CONFIG.Email.Nickname, global.GVA_CONFIG.Email.Port, global.GVA_CONFIG.Email.IsSSL, global.GVA_CONFIG.Email.IsLoginAuth, )) holder(public, private) } ================================================ FILE: server/initialize/plugin_biz_v2.go ================================================ package initialize import ( _ "github.com/flipped-aurora/gin-vue-admin/server/plugin" "github.com/flipped-aurora/gin-vue-admin/server/utils/plugin/v2" "github.com/gin-gonic/gin" ) func PluginInitV2(group *gin.Engine, plugins ...plugin.Plugin) { for i := 0; i < len(plugins); i++ { plugins[i].Register(group) } } func bizPluginV2(engine *gin.Engine) { PluginInitV2(engine, plugin.Registered()...) } ================================================ FILE: server/initialize/redis.go ================================================ package initialize import ( "context" "github.com/flipped-aurora/gin-vue-admin/server/config" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/redis/go-redis/v9" "go.uber.org/zap" ) func initRedisClient(redisCfg config.Redis) (redis.UniversalClient, error) { var client redis.UniversalClient // 使用集群模式 if redisCfg.UseCluster { client = redis.NewClusterClient(&redis.ClusterOptions{ Addrs: redisCfg.ClusterAddrs, Password: redisCfg.Password, }) } else { // 使用单例模式 client = redis.NewClient(&redis.Options{ Addr: redisCfg.Addr, Password: redisCfg.Password, DB: redisCfg.DB, }) } pong, err := client.Ping(context.Background()).Result() if err != nil { global.GVA_LOG.Error("redis connect ping failed, err:", zap.String("name", redisCfg.Name), zap.Error(err)) return nil, err } global.GVA_LOG.Info("redis connect ping response:", zap.String("name", redisCfg.Name), zap.String("pong", pong)) return client, nil } func Redis() { redisClient, err := initRedisClient(global.GVA_CONFIG.Redis) if err != nil { panic(err) } global.GVA_REDIS = redisClient } 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 } ================================================ FILE: server/initialize/register_init.go ================================================ package initialize import ( _ "github.com/flipped-aurora/gin-vue-admin/server/source/example" _ "github.com/flipped-aurora/gin-vue-admin/server/source/system" ) func init() { // do nothing,only import source package so that inits can be registered } ================================================ FILE: server/initialize/reload.go ================================================ package initialize import ( "github.com/flipped-aurora/gin-vue-admin/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 } ================================================ FILE: server/initialize/router.go ================================================ package initialize import ( "net/http" "os" "github.com/flipped-aurora/gin-vue-admin/server/docs" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/middleware" "github.com/flipped-aurora/gin-vue-admin/server/router" "github.com/gin-gonic/gin" swaggerFiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" ) type justFilesFilesystem struct { fs http.FileSystem } func (fs justFilesFilesystem) Open(name string) (http.File, error) { f, err := fs.fs.Open(name) if err != nil { return nil, err } stat, err := f.Stat() if stat.IsDir() { return nil, os.ErrPermission } return f, nil } // 初始化总路由 func Routers() *gin.Engine { Router := gin.New() // 使用自定义的 Recovery 中间件,记录 panic 并入库 Router.Use(middleware.GinRecovery(true)) if gin.Mode() == gin.DebugMode { Router.Use(gin.Logger()) } 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 // 如果想要不使用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文件") // 跨域,如需跨域可以打开下面的注释 // Router.Use(middleware.Cors()) // 直接放行全部跨域请求 // Router.Use(middleware.CorsByRules()) // 按照配置的规则放行跨域请求 // global.GVA_LOG.Info("use middleware cors") docs.SwaggerInfo.BasePath = global.GVA_CONFIG.System.RouterPrefix Router.GET(global.GVA_CONFIG.System.RouterPrefix+"/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) global.GVA_LOG.Info("register swagger handler") // 方便统一添加路由组前缀 多服务器上线使用 PublicGroup := Router.Group(global.GVA_CONFIG.System.RouterPrefix) PrivateGroup := Router.Group(global.GVA_CONFIG.System.RouterPrefix) PrivateGroup.Use(middleware.JWTAuth()).Use(middleware.CasbinHandler()) { // 健康监测 PublicGroup.GET("/health", func(c *gin.Context) { c.JSON(http.StatusOK, "ok") }) } { systemRouter.InitBaseRouter(PublicGroup) // 注册基础功能路由 不做鉴权 systemRouter.InitInitRouter(PublicGroup) // 自动初始化相关 } { systemRouter.InitApiRouter(PrivateGroup, PublicGroup) // 注册功能api路由 systemRouter.InitJwtRouter(PrivateGroup) // jwt相关路由 systemRouter.InitUserRouter(PrivateGroup) // 注册用户路由 systemRouter.InitMenuRouter(PrivateGroup) // 注册menu路由 systemRouter.InitSystemRouter(PrivateGroup) // system相关路由 systemRouter.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,PublicGroup) // Skills 定义器 exampleRouter.InitCustomerRouter(PrivateGroup) // 客户路由 exampleRouter.InitFileUploadAndDownloadRouter(PrivateGroup) // 文件上传下载功能路由 exampleRouter.InitAttachmentCategoryRouterRouter(PrivateGroup) // 文件上传下载分类 } //插件路由安装 InstallPlugin(PrivateGroup, PublicGroup, Router) // 注册业务路由 initBizRouter(PrivateGroup, PublicGroup) global.GVA_ROUTERS = Router.Routes() global.GVA_LOG.Info("router register success") return Router } ================================================ FILE: server/initialize/router_biz.go ================================================ package initialize import ( "github.com/flipped-aurora/gin-vue-admin/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) } ================================================ FILE: server/initialize/timer.go ================================================ package initialize import ( "fmt" "github.com/flipped-aurora/gin-vue-admin/server/task" "github.com/robfig/cron/v3" "github.com/flipped-aurora/gin-vue-admin/server/global" ) func Timer() { go func() { var option []cron.Option option = append(option, cron.WithSeconds()) // 清理DB定时任务 _, err := global.GVA_Timer.AddTaskByFunc("ClearDB", "@daily", func() { err := task.ClearTable(global.GVA_DB) // 定时任务方法定在task文件包中 if err != nil { fmt.Println("timer error:", err) } }, "定时清理数据库【日志,黑名单】内容", option...) if err != nil { fmt.Println("add timer error:", err) } // 其他定时任务定在这里 参考上方使用方法 //_, err := global.GVA_Timer.AddTaskByFunc("定时任务标识", "corn表达式", func() { // 具体执行内容... // ...... //}, option...) //if err != nil { // fmt.Println("add timer error:", err) //} }() } ================================================ FILE: server/initialize/validator.go ================================================ package initialize import "github.com/flipped-aurora/gin-vue-admin/server/utils" func init() { _ = utils.RegisterRule("PageVerify", utils.Rules{ "Page": {utils.NotEmpty()}, "PageSize": {utils.NotEmpty()}, }, ) _ = utils.RegisterRule("IdVerify", utils.Rules{ "Id": {utils.NotEmpty()}, }, ) _ = utils.RegisterRule("AuthorityIdVerify", utils.Rules{ "AuthorityId": {utils.NotEmpty()}, }, ) } ================================================ FILE: server/main.go ================================================ package main import ( "github.com/flipped-aurora/gin-vue-admin/server/core" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/initialize" _ "go.uber.org/automaxprocs" "go.uber.org/zap" ) //go:generate go env -w GO111MODULE=on //go:generate go env -w GOPROXY=https://goproxy.cn,direct //go:generate go mod tidy //go:generate go mod download // 这部分 @Tag 设置用于排序, 需要排序的接口请按照下面的格式添加 // swag init 对 @Tag 只会从入口文件解析, 默认 main.go // 也可通过 --generalInfo flag 指定其他文件 // @Tag.Name Base // @Tag.Name SysUser // @Tag.Description 用户 // @title Gin-Vue-Admin Swagger API接口文档 // @version v2.9.0 // @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() // 初始化表 } } ================================================ FILE: server/mcp/api_creator.go ================================================ package mcpTool import ( "context" "encoding/json" "errors" "fmt" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/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 } ================================================ FILE: server/mcp/api_lister.go ================================================ package mcpTool import ( "context" "encoding/json" "fmt" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/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 } ================================================ FILE: server/mcp/client/client.go ================================================ 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 } ================================================ FILE: server/mcp/client/client_test.go ================================================ package client import ( "context" "fmt" "github.com/mark3labs/mcp-go/mcp" "testing" ) // 测试 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("-------------------") } } ================================================ FILE: server/mcp/dictionary_generator.go ================================================ package mcpTool import ( "context" "encoding/json" "errors" "fmt" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/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) } ================================================ FILE: server/mcp/dictionary_query.go ================================================ package mcpTool import ( "context" "encoding/json" "fmt" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/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 } ================================================ FILE: server/mcp/enter.go ================================================ 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) } } ================================================ FILE: server/mcp/gva_analyze.go ================================================ package mcpTool import ( "context" "encoding/json" "errors" "fmt" model "github.com/flipped-aurora/gin-vue-admin/server/model/system" "os" "path/filepath" "strings" "github.com/flipped-aurora/gin-vue-admin/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 } ================================================ FILE: server/mcp/gva_execute.go ================================================ package mcpTool import ( "context" "encoding/json" "errors" "fmt" model "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/utils" "strings" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "github.com/flipped-aurora/gin-vue-admin/server/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, "") } ================================================ FILE: server/mcp/gva_review.go ================================================ 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() } ================================================ FILE: server/mcp/menu_creator.go ================================================ package mcpTool import ( "context" "encoding/json" "errors" "fmt" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/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 } ================================================ FILE: server/mcp/menu_lister.go ================================================ package mcpTool import ( "context" "encoding/json" "fmt" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/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 } ================================================ FILE: server/mcp/requirement_analyzer.go ================================================ 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 } ================================================ FILE: server/middleware/casbin_rbac.go ================================================ package middleware import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/gin-gonic/gin" "strconv" "strings" ) // 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() } } ================================================ FILE: server/middleware/cors.go ================================================ package middleware import ( "github.com/flipped-aurora/gin-vue-admin/server/config" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/gin-gonic/gin" "net/http" ) // Cors 直接放行所有跨域请求并放行所有 OPTIONS 方法 func Cors() gin.HandlerFunc { return func(c *gin.Context) { method := c.Request.Method origin := c.Request.Header.Get("Origin") c.Header("Access-Control-Allow-Origin", origin) c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token,X-Token,X-User-Id") c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS,DELETE,PUT") c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type, New-Token, New-Expires-At") c.Header("Access-Control-Allow-Credentials", "true") // 放行所有OPTIONS方法 if method == "OPTIONS" { c.AbortWithStatus(http.StatusNoContent) } // 处理请求 c.Next() } } // CorsByRules 按照配置处理跨域请求 func CorsByRules() gin.HandlerFunc { // 放行全部 if global.GVA_CONFIG.Cors.Mode == "allow-all" { return Cors() } return func(c *gin.Context) { whitelist := checkCors(c.GetHeader("origin")) // 通过检查, 添加请求头 if whitelist != nil { c.Header("Access-Control-Allow-Origin", whitelist.AllowOrigin) c.Header("Access-Control-Allow-Headers", whitelist.AllowHeaders) c.Header("Access-Control-Allow-Methods", whitelist.AllowMethods) c.Header("Access-Control-Expose-Headers", whitelist.ExposeHeaders) if whitelist.AllowCredentials { c.Header("Access-Control-Allow-Credentials", "true") } } // 严格白名单模式且未通过检查,直接拒绝处理请求 if whitelist == nil && global.GVA_CONFIG.Cors.Mode == "strict-whitelist" && !(c.Request.Method == "GET" && c.Request.URL.Path == "/health") { c.AbortWithStatus(http.StatusForbidden) } else { // 非严格白名单模式,无论是否通过检查均放行所有 OPTIONS 方法 if c.Request.Method == http.MethodOptions { c.AbortWithStatus(http.StatusNoContent) } } // 处理请求 c.Next() } } func checkCors(currentOrigin string) *config.CORSWhitelist { for _, whitelist := range global.GVA_CONFIG.Cors.Whitelist { // 遍历配置中的跨域头,寻找匹配项 if currentOrigin == whitelist.AllowOrigin { return &whitelist } } return nil } ================================================ FILE: server/middleware/email.go ================================================ package middleware import ( "bytes" "io" "strconv" "time" "github.com/flipped-aurora/gin-vue-admin/server/plugin/email/utils" utils2 "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/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)) } } } } ================================================ FILE: server/middleware/error.go ================================================ package middleware import ( "context" "fmt" "net" "net/http" "net/http/httputil" "os" "runtime/debug" "strings" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/service" "github.com/gin-gonic/gin" "go.uber.org/zap" ) // 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() } } ================================================ FILE: server/middleware/jwt.go ================================================ package middleware import ( "errors" "strconv" "time" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/golang-jwt/jwt/v5" "github.com/flipped-aurora/gin-vue-admin/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 } ================================================ FILE: server/middleware/limit_ip.go ================================================ package middleware import ( "context" "errors" "net/http" "time" "go.uber.org/zap" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" "github.com/gin-gonic/gin" ) type LimitConfig struct { // GenerationKey 根据业务生成key 下面CheckOrMark查询生成 GenerationKey func(c *gin.Context) string // 检查函数,用户可修改具体逻辑,更加灵活 CheckOrMark func(key string, expire int, limit int) error // Expire key 过期时间 Expire int // Limit 周期时间 Limit int } func (l LimitConfig) LimitWithTime() gin.HandlerFunc { return func(c *gin.Context) { if err := l.CheckOrMark(l.GenerationKey(c), l.Expire, l.Limit); err != nil { c.JSON(http.StatusOK, gin.H{"code": response.ERROR, "msg": err.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() } } } } ================================================ FILE: server/middleware/loadtls.go ================================================ 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() } } ================================================ FILE: server/middleware/logger.go ================================================ 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() } ================================================ FILE: server/middleware/operation.go ================================================ package middleware import ( "bytes" "encoding/json" "io" "net/http" "net/url" "strconv" "strings" "sync" "time" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/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) } ================================================ FILE: server/middleware/timeout.go ================================================ package middleware import ( "context" "github.com/gin-gonic/gin" "net/http" "time" ) // 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 } } } ================================================ FILE: server/model/common/basetypes.go ================================================ 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 } ================================================ FILE: server/model/common/clearDB.go ================================================ package common type ClearDB struct { TableName string CompareField string Interval string } ================================================ FILE: server/model/common/request/common.go ================================================ package request import ( "gorm.io/gorm" ) // PageInfo Paging common input parameter structure type PageInfo struct { Page int `json:"page" form:"page"` // 页码 PageSize int `json:"pageSize" form:"pageSize"` // 每页大小 Keyword string `json:"keyword" form:"keyword"` // 关键字 } func (r *PageInfo) Paginate() func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { if r.Page <= 0 { r.Page = 1 } switch { case r.PageSize > 100: r.PageSize = 100 case r.PageSize <= 0: r.PageSize = 10 } offset := (r.Page - 1) * r.PageSize return db.Offset(offset).Limit(r.PageSize) } } // GetById Find by id structure type GetById struct { ID int `json:"id" form:"id"` // 主键ID } func (r *GetById) Uint() uint { return uint(r.ID) } type IdsReq struct { Ids []int `json:"ids" form:"ids"` } // GetAuthorityId Get role by id structure type GetAuthorityId struct { AuthorityId uint `json:"authorityId" form:"authorityId"` // 角色ID } type Empty struct{} ================================================ FILE: server/model/common/response/common.go ================================================ package response type PageResult struct { List interface{} `json:"list"` Total int64 `json:"total"` Page int `json:"page"` PageSize int `json:"pageSize"` } ================================================ FILE: server/model/common/response/response.go ================================================ 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) } ================================================ FILE: server/model/example/exa_attachment_category.go ================================================ package example import ( "github.com/flipped-aurora/gin-vue-admin/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" } ================================================ FILE: server/model/example/exa_breakpoint_continue.go ================================================ package example import ( "github.com/flipped-aurora/gin-vue-admin/server/global" ) // file struct, 文件结构体 type ExaFile struct { global.GVA_MODEL FileName string FileMd5 string FilePath string ExaFileChunk []ExaFileChunk ChunkTotal int IsFinish bool } // file chunk struct, 切片结构体 type ExaFileChunk struct { global.GVA_MODEL ExaFileID uint FileChunkNumber int FileChunkPath string } ================================================ FILE: server/model/example/exa_customer.go ================================================ package example import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system" ) type ExaCustomer struct { global.GVA_MODEL CustomerName string `json:"customerName" form:"customerName" gorm:"comment:客户名"` // 客户名 CustomerPhoneData string `json:"customerPhoneData" form:"customerPhoneData" gorm:"comment:客户手机号"` // 客户手机号 SysUserID uint `json:"sysUserId" form:"sysUserId" gorm:"comment:管理ID"` // 管理ID SysUserAuthorityID uint `json:"sysUserAuthorityID" form:"sysUserAuthorityID" gorm:"comment:管理角色ID"` // 管理角色ID SysUser system.SysUser `json:"sysUser" form:"sysUser" gorm:"comment:管理详情"` // 管理详情 } ================================================ FILE: server/model/example/exa_file_upload_download.go ================================================ package example import ( "github.com/flipped-aurora/gin-vue-admin/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" } ================================================ FILE: server/model/example/request/exa_file_upload_and_downloads.go ================================================ package request import ( "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" ) type ExaAttachmentCategorySearch struct { ClassId int `json:"classId" form:"classId"` request.PageInfo } ================================================ FILE: server/model/example/response/exa_breakpoint_continue.go ================================================ package response import "github.com/flipped-aurora/gin-vue-admin/server/model/example" type FilePathResponse struct { FilePath string `json:"filePath"` } type FileResponse struct { File example.ExaFile `json:"file"` } ================================================ FILE: server/model/example/response/exa_customer.go ================================================ package response import "github.com/flipped-aurora/gin-vue-admin/server/model/example" type ExaCustomerResponse struct { Customer example.ExaCustomer `json:"customer"` } ================================================ FILE: server/model/example/response/exa_file_upload_download.go ================================================ package response import "github.com/flipped-aurora/gin-vue-admin/server/model/example" type ExaFileResponse struct { File example.ExaFileUploadAndDownload `json:"file"` } ================================================ FILE: server/model/system/request/jwt.go ================================================ 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 } ================================================ FILE: server/model/system/request/sys_api.go ================================================ package request import ( "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" "github.com/flipped-aurora/gin-vue-admin/server/model/system" ) // api分页条件查询及排序结构体 type SearchApiParams struct { system.SysApi request.PageInfo OrderKey string `json:"orderKey"` // 排序 Desc bool `json:"desc"` // 排序方式:升序false(默认)|降序true } // SetApiAuthorities 通过API路径和方法全量覆盖关联角色列表 type SetApiAuthorities struct { Path string `json:"path" form:"path"` // API路径 Method string `json:"method" form:"method"` // 请求方法 AuthorityIds []uint `json:"authorityIds" form:"authorityIds"` // 角色ID列表 } ================================================ FILE: server/model/system/request/sys_api_token.go ================================================ package request import ( "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" "github.com/flipped-aurora/gin-vue-admin/server/model/system" ) type SysApiTokenSearch struct { system.SysApiToken request.PageInfo Status *bool `json:"status" form:"status"` } ================================================ FILE: server/model/system/request/sys_authority_btn.go ================================================ package request type SysAuthorityBtnReq struct { MenuID uint `json:"menuID"` AuthorityId uint `json:"authorityId"` Selected []uint `json:"selected"` } ================================================ FILE: server/model/system/request/sys_auto_code.go ================================================ package request import ( "encoding/json" "fmt" "github.com/flipped-aurora/gin-vue-admin/server/global" model "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/pkg/errors" "go/token" "strings" ) type AutoCode struct { Package string `json:"package"` PackageT string `json:"-"` TableName string `json:"tableName" example:"表名"` // 表名 BusinessDB string `json:"businessDB" example:"业务数据库"` // 业务数据库 StructName string `json:"structName" example:"Struct名称"` // Struct名称 PackageName string `json:"packageName" example:"文件名称"` // 文件名称 Description string `json:"description" example:"Struct中文名称"` // Struct中文名称 Abbreviation string `json:"abbreviation" example:"Struct简称"` // Struct简称 HumpPackageName string `json:"humpPackageName" example:"go文件名称"` // go文件名称 GvaModel bool `json:"gvaModel" example:"false"` // 是否使用gva默认Model AutoMigrate bool `json:"autoMigrate" example:"false"` // 是否自动迁移表结构 AutoCreateResource bool `json:"autoCreateResource" example:"false"` // 是否自动创建资源标识 AutoCreateApiToSql bool `json:"autoCreateApiToSql" example:"false"` // 是否自动创建api AutoCreateMenuToSql bool `json:"autoCreateMenuToSql" example:"false"` // 是否自动创建menu AutoCreateBtnAuth bool `json:"autoCreateBtnAuth" example:"false"` // 是否自动创建按钮权限 OnlyTemplate bool `json:"onlyTemplate" example:"false"` // 是否只生成模板 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;"` //模式 } ================================================ FILE: server/model/system/request/sys_auto_code_mcp.go ================================================ 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"` } ================================================ FILE: server/model/system/request/sys_auto_code_package.go ================================================ package request import ( "github.com/flipped-aurora/gin-vue-admin/server/global" model "github.com/flipped-aurora/gin-vue-admin/server/model/system" ) type SysAutoCodePackageCreate struct { Desc string `json:"desc" example:"描述"` Label string `json:"label" example:"展示名"` Template string `json:"template" example:"模版"` PackageName string `json:"packageName" example:"包名"` Module string `json:"-" example:"模块"` } func (r *SysAutoCodePackageCreate) AutoCode() AutoCode { return AutoCode{ Package: r.PackageName, Module: global.GVA_CONFIG.AutoCode.Module, } } func (r *SysAutoCodePackageCreate) Create() model.SysAutoCodePackage { return model.SysAutoCodePackage{ Desc: r.Desc, Label: r.Label, Template: r.Template, PackageName: r.PackageName, Module: global.GVA_CONFIG.AutoCode.Module, } } ================================================ FILE: server/model/system/request/sys_auto_history.go ================================================ package request import ( common "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" model "github.com/flipped-aurora/gin-vue-admin/server/model/system" ) type SysAutoHistoryCreate struct { Table string // 表名 Package string // 模块名/插件名 Request string // 前端传入的结构化信息 StructName string // 结构体名称 BusinessDB string // 业务库 Description string // Struct中文名称 Injections map[string]string // 注入路径 Templates map[string]string // 模板信息 ApiIDs []uint // api表注册内容 MenuID uint // 菜单ID ExportTemplateID uint // 导出模板ID } func (r *SysAutoHistoryCreate) Create() model.SysAutoCodeHistory { entity := model.SysAutoCodeHistory{ Package: r.Package, Request: r.Request, Table: r.Table, StructName: r.StructName, 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} } ================================================ FILE: server/model/system/request/sys_casbin.go ================================================ 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"}, } } ================================================ FILE: server/model/system/request/sys_dictionary.go ================================================ 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字符串 } ================================================ FILE: server/model/system/request/sys_dictionary_detail.go ================================================ package request import ( "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" "github.com/flipped-aurora/gin-vue-admin/server/model/system" ) type SysDictionaryDetailSearch struct { system.SysDictionaryDetail request.PageInfo 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"` // 是否包含子级数据 } ================================================ FILE: server/model/system/request/sys_error.go ================================================ package request import ( "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" "time" ) type SysErrorSearch struct{ CreatedAtRange []time.Time `json:"createdAtRange" form:"createdAtRange[]"` Form *string `json:"form" form:"form"` Info *string `json:"info" form:"info"` request.PageInfo } ================================================ FILE: server/model/system/request/sys_export_template.go ================================================ package request import ( "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" "github.com/flipped-aurora/gin-vue-admin/server/model/system" "time" ) type SysExportTemplateSearch struct { system.SysExportTemplate StartCreatedAt *time.Time `json:"startCreatedAt" form:"startCreatedAt"` EndCreatedAt *time.Time `json:"endCreatedAt" form:"endCreatedAt"` request.PageInfo } ================================================ FILE: server/model/system/request/sys_init.go ================================================ package request import ( "fmt" "github.com/flipped-aurora/gin-vue-admin/server/config" "os" ) type InitDB struct { AdminPassword string `json:"adminPassword" binding:"required"` DBType string `json:"dbType"` // 数据库类型 Host string `json:"host"` // 服务器地址 Port string `json:"port"` // 数据库连接端口 UserName string `json:"userName"` // 数据库用户名 Password string `json:"password"` // 数据库密码 DBName string `json:"dbName" binding:"required"` // 数据库名 DBPath string `json:"dbPath"` // sqlite数据库文件路径 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: "", }, } } ================================================ FILE: server/model/system/request/sys_login_log.go ================================================ package request import ( "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" "github.com/flipped-aurora/gin-vue-admin/server/model/system" ) type SysLoginLogSearch struct { system.SysLoginLog request.PageInfo } ================================================ FILE: server/model/system/request/sys_menu.go ================================================ package request import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system" ) // AddMenuAuthorityInfo Add menu authority info structure type AddMenuAuthorityInfo struct { Menus []system.SysBaseMenu `json:"menus"` AuthorityId uint `json:"authorityId"` // 角色ID } // SetMenuAuthorities 通过菜单ID全量覆盖关联角色列表 type SetMenuAuthorities struct { MenuId uint `json:"menuId" form:"menuId"` // 菜单ID AuthorityIds []uint `json:"authorityIds" form:"authorityIds"` // 角色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", }, }} } ================================================ FILE: server/model/system/request/sys_operation_record.go ================================================ package request import ( "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" "github.com/flipped-aurora/gin-vue-admin/server/model/system" ) type SysOperationRecordSearch struct { system.SysOperationRecord request.PageInfo } ================================================ FILE: server/model/system/request/sys_params.go ================================================ package request import ( "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" "time" ) type SysParamsSearch struct { StartCreatedAt *time.Time `json:"startCreatedAt" form:"startCreatedAt"` EndCreatedAt *time.Time `json:"endCreatedAt" form:"endCreatedAt"` Name string `json:"name" form:"name" ` Key string `json:"key" form:"key" ` request.PageInfo } ================================================ FILE: server/model/system/request/sys_skills.go ================================================ package request import "github.com/flipped-aurora/gin-vue-admin/server/model/system" type SkillToolRequest struct { Tool string `json:"tool"` } type SkillDetailRequest struct { Tool string `json:"tool"` Skill string `json:"skill"` } type SkillDeleteRequest struct { Tool string `json:"tool"` Skill string `json:"skill"` } type SkillPackageRequest 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"` } type DownloadOnlineSkillReq struct { Tool string `json:"tool" binding:"required"` ID uint `json:"id" binding:"required"` Version string `json:"version" binding:"required"` } ================================================ FILE: server/model/system/request/sys_user.go ================================================ package request import ( common "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" "github.com/flipped-aurora/gin-vue-admin/server/model/system" ) // Register User register structure type Register struct { Username string `json:"userName" example:"用户名"` Password string `json:"passWord" example:"密码"` NickName string `json:"nickName" example:"昵称"` HeaderImg string `json:"headerImg" example:"头像链接"` AuthorityId uint `json:"authorityId" swaggertype:"string" example:"int 角色id"` Enable int `json:"enable" swaggertype:"string" example:"int 是否启用"` AuthorityIds []uint `json:"authorityIds" swaggertype:"string" example:"[]uint 角色id"` Phone string `json:"phone" example:"电话号码"` Email string `json:"email" example:"电子邮箱"` } // 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"` OrderKey string `json:"orderKey" form:"orderKey"` // 排序 Desc bool `json:"desc" form:"desc"` // 排序方式:升序false(默认)|降序true } // SetRoleUsers 通过角色ID全量覆盖关联用户列表 type SetRoleUsers struct { AuthorityId uint `json:"authorityId" form:"authorityId"` // 角色ID UserIds []uint `json:"userIds" form:"userIds"` // 用户ID列表 } ================================================ FILE: server/model/system/request/sys_version.go ================================================ package request import ( "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" "github.com/flipped-aurora/gin-vue-admin/server/model/system" "time" ) type 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"` // 导出时间 } ================================================ FILE: server/model/system/response/sys_api.go ================================================ package response import ( "github.com/flipped-aurora/gin-vue-admin/server/model/system" ) type SysAPIResponse struct { Api system.SysApi `json:"api"` } type SysAPIListResponse struct { Apis []system.SysApi `json:"apis"` } type SysSyncApis struct { NewApis []system.SysApi `json:"newApis"` DeleteApis []system.SysApi `json:"deleteApis"` } ================================================ FILE: server/model/system/response/sys_authority.go ================================================ package response import "github.com/flipped-aurora/gin-vue-admin/server/model/system" type SysAuthorityResponse struct { Authority system.SysAuthority `json:"authority"` } type SysAuthorityCopyResponse struct { Authority system.SysAuthority `json:"authority"` OldAuthorityId uint `json:"oldAuthorityId"` // 旧角色ID } ================================================ FILE: server/model/system/response/sys_authority_btn.go ================================================ package response type SysAuthorityBtnRes struct { Selected []uint `json:"selected"` } ================================================ FILE: server/model/system/response/sys_auto_code.go ================================================ package response import "github.com/flipped-aurora/gin-vue-admin/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"` } ================================================ FILE: server/model/system/response/sys_captcha.go ================================================ package response type SysCaptchaResponse struct { CaptchaId string `json:"captchaId"` PicPath string `json:"picPath"` CaptchaLength int `json:"captchaLength"` OpenCaptcha bool `json:"openCaptcha"` } ================================================ FILE: server/model/system/response/sys_casbin.go ================================================ package response import ( "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" ) type PolicyPathResponse struct { Paths []request.CasbinInfo `json:"paths"` } ================================================ FILE: server/model/system/response/sys_menu.go ================================================ package response import "github.com/flipped-aurora/gin-vue-admin/server/model/system" type SysMenusResponse struct { Menus []system.SysMenu `json:"menus"` } type SysBaseMenusResponse struct { Menus []system.SysBaseMenu `json:"menus"` } type SysBaseMenuResponse struct { Menu system.SysBaseMenu `json:"menu"` } ================================================ FILE: server/model/system/response/sys_system.go ================================================ package response import "github.com/flipped-aurora/gin-vue-admin/server/config" type SysConfigResponse struct { Config config.Server `json:"config"` } ================================================ FILE: server/model/system/response/sys_user.go ================================================ package response import ( "github.com/flipped-aurora/gin-vue-admin/server/model/system" ) type SysUserResponse struct { User system.SysUser `json:"user"` } type LoginResponse struct { User system.SysUser `json:"user"` Token string `json:"token"` ExpiresAt int64 `json:"expiresAt"` } ================================================ FILE: server/model/system/response/sys_version.go ================================================ package response import ( "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/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 } ================================================ FILE: server/model/system/sys_api.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/global" ) type SysApi struct { global.GVA_MODEL Path string `json:"path" gorm:"comment:api路径"` // api路径 Description string `json:"description" gorm:"comment:api中文描述"` // api中文描述 ApiGroup string `json:"apiGroup" gorm:"comment:api组"` // api组 Method string `json:"method" gorm:"default:POST;comment:方法"` // 方法:创建POST(默认)|查看GET|更新PUT|删除DELETE } func (SysApi) TableName() string { return "sys_apis" } type SysIgnoreApi struct { global.GVA_MODEL Path string `json:"path" gorm:"comment:api路径"` // api路径 Method string `json:"method" gorm:"default:POST;comment:方法"` // 方法:创建POST(默认)|查看GET|更新PUT|删除DELETE Flag bool `json:"flag" gorm:"-"` // 是否忽略 } func (SysIgnoreApi) TableName() string { return "sys_ignore_apis" } ================================================ FILE: server/model/system/sys_api_token.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "time" ) 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:备注"` } ================================================ FILE: server/model/system/sys_authority.go ================================================ 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" } ================================================ FILE: server/model/system/sys_authority_btn.go ================================================ package system type SysAuthorityBtn struct { AuthorityId uint `gorm:"comment:角色ID"` SysMenuID uint `gorm:"comment:菜单ID"` SysBaseMenuBtnID uint `gorm:"comment:菜单按钮ID"` SysBaseMenuBtn SysBaseMenuBtn ` gorm:"comment:按钮详情"` } ================================================ FILE: server/model/system/sys_authority_menu.go ================================================ 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" } ================================================ FILE: server/model/system/sys_auto_code_history.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "gorm.io/gorm" "os" "path" "path/filepath" "strings" ) // SysAutoCodeHistory 自动迁移代码记录,用于回滚,重放使用 type SysAutoCodeHistory struct { global.GVA_MODEL Table string `json:"tableName" gorm:"column:table_name;comment:表名"` Package string `json:"package" gorm:"column:package;comment:模块名/插件名"` Request string `json:"request" gorm:"type:text;column:request;comment:前端传入的结构化信息"` StructName string `json:"structName" gorm:"column:struct_name;comment:结构体名称"` 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" } ================================================ FILE: server/model/system/sys_auto_code_package.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/global" ) type SysAutoCodePackage struct { global.GVA_MODEL Desc string `json:"desc" gorm:"comment:描述"` Label string `json:"label" gorm:"comment:展示名"` Template string `json:"template" gorm:"comment:模版"` PackageName string `json:"packageName" gorm:"comment:包名"` Module string `json:"-" example:"模块"` } func (s *SysAutoCodePackage) TableName() string { return "sys_auto_code_packages" } ================================================ FILE: server/model/system/sys_base_menu.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/global" ) type SysBaseMenu struct { global.GVA_MODEL MenuLevel uint `json:"-"` ParentId uint `json:"parentId" gorm:"comment:父菜单ID"` // 父菜单ID Path string `json:"path" gorm:"comment:路由path"` // 路由path Name string `json:"name" gorm:"comment:路由name"` // 路由name Hidden bool `json:"hidden" gorm:"comment:是否在列表隐藏"` // 是否在列表隐藏 Component string `json:"component" gorm:"comment:对应前端文件路径"` // 对应前端文件路径 Sort int `json:"sort" gorm:"comment:排序标记"` // 排序标记 Meta `json:"meta" gorm:"embedded"` // 附加属性 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" } ================================================ FILE: server/model/system/sys_dictionary.go ================================================ // 自动生成模板SysDictionary package system import ( "github.com/flipped-aurora/gin-vue-admin/server/global" ) // 如果含有time.Time 请自行import time包 type SysDictionary struct { global.GVA_MODEL Name string `json:"name" form:"name" gorm:"column:name;comment:字典名(中)"` // 字典名(中) Type string `json:"type" form:"type" gorm:"column:type;comment:字典名(英)"` // 字典名(英) Status *bool `json:"status" form:"status" gorm:"column:status;comment:状态"` // 状态 Desc string `json:"desc" form:"desc" gorm:"column:desc;comment:描述"` // 描述 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" } ================================================ FILE: server/model/system/sys_dictionary_detail.go ================================================ // 自动生成模板SysDictionaryDetail package system import ( "github.com/flipped-aurora/gin-vue-admin/server/global" ) // 如果含有time.Time 请自行import time包 type SysDictionaryDetail struct { global.GVA_MODEL Label string `json:"label" form:"label" gorm:"column:label;comment:展示值"` // 展示值 Value string `json:"value" form:"value" gorm:"column:value;comment:字典值"` // 字典值 Extend string `json:"extend" form:"extend" gorm:"column:extend;comment:扩展值"` // 扩展值 Status *bool `json:"status" form:"status" gorm:"column:status;comment:启用状态"` // 启用状态 Sort int `json:"sort" form:"sort" gorm:"column:sort;comment:排序标记"` // 排序标记 SysDictionaryID int `json:"sysDictionaryID" form:"sysDictionaryID" gorm:"column:sys_dictionary_id;comment:关联标记"` // 关联标记 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" } ================================================ FILE: server/model/system/sys_error.go ================================================ // 自动生成模板SysError package system import ( "github.com/flipped-aurora/gin-vue-admin/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" } ================================================ FILE: server/model/system/sys_export_template.go ================================================ // 自动生成模板SysExportTemplate package system import ( "github.com/flipped-aurora/gin-vue-admin/server/global" ) // 导出模板 结构体 SysExportTemplate type SysExportTemplate struct { global.GVA_MODEL DBName string `json:"dbName" form:"dbName" gorm:"column:db_name;comment:数据库名称;"` //数据库名称 Name string `json:"name" form:"name" gorm:"column:name;comment:模板名称;"` //模板名称 TableName string `json:"tableName" form:"tableName" gorm:"column:table_name;comment:表名称;"` //表名称 TemplateID string `json:"templateID" form:"templateID" gorm:"column:template_id;comment:模板标识;"` //模板标识 TemplateInfo string `json:"templateInfo" form:"templateInfo" gorm:"column:template_info;type:text;"` //模板信息 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" } ================================================ FILE: server/model/system/sys_jwt_blacklist.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/global" ) type JwtBlacklist struct { global.GVA_MODEL Jwt string `gorm:"type:text;comment:jwt"` } ================================================ FILE: server/model/system/sys_login_log.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/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"` } ================================================ FILE: server/model/system/sys_menu_btn.go ================================================ package system import "github.com/flipped-aurora/gin-vue-admin/server/global" type SysBaseMenuBtn struct { global.GVA_MODEL Name string `json:"name" gorm:"comment:按钮关键key"` Desc string `json:"desc" gorm:"按钮备注"` SysBaseMenuID uint `json:"sysBaseMenuID" gorm:"comment:菜单ID"` } ================================================ FILE: server/model/system/sys_operation_record.go ================================================ // 自动生成模板SysOperationRecord package system import ( "time" "github.com/flipped-aurora/gin-vue-admin/server/global" ) // 如果含有time.Time 请自行import time包 type SysOperationRecord struct { global.GVA_MODEL Ip string `json:"ip" form:"ip" gorm:"column:ip;comment:请求ip"` // 请求ip Method string `json:"method" form:"method" gorm:"column:method;comment:请求方法"` // 请求方法 Path string `json:"path" form:"path" gorm:"column:path;comment:请求路径"` // 请求路径 Status int `json:"status" form:"status" gorm:"column:status;comment:请求状态"` // 请求状态 Latency time.Duration `json:"latency" form:"latency" gorm:"column:latency;comment:延迟" swaggertype:"string"` // 延迟 Agent string `json:"agent" form:"agent" gorm:"type:text;column:agent;comment:代理"` // 代理 ErrorMessage string `json:"error_message" form:"error_message" gorm:"column:error_message;comment:错误信息"` // 错误信息 Body string `json:"body" form:"body" gorm:"type:text;column:body;comment:请求Body"` // 请求Body Resp string `json:"resp" form:"resp" gorm:"type:text;column:resp;comment:响应Body"` // 响应Body UserID int `json:"user_id" form:"user_id" gorm:"column:user_id;comment:用户id"` // 用户id User SysUser `json:"user"` } ================================================ FILE: server/model/system/sys_params.go ================================================ // 自动生成模板SysParams package system import ( "github.com/flipped-aurora/gin-vue-admin/server/global" ) // 参数 结构体 SysParams type SysParams struct { global.GVA_MODEL Name string `json:"name" form:"name" gorm:"column:name;comment:参数名称;" binding:"required"` //参数名称 Key string `json:"key" form:"key" gorm:"column:key;comment:参数键;" binding:"required"` //参数键 Value string `json:"value" form:"value" gorm:"column:value;comment:参数值;" binding:"required"` //参数值 Desc string `json:"desc" form:"desc" gorm:"column:desc;comment:参数说明;"` //参数说明 } // TableName 参数 SysParams自定义表名 sys_params func (SysParams) TableName() string { return "sys_params" } ================================================ FILE: server/model/system/sys_skills.go ================================================ 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"` } ================================================ FILE: server/model/system/sys_system.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/config" ) // 配置文件结构体 type System struct { Config config.Server `json:"config"` } ================================================ FILE: server/model/system/sys_user.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/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 } ================================================ FILE: server/model/system/sys_user_authority.go ================================================ 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" } ================================================ FILE: server/model/system/sys_version.go ================================================ // 自动生成模板SysVersion package system import ( "github.com/flipped-aurora/gin-vue-admin/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" } ================================================ FILE: server/plugin/announcement/api/enter.go ================================================ package api import "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/service" var ( Api = new(api) serviceInfo = service.Service.Info ) type api struct{ Info info } ================================================ FILE: server/plugin/announcement/api/info.go ================================================ package api import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/model" "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/model/request" "github.com/gin-gonic/gin" "go.uber.org/zap" ) var Info = new(info) type info struct{} // CreateInfo 创建公告 // @Tags Info // @Summary 创建公告 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body model.Info true "创建公告" // @Success 200 {object} response.Response{msg=string} "创建成功" // @Router /info/createInfo [post] func (a *info) CreateInfo(c *gin.Context) { var info model.Info err := c.ShouldBindJSON(&info) if err != nil { response.FailWithMessage(err.Error(), c) return } err = serviceInfo.CreateInfo(&info) if err != nil { global.GVA_LOG.Error("创建失败!", zap.Error(err)) response.FailWithMessage("创建失败", c) return } response.OkWithMessage("创建成功", c) } // DeleteInfo 删除公告 // @Tags Info // @Summary 删除公告 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body model.Info true "删除公告" // @Success 200 {object} response.Response{msg=string} "删除成功" // @Router /info/deleteInfo [delete] func (a *info) DeleteInfo(c *gin.Context) { ID := c.Query("ID") err := serviceInfo.DeleteInfo(ID) if err != nil { global.GVA_LOG.Error("删除失败!", zap.Error(err)) response.FailWithMessage("删除失败", c) return } response.OkWithMessage("删除成功", c) } // DeleteInfoByIds 批量删除公告 // @Tags Info // @Summary 批量删除公告 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Success 200 {object} response.Response{msg=string} "批量删除成功" // @Router /info/deleteInfoByIds [delete] func (a *info) DeleteInfoByIds(c *gin.Context) { IDs := c.QueryArray("IDs[]") if err := serviceInfo.DeleteInfoByIds(IDs); err != nil { global.GVA_LOG.Error("批量删除失败!", zap.Error(err)) response.FailWithMessage("批量删除失败", c) return } response.OkWithMessage("批量删除成功", c) } // UpdateInfo 更新公告 // @Tags Info // @Summary 更新公告 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body model.Info true "更新公告" // @Success 200 {object} response.Response{msg=string} "更新成功" // @Router /info/updateInfo [put] func (a *info) UpdateInfo(c *gin.Context) { var info model.Info err := c.ShouldBindJSON(&info) if err != nil { response.FailWithMessage(err.Error(), c) return } err = serviceInfo.UpdateInfo(info) if err != nil { global.GVA_LOG.Error("更新失败!", zap.Error(err)) response.FailWithMessage("更新失败", c) return } response.OkWithMessage("更新成功", c) } // FindInfo 用id查询公告 // @Tags Info // @Summary 用id查询公告 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data query model.Info true "用id查询公告" // @Success 200 {object} response.Response{data=model.Info,msg=string} "查询成功" // @Router /info/findInfo [get] func (a *info) FindInfo(c *gin.Context) { ID := c.Query("ID") reinfo, err := serviceInfo.GetInfo(ID) if err != nil { global.GVA_LOG.Error("查询失败!", zap.Error(err)) response.FailWithMessage("查询失败", c) return } response.OkWithData(reinfo, c) } // GetInfoList 分页获取公告列表 // @Tags Info // @Summary 分页获取公告列表 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data query request.InfoSearch true "分页获取公告列表" // @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" // @Router /info/getInfoList [get] func (a *info) GetInfoList(c *gin.Context) { var pageInfo request.InfoSearch err := c.ShouldBindQuery(&pageInfo) if err != nil { response.FailWithMessage(err.Error(), c) return } list, total, err := serviceInfo.GetInfoInfoList(pageInfo) if err != nil { global.GVA_LOG.Error("获取失败!", zap.Error(err)) response.FailWithMessage("获取失败", c) return } response.OkWithDetailed(response.PageResult{ List: list, Total: total, Page: pageInfo.Page, PageSize: pageInfo.PageSize, }, "获取成功", c) } // GetInfoDataSource 获取Info的数据源 // @Tags Info // @Summary 获取Info的数据源 // @accept application/json // @Produce application/json // @Success 200 {object} response.Response{data=object,msg=string} "查询成功" // @Router /info/getInfoDataSource [get] func (a *info) GetInfoDataSource(c *gin.Context) { // 此接口为获取数据源定义的数据 dataSource, err := serviceInfo.GetInfoDataSource() if err != nil { global.GVA_LOG.Error("查询失败!", zap.Error(err)) response.FailWithMessage("查询失败", c) return } response.OkWithData(dataSource, c) } // GetInfoPublic 不需要鉴权的公告接口 // @Tags Info // @Summary 不需要鉴权的公告接口 // @accept application/json // @Produce application/json // @Param data query request.InfoSearch true "分页获取公告列表" // @Success 200 {object} response.Response{data=object,msg=string} "获取成功" // @Router /info/getInfoPublic [get] func (a *info) GetInfoPublic(c *gin.Context) { // 此接口不需要鉴权 示例为返回了一个固定的消息接口,一般本接口用于C端服务,需要自己实现业务逻辑 response.OkWithDetailed(gin.H{"info": "不需要鉴权的公告接口信息"}, "获取成功", c) } ================================================ FILE: server/plugin/announcement/config/config.go ================================================ package config type Config struct { } ================================================ FILE: server/plugin/announcement/gen/gen.go ================================================ package main import ( "gorm.io/gen" "path/filepath" //go:generate go mod tidy //go:generate go mod download //go:generate go run gen.go "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/model" ) func main() { g := gen.NewGenerator(gen.Config{OutPath: filepath.Join("..", "..", "..", "announcement", "blender", "model", "dao"), Mode: gen.WithoutContext | gen.WithDefaultQuery | gen.WithQueryInterface}) g.ApplyBasic( new(model.Info), ) g.Execute() } ================================================ FILE: server/plugin/announcement/initialize/api.go ================================================ package initialize import ( "context" model "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/plugin/plugin-tool/utils" ) func Api(ctx context.Context) { entities := []model.SysApi{ { Path: "/info/createInfo", Description: "新建公告", ApiGroup: "公告", Method: "POST", }, { Path: "/info/deleteInfo", Description: "删除公告", ApiGroup: "公告", Method: "DELETE", }, { Path: "/info/deleteInfoByIds", Description: "批量删除公告", ApiGroup: "公告", Method: "DELETE", }, { Path: "/info/updateInfo", Description: "更新公告", ApiGroup: "公告", Method: "PUT", }, { Path: "/info/findInfo", Description: "根据ID获取公告", ApiGroup: "公告", Method: "GET", }, { Path: "/info/getInfoList", Description: "获取公告列表", ApiGroup: "公告", Method: "GET", }, } utils.RegisterApis(entities...) } ================================================ FILE: server/plugin/announcement/initialize/dictionary.go ================================================ package initialize import ( "context" model "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/plugin/plugin-tool/utils" ) func Dictionary(ctx context.Context) { entities := []model.SysDictionary{} utils.RegisterDictionaries(entities...) } ================================================ FILE: server/plugin/announcement/initialize/gorm.go ================================================ package initialize import ( "context" "fmt" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/model" "github.com/pkg/errors" "go.uber.org/zap" ) func Gorm(ctx context.Context) { err := global.GVA_DB.WithContext(ctx).AutoMigrate( new(model.Info), ) if err != nil { err = errors.Wrap(err, "注册表失败!") zap.L().Error(fmt.Sprintf("%+v", err)) } } ================================================ FILE: server/plugin/announcement/initialize/menu.go ================================================ package initialize import ( "context" model "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/plugin/plugin-tool/utils" ) func Menu(ctx context.Context) { entities := []model.SysBaseMenu{ { ParentId: 9, Path: "anInfo", Name: "anInfo", Hidden: false, Component: "plugin/announcement/view/info.vue", Sort: 5, Meta: model.Meta{Title: "公告管理", Icon: "box"}, }, } utils.RegisterMenus(entities...) } ================================================ FILE: server/plugin/announcement/initialize/router.go ================================================ package initialize import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/middleware" "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/router" "github.com/gin-gonic/gin" ) func Router(engine *gin.Engine) { public := engine.Group(global.GVA_CONFIG.System.RouterPrefix).Group("") private := engine.Group(global.GVA_CONFIG.System.RouterPrefix).Group("") private.Use(middleware.JWTAuth()).Use(middleware.CasbinHandler()) router.Router.Info.Init(public, private) } ================================================ FILE: server/plugin/announcement/initialize/viper.go ================================================ package initialize import ( "fmt" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/plugin" "github.com/pkg/errors" "go.uber.org/zap" ) func Viper() { err := global.GVA_VP.UnmarshalKey("announcement", &plugin.Config) if err != nil { err = errors.Wrap(err, "初始化配置文件失败!") zap.L().Error(fmt.Sprintf("%+v", err)) } } ================================================ FILE: server/plugin/announcement/model/info.go ================================================ package model import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "gorm.io/datatypes" ) // Info 公告 结构体 type Info struct { global.GVA_MODEL Title string `json:"title" form:"title" gorm:"column:title;comment:公告标题;"` //标题 Content string `json:"content" form:"content" gorm:"column:content;comment:公告内容;type:text;"` //内容 UserID *int `json:"userID" form:"userID" gorm:"column:user_id;comment:发布者;"` //作者 Attachments datatypes.JSON `json:"attachments" form:"attachments" gorm:"column:attachments;comment:相关附件;" swaggertype:"array,object"` //附件 } // TableName 公告 Info自定义表名 gva_announcements_info func (Info) TableName() string { return "gva_announcements_info" } ================================================ FILE: server/plugin/announcement/model/request/info.go ================================================ package request import ( "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" "time" ) type InfoSearch struct { StartCreatedAt *time.Time `json:"startCreatedAt" form:"startCreatedAt"` EndCreatedAt *time.Time `json:"endCreatedAt" form:"endCreatedAt"` request.PageInfo } ================================================ FILE: server/plugin/announcement/plugin/plugin.go ================================================ package plugin import "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/config" var Config config.Config ================================================ FILE: server/plugin/announcement/plugin.go ================================================ package announcement import ( "context" "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/initialize" interfaces "github.com/flipped-aurora/gin-vue-admin/server/utils/plugin/v2" "github.com/gin-gonic/gin" ) var _ interfaces.Plugin = (*plugin)(nil) var Plugin = new(plugin) type plugin struct{} func init() { interfaces.Register(Plugin) } func (p *plugin) Register(group *gin.Engine) { ctx := context.Background() // 如果需要配置文件,请到config.Config中填充配置结构,且到下方发放中填入其在config.yaml中的key // initialize.Viper() // 安装插件时候自动注册的api数据请到下方法.Api方法中实现 initialize.Api(ctx) // 安装插件时候自动注册的Menu数据请到下方法.Menu方法中实现 initialize.Menu(ctx) // 安装插件时候自动注册的Dictionary数据请到下方法.Dictionary方法中实现 initialize.Dictionary(ctx) initialize.Gorm(ctx) initialize.Router(group) } ================================================ FILE: server/plugin/announcement/router/enter.go ================================================ package router import "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/api" var ( Router = new(router) apiInfo = api.Api.Info ) type router struct{ Info info } ================================================ FILE: server/plugin/announcement/router/info.go ================================================ package router import ( "github.com/flipped-aurora/gin-vue-admin/server/middleware" "github.com/gin-gonic/gin" ) var Info = new(info) type info struct{} // Init 初始化 公告 路由信息 func (r *info) Init(public *gin.RouterGroup, private *gin.RouterGroup) { { group := private.Group("info").Use(middleware.OperationRecord()) group.POST("createInfo", apiInfo.CreateInfo) // 新建公告 group.DELETE("deleteInfo", apiInfo.DeleteInfo) // 删除公告 group.DELETE("deleteInfoByIds", apiInfo.DeleteInfoByIds) // 批量删除公告 group.PUT("updateInfo", apiInfo.UpdateInfo) // 更新公告 } { group := private.Group("info") group.GET("findInfo", apiInfo.FindInfo) // 根据ID获取公告 group.GET("getInfoList", apiInfo.GetInfoList) // 获取公告列表 } { group := public.Group("info") group.GET("getInfoDataSource", apiInfo.GetInfoDataSource) // 获取公告数据源 group.GET("getInfoPublic", apiInfo.GetInfoPublic) // 获取公告列表 } } ================================================ FILE: server/plugin/announcement/service/enter.go ================================================ package service var Service = new(service) type service struct{ Info info } ================================================ FILE: server/plugin/announcement/service/info.go ================================================ package service import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/model" "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/model/request" ) var Info = new(info) type info struct{} // CreateInfo 创建公告记录 // Author [piexlmax](https://github.com/piexlmax) func (s *info) CreateInfo(info *model.Info) (err error) { err = global.GVA_DB.Create(info).Error return err } // DeleteInfo 删除公告记录 // Author [piexlmax](https://github.com/piexlmax) func (s *info) DeleteInfo(ID string) (err error) { err = global.GVA_DB.Delete(&model.Info{}, "id = ?", ID).Error return err } // DeleteInfoByIds 批量删除公告记录 // Author [piexlmax](https://github.com/piexlmax) func (s *info) DeleteInfoByIds(IDs []string) (err error) { err = global.GVA_DB.Delete(&[]model.Info{}, "id in ?", IDs).Error return err } // UpdateInfo 更新公告记录 // Author [piexlmax](https://github.com/piexlmax) func (s *info) UpdateInfo(info model.Info) (err error) { err = global.GVA_DB.Model(&model.Info{}).Where("id = ?", info.ID).Updates(&info).Error return err } // GetInfo 根据ID获取公告记录 // Author [piexlmax](https://github.com/piexlmax) func (s *info) GetInfo(ID string) (info model.Info, err error) { err = global.GVA_DB.Where("id = ?", ID).First(&info).Error return } // GetInfoInfoList 分页获取公告记录 // Author [piexlmax](https://github.com/piexlmax) func (s *info) GetInfoInfoList(info request.InfoSearch) (list []model.Info, total int64, err error) { limit := info.PageSize offset := info.PageSize * (info.Page - 1) // 创建db db := global.GVA_DB.Model(&model.Info{}) var infos []model.Info // 如果有条件搜索 下方会自动创建搜索语句 if info.StartCreatedAt != nil && info.EndCreatedAt != nil { db = db.Where("created_at BETWEEN ? AND ?", info.StartCreatedAt, info.EndCreatedAt) } err = db.Count(&total).Error if err != nil { return } if limit != 0 { db = db.Limit(limit).Offset(offset) } err = db.Find(&infos).Error return infos, total, err } func (s *info) GetInfoDataSource() (res map[string][]map[string]any, err error) { res = make(map[string][]map[string]any) userID := make([]map[string]any, 0) global.GVA_DB.Table("sys_users").Select("nick_name as label,id as value").Scan(&userID) res["userID"] = userID return } ================================================ FILE: server/plugin/email/README.MD ================================================ ## 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"` // 邮件内容 } ================================================ FILE: server/plugin/email/api/enter.go ================================================ package api type ApiGroup struct { EmailApi } var ApiGroupApp = new(ApiGroup) ================================================ FILE: server/plugin/email/api/sys_email.go ================================================ package api import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" email_response "github.com/flipped-aurora/gin-vue-admin/server/plugin/email/model/response" "github.com/flipped-aurora/gin-vue-admin/server/plugin/email/service" "github.com/gin-gonic/gin" "go.uber.org/zap" ) type EmailApi struct{} // EmailTest // @Tags System // @Summary 发送测试邮件 // @Security ApiKeyAuth // @Produce application/json // @Success 200 {string} string "{"success":true,"data":{},"msg":"发送成功"}" // @Router /email/emailTest [post] func (s *EmailApi) EmailTest(c *gin.Context) { err := service.ServiceGroupApp.EmailTest() if err != nil { global.GVA_LOG.Error("发送失败!", zap.Error(err)) response.FailWithMessage("发送失败", c) return } response.OkWithMessage("发送成功", c) } // SendEmail // @Tags System // @Summary 发送邮件 // @Security ApiKeyAuth // @Produce application/json // @Param data body email_response.Email true "发送邮件必须的参数" // @Success 200 {string} string "{"success":true,"data":{},"msg":"发送成功"}" // @Router /email/sendEmail [post] func (s *EmailApi) SendEmail(c *gin.Context) { var email email_response.Email err := c.ShouldBindJSON(&email) if err != nil { response.FailWithMessage(err.Error(), c) return } err = service.ServiceGroupApp.SendEmail(email.To, email.Subject, email.Body) if err != nil { global.GVA_LOG.Error("发送失败!", zap.Error(err)) response.FailWithMessage("发送失败", c) return } response.OkWithMessage("发送成功", c) } ================================================ FILE: server/plugin/email/config/email.go ================================================ 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认证 } ================================================ FILE: server/plugin/email/global/gloabl.go ================================================ package global import "github.com/flipped-aurora/gin-vue-admin/server/plugin/email/config" var GlobalConfig = new(config.Email) ================================================ FILE: server/plugin/email/main.go ================================================ package email import ( "github.com/flipped-aurora/gin-vue-admin/server/plugin/email/global" "github.com/flipped-aurora/gin-vue-admin/server/plugin/email/router" "github.com/gin-gonic/gin" ) type emailPlugin struct{} func CreateEmailPlug(To, From, Host, Secret, Nickname string, Port int, IsSSL bool, 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" } ================================================ FILE: server/plugin/email/model/response/email.go ================================================ package response type Email struct { To string `json:"to"` // 邮件发送给谁 Subject string `json:"subject"` // 邮件标题 Body string `json:"body"` // 邮件内容 } ================================================ FILE: server/plugin/email/router/enter.go ================================================ package router type RouterGroup struct { EmailRouter } var RouterGroupApp = new(RouterGroup) ================================================ FILE: server/plugin/email/router/sys_email.go ================================================ package router import ( "github.com/flipped-aurora/gin-vue-admin/server/middleware" "github.com/flipped-aurora/gin-vue-admin/server/plugin/email/api" "github.com/gin-gonic/gin" ) type EmailRouter struct{} func (s *EmailRouter) InitEmailRouter(Router *gin.RouterGroup) { emailRouter := Router.Use(middleware.OperationRecord()) EmailApi := api.ApiGroupApp.EmailApi.EmailTest SendEmail := api.ApiGroupApp.EmailApi.SendEmail { emailRouter.POST("emailTest", EmailApi) // 发送测试邮件 emailRouter.POST("sendEmail", SendEmail) // 发送邮件 } } ================================================ FILE: server/plugin/email/service/enter.go ================================================ package service type ServiceGroup struct { EmailService } var ServiceGroupApp = new(ServiceGroup) ================================================ FILE: server/plugin/email/service/sys_email.go ================================================ package service import ( "github.com/flipped-aurora/gin-vue-admin/server/plugin/email/utils" ) type EmailService struct{} //@author: [maplepie](https://github.com/maplepie) //@function: EmailTest //@description: 发送邮件测试 //@return: err error func (e *EmailService) EmailTest() (err error) { subject := "test" body := "test" err = utils.EmailTest(subject, body) return err } //@author: [maplepie](https://github.com/maplepie) //@function: EmailTest //@description: 发送邮件测试 //@return: err error //@params to string 收件人 //@params subject string 标题(主题) //@params body string 邮件内容 func (e *EmailService) SendEmail(to, subject, body string) (err error) { err = utils.Email(to, subject, body) return err } ================================================ FILE: server/plugin/email/utils/email.go ================================================ package utils import ( "crypto/tls" "fmt" "net/smtp" "strings" "github.com/flipped-aurora/gin-vue-admin/server/plugin/email/global" "github.com/jordan-wright/email" ) //@author: [maplepie](https://github.com/maplepie) //@function: Email //@description: Email发送方法 //@param: subject string, body string //@return: error func Email(To, subject string, body string) error { to := strings.Split(To, ",") return send(to, subject, body) } //@author: [SliverHorn](https://github.com/SliverHorn) //@function: ErrorToEmail //@description: 给email中间件错误发送邮件到指定邮箱 //@param: subject string, body string //@return: error func ErrorToEmail(subject string, body string) error { to := strings.Split(global.GlobalConfig.To, ",") if to[len(to)-1] == "" { // 判断切片的最后一个元素是否为空,为空则移除 to = to[:len(to)-1] } return send(to, subject, body) } //@author: [maplepie](https://github.com/maplepie) //@function: EmailTest //@description: Email测试方法 //@param: subject string, body string //@return: error func EmailTest(subject string, body string) error { to := []string{global.GlobalConfig.To} return send(to, subject, body) } //@author: [maplepie](https://github.com/maplepie) //@function: send //@description: Email发送方法 //@param: subject string, body string //@return: error func send(to []string, subject string, body string) error { from := global.GlobalConfig.From nickname := global.GlobalConfig.Nickname secret := global.GlobalConfig.Secret host := global.GlobalConfig.Host port := global.GlobalConfig.Port isSSL := global.GlobalConfig.IsSSL 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 } ================================================ FILE: server/plugin/plugin-tool/utils/check.go ================================================ package utils import ( "github.com/pkg/errors" "go.uber.org/zap" "gorm.io/gorm" "path/filepath" "runtime" "strings" "sync" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/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] } ================================================ FILE: server/plugin/register.go ================================================ package plugin import ( _ "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement" ) ================================================ FILE: server/resource/function/api.go.tpl ================================================ {{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}} ================================================ FILE: server/resource/function/api.js.tpl ================================================ {{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 -}} ================================================ FILE: server/resource/function/server.go.tpl ================================================ {{- $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}} ================================================ FILE: server/resource/mcp/tools.tpl ================================================ 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}} ) } ================================================ FILE: server/resource/package/readme.txt.tpl ================================================ 代码解压后把fe的api文件内容粘贴进前端api文件夹下并修改为自己想要的名字即可 后端代码解压后同理,放到自己想要的 mvc对应路径 并且到 initRouter中注册自动生成的路由 到registerTable中注册自动生成的model 项目github:"https://github.com/piexlmax/github.com/flipped-aurora/gin-vue-admin/server" 希望大家给个star多多鼓励 ================================================ FILE: server/resource/package/server/api/api.go.tpl ================================================ 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) } ================================================ FILE: server/resource/package/server/api/enter.go.tpl ================================================ package {{ .Package }} type ApiGroup struct { } ================================================ FILE: server/resource/package/server/model/model.go.tpl ================================================ {{- 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 }} ================================================ FILE: server/resource/package/server/model/request/request.go.tpl ================================================ {{- 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 }} ================================================ FILE: server/resource/package/server/router/enter.go.tpl ================================================ package {{ .Package }} type RouterGroup struct { } ================================================ FILE: server/resource/package/server/router/router.go.tpl ================================================ 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 }} } ================================================ FILE: server/resource/package/server/service/enter.go.tpl ================================================ package {{ .Package }} type ServiceGroup struct { } ================================================ FILE: server/resource/package/server/service/service.go.tpl ================================================ {{- $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 }} ================================================ FILE: server/resource/package/web/api/api.js.tpl ================================================ 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', }) } ================================================ FILE: server/resource/package/web/view/form.vue.tpl ================================================ {{- 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 }} {{- else }} {{- end }} {{- end }} ================================================ FILE: server/resource/package/web/view/table.vue.tpl ================================================ {{- $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}} {{- else}} {{- end }} {{- end }} ================================================ FILE: server/resource/plugin/server/api/api.go.tpl ================================================ 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) } ================================================ FILE: server/resource/plugin/server/api/enter.go.tpl ================================================ package api var Api = new(api) type api struct { } ================================================ FILE: server/resource/plugin/server/config/config.go.tpl ================================================ package config type Config struct { } ================================================ FILE: server/resource/plugin/server/gen/gen.go.tpl ================================================ 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() } ================================================ FILE: server/resource/plugin/server/initialize/api.go.tpl ================================================ package initialize import ( "context" model "{{.Module}}/model/system" "{{.Module}}/plugin/plugin-tool/utils" ) func Api(ctx context.Context) { entities := []model.SysApi{} utils.RegisterApis(entities...) } ================================================ FILE: server/resource/plugin/server/initialize/dictionary.go.tpl ================================================ package initialize import ( "context" model "{{.Module}}/model/system" "{{.Module}}/plugin/plugin-tool/utils" ) func Dictionary(ctx context.Context) { entities := []model.SysDictionary{} utils.RegisterDictionaries(entities...) } ================================================ FILE: server/resource/plugin/server/initialize/gorm.go.tpl ================================================ 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)) } } ================================================ FILE: server/resource/plugin/server/initialize/menu.go.tpl ================================================ package initialize import ( "context" model "{{.Module}}/model/system" "{{.Module}}/plugin/plugin-tool/utils" ) func Menu(ctx context.Context) { entities := []model.SysBaseMenu{} utils.RegisterMenus(entities...) } ================================================ FILE: server/resource/plugin/server/initialize/router.go.tpl ================================================ 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()) } ================================================ FILE: server/resource/plugin/server/initialize/viper.go.tpl ================================================ 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)) } } ================================================ FILE: server/resource/plugin/server/model/model.go.tpl ================================================ {{- 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 }} ================================================ FILE: server/resource/plugin/server/model/request/request.go.tpl ================================================ {{- 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 }} ================================================ FILE: server/resource/plugin/server/plugin/plugin.go.tpl ================================================ package plugin import "{{.Module}}/plugin/{{ .Package }}/config" var Config config.Config ================================================ FILE: server/resource/plugin/server/plugin.go.tpl ================================================ 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) } ================================================ FILE: server/resource/plugin/server/router/enter.go.tpl ================================================ package router var Router = new(router) type router struct { } ================================================ FILE: server/resource/plugin/server/router/router.go.tpl ================================================ 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}} } ================================================ FILE: server/resource/plugin/server/service/enter.go.tpl ================================================ package service var Service = new(service) type service struct { } ================================================ FILE: server/resource/plugin/server/service/service.go.tpl ================================================ {{- $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 }} ================================================ FILE: server/resource/plugin/web/api/api.js.tpl ================================================ 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', }) } ================================================ FILE: server/resource/plugin/web/form/form.vue.tpl ================================================ {{- if .IsAdd }} // 新增表单中增加如下代码 {{- range .Fields}} {{- if .Form}} {{- if .CheckDataSource}} {{- else }} {{- if eq .FieldType "bool" }} {{- end }} {{- if eq .FieldType "string" }} {{- if .DictType}} {{- else }} {{- end }} {{- end }} {{- if eq .FieldType "richtext" }} {{- end }} {{- if eq .FieldType "json" }} // 此字段为json结构,可以前端自行控制展示和数据绑定模式 需绑定json的key为 formData.{{.FieldJson}} 后端会按照json的类型进行存取 {{"{{"}} formData.{{.FieldJson}} {{"}}"}} {{- end }} {{- if eq .FieldType "array" }} {{- end }} {{- if eq .FieldType "int" }} {{- end }} {{- if eq .FieldType "time.Time" }} {{- end }} {{- if eq .FieldType "float64" }} {{- end }} {{- if eq .FieldType "enum" }} {{- end }} {{- if eq .FieldType "picture" }} {{- end }} {{- if eq .FieldType "pictures" }} {{- end }} {{- if eq .FieldType "video" }} {{- end }} {{- if eq .FieldType "file" }} {{- end }} {{- end }} {{- end }} {{- end }} // 字典增加如下代码 {{- range $index, $element := .DictTypes}} const {{ $element }}Options = ref([]) {{- end }} // init方法中增加如下调用 {{- range $index, $element := .DictTypes }} {{ $element }}Options.value = await getDictFunc('{{$element}}') {{- end }} // 基础formData结构增加如下字段 {{- range .Fields}} {{- if .Form}} {{- if eq .FieldType "bool" }} {{.FieldJson}}: false, {{- end }} {{- if eq .FieldType "string" }} {{.FieldJson}}: '', {{- end }} {{- if eq .FieldType "richtext" }} {{.FieldJson}}: '', {{- end }} {{- if eq .FieldType "int" }} {{.FieldJson}}: {{- if or .DataSource}} undefined{{ else }} 0{{- end }}, {{- end }} {{- if eq .FieldType "time.Time" }} {{.FieldJson}}: new Date(), {{- end }} {{- if eq .FieldType "float64" }} {{.FieldJson}}: 0, {{- end }} {{- if eq .FieldType "picture" }} {{.FieldJson}}: "", {{- end }} {{- if eq .FieldType "video" }} {{.FieldJson}}: "", {{- end }} {{- if eq .FieldType "pictures" }} {{.FieldJson}}: [], {{- end }} {{- if eq .FieldType "file" }} {{.FieldJson}}: [], {{- end }} {{- if eq .FieldType "json" }} {{.FieldJson}}: {}, {{- end }} {{- if eq .FieldType "array" }} {{.FieldJson}}: [], {{- end }} {{- end }} {{- end }} // 验证规则中增加如下字段 {{- range .Fields }} {{- if .Form }} {{- if eq .Require true }} {{.FieldJson }} : [{ required: true, message: '{{ .ErrorText }}', trigger: ['input','blur'], }, {{- if eq .FieldType "string" }} { whitespace: true, message: '不能只输入空格', trigger: ['input', 'blur'], } {{- end }} ], {{- end }} {{- end }} {{- end }} {{- if .HasDataSource }} // 请引用 get{{.StructName}}DataSource, // 获取数据源 const dataSource = ref([]) const getDataSourceFunc = async()=>{ const res = await get{{.StructName}}DataSource() if (res.code === 0) { dataSource.value = res.data } } getDataSourceFunc() {{- end }} {{- else }} {{- if not .OnlyTemplate }} {{- else }} {{- end }} {{- end }} ================================================ FILE: server/resource/plugin/web/view/view.vue.tpl ================================================ {{- $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}} {{- else}} {{- end }} {{- end }} ================================================ FILE: server/router/enter.go ================================================ package router import ( "github.com/flipped-aurora/gin-vue-admin/server/router/example" "github.com/flipped-aurora/gin-vue-admin/server/router/system" ) var RouterGroupApp = new(RouterGroup) type RouterGroup struct { System system.RouterGroup Example example.RouterGroup } ================================================ FILE: server/router/example/enter.go ================================================ package example import ( api "github.com/flipped-aurora/gin-vue-admin/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 ) ================================================ FILE: server/router/example/exa_attachment_category.go ================================================ 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) // 删除分类 } } ================================================ FILE: server/router/example/exa_customer.go ================================================ package example import ( "github.com/flipped-aurora/gin-vue-admin/server/middleware" "github.com/gin-gonic/gin" ) type CustomerRouter struct{} func (e *CustomerRouter) InitCustomerRouter(Router *gin.RouterGroup) { customerRouter := Router.Group("customer").Use(middleware.OperationRecord()) customerRouterWithoutRecord := Router.Group("customer") { customerRouter.POST("customer", exaCustomerApi.CreateExaCustomer) // 创建客户 customerRouter.PUT("customer", exaCustomerApi.UpdateExaCustomer) // 更新客户 customerRouter.DELETE("customer", exaCustomerApi.DeleteExaCustomer) // 删除客户 } { customerRouterWithoutRecord.GET("customer", exaCustomerApi.GetExaCustomer) // 获取单一客户信息 customerRouterWithoutRecord.GET("customerList", exaCustomerApi.GetExaCustomerList) // 获取客户列表 } } ================================================ FILE: server/router/example/exa_file_upload_and_download.go ================================================ 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 } } ================================================ FILE: server/router/system/enter.go ================================================ package system import api "github.com/flipped-aurora/gin-vue-admin/server/api/v1" type RouterGroup struct { ApiRouter JwtRouter SysRouter BaseRouter InitRouter MenuRouter UserRouter CasbinRouter AutoCodeRouter AuthorityRouter DictionaryRouter OperationRecordRouter DictionaryDetailRouter AuthorityBtnRouter SysExportTemplateRouter SysParamsRouter 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 ) ================================================ FILE: server/router/system/sys_api.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/middleware" "github.com/gin-gonic/gin" ) type ApiRouter struct{} func (s *ApiRouter) InitApiRouter(Router *gin.RouterGroup, RouterPub *gin.RouterGroup) { apiRouter := Router.Group("api").Use(middleware.OperationRecord()) apiRouterWithoutRecord := Router.Group("api") apiPublicRouterWithoutRecord := RouterPub.Group("api") { apiRouter.GET("getApiGroups", apiRouterApi.GetApiGroups) // 获取路由组 apiRouter.GET("syncApi", apiRouterApi.SyncApi) // 同步Api apiRouter.POST("ignoreApi", apiRouterApi.IgnoreApi) // 忽略Api apiRouter.POST("enterSyncApi", apiRouterApi.EnterSyncApi) // 确认同步Api apiRouter.POST("createApi", apiRouterApi.CreateApi) // 创建Api apiRouter.POST("deleteApi", apiRouterApi.DeleteApi) // 删除Api apiRouter.POST("getApiById", apiRouterApi.GetApiById) // 获取单条Api消息 apiRouter.POST("updateApi", apiRouterApi.UpdateApi) // 更新api apiRouter.DELETE("deleteApisByIds", apiRouterApi.DeleteApisByIds) // 删除选中api apiRouter.POST("setApiRoles", apiRouterApi.SetApiRoles) // 全量覆盖API关联角色 } { apiRouterWithoutRecord.POST("getAllApis", apiRouterApi.GetAllApis) // 获取所有api apiRouterWithoutRecord.POST("getApiList", apiRouterApi.GetApiList) // 获取Api列表 apiRouterWithoutRecord.GET("getApiRoles", apiRouterApi.GetApiRoles) // 获取API关联角色ID列表 } { apiPublicRouterWithoutRecord.GET("freshCasbin", apiRouterApi.FreshCasbin) // 刷新casbin权限 } } ================================================ FILE: server/router/system/sys_api_token.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/api/v1" "github.com/flipped-aurora/gin-vue-admin/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 } } ================================================ FILE: server/router/system/sys_authority.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/middleware" "github.com/gin-gonic/gin" ) type AuthorityRouter struct{} func (s *AuthorityRouter) InitAuthorityRouter(Router *gin.RouterGroup) { authorityRouter := Router.Group("authority").Use(middleware.OperationRecord()) authorityRouterWithoutRecord := Router.Group("authority") { authorityRouter.POST("createAuthority", authorityApi.CreateAuthority) // 创建角色 authorityRouter.POST("deleteAuthority", authorityApi.DeleteAuthority) // 删除角色 authorityRouter.PUT("updateAuthority", authorityApi.UpdateAuthority) // 更新角色 authorityRouter.POST("copyAuthority", authorityApi.CopyAuthority) // 拷贝角色 authorityRouter.POST("setDataAuthority", authorityApi.SetDataAuthority) // 设置角色资源权限 authorityRouter.POST("setRoleUsers", authorityApi.SetRoleUsers) // 全量覆盖角色关联用户 } { authorityRouterWithoutRecord.POST("getAuthorityList", authorityApi.GetAuthorityList) // 获取角色列表 authorityRouterWithoutRecord.GET("getUsersByAuthority", authorityApi.GetUsersByAuthority) // 获取角色关联用户ID列表 } } ================================================ FILE: server/router/system/sys_authority_btn.go ================================================ 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) } } ================================================ FILE: server/router/system/sys_auto_code.go ================================================ 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) // 同步插件字典 } } ================================================ FILE: server/router/system/sys_auto_code_history.go ================================================ 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) // 获取回滚记录分页 } } ================================================ FILE: server/router/system/sys_base.go ================================================ 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 } ================================================ FILE: server/router/system/sys_casbin.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/middleware" "github.com/gin-gonic/gin" ) type CasbinRouter struct{} func (s *CasbinRouter) InitCasbinRouter(Router *gin.RouterGroup) { casbinRouter := Router.Group("casbin").Use(middleware.OperationRecord()) casbinRouterWithoutRecord := Router.Group("casbin") { casbinRouter.POST("updateCasbin", casbinApi.UpdateCasbin) } { casbinRouterWithoutRecord.POST("getPolicyPathByAuthorityId", casbinApi.GetPolicyPathByAuthorityId) } } ================================================ FILE: server/router/system/sys_dictionary.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/middleware" "github.com/gin-gonic/gin" ) type DictionaryRouter struct{} func (s *DictionaryRouter) InitSysDictionaryRouter(Router *gin.RouterGroup) { sysDictionaryRouter := Router.Group("sysDictionary").Use(middleware.OperationRecord()) sysDictionaryRouterWithoutRecord := Router.Group("sysDictionary") { sysDictionaryRouter.POST("createSysDictionary", dictionaryApi.CreateSysDictionary) // 新建SysDictionary sysDictionaryRouter.DELETE("deleteSysDictionary", dictionaryApi.DeleteSysDictionary) // 删除SysDictionary sysDictionaryRouter.PUT("updateSysDictionary", dictionaryApi.UpdateSysDictionary) // 更新SysDictionary 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列表 } } ================================================ FILE: server/router/system/sys_dictionary_detail.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/middleware" "github.com/gin-gonic/gin" ) type DictionaryDetailRouter struct{} func (s *DictionaryDetailRouter) InitSysDictionaryDetailRouter(Router *gin.RouterGroup) { dictionaryDetailRouter := Router.Group("sysDictionaryDetail").Use(middleware.OperationRecord()) dictionaryDetailRouterWithoutRecord := Router.Group("sysDictionaryDetail") { dictionaryDetailRouter.POST("createSysDictionaryDetail", dictionaryDetailApi.CreateSysDictionaryDetail) // 新建SysDictionaryDetail dictionaryDetailRouter.DELETE("deleteSysDictionaryDetail", dictionaryDetailApi.DeleteSysDictionaryDetail) // 删除SysDictionaryDetail dictionaryDetailRouter.PUT("updateSysDictionaryDetail", dictionaryDetailApi.UpdateSysDictionaryDetail) // 更新SysDictionaryDetail } { dictionaryDetailRouterWithoutRecord.GET("findSysDictionaryDetail", dictionaryDetailApi.FindSysDictionaryDetail) // 根据ID获取SysDictionaryDetail dictionaryDetailRouterWithoutRecord.GET("getSysDictionaryDetailList", dictionaryDetailApi.GetSysDictionaryDetailList) // 获取SysDictionaryDetail列表 dictionaryDetailRouterWithoutRecord.GET("getDictionaryTreeList", dictionaryDetailApi.GetDictionaryTreeList) // 获取字典详情树形结构 dictionaryDetailRouterWithoutRecord.GET("getDictionaryTreeListByType", dictionaryDetailApi.GetDictionaryTreeListByType) // 根据字典类型获取字典详情树形结构 dictionaryDetailRouterWithoutRecord.GET("getDictionaryDetailsByParent", dictionaryDetailApi.GetDictionaryDetailsByParent) // 根据父级ID获取字典详情 dictionaryDetailRouterWithoutRecord.GET("getDictionaryPath", dictionaryDetailApi.GetDictionaryPath) // 获取字典详情的完整路径 } } ================================================ FILE: server/router/system/sys_error.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/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) // 新建错误日志 } } ================================================ FILE: server/router/system/sys_export_template.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/middleware" "github.com/gin-gonic/gin" ) type SysExportTemplateRouter struct { } // InitSysExportTemplateRouter 初始化 导出模板 路由信息 func (s *SysExportTemplateRouter) InitSysExportTemplateRouter(Router *gin.RouterGroup, 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导出模板 } } ================================================ FILE: server/router/system/sys_initdb.go ================================================ 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) // 检测是否需要初始化数据库 } } ================================================ FILE: server/router/system/sys_jwt.go ================================================ 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加入黑名单 } } ================================================ FILE: server/router/system/sys_login_log.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/api/v1" "github.com/flipped-aurora/gin-vue-admin/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) // 获取登录日志列表 } } ================================================ FILE: server/router/system/sys_menu.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/middleware" "github.com/gin-gonic/gin" ) type MenuRouter struct{} func (s *MenuRouter) InitMenuRouter(Router *gin.RouterGroup) (R gin.IRoutes) { menuRouter := Router.Group("menu").Use(middleware.OperationRecord()) menuRouterWithoutRecord := Router.Group("menu") { menuRouter.POST("addBaseMenu", authorityMenuApi.AddBaseMenu) // 新增菜单 menuRouter.POST("addMenuAuthority", authorityMenuApi.AddMenuAuthority) // 增加menu和角色关联关系 menuRouter.POST("deleteBaseMenu", authorityMenuApi.DeleteBaseMenu) // 删除菜单 menuRouter.POST("updateBaseMenu", authorityMenuApi.UpdateBaseMenu) // 更新菜单 menuRouter.POST("setMenuRoles", authorityMenuApi.SetMenuRoles) // 全量覆盖菜单关联角色 } { 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获取菜单 menuRouterWithoutRecord.GET("getMenuRoles", authorityMenuApi.GetMenuRoles) // 获取菜单关联角色ID列表 } return menuRouter } ================================================ FILE: server/router/system/sys_operation_record.go ================================================ 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列表 } } ================================================ FILE: server/router/system/sys_params.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/middleware" "github.com/gin-gonic/gin" ) type SysParamsRouter struct{} // InitSysParamsRouter 初始化 参数 路由信息 func (s *SysParamsRouter) InitSysParamsRouter(Router *gin.RouterGroup, PublicRouter *gin.RouterGroup) { sysParamsRouter := Router.Group("sysParams").Use(middleware.OperationRecord()) sysParamsRouterWithoutRecord := Router.Group("sysParams") { sysParamsRouter.POST("createSysParams", sysParamsApi.CreateSysParams) // 新建参数 sysParamsRouter.DELETE("deleteSysParams", sysParamsApi.DeleteSysParams) // 删除参数 sysParamsRouter.DELETE("deleteSysParamsByIds", sysParamsApi.DeleteSysParamsByIds) // 批量删除参数 sysParamsRouter.PUT("updateSysParams", sysParamsApi.UpdateSysParams) // 更新参数 } { sysParamsRouterWithoutRecord.GET("findSysParams", sysParamsApi.FindSysParams) // 根据ID获取参数 sysParamsRouterWithoutRecord.GET("getSysParamsList", sysParamsApi.GetSysParamsList) // 获取参数列表 sysParamsRouterWithoutRecord.GET("getSysParam", sysParamsApi.GetSysParam) // 根据Key获取参数 } } ================================================ FILE: server/router/system/sys_skills.go ================================================ package system import "github.com/gin-gonic/gin" type SkillsRouter struct{} func (s *SkillsRouter) InitSkillsRouter(Router *gin.RouterGroup, pubRouter *gin.RouterGroup) { skillsRouter := Router.Group("skills") skillsRouterPub := pubRouter.Group("skills") { skillsRouter.GET("getTools", skillsApi.GetTools) skillsRouter.POST("getSkillList", skillsApi.GetSkillList) skillsRouter.POST("getSkillDetail", skillsApi.GetSkillDetail) skillsRouter.POST("saveSkill", skillsApi.SaveSkill) skillsRouter.POST("deleteSkill", skillsApi.DeleteSkill) 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) skillsRouter.POST("packageSkill", skillsApi.PackageSkill) } { skillsRouterPub.POST("downloadOnlineSkill", skillsApi.DownloadOnlineSkill) } } ================================================ FILE: server/router/system/sys_system.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/middleware" "github.com/gin-gonic/gin" ) type SysRouter struct{} func (s *SysRouter) InitSystemRouter(Router *gin.RouterGroup) { sysRouter := Router.Group("system").Use(middleware.OperationRecord()) sysRouterWithoutRecord := Router.Group("system") { sysRouter.POST("setSystemConfig", systemApi.SetSystemConfig) // 设置配置文件内容 sysRouter.POST("reloadSystem", systemApi.ReloadSystem) // 重启服务 } { sysRouterWithoutRecord.POST("getSystemConfig", systemApi.GetSystemConfig) // 获取配置文件内容 sysRouterWithoutRecord.POST("getServerInfo", systemApi.GetServerInfo) // 获取服务器信息 } } ================================================ FILE: server/router/system/sys_user.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/middleware" "github.com/gin-gonic/gin" ) type UserRouter struct{} func (s *UserRouter) InitUserRouter(Router *gin.RouterGroup) { userRouter := Router.Group("user").Use(middleware.OperationRecord()) userRouterWithoutRecord := Router.Group("user") { userRouter.POST("admin_register", baseApi.Register) // 管理员注册账号 userRouter.POST("changePassword", baseApi.ChangePassword) // 用户修改密码 userRouter.POST("setUserAuthority", baseApi.SetUserAuthority) // 设置用户权限 userRouter.DELETE("deleteUser", baseApi.DeleteUser) // 删除用户 userRouter.PUT("setUserInfo", baseApi.SetUserInfo) // 设置用户信息 userRouter.PUT("setSelfInfo", baseApi.SetSelfInfo) // 设置自身信息 userRouter.POST("setUserAuthorities", baseApi.SetUserAuthorities) // 设置用户权限组 userRouter.POST("resetPassword", baseApi.ResetPassword) // 重置用户密码 userRouter.PUT("setSelfSetting", baseApi.SetSelfSetting) // 用户界面配置 } { userRouterWithoutRecord.POST("getUserList", baseApi.GetUserList) // 分页获取用户列表 userRouterWithoutRecord.GET("getUserInfo", baseApi.GetUserInfo) // 获取自身信息 } } ================================================ FILE: server/router/system/sys_version.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/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数据 } } ================================================ FILE: server/service/enter.go ================================================ package service import ( "github.com/flipped-aurora/gin-vue-admin/server/service/example" "github.com/flipped-aurora/gin-vue-admin/server/service/system" ) var ServiceGroupApp = new(ServiceGroup) type ServiceGroup struct { SystemServiceGroup system.ServiceGroup ExampleServiceGroup example.ServiceGroup } ================================================ FILE: server/service/example/enter.go ================================================ package example type ServiceGroup struct { CustomerService FileUploadAndDownloadService AttachmentCategoryService } ================================================ FILE: server/service/example/exa_attachment_category.go ================================================ package example import ( "errors" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/example" "gorm.io/gorm" ) type 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 } ================================================ FILE: server/service/example/exa_breakpoint_continue.go ================================================ package example import ( "errors" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/example" "gorm.io/gorm" ) type FileUploadAndDownloadService struct{} var FileUploadAndDownloadServiceApp = new(FileUploadAndDownloadService) //@author: [piexlmax](https://github.com/piexlmax) //@function: FindOrCreateFile //@description: 上传文件时检测当前文件属性,如果没有文件则创建,有则返回文件的当前切片 //@param: fileMd5 string, fileName string, chunkTotal int //@return: file model.ExaFile, err error func (e *FileUploadAndDownloadService) FindOrCreateFile(fileMd5 string, fileName string, chunkTotal int) (file example.ExaFile, err error) { var cfile example.ExaFile cfile.FileMd5 = fileMd5 cfile.FileName = fileName cfile.ChunkTotal = chunkTotal if errors.Is(global.GVA_DB.Where("file_md5 = ? AND 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 } ================================================ FILE: server/service/example/exa_customer.go ================================================ package example import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" "github.com/flipped-aurora/gin-vue-admin/server/model/example" "github.com/flipped-aurora/gin-vue-admin/server/model/system" systemService "github.com/flipped-aurora/gin-vue-admin/server/service/system" ) type CustomerService struct{} var CustomerServiceApp = new(CustomerService) //@author: [piexlmax](https://github.com/piexlmax) //@function: CreateExaCustomer //@description: 创建客户 //@param: e model.ExaCustomer //@return: err error func (exa *CustomerService) CreateExaCustomer(e example.ExaCustomer) (err error) { err = global.GVA_DB.Create(&e).Error return err } //@author: [piexlmax](https://github.com/piexlmax) //@function: DeleteFileChunk //@description: 删除客户 //@param: e model.ExaCustomer //@return: err error func (exa *CustomerService) DeleteExaCustomer(e example.ExaCustomer) (err error) { err = global.GVA_DB.Delete(&e).Error return err } //@author: [piexlmax](https://github.com/piexlmax) //@function: UpdateExaCustomer //@description: 更新客户 //@param: e *model.ExaCustomer //@return: err error func (exa *CustomerService) UpdateExaCustomer(e *example.ExaCustomer) (err error) { err = global.GVA_DB.Save(e).Error return err } //@author: [piexlmax](https://github.com/piexlmax) //@function: GetExaCustomer //@description: 获取客户信息 //@param: id uint //@return: customer model.ExaCustomer, err error func (exa *CustomerService) GetExaCustomer(id uint) (customer example.ExaCustomer, err error) { err = global.GVA_DB.Where("id = ?", id).First(&customer).Error return } //@author: [piexlmax](https://github.com/piexlmax) //@function: GetCustomerInfoList //@description: 分页获取客户列表 //@param: sysUserAuthorityID string, info request.PageInfo //@return: list interface{}, total int64, err error func (exa *CustomerService) GetCustomerInfoList(sysUserAuthorityID uint, info request.PageInfo) (list interface{}, total int64, err error) { limit := info.PageSize offset := info.PageSize * (info.Page - 1) db := global.GVA_DB.Model(&example.ExaCustomer{}) var a system.SysAuthority a.AuthorityId = sysUserAuthorityID auth, err := systemService.AuthorityServiceApp.GetAuthorityInfo(a) if err != nil { return } var dataId []uint for _, v := range auth.DataAuthorityId { dataId = append(dataId, v.AuthorityId) } var CustomerList []example.ExaCustomer err = db.Where("sys_user_authority_id in ?", dataId).Count(&total).Error if err != nil { return CustomerList, total, err } else { err = db.Limit(limit).Offset(offset).Preload("SysUser").Where("sys_user_authority_id in ?", dataId).Find(&CustomerList).Error } return CustomerList, total, err } ================================================ FILE: server/service/example/exa_file_upload_download.go ================================================ package example import ( "errors" "mime/multipart" "strings" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/example" "github.com/flipped-aurora/gin-vue-admin/server/model/example/request" "github.com/flipped-aurora/gin-vue-admin/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 } ================================================ FILE: server/service/system/auto_code_history.go ================================================ package system import ( "context" "encoding/json" "fmt" "github.com/flipped-aurora/gin-vue-admin/server/utils/ast" "github.com/pkg/errors" "path" "path/filepath" "strconv" "strings" "time" "github.com/flipped-aurora/gin-vue-admin/server/global" common "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" model "github.com/flipped-aurora/gin-vue-admin/server/model/system" request "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "github.com/flipped-aurora/gin-vue-admin/server/utils" "go.uber.org/zap" ) var AutocodeHistory = new(autoCodeHistory) type autoCodeHistory struct{} // Create 创建代码生成器历史记录 // Author [SliverHorn](https://github.com/SliverHorn) // Author [songzhibin97](https://github.com/songzhibin97) func (s *autoCodeHistory) Create(ctx context.Context, info request.SysAutoHistoryCreate) error { create := info.Create() err := global.GVA_DB.WithContext(ctx).Create(&create).Error if err != nil { return errors.Wrap(err, "创建失败!") } return nil } // First 根据id获取代码生成器历史的数据 // Author [SliverHorn](https://github.com/SliverHorn) // Author [songzhibin97](https://github.com/songzhibin97) func (s *autoCodeHistory) First(ctx context.Context, info common.GetById) (string, error) { var meta string err := global.GVA_DB.WithContext(ctx).Model(model.SysAutoCodeHistory{}).Where("id = ?", info.ID).Pluck("request", &meta).Error if err != nil { return "", errors.Wrap(err, "获取失败!") } return meta, nil } // Repeat 检测重复 // Author [SliverHorn](https://github.com/SliverHorn) // Author [songzhibin97](https://github.com/songzhibin97) func (s *autoCodeHistory) Repeat(businessDB, structName, 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 } } ================================================ FILE: server/service/system/auto_code_llm.go ================================================ package system import ( "context" "errors" "fmt" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common" commonResp "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" "github.com/flipped-aurora/gin-vue-admin/server/utils/request" "github.com/goccy/go-json" "io" "strings" ) // 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 } ================================================ FILE: server/service/system/auto_code_mcp.go ================================================ package system import ( "context" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/flipped-aurora/gin-vue-admin/server/utils/autocode" "os" "path/filepath" "text/template" ) 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 } ================================================ FILE: server/service/system/auto_code_package.go ================================================ package system import ( "context" "fmt" "go/token" "os" "path/filepath" "strings" "text/template" "github.com/flipped-aurora/gin-vue-admin/server/global" common "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" model "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/flipped-aurora/gin-vue-admin/server/utils/ast" "github.com/flipped-aurora/gin-vue-admin/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 } ================================================ FILE: server/service/system/auto_code_package_test.go ================================================ package system import ( "context" "reflect" "testing" model "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/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) } }) } } ================================================ FILE: server/service/system/auto_code_plugin.go ================================================ package system import ( "bytes" "context" "fmt" goast "go/ast" "go/parser" "go/printer" "go/token" "io" "mime/multipart" "os" "path/filepath" "strings" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" pluginUtils "github.com/flipped-aurora/gin-vue-admin/server/plugin/plugin-tool/utils" "github.com/flipped-aurora/gin-vue-admin/server/utils" ast "github.com/flipped-aurora/gin-vue-admin/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) } ================================================ FILE: server/service/system/auto_code_template.go ================================================ package system import ( "context" "encoding/json" "fmt" "github.com/flipped-aurora/gin-vue-admin/server/utils/autocode" "go/ast" "go/format" "go/parser" "go/token" "os" "path/filepath" "strings" "text/template" "github.com/flipped-aurora/gin-vue-admin/server/global" model "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" utilsAst "github.com/flipped-aurora/gin-vue-admin/server/utils/ast" "github.com/pkg/errors" "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 } ================================================ FILE: server/service/system/auto_code_template_test.go ================================================ package system import ( "context" "encoding/json" "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "reflect" "testing" ) func Test_autoCodeTemplate_Create(t *testing.T) { type args struct { ctx context.Context info request.AutoCode } tests := []struct { name string args args wantErr bool }{ // TODO: Add test cases. } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := &autoCodeTemplate{} if err := s.Create(tt.args.ctx, tt.args.info); (err != nil) != tt.wantErr { t.Errorf("Create() error = %v, wantErr %v", err, tt.wantErr) } }) } } func Test_autoCodeTemplate_Preview(t *testing.T) { type args struct { ctx context.Context info request.AutoCode } tests := []struct { name string args args want map[string]string wantErr bool }{ { name: "测试 package", args: args{ ctx: context.Background(), info: request.AutoCode{}, }, wantErr: false, }, { name: "测试 plugin", args: args{ ctx: context.Background(), info: request.AutoCode{}, }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { testJson := `{"structName":"SysUser","tableName":"sys_users","packageName":"sysUsers","package":"gva","abbreviation":"sysUsers","description":"sysUsers表","businessDB":"","autoCreateApiToSql":true,"autoCreateMenuToSql":true,"autoMigrate":true,"gvaModel":true,"autoCreateResource":false,"fields":[{"fieldName":"Uuid","fieldDesc":"用户UUID","fieldType":"string","dataType":"varchar","fieldJson":"uuid","primaryKey":false,"dataTypeLong":"191","columnName":"uuid","comment":"用户UUID","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"Username","fieldDesc":"用户登录名","fieldType":"string","dataType":"varchar","fieldJson":"username","primaryKey":false,"dataTypeLong":"191","columnName":"username","comment":"用户登录名","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"Password","fieldDesc":"用户登录密码","fieldType":"string","dataType":"varchar","fieldJson":"password","primaryKey":false,"dataTypeLong":"191","columnName":"password","comment":"用户登录密码","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"NickName","fieldDesc":"用户昵称","fieldType":"string","dataType":"varchar","fieldJson":"nickName","primaryKey":false,"dataTypeLong":"191","columnName":"nick_name","comment":"用户昵称","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"SideMode","fieldDesc":"用户侧边主题","fieldType":"string","dataType":"varchar","fieldJson":"sideMode","primaryKey":false,"dataTypeLong":"191","columnName":"side_mode","comment":"用户侧边主题","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"HeaderImg","fieldDesc":"用户头像","fieldType":"string","dataType":"varchar","fieldJson":"headerImg","primaryKey":false,"dataTypeLong":"191","columnName":"header_img","comment":"用户头像","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"BaseColor","fieldDesc":"基础颜色","fieldType":"string","dataType":"varchar","fieldJson":"baseColor","primaryKey":false,"dataTypeLong":"191","columnName":"base_color","comment":"基础颜色","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"AuthorityId","fieldDesc":"用户角色ID","fieldType":"int","dataType":"bigint","fieldJson":"authorityId","primaryKey":false,"dataTypeLong":"20","columnName":"authority_id","comment":"用户角色ID","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"Phone","fieldDesc":"用户手机号","fieldType":"string","dataType":"varchar","fieldJson":"phone","primaryKey":false,"dataTypeLong":"191","columnName":"phone","comment":"用户手机号","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"Email","fieldDesc":"用户邮箱","fieldType":"string","dataType":"varchar","fieldJson":"email","primaryKey":false,"dataTypeLong":"191","columnName":"email","comment":"用户邮箱","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"Enable","fieldDesc":"用户是否被冻结 1正常 2冻结","fieldType":"int","dataType":"bigint","fieldJson":"enable","primaryKey":false,"dataTypeLong":"19","columnName":"enable","comment":"用户是否被冻结 1正常 2冻结","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}}],"humpPackageName":"sys_users"}` err := json.Unmarshal([]byte(testJson), &tt.args.info) if err != nil { t.Error(err) return } err = tt.args.info.Pretreatment() if err != nil { t.Error(err) return } got, err := AutoCodeTemplate.Preview(tt.args.ctx, tt.args.info) if (err != nil) != tt.wantErr { t.Errorf("Preview() error = %+v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { t.Errorf("Preview() got = %v, want %v", got, tt.want) } }) } } ================================================ FILE: server/service/system/enter.go ================================================ 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 } ================================================ FILE: server/service/system/jwt_black_list.go ================================================ package system import ( "context" "go.uber.org/zap" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system" ) 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 中 } ================================================ FILE: server/service/system/sys_api.go ================================================ package system import ( "errors" "fmt" "strings" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" "github.com/flipped-aurora/gin-vue-admin/server/model/system" systemRes "github.com/flipped-aurora/gin-vue-admin/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 }) } ================================================ FILE: server/service/system/sys_api_token.go ================================================ package system import ( "errors" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system" sysReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/golang-jwt/jwt/v5" "time" ) 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 } ================================================ FILE: server/service/system/sys_authority.go ================================================ package system import ( "errors" "strconv" systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" "gorm.io/gorm" ) var ErrRoleExistence = errors.New("存在相同角色id") //@author: [piexlmax](https://github.com/piexlmax) //@function: CreateAuthority //@description: 创建一个角色 //@param: auth model.SysAuthority //@return: authority system.SysAuthority, err error type AuthorityService struct{} var AuthorityServiceApp = new(AuthorityService) func (authorityService *AuthorityService) CreateAuthority(auth system.SysAuthority) (authority system.SysAuthority, err error) { if err = global.GVA_DB.Where("authority_id = ?", auth.AuthorityId).First(&system.SysAuthority{}).Error; !errors.Is(err, gorm.ErrRecordNotFound) { return auth, ErrRoleExistence } e := global.GVA_DB.Transaction(func(tx *gorm.DB) error { if err = tx.Create(&auth).Error; err != nil { return err } auth.SysBaseMenus = systemReq.DefaultMenu() if err = tx.Model(&auth).Association("SysBaseMenus").Replace(&auth.SysBaseMenus); err != nil { return err } casbinInfos := systemReq.DefaultCasbin() authorityId := strconv.Itoa(int(auth.AuthorityId)) rules := [][]string{} for _, v := range casbinInfos { rules = append(rules, []string{authorityId, v.Path, v.Method}) } return CasbinServiceApp.AddPolicies(tx, rules) }) return auth, e } //@author: [piexlmax](https://github.com/piexlmax) //@function: CopyAuthority //@description: 复制一个角色 //@param: copyInfo response.SysAuthorityCopyResponse //@return: authority system.SysAuthority, err error func (authorityService *AuthorityService) CopyAuthority(adminAuthorityID uint, copyInfo response.SysAuthorityCopyResponse) (authority system.SysAuthority, err error) { var authorityBox system.SysAuthority if !errors.Is(global.GVA_DB.Where("authority_id = ?", copyInfo.Authority.AuthorityId).First(&authorityBox).Error, gorm.ErrRecordNotFound) { return authority, ErrRoleExistence } copyInfo.Authority.Children = []system.SysAuthority{} menus, err := MenuServiceApp.GetMenuAuthority(&request.GetAuthorityId{AuthorityId: copyInfo.OldAuthorityId}) if err != nil { return } var baseMenu []system.SysBaseMenu for _, v := range menus { intNum := v.MenuId v.SysBaseMenu.ID = uint(intNum) baseMenu = append(baseMenu, v.SysBaseMenu) } copyInfo.Authority.SysBaseMenus = baseMenu err = global.GVA_DB.Create(©Info.Authority).Error if err != nil { return } var btns []system.SysAuthorityBtn err = global.GVA_DB.Find(&btns, "authority_id = ?", copyInfo.OldAuthorityId).Error if err != nil { return } if len(btns) > 0 { for i := range btns { btns[i].AuthorityId = copyInfo.Authority.AuthorityId } err = global.GVA_DB.Create(&btns).Error if err != nil { return } } paths := CasbinServiceApp.GetPolicyPathByAuthorityId(copyInfo.OldAuthorityId) err = CasbinServiceApp.UpdateCasbin(adminAuthorityID, copyInfo.Authority.AuthorityId, paths) if err != nil { _ = authorityService.DeleteAuthority(©Info.Authority) } return copyInfo.Authority, err } //@author: [piexlmax](https://github.com/piexlmax) //@function: UpdateAuthority //@description: 更改一个角色 //@param: auth model.SysAuthority //@return: authority system.SysAuthority, err error func (authorityService *AuthorityService) UpdateAuthority(auth system.SysAuthority) (authority system.SysAuthority, err error) { var oldAuthority system.SysAuthority err = global.GVA_DB.Where("authority_id = ?", auth.AuthorityId).First(&oldAuthority).Error if err != nil { global.GVA_LOG.Debug(err.Error()) return system.SysAuthority{}, errors.New("查询角色数据失败") } err = global.GVA_DB.Model(&oldAuthority).Updates(&auth).Error return auth, err } //@author: [piexlmax](https://github.com/piexlmax) //@function: DeleteAuthority //@description: 删除角色 //@param: auth *model.SysAuthority //@return: err error func (authorityService *AuthorityService) DeleteAuthority(auth *system.SysAuthority) error { if errors.Is(global.GVA_DB.Debug().Preload("Users").First(&auth).Error, gorm.ErrRecordNotFound) { return errors.New("该角色不存在") } if len(auth.Users) != 0 { return errors.New("此角色有用户正在使用禁止删除") } if !errors.Is(global.GVA_DB.Where("authority_id = ?", auth.AuthorityId).First(&system.SysUser{}).Error, gorm.ErrRecordNotFound) { return errors.New("此角色有用户正在使用禁止删除") } if !errors.Is(global.GVA_DB.Where("parent_id = ?", auth.AuthorityId).First(&system.SysAuthority{}).Error, gorm.ErrRecordNotFound) { return errors.New("此角色存在子角色不允许删除") } return global.GVA_DB.Transaction(func(tx *gorm.DB) error { var err error if err = tx.Preload("SysBaseMenus").Preload("DataAuthorityId").Where("authority_id = ?", auth.AuthorityId).First(auth).Unscoped().Delete(auth).Error; err != nil { return err } if len(auth.SysBaseMenus) > 0 { if err = tx.Model(auth).Association("SysBaseMenus").Delete(auth.SysBaseMenus); err != nil { return err } // err = db.Association("SysBaseMenus").Delete(&auth) } if len(auth.DataAuthorityId) > 0 { if err = tx.Model(auth).Association("DataAuthorityId").Delete(auth.DataAuthorityId); err != nil { return err } } if err = tx.Delete(&system.SysUserAuthority{}, "sys_authority_authority_id = ?", auth.AuthorityId).Error; err != nil { return err } if err = tx.Where("authority_id = ?", auth.AuthorityId).Delete(&[]system.SysAuthorityBtn{}).Error; err != nil { return err } authorityId := strconv.Itoa(int(auth.AuthorityId)) if err = CasbinServiceApp.RemoveFilteredPolicy(tx, authorityId); err != nil { return err } return nil }) } //@author: [piexlmax](https://github.com/piexlmax) //@function: GetAuthorityInfoList //@description: 分页获取数据 //@param: info request.PageInfo //@return: list interface{}, total int64, err error func (authorityService *AuthorityService) GetAuthorityInfoList(authorityID uint) (list []system.SysAuthority, err error) { var authority system.SysAuthority err = global.GVA_DB.Where("authority_id = ?", authorityID).First(&authority).Error if err != nil { return nil, err } var authorities []system.SysAuthority db := global.GVA_DB.Model(&system.SysAuthority{}) if global.GVA_CONFIG.System.UseStrictAuth { // 当开启了严格树形结构后 if *authority.ParentId == 0 { // 只有顶级角色可以修改自己的权限和以下权限 err = db.Preload("DataAuthorityId").Where("authority_id = ?", authorityID).Find(&authorities).Error } else { // 非顶级角色只能修改以下权限 err = db.Debug().Preload("DataAuthorityId").Where("parent_id = ?", authorityID).Find(&authorities).Error } } else { err = db.Preload("DataAuthorityId").Where("parent_id = ?", "0").Find(&authorities).Error } for k := range authorities { err = authorityService.findChildrenAuthority(&authorities[k]) } return authorities, err } //@author: [piexlmax](https://github.com/piexlmax) //@function: GetAuthorityInfoList //@description: 分页获取数据 //@param: info request.PageInfo //@return: list interface{}, total int64, err error func (authorityService *AuthorityService) GetStructAuthorityList(authorityID uint) (list []uint, err error) { var auth system.SysAuthority _ = global.GVA_DB.First(&auth, "authority_id = ?", authorityID).Error var authorities []system.SysAuthority err = global.GVA_DB.Preload("DataAuthorityId").Where("parent_id = ?", authorityID).Find(&authorities).Error if len(authorities) > 0 { for k := range authorities { list = append(list, authorities[k].AuthorityId) 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 } // GetUserIdsByAuthorityId 获取拥有指定角色的所有用户ID func (authorityService *AuthorityService) GetUserIdsByAuthorityId(authorityId uint) (userIds []uint, err error) { var records []system.SysUserAuthority err = global.GVA_DB.Where("sys_authority_authority_id = ?", authorityId).Find(&records).Error if err != nil { return nil, err } for _, r := range records { userIds = append(userIds, r.SysUserId) } return userIds, nil } // SetRoleUsers 全量覆盖某角色关联的用户列表 // 入参:角色ID + 目标用户ID列表,保存时将该角色的关联关系完全替换为传入列表 func (authorityService *AuthorityService) SetRoleUsers(authorityId uint, userIds []uint) error { return global.GVA_DB.Transaction(func(tx *gorm.DB) error { // 1. 查出当前拥有该角色的所有用户ID var existingRecords []system.SysUserAuthority if err := tx.Where("sys_authority_authority_id = ?", authorityId).Find(&existingRecords).Error; err != nil { return err } currentSet := make(map[uint]struct{}) for _, r := range existingRecords { currentSet[r.SysUserId] = struct{}{} } targetSet := make(map[uint]struct{}) for _, id := range userIds { targetSet[id] = struct{}{} } // 2. 删除该角色所有已有的用户关联 if err := tx.Delete(&system.SysUserAuthority{}, "sys_authority_authority_id = ?", authorityId).Error; err != nil { return err } // 3. 对被移除的用户:若该角色是其主角色,则将主角色切换为其剩余的其他角色 for userId := range currentSet { if _, ok := targetSet[userId]; ok { continue // 仍在目标列表中,不处理 } var user system.SysUser if err := tx.First(&user, "id = ?", userId).Error; err != nil { continue } if user.AuthorityId == authorityId { // 从剩余关联(已删除当前角色后)中找另一个角色作为主角色 var another system.SysUserAuthority if err := tx.Where("sys_user_id = ?", userId).First(&another).Error; err != nil { // 没有其他角色,主角色保持不变,不做处理 continue } if err := tx.Model(&system.SysUser{}).Where("id = ?", userId). Update("authority_id", another.SysAuthorityAuthorityId).Error; err != nil { return err } } } // 4. 批量插入新的关联记录 if len(userIds) > 0 { newRecords := make([]system.SysUserAuthority, 0, len(userIds)) for _, userId := range userIds { newRecords = append(newRecords, system.SysUserAuthority{ SysUserId: userId, SysAuthorityAuthorityId: authorityId, }) } if err := tx.Create(&newRecords).Error; err != nil { return err } } return nil }) } ================================================ FILE: server/service/system/sys_authority_btn.go ================================================ package system import ( "errors" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" "gorm.io/gorm" ) type AuthorityBtnService struct{} var AuthorityBtnServiceApp = new(AuthorityBtnService) func (a *AuthorityBtnService) GetAuthorityBtn(req request.SysAuthorityBtnReq) (res response.SysAuthorityBtnRes, err error) { var authorityBtn []system.SysAuthorityBtn err = global.GVA_DB.Find(&authorityBtn, "authority_id = ? and sys_menu_id = ?", req.AuthorityId, req.MenuID).Error if err != nil { return } var selected []uint for _, v := range authorityBtn { selected = append(selected, v.SysBaseMenuBtnID) } res.Selected = selected return res, err } func (a *AuthorityBtnService) SetAuthorityBtn(req request.SysAuthorityBtnReq) (err error) { return global.GVA_DB.Transaction(func(tx *gorm.DB) error { var authorityBtn []system.SysAuthorityBtn err = tx.Delete(&[]system.SysAuthorityBtn{}, "authority_id = ? and sys_menu_id = ?", req.AuthorityId, req.MenuID).Error if err != nil { return err } for _, v := range req.Selected { authorityBtn = append(authorityBtn, system.SysAuthorityBtn{ AuthorityId: req.AuthorityId, SysMenuID: req.MenuID, SysBaseMenuBtnID: v, }) } if len(authorityBtn) > 0 { err = tx.Create(&authorityBtn).Error } if err != nil { return err } return err }) } func (a *AuthorityBtnService) CanRemoveAuthorityBtn(ID string) (err error) { fErr := global.GVA_DB.First(&system.SysAuthorityBtn{}, "sys_base_menu_btn_id = ?", ID).Error if errors.Is(fErr, gorm.ErrRecordNotFound) { return nil } return errors.New("此按钮正在被使用无法删除") } ================================================ FILE: server/service/system/sys_auto_code_interface.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" ) type AutoCodeService struct{} type Database interface { GetDB(businessDB string) (data []response.Db, err error) GetTables(businessDB string, dbName string) (data []response.Table, err error) GetColumn(businessDB string, tableName string, dbName string) (data []response.Column, err error) } func (autoCodeService *AutoCodeService) Database(businessDB string) Database { if businessDB == "" { switch global.GVA_CONFIG.System.DbType { case "mysql": return AutoCodeMysql case "pgsql": return AutoCodePgsql case "mssql": return AutoCodeMssql case "oracle": return AutoCodeOracle case "sqlite": return AutoCodeSqlite default: return AutoCodeMysql } } else { for _, info := range global.GVA_CONFIG.DBList { if info.AliasName == businessDB { switch info.Type { case "mysql": return AutoCodeMysql case "mssql": return AutoCodeMssql case "pgsql": return AutoCodePgsql case "oracle": return AutoCodeOracle case "sqlite": return AutoCodeSqlite default: return AutoCodeMysql } } } return AutoCodeMysql } } ================================================ FILE: server/service/system/sys_auto_code_mssql.go ================================================ package system import ( "fmt" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" ) var AutoCodeMssql = new(autoCodeMssql) type autoCodeMssql struct{} // GetDB 获取数据库的所有数据库名 // Author [piexlmax](https://github.com/piexlmax) // Author [SliverHorn](https://github.com/SliverHorn) func (s *autoCodeMssql) GetDB(businessDB string) (data []response.Db, err error) { var entities []response.Db sql := "select name AS 'database' from 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 } ================================================ FILE: server/service/system/sys_auto_code_mysql.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" ) var AutoCodeMysql = new(autoCodeMysql) type autoCodeMysql struct{} // GetDB 获取数据库的所有数据库名 // Author [piexlmax](https://github.com/piexlmax) // Author [SliverHorn](https://github.com/SliverHorn) func (s *autoCodeMysql) GetDB(businessDB string) (data []response.Db, err error) { var entities []response.Db sql := "SELECT SCHEMA_NAME AS `database` FROM INFORMATION_SCHEMA.SCHEMATA;" if businessDB == "" { err = global.GVA_DB.Raw(sql).Scan(&entities).Error } else { err = global.GVA_DBList[businessDB].Raw(sql).Scan(&entities).Error } return entities, err } // GetTables 获取数据库的所有表名 // Author [piexlmax](https://github.com/piexlmax) // Author [SliverHorn](https://github.com/SliverHorn) func (s *autoCodeMysql) GetTables(businessDB string, dbName string) (data []response.Table, err error) { var entities []response.Table sql := `select table_name as table_name from information_schema.tables where table_schema = ?` if businessDB == "" { err = global.GVA_DB.Raw(sql, dbName).Scan(&entities).Error } else { err = global.GVA_DBList[businessDB].Raw(sql, dbName).Scan(&entities).Error } return entities, err } // GetColumn 获取指定数据库和指定数据表的所有字段名,类型值等 // Author [piexlmax](https://github.com/piexlmax) // Author [SliverHorn](https://github.com/SliverHorn) func (s *autoCodeMysql) GetColumn(businessDB string, tableName string, dbName string) (data []response.Column, err error) { var entities []response.Column sql := ` SELECT c.COLUMN_NAME column_name, c.DATA_TYPE data_type, CASE c.DATA_TYPE WHEN 'longtext' THEN c.CHARACTER_MAXIMUM_LENGTH WHEN 'varchar' THEN c.CHARACTER_MAXIMUM_LENGTH WHEN 'double' THEN CONCAT_WS(',', c.NUMERIC_PRECISION, c.NUMERIC_SCALE) WHEN 'decimal' THEN CONCAT_WS(',', c.NUMERIC_PRECISION, c.NUMERIC_SCALE) WHEN 'int' THEN c.NUMERIC_PRECISION WHEN 'bigint' THEN c.NUMERIC_PRECISION ELSE '' END AS data_type_long, c.COLUMN_COMMENT column_comment, CASE WHEN kcu.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END AS primary_key, c.ORDINAL_POSITION FROM INFORMATION_SCHEMA.COLUMNS c LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu ON c.TABLE_SCHEMA = kcu.TABLE_SCHEMA AND c.TABLE_NAME = kcu.TABLE_NAME AND c.COLUMN_NAME = kcu.COLUMN_NAME AND kcu.CONSTRAINT_NAME = 'PRIMARY' WHERE c.TABLE_NAME = ? AND c.TABLE_SCHEMA = ? ORDER BY c.ORDINAL_POSITION;` if businessDB == "" { err = global.GVA_DB.Raw(sql, tableName, dbName).Scan(&entities).Error } else { err = global.GVA_DBList[businessDB].Raw(sql, tableName, dbName).Scan(&entities).Error } return entities, err } ================================================ FILE: server/service/system/sys_auto_code_oracle.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" ) var AutoCodeOracle = new(autoCodeOracle) type autoCodeOracle struct{} // GetDB 获取数据库的所有数据库名 // Author [piexlmax](https://github.com/piexlmax) // Author [SliverHorn](https://github.com/SliverHorn) func (s *autoCodeOracle) GetDB(businessDB string) (data []response.Db, err error) { var entities []response.Db sql := `SELECT lower(username) AS "database" FROM all_users` err = global.GVA_DBList[businessDB].Raw(sql).Scan(&entities).Error return entities, err } // GetTables 获取数据库的所有表名 // Author [piexlmax](https://github.com/piexlmax) // Author [SliverHorn](https://github.com/SliverHorn) func (s *autoCodeOracle) GetTables(businessDB string, dbName string) (data []response.Table, err error) { var entities []response.Table sql := `select lower(table_name) as "table_name" from all_tables where lower(owner) = ?` err = global.GVA_DBList[businessDB].Raw(sql, dbName).Scan(&entities).Error return entities, err } // GetColumn 获取指定数据库和指定数据表的所有字段名,类型值等 // Author [piexlmax](https://github.com/piexlmax) // Author [SliverHorn](https://github.com/SliverHorn) func (s *autoCodeOracle) GetColumn(businessDB string, tableName string, dbName string) (data []response.Column, err error) { var entities []response.Column sql := ` SELECT lower(a.COLUMN_NAME) as "column_name", (CASE WHEN a.DATA_TYPE = 'NUMBER' AND a.DATA_SCALE=0 THEN 'int' else lower(a.DATA_TYPE) end) as "data_type", (CASE WHEN a.DATA_TYPE = 'NUMBER' THEN a.DATA_PRECISION else a.DATA_LENGTH end) as "data_type_long", b.COMMENTS as "column_comment", (CASE WHEN pk.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END) as "primary_key", a.COLUMN_ID FROM all_tab_columns a JOIN all_col_comments b ON a.OWNER = b.OWNER AND a.TABLE_NAME = b.TABLE_NAME AND a.COLUMN_NAME = b.COLUMN_NAME LEFT JOIN ( SELECT acc.OWNER, acc.TABLE_NAME, acc.COLUMN_NAME FROM all_cons_columns acc JOIN all_constraints ac ON acc.OWNER = ac.OWNER AND acc.CONSTRAINT_NAME = ac.CONSTRAINT_NAME WHERE ac.CONSTRAINT_TYPE = 'P' ) pk ON a.OWNER = pk.OWNER AND a.TABLE_NAME = pk.TABLE_NAME AND a.COLUMN_NAME = pk.COLUMN_NAME WHERE lower(a.table_name) = ? AND lower(a.OWNER) = ? ORDER BY a.COLUMN_ID ` err = global.GVA_DBList[businessDB].Raw(sql, tableName, dbName).Scan(&entities).Error return entities, err } ================================================ FILE: server/service/system/sys_auto_code_pgsql.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" ) var AutoCodePgsql = new(autoCodePgsql) type autoCodePgsql struct{} // GetDB 获取数据库的所有数据库名 // Author [piexlmax](https://github.com/piexlmax) // Author [SliverHorn](https://github.com/SliverHorn) func (a *autoCodePgsql) GetDB(businessDB string) (data []response.Db, err error) { var entities []response.Db sql := `SELECT datname as database FROM pg_database WHERE datistemplate = false` if businessDB == "" { err = global.GVA_DB.Raw(sql).Scan(&entities).Error } else { err = global.GVA_DBList[businessDB].Raw(sql).Scan(&entities).Error } return entities, err } // GetTables 获取数据库的所有表名 // Author [piexlmax](https://github.com/piexlmax) // Author [SliverHorn](https://github.com/SliverHorn) func (a *autoCodePgsql) GetTables(businessDB string, dbName string) (data []response.Table, err error) { var entities []response.Table sql := `select table_name as table_name from information_schema.tables where table_catalog = ? and table_schema = ?` db := global.GVA_DB if businessDB != "" { db = global.GVA_DBList[businessDB] } err = db.Raw(sql, dbName, "public").Scan(&entities).Error return entities, err } // GetColumn 获取指定数据库和指定数据表的所有字段名,类型值等 // Author [piexlmax](https://github.com/piexlmax) // Author [SliverHorn](https://github.com/SliverHorn) func (a *autoCodePgsql) GetColumn(businessDB string, tableName string, dbName string) (data []response.Column, err error) { // todo 数据获取不全, 待完善sql sql := ` SELECT psc.COLUMN_NAME AS COLUMN_NAME, psc.udt_name AS data_type, CASE psc.udt_name WHEN 'text' THEN concat_ws ( '', '', psc.CHARACTER_MAXIMUM_LENGTH ) WHEN 'varchar' THEN concat_ws ( '', '', psc.CHARACTER_MAXIMUM_LENGTH ) WHEN 'smallint' THEN concat_ws ( ',', psc.NUMERIC_PRECISION, psc.NUMERIC_SCALE ) WHEN 'decimal' THEN concat_ws ( ',', psc.NUMERIC_PRECISION, psc.NUMERIC_SCALE ) WHEN 'integer' THEN concat_ws ( '', '', psc.NUMERIC_PRECISION ) WHEN 'int4' THEN concat_ws ( '', '', psc.NUMERIC_PRECISION ) WHEN 'int8' THEN concat_ws ( '', '', psc.NUMERIC_PRECISION ) WHEN 'bigint' THEN concat_ws ( '', '', psc.NUMERIC_PRECISION ) WHEN 'timestamp' THEN concat_ws ( '', '', psc.datetime_precision ) ELSE '' END AS data_type_long, ( SELECT pd.description FROM pg_description pd WHERE (pd.objoid,pd.objsubid) in ( SELECT pa.attrelid,pa.attnum FROM pg_attribute pa WHERE pa.attrelid = ( SELECT oid FROM pg_class pc WHERE pc.relname = psc.table_name ) and attname = psc.column_name ) ) AS column_comment, ( SELECT COUNT(*) FROM pg_constraint WHERE contype = 'p' AND conrelid = ( SELECT oid FROM pg_class WHERE relname = psc.table_name ) AND conkey::int[] @> ARRAY[( SELECT attnum::integer FROM pg_attribute WHERE attrelid = conrelid AND attname = psc.column_name )] ) > 0 AS primary_key, psc.ordinal_position FROM INFORMATION_SCHEMA.COLUMNS psc WHERE table_catalog = ? AND table_schema = 'public' AND TABLE_NAME = ? ORDER BY psc.ordinal_position; ` var entities []response.Column //sql = strings.ReplaceAll(sql, "@table_catalog", dbName) //sql = strings.ReplaceAll(sql, "@table_name", tableName) db := global.GVA_DB if businessDB != "" { db = global.GVA_DBList[businessDB] } err = db.Raw(sql, dbName, tableName).Scan(&entities).Error return entities, err } ================================================ FILE: server/service/system/sys_auto_code_sqlite.go ================================================ package system import ( "fmt" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" "path/filepath" "strings" ) var AutoCodeSqlite = new(autoCodeSqlite) type autoCodeSqlite struct{} // GetDB 获取数据库的所有数据库名 // Author [piexlmax](https://github.com/piexlmax) // Author [SliverHorn](https://github.com/SliverHorn) func (a *autoCodeSqlite) GetDB(businessDB string) (data []response.Db, err error) { var entities []response.Db sql := "PRAGMA database_list;" var databaseList []struct { File string `gorm:"column:file"` } if businessDB == "" { err = global.GVA_DB.Raw(sql).Find(&databaseList).Error } else { err = global.GVA_DBList[businessDB].Raw(sql).Find(&databaseList).Error } for _, database := range databaseList { if database.File != "" { fileName := filepath.Base(database.File) fileExt := filepath.Ext(fileName) fileNameWithoutExt := strings.TrimSuffix(fileName, fileExt) entities = append(entities, response.Db{fileNameWithoutExt}) } } // entities = append(entities, response.Db{global.GVA_CONFIG.Sqlite.Dbname}) return entities, err } // GetTables 获取数据库的所有表名 // Author [piexlmax](https://github.com/piexlmax) // Author [SliverHorn](https://github.com/SliverHorn) func (a *autoCodeSqlite) GetTables(businessDB string, dbName string) (data []response.Table, err error) { var entities []response.Table sql := `SELECT name FROM sqlite_master WHERE type='table'` tabelNames := []string{} if businessDB == "" { err = global.GVA_DB.Raw(sql).Find(&tabelNames).Error } else { err = global.GVA_DBList[businessDB].Raw(sql).Find(&tabelNames).Error } for _, tabelName := range tabelNames { entities = append(entities, response.Table{tabelName}) } return entities, err } // GetColumn 获取指定数据表的所有字段名,类型值等 // Author [piexlmax](https://github.com/piexlmax) // Author [SliverHorn](https://github.com/SliverHorn) func (a *autoCodeSqlite) GetColumn(businessDB string, tableName string, dbName string) (data []response.Column, err error) { var entities []response.Column sql := fmt.Sprintf("PRAGMA table_info(%s);", tableName) var columnInfos []struct { Name string `gorm:"column:name"` Type string `gorm:"column:type"` Pk int `gorm:"column:pk"` } if businessDB == "" { err = global.GVA_DB.Raw(sql).Scan(&columnInfos).Error } else { err = global.GVA_DBList[businessDB].Raw(sql).Scan(&columnInfos).Error } for _, columnInfo := range columnInfos { entities = append(entities, response.Column{ ColumnName: columnInfo.Name, DataType: columnInfo.Type, PrimaryKey: columnInfo.Pk == 1, }) } return entities, err } ================================================ FILE: server/service/system/sys_base_menu.go ================================================ package system import ( "errors" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system" "gorm.io/gorm" ) type BaseMenuService struct{} //@author: [piexlmax](https://github.com/piexlmax) //@function: DeleteBaseMenu //@description: 删除基础路由 //@param: id float64 //@return: err error var BaseMenuServiceApp = new(BaseMenuService) func (baseMenuService *BaseMenuService) DeleteBaseMenu(id int) (err error) { err = global.GVA_DB.First(&system.SysBaseMenu{}, "parent_id = ?", id).Error if err == nil { return errors.New("此菜单存在子菜单不可删除") } var menu system.SysBaseMenu err = global.GVA_DB.First(&menu, id).Error if err != nil { return errors.New("记录不存在") } err = global.GVA_DB.First(&system.SysAuthority{}, "default_router = ?", menu.Name).Error if err == nil { return errors.New("此菜单有角色正在作为首页,不可删除") } return global.GVA_DB.Transaction(func(tx *gorm.DB) error { err = tx.Delete(&system.SysBaseMenu{}, "id = ?", id).Error if err != nil { return err } err = tx.Delete(&system.SysBaseMenuParameter{}, "sys_base_menu_id = ?", id).Error if err != nil { return err } err = tx.Delete(&system.SysBaseMenuBtn{}, "sys_base_menu_id = ?", id).Error if err != nil { return err } err = tx.Delete(&system.SysAuthorityBtn{}, "sys_menu_id = ?", id).Error if err != nil { return err } err = tx.Delete(&system.SysAuthorityMenu{}, "sys_base_menu_id = ?", id).Error if err != nil { return err } return nil }) } //@author: [piexlmax](https://github.com/piexlmax) //@function: UpdateBaseMenu //@description: 更新路由 //@param: menu model.SysBaseMenu //@return: err error func (baseMenuService *BaseMenuService) UpdateBaseMenu(menu system.SysBaseMenu) (err error) { var oldMenu system.SysBaseMenu upDateMap := make(map[string]interface{}) upDateMap["keep_alive"] = menu.KeepAlive upDateMap["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 } ================================================ FILE: server/service/system/sys_casbin.go ================================================ package system import ( "errors" "strconv" "gorm.io/gorm" gormadapter "github.com/casbin/gorm-adapter/v3" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "github.com/flipped-aurora/gin-vue-admin/server/utils" _ "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 } // GetAuthoritiesByApi 获取拥有指定API权限的所有角色ID func (casbinService *CasbinService) GetAuthoritiesByApi(path, method string) (authorityIds []uint, err error) { var rules []gormadapter.CasbinRule err = global.GVA_DB.Where("ptype = 'p' AND v1 = ? AND v2 = ?", path, method).Find(&rules).Error if err != nil { return nil, err } for _, r := range rules { id, e := strconv.Atoi(r.V0) if e == nil { authorityIds = append(authorityIds, uint(id)) } } return authorityIds, nil } // SetApiAuthorities 全量覆盖某API关联的角色列表 func (casbinService *CasbinService) SetApiAuthorities(path, method string, authorityIds []uint) error { return global.GVA_DB.Transaction(func(tx *gorm.DB) error { // 1. 删除该API所有已有的角色关联 if err := tx.Where("ptype = 'p' AND v1 = ? AND v2 = ?", path, method).Delete(&gormadapter.CasbinRule{}).Error; err != nil { return err } // 2. 批量插入新的关联记录 if len(authorityIds) > 0 { newRules := make([]gormadapter.CasbinRule, 0, len(authorityIds)) for _, authorityId := range authorityIds { newRules = append(newRules, gormadapter.CasbinRule{ Ptype: "p", V0: strconv.Itoa(int(authorityId)), V1: path, V2: method, }) } if err := tx.Create(&newRules).Error; err != nil { return err } } return nil }) } ================================================ FILE: server/service/system/sys_dictionary.go ================================================ package system import ( "encoding/json" "errors" "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "github.com/gin-gonic/gin" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system" "gorm.io/gorm" ) //@author: [piexlmax](https://github.com/piexlmax) //@function: CreateSysDictionary //@description: 创建字典数据 //@param: sysDictionary model.SysDictionary //@return: err error type DictionaryService struct{} var DictionaryServiceApp = new(DictionaryService) func (dictionaryService *DictionaryService) CreateSysDictionary(sysDictionary system.SysDictionary) (err error) { if (!errors.Is(global.GVA_DB.First(&system.SysDictionary{}, "type = ?", sysDictionary.Type).Error, gorm.ErrRecordNotFound)) { return errors.New("存在相同的type,不允许创建") } err = global.GVA_DB.Create(&sysDictionary).Error return err } //@author: [piexlmax](https://github.com/piexlmax) //@function: DeleteSysDictionary //@description: 删除字典数据 //@param: sysDictionary model.SysDictionary //@return: err error func (dictionaryService *DictionaryService) DeleteSysDictionary(sysDictionary system.SysDictionary) (err error) { err = global.GVA_DB.Where("id = ?", sysDictionary.ID).Preload("SysDictionaryDetails").First(&sysDictionary).Error if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("请不要搞事") } if err != nil { return err } err = global.GVA_DB.Delete(&sysDictionary).Error if err != nil { return err } if sysDictionary.SysDictionaryDetails != nil { return global.GVA_DB.Where("sys_dictionary_id=?", sysDictionary.ID).Delete(sysDictionary.SysDictionaryDetails).Error } return } //@author: [piexlmax](https://github.com/piexlmax) //@function: UpdateSysDictionary //@description: 更新字典数据 //@param: sysDictionary *model.SysDictionary //@return: err error func (dictionaryService *DictionaryService) UpdateSysDictionary(sysDictionary *system.SysDictionary) (err error) { var dict system.SysDictionary sysDictionaryMap := map[string]interface{}{ "Name": sysDictionary.Name, "Type": sysDictionary.Type, "Status": sysDictionary.Status, "Desc": sysDictionary.Desc, "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 }) } ================================================ FILE: server/service/system/sys_dictionary_detail.go ================================================ package system import ( "fmt" "strconv" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" ) //@author: [piexlmax](https://github.com/piexlmax) //@function: CreateSysDictionaryDetail //@description: 创建字典详情数据 //@param: sysDictionaryDetail model.SysDictionaryDetail //@return: err error type DictionaryDetailService struct{} var DictionaryDetailServiceApp = new(DictionaryDetailService) func (dictionaryDetailService *DictionaryDetailService) CreateSysDictionaryDetail(sysDictionaryDetail system.SysDictionaryDetail) (err error) { // 计算层级和路径 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) } ================================================ FILE: server/service/system/sys_error.go ================================================ package system import ( "context" "fmt" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common" "github.com/flipped-aurora/gin-vue-admin/server/model/system" systemReq "github.com/flipped-aurora/gin-vue-admin/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 } ================================================ FILE: server/service/system/sys_export_template.go ================================================ package system import ( "bytes" "encoding/json" "errors" "fmt" "mime/multipart" "net/url" "strconv" "strings" "time" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" "github.com/flipped-aurora/gin-vue-admin/server/model/system" systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/xuri/excelize/v2" "gorm.io/gorm" ) 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 } ================================================ FILE: server/service/system/sys_initdb.go ================================================ package system import ( "context" "database/sql" "errors" "fmt" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "gorm.io/gorm" "sort" ) const ( Mysql = "mysql" Pgsql = "pgsql" Sqlite = "sqlite" Mssql = "mssql" InitSuccess = "\n[%v] --> 初始数据成功!\n" InitDataExist = "\n[%v] --> %v 的初始数据已存在!\n" InitDataFailed = "\n[%v] --> %v 初始数据失败! \nerr: %+v\n" InitDataSuccess = "\n[%v] --> %v 初始数据成功!\n" ) const ( InitOrderSystem = 10 InitOrderInternal = 1000 InitOrderExternal = 100000 ) var ( ErrMissingDBContext = errors.New("missing db in context") ErrMissingDependentContext = errors.New("missing dependent value in context") ErrDBTypeMismatch = errors.New("db type mismatch") ) // SubInitializer 提供 source/*/init() 使用的接口,每个 initializer 完成一个初始化过程 type SubInitializer interface { InitializerName() string // 不一定代表单独一个表,所以改成了更宽泛的语义 MigrateTable(ctx context.Context) (next context.Context, err error) InitializeData(ctx context.Context) (next context.Context, err error) TableCreated(ctx context.Context) bool DataInserted(ctx context.Context) bool } // TypedDBInitHandler 执行传入的 initializer type TypedDBInitHandler interface { EnsureDB(ctx context.Context, conf *request.InitDB) (context.Context, error) // 建库,失败属于 fatal error,因此让它 panic WriteConfig(ctx context.Context) error // 回写配置 InitTables(ctx context.Context, inits initSlice) error // 建表 handler InitData(ctx context.Context, inits initSlice) error // 建数据 handler } // orderedInitializer 组合一个顺序字段,以供排序 type orderedInitializer struct { order int SubInitializer } // initSlice 供 initializer 排序依赖时使用 type initSlice []*orderedInitializer var ( initializers initSlice cache map[string]*orderedInitializer ) // RegisterInit 注册要执行的初始化过程,会在 InitDB() 时调用 func RegisterInit(order int, i SubInitializer) { if initializers == nil { initializers = initSlice{} } if cache == nil { cache = map[string]*orderedInitializer{} } name := i.InitializerName() if _, existed := cache[name]; existed { panic(fmt.Sprintf("Name conflict on %s", name)) } ni := orderedInitializer{order, i} initializers = append(initializers, &ni) cache[name] = &ni } /* ---- * service * ---- */ type InitDBService struct{} // InitDB 创建数据库并初始化 总入口 func (initDBService *InitDBService) InitDB(conf request.InitDB) (err error) { ctx := context.TODO() ctx = context.WithValue(ctx, "adminPassword", conf.AdminPassword) if len(initializers) == 0 { return errors.New("无可用初始化过程,请检查初始化是否已执行完成") } sort.Sort(&initializers) // 保证有依赖的 initializer 排在后面执行 // Note: 若 initializer 只有单一依赖,可以写为 B=A+1, C=A+1; 由于 BC 之间没有依赖关系,所以谁先谁后并不影响初始化 // 若存在多个依赖,可以写为 C=A+B, D=A+B+C, E=A+1; // C必然>A|B,因此在AB之后执行,D必然>A|B|C,因此在ABC后执行,而E只依赖A,顺序与CD无关,因此E与CD哪个先执行并不影响 var initHandler TypedDBInitHandler switch conf.DBType { case "mysql": initHandler = NewMysqlInitHandler() ctx = context.WithValue(ctx, "dbtype", "mysql") case "pgsql": initHandler = NewPgsqlInitHandler() ctx = context.WithValue(ctx, "dbtype", "pgsql") case "sqlite": initHandler = NewSqliteInitHandler() ctx = context.WithValue(ctx, "dbtype", "sqlite") case "mssql": initHandler = NewMssqlInitHandler() ctx = context.WithValue(ctx, "dbtype", "mssql") default: initHandler = NewMysqlInitHandler() ctx = context.WithValue(ctx, "dbtype", "mysql") } ctx, err = initHandler.EnsureDB(ctx, &conf) if err != nil { return err } db := ctx.Value("db").(*gorm.DB) global.GVA_DB = db if err = initHandler.InitTables(ctx, initializers); err != nil { return err } if err = initHandler.InitData(ctx, initializers); err != nil { return err } if err = initHandler.WriteConfig(ctx); err != nil { return err } initializers = initSlice{} cache = map[string]*orderedInitializer{} return nil } // createDatabase 创建数据库( EnsureDB() 中调用 ) func createDatabase(dsn string, driver string, createSql string) error { db, err := sql.Open(driver, dsn) if err != nil { return err } defer func(db *sql.DB) { err = db.Close() if err != nil { fmt.Println(err) } }(db) if err = db.Ping(); err != nil { return err } _, err = db.Exec(createSql) return err } // createTables 创建表(默认 dbInitHandler.initTables 行为) func createTables(ctx context.Context, inits initSlice) error { next, cancel := context.WithCancel(ctx) defer 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] } ================================================ FILE: server/service/system/sys_initdb_mssql.go ================================================ package system import ( "context" "errors" "github.com/flipped-aurora/gin-vue-admin/server/config" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/google/uuid" "github.com/gookit/color" "gorm.io/driver/sqlserver" "gorm.io/gorm" "path/filepath" ) type MssqlInitHandler struct{} func NewMssqlInitHandler() *MssqlInitHandler { return &MssqlInitHandler{} } // WriteConfig mssql回写配置 func (h MssqlInitHandler) WriteConfig(ctx context.Context) error { c, ok := ctx.Value("config").(config.Mssql) if !ok { return errors.New("mssql config invalid") } global.GVA_CONFIG.System.DbType = "mssql" global.GVA_CONFIG.Mssql = c global.GVA_CONFIG.JWT.SigningKey = uuid.New().String() cs := utils.StructToMap(global.GVA_CONFIG) for k, v := range cs { global.GVA_VP.Set(k, v) } global.GVA_ACTIVE_DBNAME = &c.Dbname return global.GVA_VP.WriteConfig() } // EnsureDB 创建数据库并初始化 mssql func (h MssqlInitHandler) EnsureDB(ctx context.Context, conf *request.InitDB) (next context.Context, err error) { if s, ok := ctx.Value("dbtype").(string); !ok || s != "mssql" { return ctx, ErrDBTypeMismatch } c := conf.ToMssqlConfig() next = context.WithValue(ctx, "config", c) if c.Dbname == "" { return ctx, nil } // 如果没有数据库名, 则跳出初始化数据 dsn := conf.MssqlEmptyDsn() mssqlConfig := sqlserver.Config{ DSN: dsn, // DSN data source name DefaultStringSize: 191, // string 类型字段的默认长度 } var db *gorm.DB if db, err = gorm.Open(sqlserver.New(mssqlConfig), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true}); err != nil { return nil, err } global.GVA_CONFIG.AutoCode.Root, _ = filepath.Abs("..") next = context.WithValue(next, "db", db) return next, err } func (h MssqlInitHandler) InitTables(ctx context.Context, inits initSlice) error { return createTables(ctx, inits) } func (h MssqlInitHandler) InitData(ctx context.Context, inits initSlice) error { next, cancel := context.WithCancel(ctx) defer 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 } ================================================ FILE: server/service/system/sys_initdb_mysql.go ================================================ package system import ( "context" "errors" "fmt" "path/filepath" "github.com/flipped-aurora/gin-vue-admin/server/config" "github.com/gookit/color" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "github.com/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 } ================================================ FILE: server/service/system/sys_initdb_pgsql.go ================================================ package system import ( "context" "errors" "fmt" "path/filepath" "github.com/flipped-aurora/gin-vue-admin/server/config" "github.com/gookit/color" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "github.com/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 } ================================================ FILE: server/service/system/sys_initdb_sqlite.go ================================================ package system import ( "context" "errors" "github.com/glebarez/sqlite" "github.com/google/uuid" "github.com/gookit/color" "gorm.io/gorm" "path/filepath" "github.com/flipped-aurora/gin-vue-admin/server/config" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "github.com/flipped-aurora/gin-vue-admin/server/utils" ) type SqliteInitHandler struct{} func NewSqliteInitHandler() *SqliteInitHandler { return &SqliteInitHandler{} } // WriteConfig mysql回写配置 func (h SqliteInitHandler) WriteConfig(ctx context.Context) error { c, ok := ctx.Value("config").(config.Sqlite) if !ok { return errors.New("sqlite config invalid") } global.GVA_CONFIG.System.DbType = "sqlite" global.GVA_CONFIG.Sqlite = c global.GVA_CONFIG.JWT.SigningKey = uuid.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 } ================================================ FILE: server/service/system/sys_login_log.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" "github.com/flipped-aurora/gin-vue-admin/server/model/system" systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" ) 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 } ================================================ FILE: server/service/system/sys_menu.go ================================================ package system import ( "errors" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" "github.com/flipped-aurora/gin-vue-admin/server/model/system" "gorm.io/gorm" "strconv" ) //@author: [piexlmax](https://github.com/piexlmax) //@function: getMenuTreeMap //@description: 获取路由总树map //@param: authorityId string //@return: treeMap map[string][]system.SysMenu, err error type MenuService struct{} var MenuServiceApp = new(MenuService) func (menuService *MenuService) getMenuTreeMap(authorityId uint) (treeMap map[uint][]system.SysMenu, err error) { var allMenus []system.SysMenu var baseMenu []system.SysBaseMenu var btns []system.SysAuthorityBtn treeMap = make(map[uint][]system.SysMenu) var SysAuthorityMenus []system.SysAuthorityMenu err = global.GVA_DB.Where("sys_authority_authority_id = ?", authorityId).Find(&SysAuthorityMenus).Error if err != nil { return } var MenuIds []string for i := range SysAuthorityMenus { MenuIds = append(MenuIds, SysAuthorityMenus[i].MenuId) } err = global.GVA_DB.Where("id in (?)", MenuIds).Order("sort").Preload("Parameters").Find(&baseMenu).Error if err != nil { return } for i := range baseMenu { allMenus = append(allMenus, system.SysMenu{ SysBaseMenu: baseMenu[i], AuthorityId: authorityId, MenuId: baseMenu[i].ID, Parameters: baseMenu[i].Parameters, }) } err = global.GVA_DB.Where("authority_id = ?", authorityId).Preload("SysBaseMenuBtn").Find(&btns).Error if err != nil { return } var btnMap = make(map[uint]map[string]uint) for _, v := range btns { if btnMap[v.SysMenuID] == nil { btnMap[v.SysMenuID] = make(map[string]uint) } btnMap[v.SysMenuID][v.SysBaseMenuBtn.Name] = authorityId } for _, v := range allMenus { v.Btns = btnMap[v.SysBaseMenu.ID] treeMap[v.ParentId] = append(treeMap[v.ParentId], v) } return treeMap, err } //@author: [piexlmax](https://github.com/piexlmax) //@function: GetMenuTree //@description: 获取动态菜单树 //@param: authorityId string //@return: menus []system.SysMenu, err error func (menuService *MenuService) GetMenuTree(authorityId uint) (menus []system.SysMenu, err error) { menuTree, err := menuService.getMenuTreeMap(authorityId) menus = menuTree[0] for i := 0; i < len(menus); i++ { err = menuService.getChildrenList(&menus[i], menuTree) } return menus, err } //@author: [piexlmax](https://github.com/piexlmax) //@function: getChildrenList //@description: 获取子菜单 //@param: menu *model.SysMenu, treeMap map[string][]model.SysMenu //@return: err error func (menuService *MenuService) getChildrenList(menu *system.SysMenu, treeMap map[uint][]system.SysMenu) (err error) { menu.Children = treeMap[menu.MenuId] for i := 0; i < len(menu.Children); i++ { err = menuService.getChildrenList(&menu.Children[i], treeMap) } return err } //@author: [piexlmax](https://github.com/piexlmax) //@function: GetInfoList //@description: 获取路由分页 //@return: list interface{}, total int64,err error func (menuService *MenuService) GetInfoList(authorityID uint) (list interface{}, err error) { var menuList []system.SysBaseMenu treeMap, err := menuService.getBaseMenuTreeMap(authorityID) menuList = treeMap[0] for i := 0; i < len(menuList); i++ { err = menuService.getBaseChildrenList(&menuList[i], treeMap) } return menuList, err } //@author: [piexlmax](https://github.com/piexlmax) //@function: getBaseChildrenList //@description: 获取菜单的子菜单 //@param: menu *model.SysBaseMenu, treeMap map[string][]model.SysBaseMenu //@return: err error func (menuService *MenuService) getBaseChildrenList(menu *system.SysBaseMenu, treeMap map[uint][]system.SysBaseMenu) (err error) { menu.Children = treeMap[menu.ID] for i := 0; i < len(menu.Children); i++ { err = menuService.getBaseChildrenList(&menu.Children[i], treeMap) } return err } //@author: [piexlmax](https://github.com/piexlmax) //@function: AddBaseMenu //@description: 添加基础路由 //@param: menu model.SysBaseMenu //@return: error func (menuService *MenuService) AddBaseMenu(menu system.SysBaseMenu) error { 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 } // GetAuthoritiesByMenuId 获取拥有指定菜单的所有角色ID func (menuService *MenuService) GetAuthoritiesByMenuId(menuId uint) (authorityIds []uint, err error) { var records []system.SysAuthorityMenu err = global.GVA_DB.Where("sys_base_menu_id = ?", menuId).Find(&records).Error if err != nil { return nil, err } for _, r := range records { id, e := strconv.Atoi(r.AuthorityId) if e == nil { authorityIds = append(authorityIds, uint(id)) } } return authorityIds, nil } // GetDefaultRouterAuthorityIds 获取将指定菜单设为首页的角色ID列表 func (menuService *MenuService) GetDefaultRouterAuthorityIds(menuId uint) (authorityIds []uint, err error) { var menu system.SysBaseMenu err = global.GVA_DB.First(&menu, menuId).Error if err != nil { return nil, err } var authorities []system.SysAuthority err = global.GVA_DB.Where("default_router = ?", menu.Name).Find(&authorities).Error if err != nil { return nil, err } for _, auth := range authorities { authorityIds = append(authorityIds, auth.AuthorityId) } return authorityIds, nil } // SetMenuAuthorities 全量覆盖某菜单关联的角色列表 func (menuService *MenuService) SetMenuAuthorities(menuId uint, authorityIds []uint) error { return global.GVA_DB.Transaction(func(tx *gorm.DB) error { // 1. 删除该菜单所有已有的角色关联 if err := tx.Where("sys_base_menu_id = ?", menuId).Delete(&system.SysAuthorityMenu{}).Error; err != nil { return err } // 2. 批量插入新的关联记录 if len(authorityIds) > 0 { menuIdStr := strconv.Itoa(int(menuId)) newRecords := make([]system.SysAuthorityMenu, 0, len(authorityIds)) for _, authorityId := range authorityIds { newRecords = append(newRecords, system.SysAuthorityMenu{ MenuId: menuIdStr, AuthorityId: strconv.Itoa(int(authorityId)), }) } if err := tx.Create(&newRecords).Error; err != nil { return err } } return nil }) } // 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" } } ================================================ FILE: server/service/system/sys_operation_record.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" "github.com/flipped-aurora/gin-vue-admin/server/model/system" systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" ) //@author: [granty1](https://github.com/granty1) //@function: CreateSysOperationRecord //@description: 创建记录 //@param: sysOperationRecord model.SysOperationRecord //@return: err error type OperationRecordService struct{} var OperationRecordServiceApp = new(OperationRecordService) //@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 } ================================================ FILE: server/service/system/sys_params.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system" systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" ) type SysParamsService struct{} // CreateSysParams 创建参数记录 // Author [Mr.奇淼](https://github.com/pixelmaxQm) func (sysParamsService *SysParamsService) CreateSysParams(sysParams *system.SysParams) (err error) { err = global.GVA_DB.Create(sysParams).Error return err } // DeleteSysParams 删除参数记录 // Author [Mr.奇淼](https://github.com/pixelmaxQm) func (sysParamsService *SysParamsService) DeleteSysParams(ID string) (err error) { err = global.GVA_DB.Delete(&system.SysParams{}, "id = ?", ID).Error return err } // DeleteSysParamsByIds 批量删除参数记录 // Author [Mr.奇淼](https://github.com/pixelmaxQm) func (sysParamsService *SysParamsService) DeleteSysParamsByIds(IDs []string) (err error) { err = global.GVA_DB.Delete(&[]system.SysParams{}, "id in ?", IDs).Error return err } // UpdateSysParams 更新参数记录 // Author [Mr.奇淼](https://github.com/pixelmaxQm) func (sysParamsService *SysParamsService) UpdateSysParams(sysParams system.SysParams) (err error) { err = global.GVA_DB.Model(&system.SysParams{}).Where("id = ?", sysParams.ID).Updates(&sysParams).Error return err } // GetSysParams 根据ID获取参数记录 // Author [Mr.奇淼](https://github.com/pixelmaxQm) func (sysParamsService *SysParamsService) GetSysParams(ID string) (sysParams system.SysParams, err error) { err = global.GVA_DB.Where("id = ?", ID).First(&sysParams).Error return } // GetSysParamsInfoList 分页获取参数记录 // Author [Mr.奇淼](https://github.com/pixelmaxQm) func (sysParamsService *SysParamsService) GetSysParamsInfoList(info systemReq.SysParamsSearch) (list []system.SysParams, total int64, err error) { limit := info.PageSize offset := info.PageSize * (info.Page - 1) // 创建db db := global.GVA_DB.Model(&system.SysParams{}) var sysParamss []system.SysParams // 如果有条件搜索 下方会自动创建搜索语句 if info.StartCreatedAt != nil && info.EndCreatedAt != nil { db = db.Where("created_at BETWEEN ? AND ?", info.StartCreatedAt, info.EndCreatedAt) } if info.Name != "" { db = db.Where("name LIKE ?", "%"+info.Name+"%") } if info.Key != "" { db = db.Where("key LIKE ?", "%"+info.Key+"%") } err = db.Count(&total).Error if err != nil { return } if limit != 0 { db = db.Limit(limit).Offset(offset) } err = db.Find(&sysParamss).Error return sysParamss, total, err } // GetSysParam 根据key获取参数value // Author [Mr.奇淼](https://github.com/pixelmaxQm) func (sysParamsService *SysParamsService) GetSysParam(key string) (param system.SysParams, err error) { err = global.GVA_DB.Where(system.SysParams{Key: key}).First(¶m).Error return } ================================================ FILE: server/service/system/sys_skills.go ================================================ package system import ( "archive/zip" "bytes" "context" "encoding/json" "errors" "fmt" "io" "io/fs" "net/http" "os" "path/filepath" "sort" "strings" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "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) Delete(_ context.Context, req request.SkillDeleteRequest) error { if strings.TrimSpace(req.Tool) == "" { return errors.New("工具类型不能为空") } if !isSafeName(req.Skill) { return errors.New("技能名称不合法") } skillDir, err := s.skillDir(req.Tool, req.Skill) if err != nil { return err } info, err := os.Stat(skillDir) if err != nil { if os.IsNotExist(err) { return errors.New("技能不存在") } return err } if !info.IsDir() { return errors.New("技能目录异常") } return os.RemoveAll(skillDir) } func (s *SkillsService) Package(_ context.Context, req request.SkillPackageRequest) (string, []byte, error) { if strings.TrimSpace(req.Tool) == "" { return "", nil, errors.New("工具类型不能为空") } if !isSafeName(req.Skill) { return "", nil, errors.New("技能名称不合法") } skillDir, err := s.skillDir(req.Tool, req.Skill) if err != nil { return "", nil, err } info, err := os.Stat(skillDir) if err != nil { if os.IsNotExist(err) { return "", nil, errors.New("技能不存在") } return "", nil, err } if !info.IsDir() { return "", nil, errors.New("技能目录异常") } buf := bytes.NewBuffer(nil) zw := zip.NewWriter(buf) walkErr := filepath.WalkDir(skillDir, func(path string, d fs.DirEntry, walkErr error) error { if walkErr != nil { return walkErr } rel, err := filepath.Rel(skillDir, path) if err != nil { return err } if rel == "." { return nil } zipName := filepath.ToSlash(rel) if d.IsDir() { _, err = zw.Create(strings.TrimSuffix(zipName, "/") + "/") return err } fileInfo, err := d.Info() if err != nil { return err } header, err := zip.FileInfoHeader(fileInfo) if err != nil { return err } header.Name = zipName header.Method = zip.Deflate writer, err := zw.CreateHeader(header) if err != nil { return err } content, err := os.ReadFile(path) if err != nil { return err } _, err = writer.Write(content) return err }) if walkErr != nil { _ = zw.Close() return "", nil, walkErr } if err = zw.Close(); err != nil { return "", nil, err } return req.Skill + ".zip", buf.Bytes(), 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) DownloadOnlineSkill(_ context.Context, req request.DownloadOnlineSkillReq) error { skillsDir, err := s.toolSkillsDir(req.Tool) if err != nil { return err } body, err := json.Marshal(map[string]interface{}{ "plugin_id": req.ID, "version": req.Version, }) if err != nil { return fmt.Errorf("构建下载请求失败: %w", err) } downloadReq, err := http.NewRequest(http.MethodPost, "https://plugin.gin-vue-admin.com/api/shopPlugin/downloadSkill", bytes.NewReader(body)) if err != nil { return fmt.Errorf("构建下载请求失败: %w", err) } downloadReq.Header.Set("Content-Type", "application/json") downloadResp, err := http.DefaultClient.Do(downloadReq) if err != nil { return fmt.Errorf("下载技能失败: %w", err) } defer downloadResp.Body.Close() if downloadResp.StatusCode != http.StatusOK { return fmt.Errorf("下载技能失败, HTTP状态码: %d", downloadResp.StatusCode) } metaBody, err := io.ReadAll(downloadResp.Body) if err != nil { return fmt.Errorf("读取下载结果失败: %w", err) } var meta struct { Data struct { URL string `json:"url"` } `json:"data"` } if err = json.Unmarshal(metaBody, &meta); err != nil { return fmt.Errorf("解析下载结果失败: %w", err) } realDownloadURL := strings.TrimSpace(meta.Data.URL) if realDownloadURL == "" { return errors.New("下载结果缺少 url") } zipResp, err := http.Get(realDownloadURL) if err != nil { return fmt.Errorf("下载压缩包失败: %w", err) } defer zipResp.Body.Close() if zipResp.StatusCode != http.StatusOK { return fmt.Errorf("下载压缩包失败, HTTP状态码: %d", zipResp.StatusCode) } tmpFile, err := os.CreateTemp("", "gva-skill-*.zip") if err != nil { return fmt.Errorf("创建临时文件失败: %w", err) } tmpPath := tmpFile.Name() defer os.Remove(tmpPath) if _, err = io.Copy(tmpFile, zipResp.Body); err != nil { tmpFile.Close() return fmt.Errorf("保存技能包失败: %w", err) } tmpFile.Close() if err = extractZipToDir(tmpPath, skillsDir); err != nil { return fmt.Errorf("解压技能包失败: %w", err) } return nil } func extractZipToDir(zipPath, destDir string) error { r, err := zip.OpenReader(zipPath) if err != nil { return err } defer r.Close() for _, f := range r.File { name := filepath.FromSlash(f.Name) if strings.Contains(name, "..") { continue } target := filepath.Join(destDir, name) if !strings.HasPrefix(filepath.Clean(target), filepath.Clean(destDir)) { continue } if f.FileInfo().IsDir() { if err := os.MkdirAll(target, os.ModePerm); err != nil { return err } continue } if err := os.MkdirAll(filepath.Dir(target), os.ModePerm); err != nil { return err } rc, err := f.Open() if err != nil { return err } out, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) if err != nil { rc.Close() return err } _, err = io.Copy(out, rc) rc.Close() out.Close() if 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) }) } ================================================ FILE: server/service/system/sys_system.go ================================================ package system import ( "github.com/flipped-aurora/gin-vue-admin/server/config" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/utils" "go.uber.org/zap" ) //@author: [piexlmax](https://github.com/piexlmax) //@function: GetSystemConfig //@description: 读取配置文件 //@return: conf config.Server, err error type SystemConfigService struct{} var SystemConfigServiceApp = new(SystemConfigService) func (systemConfigService *SystemConfigService) GetSystemConfig() (conf config.Server, err error) { return global.GVA_CONFIG, nil } // @description set system config, //@author: [piexlmax](https://github.com/piexlmax) //@function: SetSystemConfig //@description: 设置配置文件 //@param: system model.System //@return: err error func (systemConfigService *SystemConfigService) SetSystemConfig(system system.System) (err error) { cs := utils.StructToMap(system.Config) for k, v := range cs { global.GVA_VP.Set(k, v) } err = global.GVA_VP.WriteConfig() return err } //@author: [SliverHorn](https://github.com/SliverHorn) //@function: GetServerInfo //@description: 获取服务器信息 //@return: server *utils.Server, err error func (systemConfigService *SystemConfigService) GetServerInfo() (server *utils.Server, err error) { var s utils.Server s.Os = utils.InitOS() if s.Cpu, err = utils.InitCPU(); err != nil { global.GVA_LOG.Error("func utils.InitCPU() Failed", zap.String("err", err.Error())) return &s, err } if s.Ram, err = utils.InitRAM(); err != nil { global.GVA_LOG.Error("func utils.InitRAM() Failed", zap.String("err", err.Error())) return &s, err } if s.Disk, err = utils.InitDisk(); err != nil { global.GVA_LOG.Error("func utils.InitDisk() Failed", zap.String("err", err.Error())) return &s, err } return &s, nil } ================================================ FILE: server/service/system/sys_user.go ================================================ package system import ( "errors" "fmt" "time" "github.com/flipped-aurora/gin-vue-admin/server/model/common" systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/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 } orderStr := "id desc" if info.OrderKey != "" { allowedOrders := map[string]bool{ "id": true, "username": true, "nick_name": true, "phone": true, "email": true, } if allowedOrders[info.OrderKey] { orderStr = info.OrderKey if info.Desc { orderStr = info.OrderKey + " desc" } } } err = db.Limit(limit).Offset(offset).Order(orderStr).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 } ================================================ FILE: server/service/system/sys_version.go ================================================ package system import ( "context" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system" systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "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 }) } ================================================ FILE: server/source/example/file_upload_download.go ================================================ package example import ( "context" "github.com/flipped-aurora/gin-vue-admin/server/model/example" "github.com/flipped-aurora/gin-vue-admin/server/service/system" "github.com/pkg/errors" "gorm.io/gorm" ) const initOrderExaFile = system.InitOrderInternal + 1 type initExaFileMysql struct{} // auto run func init() { system.RegisterInit(initOrderExaFile, &initExaFileMysql{}) } func (i *initExaFileMysql) MigrateTable(ctx context.Context) (context.Context, error) { db, ok := ctx.Value("db").(*gorm.DB) if !ok { return ctx, system.ErrMissingDBContext } return ctx, db.AutoMigrate(&example.ExaFileUploadAndDownload{}) } func (i *initExaFileMysql) TableCreated(ctx context.Context) bool { db, ok := ctx.Value("db").(*gorm.DB) if !ok { return false } return db.Migrator().HasTable(&example.ExaFileUploadAndDownload{}) } func (i *initExaFileMysql) InitializerName() string { return example.ExaFileUploadAndDownload{}.TableName() } func (i *initExaFileMysql) InitializeData(ctx context.Context) (context.Context, error) { db, ok := ctx.Value("db").(*gorm.DB) if !ok { return ctx, system.ErrMissingDBContext } entities := []example.ExaFileUploadAndDownload{ {Name: "10.png", Url: "https://qmplusimg.henrongyi.top/gvalogo.png", Tag: "png", Key: "158787308910.png"}, {Name: "logo.png", Url: "https://qmplusimg.henrongyi.top/1576554439myAvatar.png", Tag: "png", Key: "1587973709logo.png"}, } if err := db.Create(&entities).Error; err != nil { return ctx, errors.Wrap(err, example.ExaFileUploadAndDownload{}.TableName()+"表数据初始化失败!") } return ctx, nil } func (i *initExaFileMysql) DataInserted(ctx context.Context) bool { db, ok := ctx.Value("db").(*gorm.DB) if !ok { return false } lookup := example.ExaFileUploadAndDownload{Name: "logo.png", Key: "1587973709logo.png"} if errors.Is(db.First(&lookup, &lookup).Error, gorm.ErrRecordNotFound) { return false } return true } ================================================ FILE: server/source/system/api.go ================================================ package system import ( "context" sysModel "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/service/system" "github.com/pkg/errors" "gorm.io/gorm" ) type initApi struct{} const initOrderApi = system.InitOrderSystem + 1 // auto run func init() { system.RegisterInit(initOrderApi, &initApi{}) } func (i *initApi) InitializerName() string { return sysModel.SysApi{}.TableName() } func (i *initApi) MigrateTable(ctx context.Context) (context.Context, error) { db, ok := ctx.Value("db").(*gorm.DB) if !ok { return ctx, system.ErrMissingDBContext } return ctx, db.AutoMigrate(&sysModel.SysApi{}) } func (i *initApi) TableCreated(ctx context.Context) bool { db, ok := ctx.Value("db").(*gorm.DB) if !ok { return false } return db.Migrator().HasTable(&sysModel.SysApi{}) } func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) { db, ok := ctx.Value("db").(*gorm.DB) if !ok { return ctx, system.ErrMissingDBContext } entities := []sysModel.SysApi{ {ApiGroup: "jwt", Method: "POST", Path: "/jwt/jsonInBlacklist", Description: "jwt加入黑名单(退出,必选)"}, {ApiGroup: "登录日志", Method: "DELETE", Path: "/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: "角色", Method: "GET", Path: "/authority/getUsersByAuthority", Description: "获取角色关联用户ID列表"}, {ApiGroup: "角色", Method: "POST", Path: "/authority/setRoleUsers", 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/deleteSkill", 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: "skills", Method: "POST", Path: "/skills/packageSkill", 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 } ================================================ FILE: server/source/system/api_ignore.go ================================================ package system import ( "context" sysModel "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/service/system" "github.com/pkg/errors" "gorm.io/gorm" ) type initApiIgnore struct{} const initOrderApiIgnore = initOrderApi + 1 // auto run func init() { system.RegisterInit(initOrderApiIgnore, &initApiIgnore{}) } func (i *initApiIgnore) InitializerName() string { return sysModel.SysIgnoreApi{}.TableName() } func (i *initApiIgnore) MigrateTable(ctx context.Context) (context.Context, error) { db, ok := ctx.Value("db").(*gorm.DB) if !ok { return ctx, system.ErrMissingDBContext } return ctx, db.AutoMigrate(&sysModel.SysIgnoreApi{}) } func (i *initApiIgnore) TableCreated(ctx context.Context) bool { db, ok := ctx.Value("db").(*gorm.DB) if !ok { return false } return db.Migrator().HasTable(&sysModel.SysIgnoreApi{}) } func (i *initApiIgnore) InitializeData(ctx context.Context) (context.Context, error) { db, ok := ctx.Value("db").(*gorm.DB) if !ok { return ctx, system.ErrMissingDBContext } entities := []sysModel.SysIgnoreApi{ {Method: "GET", Path: "/swagger/*any"}, {Method: "GET", Path: "/api/freshCasbin"}, {Method: "GET", Path: "/uploads/file/*filepath"}, {Method: "GET", Path: "/health"}, {Method: "HEAD", Path: "/uploads/file/*filepath"}, {Method: "POST", Path: "/autoCode/llmAuto"}, {Method: "POST", Path: "/system/reloadSystem"}, {Method: "POST", Path: "/base/login"}, {Method: "POST", Path: "/base/captcha"}, {Method: "POST", Path: "/init/initdb"}, {Method: "POST", Path: "/init/checkdb"}, {Method: "GET", Path: "/info/getInfoDataSource"}, {Method: "GET", Path: "/info/getInfoPublic"}, } if err := db.Create(&entities).Error; err != nil { return ctx, errors.Wrap(err, sysModel.SysIgnoreApi{}.TableName()+"表数据初始化失败!") } next := context.WithValue(ctx, i.InitializerName(), entities) return next, nil } func (i *initApiIgnore) DataInserted(ctx context.Context) bool { db, ok := ctx.Value("db").(*gorm.DB) if !ok { return false } if errors.Is(db.Where("path = ? AND method = ?", "/swagger/*any", "GET"). First(&sysModel.SysIgnoreApi{}).Error, gorm.ErrRecordNotFound) { return false } return true } ================================================ FILE: server/source/system/authorities_menus.go ================================================ package system import ( "context" sysModel "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/service/system" "github.com/pkg/errors" "gorm.io/gorm" ) const initOrderMenuAuthority = initOrderMenu + initOrderAuthority type initMenuAuthority struct{} // auto run func init() { system.RegisterInit(initOrderMenuAuthority, &initMenuAuthority{}) } func (i *initMenuAuthority) MigrateTable(ctx context.Context) (context.Context, error) { return ctx, nil // do nothing } func (i *initMenuAuthority) TableCreated(ctx context.Context) bool { return false // always replace } func (i *initMenuAuthority) InitializerName() string { return "sys_menu_authorities" } func (i *initMenuAuthority) InitializeData(ctx context.Context) (next context.Context, err error) { db, ok := ctx.Value("db").(*gorm.DB) if !ok { return ctx, system.ErrMissingDBContext } 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 } ================================================ FILE: server/source/system/authority.go ================================================ package system import ( "context" sysModel "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/service/system" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/pkg/errors" "gorm.io/gorm" ) const initOrderAuthority = initOrderCasbin + 1 type initAuthority struct{} // auto run func init() { system.RegisterInit(initOrderAuthority, &initAuthority{}) } func (i *initAuthority) MigrateTable(ctx context.Context) (context.Context, error) { db, ok := ctx.Value("db").(*gorm.DB) if !ok { return ctx, system.ErrMissingDBContext } return ctx, db.AutoMigrate(&sysModel.SysAuthority{}) } func (i *initAuthority) TableCreated(ctx context.Context) bool { db, ok := ctx.Value("db").(*gorm.DB) if !ok { return false } return db.Migrator().HasTable(&sysModel.SysAuthority{}) } func (i *initAuthority) InitializerName() string { return sysModel.SysAuthority{}.TableName() } func (i *initAuthority) InitializeData(ctx context.Context) (context.Context, error) { db, ok := ctx.Value("db").(*gorm.DB) if !ok { return ctx, system.ErrMissingDBContext } entities := []sysModel.SysAuthority{ {AuthorityId: 888, AuthorityName: "普通用户", ParentId: utils.Pointer[uint](0), DefaultRouter: "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 } ================================================ FILE: server/source/system/casbin.go ================================================ package system import ( "context" adapter "github.com/casbin/gorm-adapter/v3" "github.com/flipped-aurora/gin-vue-admin/server/service/system" "github.com/pkg/errors" "gorm.io/gorm" ) const initOrderCasbin = initOrderApiIgnore + 1 type initCasbin struct{} // auto run func init() { system.RegisterInit(initOrderCasbin, &initCasbin{}) } func (i *initCasbin) MigrateTable(ctx context.Context) (context.Context, error) { db, ok := ctx.Value("db").(*gorm.DB) if !ok { return ctx, system.ErrMissingDBContext } return ctx, db.AutoMigrate(&adapter.CasbinRule{}) } func (i *initCasbin) TableCreated(ctx context.Context) bool { db, ok := ctx.Value("db").(*gorm.DB) if !ok { return false } return db.Migrator().HasTable(&adapter.CasbinRule{}) } func (i *initCasbin) InitializerName() string { var entity adapter.CasbinRule return entity.TableName() } func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error) { db, ok := ctx.Value("db").(*gorm.DB) if !ok { return ctx, system.ErrMissingDBContext } entities := []adapter.CasbinRule{ {Ptype: "p", V0: "888", V1: "/user/admin_register", V2: "POST"}, {Ptype: "p", V0: "888", V1: "/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: "/authority/getUsersByAuthority", V2: "GET"}, {Ptype: "p", V0: "888", V1: "/authority/setRoleUsers", 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/deleteSkill", 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: "/skills/packageSkill", 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: "/authority/getUsersByAuthority", V2: "GET"}, {Ptype: "p", V0: "8881", V1: "/authority/setRoleUsers", 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: "/authority/getUsersByAuthority", V2: "GET"}, {Ptype: "p", V0: "9528", V1: "/authority/setRoleUsers", 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 } ================================================ FILE: server/source/system/dictionary.go ================================================ package system import ( "context" sysModel "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/service/system" "github.com/pkg/errors" "gorm.io/gorm" ) const initOrderDict = initOrderCasbin + 1 type initDict struct{} // auto run func init() { system.RegisterInit(initOrderDict, &initDict{}) } func (i *initDict) MigrateTable(ctx context.Context) (context.Context, error) { db, ok := ctx.Value("db").(*gorm.DB) if !ok { return ctx, system.ErrMissingDBContext } return ctx, db.AutoMigrate(&sysModel.SysDictionary{}) } func (i *initDict) TableCreated(ctx context.Context) bool { db, ok := ctx.Value("db").(*gorm.DB) if !ok { return false } return db.Migrator().HasTable(&sysModel.SysDictionary{}) } func (i *initDict) InitializerName() string { return sysModel.SysDictionary{}.TableName() } func (i *initDict) InitializeData(ctx context.Context) (next context.Context, err error) { db, ok := ctx.Value("db").(*gorm.DB) if !ok { return ctx, system.ErrMissingDBContext } True := true entities := []sysModel.SysDictionary{ {Name: "性别", Type: "gender", Status: &True, Desc: "性别字典"}, {Name: "数据库int类型", Type: "int", Status: &True, Desc: "int类型对应的数据库类型"}, {Name: "数据库时间日期类型", Type: "time.Time", Status: &True, Desc: "数据库时间日期类型"}, {Name: "数据库浮点型", Type: "float64", Status: &True, Desc: "数据库浮点型"}, {Name: "数据库字符串", Type: "string", Status: &True, Desc: "数据库字符串"}, {Name: "数据库bool类型", Type: "bool", Status: &True, Desc: "数据库bool类型"}, } if err = db.Create(&entities).Error; err != nil { return ctx, errors.Wrap(err, sysModel.SysDictionary{}.TableName()+"表数据初始化失败!") } next = context.WithValue(ctx, i.InitializerName(), entities) return next, nil } func (i *initDict) DataInserted(ctx context.Context) bool { db, ok := ctx.Value("db").(*gorm.DB) if !ok { return false } if errors.Is(db.Where("type = ?", "bool").First(&sysModel.SysDictionary{}).Error, gorm.ErrRecordNotFound) { // 判断是否存在数据 return false } return true } ================================================ FILE: server/source/system/dictionary_detail.go ================================================ package system import ( "context" "fmt" sysModel "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/service/system" "github.com/pkg/errors" "gorm.io/gorm" ) const initOrderDictDetail = initOrderDict + 1 type initDictDetail struct{} // auto run func init() { system.RegisterInit(initOrderDictDetail, &initDictDetail{}) } func (i *initDictDetail) MigrateTable(ctx context.Context) (context.Context, error) { db, ok := ctx.Value("db").(*gorm.DB) if !ok { return ctx, system.ErrMissingDBContext } return ctx, db.AutoMigrate(&sysModel.SysDictionaryDetail{}) } func (i *initDictDetail) TableCreated(ctx context.Context) bool { db, ok := ctx.Value("db").(*gorm.DB) if !ok { return false } return db.Migrator().HasTable(&sysModel.SysDictionaryDetail{}) } func (i *initDictDetail) InitializerName() string { return sysModel.SysDictionaryDetail{}.TableName() } func (i *initDictDetail) InitializeData(ctx context.Context) (context.Context, error) { db, ok := ctx.Value("db").(*gorm.DB) if !ok { return ctx, system.ErrMissingDBContext } dicts, ok := ctx.Value(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" } ================================================ FILE: server/source/system/excel_template.go ================================================ package system import ( "context" sysModel "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/service/system" "github.com/pkg/errors" "gorm.io/gorm" ) type initExcelTemplate struct{} const initOrderExcelTemplate = initOrderDictDetail + 1 // auto run func init() { system.RegisterInit(initOrderExcelTemplate, &initExcelTemplate{}) } func (i *initExcelTemplate) InitializerName() string { return "sys_export_templates" } func (i *initExcelTemplate) MigrateTable(ctx context.Context) (context.Context, error) { db, ok := ctx.Value("db").(*gorm.DB) if !ok { return ctx, system.ErrMissingDBContext } return ctx, db.AutoMigrate(&sysModel.SysExportTemplate{}) } func (i *initExcelTemplate) TableCreated(ctx context.Context) bool { db, ok := ctx.Value("db").(*gorm.DB) if !ok { return false } return db.Migrator().HasTable(&sysModel.SysExportTemplate{}) } func (i *initExcelTemplate) InitializeData(ctx context.Context) (context.Context, error) { db, ok := ctx.Value("db").(*gorm.DB) if !ok { return ctx, system.ErrMissingDBContext } entities := []sysModel.SysExportTemplate{ { Name: "api", TableName: "sys_apis", TemplateID: "api", TemplateInfo: `{ "path":"路径", "method":"方法(大写)", "description":"方法介绍", "api_group":"方法分组" }`, }, } if err := db.Create(&entities).Error; err != nil { return ctx, errors.Wrap(err, "sys_export_templates"+"表数据初始化失败!") } next := context.WithValue(ctx, i.InitializerName(), entities) return next, nil } func (i *initExcelTemplate) DataInserted(ctx context.Context) bool { db, ok := ctx.Value("db").(*gorm.DB) if !ok { return false } if errors.Is(db.First(&sysModel.SysExportTemplate{}).Error, gorm.ErrRecordNotFound) { return false } return true } ================================================ FILE: server/source/system/menu.go ================================================ package system import ( "context" . "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/service/system" "github.com/pkg/errors" "gorm.io/gorm" ) const initOrderMenu = initOrderAuthority + 1 type initMenu struct{} // auto run func init() { system.RegisterInit(initOrderMenu, &initMenu{}) } func (i *initMenu) InitializerName() string { return SysBaseMenu{}.TableName() } func (i *initMenu) MigrateTable(ctx context.Context) (context.Context, error) { db, ok := ctx.Value("db").(*gorm.DB) if !ok { return ctx, system.ErrMissingDBContext } return ctx, db.AutoMigrate( &SysBaseMenu{}, &SysBaseMenuParameter{}, &SysBaseMenuBtn{}, ) } func (i *initMenu) TableCreated(ctx context.Context) bool { db, ok := ctx.Value("db").(*gorm.DB) if !ok { return false } m := db.Migrator() return m.HasTable(&SysBaseMenu{}) && m.HasTable(&SysBaseMenuParameter{}) && m.HasTable(&SysBaseMenuBtn{}) } func (i *initMenu) InitializeData(ctx context.Context) (next context.Context, err error) { db, ok := ctx.Value("db").(*gorm.DB) if !ok { return ctx, system.ErrMissingDBContext } // 定义所有菜单 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 } ================================================ FILE: server/source/system/user.go ================================================ package system import ( "context" sysModel "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/service/system" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/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 } ================================================ FILE: server/task/clearTable.go ================================================ package task import ( "errors" "fmt" "github.com/flipped-aurora/gin-vue-admin/server/model/common" "time" "gorm.io/gorm" ) //@author: [songzhibin97](https://github.com/songzhibin97) //@function: ClearTable //@description: 清理数据库表数据 //@param: db(数据库对象) *gorm.DB, tableName(表名) string, compareField(比较字段) string, interval(间隔) string //@return: error func ClearTable(db *gorm.DB) error { var ClearTableDetail []common.ClearDB ClearTableDetail = append(ClearTableDetail, common.ClearDB{ TableName: "sys_operation_records", CompareField: "created_at", Interval: "2160h", }) ClearTableDetail = append(ClearTableDetail, common.ClearDB{ TableName: "jwt_blacklists", CompareField: "created_at", Interval: "168h", }) if db == nil { return errors.New("db Cannot be empty") } for _, detail := range ClearTableDetail { duration, err := time.ParseDuration(detail.Interval) if err != nil { return err } if duration < 0 { return errors.New("parse duration < 0") } err = db.Debug().Exec(fmt.Sprintf("DELETE FROM %s WHERE %s < ?", detail.TableName, detail.CompareField), time.Now().Add(-duration)).Error if err != nil { return err } } return nil } ================================================ FILE: server/utils/ast/ast.go ================================================ package ast import ( "fmt" "github.com/flipped-aurora/gin-vue-admin/server/model/system" "go/ast" "go/parser" "go/token" "log" ) // AddImport 增加 import 方法 func AddImport(astNode ast.Node, imp string) { impStr := fmt.Sprintf("\"%s\"", imp) ast.Inspect(astNode, func(node ast.Node) bool { if genDecl, ok := node.(*ast.GenDecl); ok { if genDecl.Tok == token.IMPORT { for i := range genDecl.Specs { if impNode, ok := genDecl.Specs[i].(*ast.ImportSpec); ok { if impNode.Path.Value == impStr { return false } } } genDecl.Specs = append(genDecl.Specs, &ast.ImportSpec{ Path: &ast.BasicLit{ Kind: token.STRING, Value: impStr, }, }) } } return true }) } // FindFunction 查询特定function方法 func FindFunction(astNode ast.Node, FunctionName string) *ast.FuncDecl { var funcDeclP *ast.FuncDecl ast.Inspect(astNode, func(node ast.Node) bool { if funcDecl, ok := node.(*ast.FuncDecl); ok { if funcDecl.Name.String() == FunctionName { funcDeclP = funcDecl return false } } return true }) return funcDeclP } // FindArray 查询特定数组方法 func FindArray(astNode ast.Node, identName, selectorExprName string) *ast.CompositeLit { var assignStmt *ast.CompositeLit ast.Inspect(astNode, func(n ast.Node) bool { switch node := n.(type) { case *ast.AssignStmt: for _, expr := range node.Rhs { if exprType, ok := expr.(*ast.CompositeLit); ok { if arrayType, ok := exprType.Type.(*ast.ArrayType); ok { sel, ok1 := arrayType.Elt.(*ast.SelectorExpr) x, ok2 := sel.X.(*ast.Ident) if ok1 && ok2 && x.Name == identName && sel.Sel.Name == selectorExprName { assignStmt = exprType return false } } } } } return true }) return assignStmt } func CreateMenuStructAst(menus []system.SysBaseMenu) *[]ast.Expr { var menuElts []ast.Expr for i := range menus { elts := []ast.Expr{ // 结构体的字段 &ast.KeyValueExpr{ Key: &ast.Ident{Name: "ParentId"}, Value: &ast.BasicLit{Kind: token.INT, Value: "0"}, }, &ast.KeyValueExpr{ Key: &ast.Ident{Name: "Path"}, Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", menus[i].Path)}, }, &ast.KeyValueExpr{ Key: &ast.Ident{Name: "Name"}, Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", menus[i].Name)}, }, &ast.KeyValueExpr{ Key: &ast.Ident{Name: "Hidden"}, Value: &ast.Ident{Name: "false"}, }, &ast.KeyValueExpr{ Key: &ast.Ident{Name: "Component"}, Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", menus[i].Component)}, }, &ast.KeyValueExpr{ Key: &ast.Ident{Name: "Sort"}, Value: &ast.BasicLit{Kind: token.INT, Value: fmt.Sprintf("%d", menus[i].Sort)}, }, &ast.KeyValueExpr{ Key: &ast.Ident{Name: "Meta"}, Value: &ast.CompositeLit{ Type: &ast.SelectorExpr{ X: &ast.Ident{Name: "model"}, Sel: &ast.Ident{Name: "Meta"}, }, Elts: []ast.Expr{ &ast.KeyValueExpr{ Key: &ast.Ident{Name: "Title"}, Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", menus[i].Title)}, }, &ast.KeyValueExpr{ Key: &ast.Ident{Name: "Icon"}, Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", menus[i].Icon)}, }, }, }, }, } // 添加菜单参数 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 } ================================================ FILE: server/utils/ast/ast_auto_enter.go ================================================ 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) } ================================================ FILE: server/utils/ast/ast_enter.go ================================================ package ast import ( "bytes" "go/ast" "go/format" "go/parser" "go/token" "golang.org/x/text/cases" "golang.org/x/text/language" "log" "os" "strconv" "strings" ) type Visitor struct { ImportCode string StructName string PackageName string GroupName string } func (vi *Visitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.GenDecl: // 查找有没有import context包 // Notice:没有考虑没有import任何包的情况 if n.Tok == token.IMPORT && vi.ImportCode != "" { vi.addImport(n) // 不需要再遍历子树 return nil } if n.Tok == token.TYPE && vi.StructName != "" && vi.PackageName != "" && vi.GroupName != "" { vi.addStruct(n) return nil } case *ast.FuncDecl: if n.Name.Name == "Routers" { vi.addFuncBodyVar(n) return nil } } return vi } func (vi *Visitor) addStruct(genDecl *ast.GenDecl) ast.Visitor { for i := range genDecl.Specs { switch n := genDecl.Specs[i].(type) { case *ast.TypeSpec: if strings.Index(n.Name.Name, "Group") > -1 { switch t := n.Type.(type) { case *ast.StructType: f := &ast.Field{ Names: []*ast.Ident{ { Name: vi.StructName, Obj: &ast.Object{ Kind: ast.Var, Name: vi.StructName, }, }, }, Type: &ast.SelectorExpr{ X: &ast.Ident{ Name: vi.PackageName, }, Sel: &ast.Ident{ Name: vi.GroupName, }, }, } t.Fields.List = append(t.Fields.List, f) } } } } return vi } func (vi *Visitor) addImport(genDecl *ast.GenDecl) ast.Visitor { // 是否已经import hasImported := false for _, v := range genDecl.Specs { importSpec := v.(*ast.ImportSpec) // 如果已经包含 if importSpec.Path.Value == strconv.Quote(vi.ImportCode) { hasImported = true } } if !hasImported { genDecl.Specs = append(genDecl.Specs, &ast.ImportSpec{ Path: &ast.BasicLit{ Kind: token.STRING, Value: strconv.Quote(vi.ImportCode), }, }) } return vi } func (vi *Visitor) addFuncBodyVar(funDecl *ast.FuncDecl) ast.Visitor { hasVar := false for _, v := range funDecl.Body.List { switch varSpec := v.(type) { case *ast.AssignStmt: for i := range varSpec.Lhs { switch nn := varSpec.Lhs[i].(type) { case *ast.Ident: if nn.Name == vi.PackageName+"Router" { hasVar = true } } } } } if !hasVar { assignStmt := &ast.AssignStmt{ Lhs: []ast.Expr{ &ast.Ident{ Name: vi.PackageName + "Router", Obj: &ast.Object{ Kind: ast.Var, Name: vi.PackageName + "Router", }, }, }, Tok: token.DEFINE, Rhs: []ast.Expr{ &ast.SelectorExpr{ X: &ast.SelectorExpr{ X: &ast.Ident{ Name: "router", }, Sel: &ast.Ident{ Name: "RouterGroupApp", }, }, Sel: &ast.Ident{ Name: cases.Title(language.English).String(vi.PackageName), }, }, }, } funDecl.Body.List = append(funDecl.Body.List, funDecl.Body.List[1]) index := 1 copy(funDecl.Body.List[index+1:], funDecl.Body.List[index:]) funDecl.Body.List[index] = assignStmt } return vi } func ImportReference(filepath, importCode, structName, packageName, groupName string) error { fSet := token.NewFileSet() fParser, err := parser.ParseFile(fSet, filepath, nil, parser.ParseComments) if err != nil { return err } importCode = strings.TrimSpace(importCode) v := &Visitor{ ImportCode: importCode, StructName: structName, PackageName: packageName, GroupName: groupName, } if importCode == "" { ast.Print(fSet, fParser) } ast.Walk(v, fParser) var output []byte buffer := bytes.NewBuffer(output) err = format.Node(buffer, fSet, fParser) if err != nil { log.Fatal(err) } // 写回数据 return os.WriteFile(filepath, buffer.Bytes(), 0o600) } ================================================ FILE: server/utils/ast/ast_gorm.go ================================================ 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("github.com/flipped-aurora/gin-vue-admin/server/model/%s", pk) src, err := os.ReadFile(path) if err != nil { fmt.Println(err) } fileSet := token.NewFileSet() astFile, err := parser.ParseFile(fileSet, "", src, 0) if err != nil { fmt.Println(err) } AddImport(astFile, modelPk) FuncNode := FindFunction(astFile, funcName) if FuncNode != nil { ast.Print(fileSet, FuncNode) } addDBVar(FuncNode.Body, varName, dbName) addAutoMigrate(FuncNode.Body, varName, pk, model) var out []byte bf := bytes.NewBuffer(out) printer.Fprint(bf, fileSet, astFile) os.WriteFile(path, bf.Bytes(), 0666) } // 增加一个 db库变量 func addDBVar(astBody *ast.BlockStmt, varName, dbName string) { if dbName == "" { return } dbStr := fmt.Sprintf("\"%s\"", dbName) for i := range astBody.List { if assignStmt, ok := astBody.List[i].(*ast.AssignStmt); ok { if ident, ok := assignStmt.Lhs[0].(*ast.Ident); ok { if ident.Name == varName { return } } } } assignNode := &ast.AssignStmt{ Lhs: []ast.Expr{ &ast.Ident{ Name: varName, }, }, Tok: token.DEFINE, Rhs: []ast.Expr{ &ast.CallExpr{ Fun: &ast.SelectorExpr{ X: &ast.Ident{ Name: "global", }, Sel: &ast.Ident{ Name: "GetGlobalDBByDBName", }, }, Args: []ast.Expr{ &ast.BasicLit{ Kind: token.STRING, Value: dbStr, }, }, }, }, } astBody.List = append([]ast.Stmt{assignNode}, astBody.List...) } // 为db库变量增加 AutoMigrate 方法 func addAutoMigrate(astBody *ast.BlockStmt, dbname string, pk string, model string) { if dbname == "" { dbname = "db" } flag := true ast.Inspect(astBody, func(node ast.Node) bool { // 首先判断需要加入的方法调用语句是否存在 不存在则直接走到下方逻辑 switch n := node.(type) { case *ast.CallExpr: // 判断是否找到了AutoMigrate语句 if s, ok := n.Fun.(*ast.SelectorExpr); ok { if x, ok := s.X.(*ast.Ident); ok { if s.Sel.Name == "AutoMigrate" && x.Name == dbname { flag = false if !NeedAppendModel(n, pk, model) { return false } // 判断已经找到了AutoMigrate语句 n.Args = append(n.Args, &ast.CompositeLit{ Type: &ast.SelectorExpr{ X: &ast.Ident{ Name: pk, }, Sel: &ast.Ident{ Name: model, }, }, }) return false } } } } return true //然后判断 pk.model是否存在 如果存在直接跳出 如果不存在 则向已经找到的方法调用语句的node里面push一条 }) if flag { exprStmt := &ast.ExprStmt{ X: &ast.CallExpr{ Fun: &ast.SelectorExpr{ X: &ast.Ident{ Name: dbname, }, Sel: &ast.Ident{ Name: "AutoMigrate", }, }, Args: []ast.Expr{ &ast.CompositeLit{ Type: &ast.SelectorExpr{ X: &ast.Ident{ Name: pk, }, Sel: &ast.Ident{ Name: model, }, }, }, }, }} astBody.List = append(astBody.List, exprStmt) } } // 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 } ================================================ FILE: server/utils/ast/ast_init_test.go ================================================ package ast import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "path/filepath" ) func init() { global.GVA_CONFIG.AutoCode.Root, _ = filepath.Abs("../../../") global.GVA_CONFIG.AutoCode.Server = "server" } ================================================ FILE: server/utils/ast/ast_rollback.go ================================================ package ast import ( "bytes" "fmt" "github.com/flipped-aurora/gin-vue-admin/server/global" "go/ast" "go/parser" "go/printer" "go/token" "os" "path/filepath" ) func RollBackAst(pk, model string) { RollGormBack(pk, model) RollRouterBack(pk, model) } func RollGormBack(pk, model string) { // 首先分析存在多少个ttt作为调用方的node块 // 如果多个 仅仅删除对应块即可 // 如果单个 那么还需要剔除import path := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go") src, err := os.ReadFile(path) if err != nil { fmt.Println(err) } fileSet := token.NewFileSet() astFile, err := parser.ParseFile(fileSet, "", src, 0) if err != nil { fmt.Println(err) } var n *ast.CallExpr var k int = -1 var pkNum = 0 ast.Inspect(astFile, func(node ast.Node) bool { if node, ok := node.(*ast.CallExpr); ok { for i := range node.Args { pkOK := false modelOK := false ast.Inspect(node.Args[i], func(item ast.Node) bool { if ii, ok := item.(*ast.Ident); ok { if ii.Name == pk { pkOK = true pkNum++ } if ii.Name == model { modelOK = true } } if pkOK && modelOK { n = node k = i } return true }) } } return true }) if k > -1 { n.Args = append(append([]ast.Expr{}, n.Args[:k]...), n.Args[k+1:]...) } if pkNum == 1 { var imI int = -1 var gp *ast.GenDecl ast.Inspect(astFile, func(node ast.Node) bool { if gen, ok := node.(*ast.GenDecl); ok { for i := range gen.Specs { if imspec, ok := gen.Specs[i].(*ast.ImportSpec); ok { if imspec.Path.Value == "\"github.com/flipped-aurora/gin-vue-admin/server/model/"+pk+"\"" { gp = gen imI = i return false } } } } return true }) if imI > -1 { gp.Specs = append(append([]ast.Spec{}, gp.Specs[:imI]...), gp.Specs[imI+1:]...) } } var out []byte bf := bytes.NewBuffer(out) printer.Fprint(bf, fileSet, astFile) os.Remove(path) os.WriteFile(path, bf.Bytes(), 0666) } func RollRouterBack(pk, model string) { // 首先抓到所有的代码块结构 {} // 分析结构中是否存在一个变量叫做 pk+Router // 然后获取到代码块指针 对内部需要回滚的代码进行剔除 path := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "router_biz.go") src, err := os.ReadFile(path) if err != nil { fmt.Println(err) } fileSet := token.NewFileSet() astFile, err := parser.ParseFile(fileSet, "", src, 0) if err != nil { fmt.Println(err) } var block *ast.BlockStmt var routerStmt *ast.FuncDecl ast.Inspect(astFile, func(node ast.Node) bool { if n, ok := node.(*ast.FuncDecl); ok { if n.Name.Name == "initBizRouter" { routerStmt = n } } if n, ok := node.(*ast.BlockStmt); ok { ast.Inspect(n, func(bNode ast.Node) bool { if in, ok := bNode.(*ast.Ident); ok { if in.Name == pk+"Router" { block = n return false } } return true }) return true } return true }) var k int for i := range block.List { if stmtNode, ok := block.List[i].(*ast.ExprStmt); ok { ast.Inspect(stmtNode, func(node ast.Node) bool { if n, ok := node.(*ast.Ident); ok { if n.Name == "Init"+model+"Router" { k = i return false } } return true }) } } block.List = append(append([]ast.Stmt{}, block.List[:k]...), block.List[k+1:]...) if len(block.List) == 1 { // 说明这个块就没任何意义了 block.List = nil } for i, n := range routerStmt.Body.List { if n, ok := n.(*ast.BlockStmt); ok { if n.List == nil { routerStmt.Body.List = append(append([]ast.Stmt{}, routerStmt.Body.List[:i]...), routerStmt.Body.List[i+1:]...) i-- } } } var out []byte bf := bytes.NewBuffer(out) printer.Fprint(bf, fileSet, astFile) os.Remove(path) os.WriteFile(path, bf.Bytes(), 0666) } ================================================ FILE: server/utils/ast/ast_router.go ================================================ 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 } ================================================ FILE: server/utils/ast/ast_test.go ================================================ package ast import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "go/ast" "go/parser" "go/printer" "go/token" "os" "path/filepath" "testing" ) func TestAst(t *testing.T) { filename := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "plugin.go") fileSet := token.NewFileSet() file, err := parser.ParseFile(fileSet, filename, nil, parser.ParseComments) if err != nil { t.Error(err) return } err = ast.Print(fileSet, file) if err != nil { t.Error(err) return } err = printer.Fprint(os.Stdout, token.NewFileSet(), file) if err != nil { panic(err) } } ================================================ FILE: server/utils/ast/ast_type.go ================================================ 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 ) ================================================ FILE: server/utils/ast/extract_func.go ================================================ 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 } ================================================ FILE: server/utils/ast/import.go ================================================ 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) } ================================================ FILE: server/utils/ast/interfaces.go ================================================ 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 } ================================================ FILE: server/utils/ast/interfaces_base.go ================================================ package ast import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/pkg/errors" "go/ast" "go/format" "go/parser" "go/token" "io" "os" "path" "path/filepath" "strings" ) type Base struct{} func (a *Base) Parse(filename string, writer io.Writer) (file *ast.File, err error) { fileSet := token.NewFileSet() if writer != nil { file, err = parser.ParseFile(fileSet, filename, nil, parser.ParseComments) } else { file, err = parser.ParseFile(fileSet, filename, writer, parser.ParseComments) } if err != nil { return nil, errors.Wrapf(err, "[filepath:%s]打开/解析文件失败!", filename) } return file, nil } func (a *Base) Rollback(file *ast.File) error { return nil } func (a *Base) Injection(file *ast.File) error { return nil } func (a *Base) Format(filename string, writer io.Writer, file *ast.File) error { fileSet := token.NewFileSet() if writer == nil { open, err := os.OpenFile(filename, os.O_WRONLY|os.O_TRUNC, 0666) defer open.Close() if err != nil { return errors.Wrapf(err, "[filepath:%s]打开文件失败!", filename) } writer = open } err := format.Node(writer, fileSet, file) if err != nil { return errors.Wrapf(err, "[filepath:%s]注入失败!", filename) } return nil } // RelativePath 绝对路径转相对路径 func (a *Base) RelativePath(filePath string) string { server := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server) hasServer := strings.Index(filePath, server) if hasServer != -1 { filePath = strings.TrimPrefix(filePath, server) keys := strings.Split(filePath, string(filepath.Separator)) filePath = path.Join(keys...) } return filePath } // AbsolutePath 相对路径转绝对路径 func (a *Base) AbsolutePath(filePath string) string { server := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server) keys := strings.Split(filePath, "/") filePath = filepath.Join(keys...) filePath = filepath.Join(server, filePath) return filePath } ================================================ FILE: server/utils/ast/package_enter.go ================================================ 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) } ================================================ FILE: server/utils/ast/package_enter_test.go ================================================ package ast import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "path/filepath" "testing" ) func TestPackageEnter_Rollback(t *testing.T) { type fields struct { Type Type Path string ImportPath string StructName string PackageName string PackageStructName string } tests := []struct { name string fields fields wantErr bool }{ { name: "测试ExampleApiGroup回滚", fields: fields{ Type: TypePackageApiEnter, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", "enter.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/api/v1/example"`, StructName: "ExampleApiGroup", PackageName: "example", PackageStructName: "ApiGroup", }, wantErr: false, }, { name: "测试ExampleRouterGroup回滚", fields: fields{ Type: TypePackageRouterEnter, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", "enter.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/router/example"`, StructName: "Example", PackageName: "example", PackageStructName: "RouterGroup", }, wantErr: false, }, { name: "测试ExampleServiceGroup回滚", fields: fields{ Type: TypePackageServiceEnter, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", "enter.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/service/example"`, StructName: "ExampleServiceGroup", PackageName: "example", PackageStructName: "ServiceGroup", }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := &PackageEnter{ Type: tt.fields.Type, Path: tt.fields.Path, ImportPath: tt.fields.ImportPath, StructName: tt.fields.StructName, PackageName: tt.fields.PackageName, PackageStructName: tt.fields.PackageStructName, } file, err := a.Parse(a.Path, nil) if err != nil { t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) } a.Rollback(file) err = a.Format(a.Path, nil, file) if (err != nil) != tt.wantErr { t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr) } }) } } func TestPackageEnter_Injection(t *testing.T) { type fields struct { Type Type Path string ImportPath string StructName string PackageName string PackageStructName string } tests := []struct { name string fields fields wantErr bool }{ { name: "测试ExampleApiGroup注入", fields: fields{ Type: TypePackageApiEnter, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", "enter.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/api/v1/example"`, StructName: "ExampleApiGroup", PackageName: "example", PackageStructName: "ApiGroup", }, }, { name: "测试ExampleRouterGroup注入", fields: fields{ Type: TypePackageRouterEnter, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", "enter.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/router/example"`, StructName: "Example", PackageName: "example", PackageStructName: "RouterGroup", }, wantErr: false, }, { name: "测试ExampleServiceGroup注入", fields: fields{ Type: TypePackageServiceEnter, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", "enter.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/service/example"`, StructName: "ExampleServiceGroup", PackageName: "example", PackageStructName: "ServiceGroup", }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := &PackageEnter{ Type: tt.fields.Type, Path: tt.fields.Path, ImportPath: tt.fields.ImportPath, StructName: tt.fields.StructName, PackageName: tt.fields.PackageName, PackageStructName: tt.fields.PackageStructName, } file, err := a.Parse(a.Path, nil) if err != nil { t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) } a.Injection(file) err = a.Format(a.Path, nil, file) if (err != nil) != tt.wantErr { t.Errorf("Format() error = %v, wantErr %v", err, tt.wantErr) } }) } } ================================================ FILE: server/utils/ast/package_initialize_gorm.go ================================================ 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) } ================================================ FILE: server/utils/ast/package_initialize_gorm_test.go ================================================ package ast import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "path/filepath" "testing" ) func TestPackageInitializeGorm_Injection(t *testing.T) { type fields struct { Type Type Path string ImportPath string StructName string PackageName string IsNew bool } tests := []struct { name string fields fields wantErr bool }{ { name: "测试 &example.ExaFileUploadAndDownload{} 注入", fields: fields{ Type: TypePackageInitializeGorm, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/model/example"`, StructName: "ExaFileUploadAndDownload", PackageName: "example", IsNew: false, }, }, { name: "测试 &example.ExaCustomer{} 注入", fields: fields{ Type: TypePackageInitializeGorm, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/model/example"`, StructName: "ExaCustomer", PackageName: "example", IsNew: false, }, }, { name: "测试 new(example.ExaFileUploadAndDownload) 注入", fields: fields{ Type: TypePackageInitializeGorm, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/model/example"`, StructName: "ExaFileUploadAndDownload", PackageName: "example", IsNew: true, }, }, { name: "测试 new(example.ExaCustomer) 注入", fields: fields{ Type: TypePackageInitializeGorm, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/model/example"`, StructName: "ExaCustomer", PackageName: "example", IsNew: true, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := &PackageInitializeGorm{ Type: tt.fields.Type, Path: tt.fields.Path, ImportPath: tt.fields.ImportPath, StructName: tt.fields.StructName, PackageName: tt.fields.PackageName, IsNew: tt.fields.IsNew, } file, err := a.Parse(a.Path, nil) if err != nil { t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) } a.Injection(file) err = a.Format(a.Path, nil, file) if (err != nil) != tt.wantErr { t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr) } }) } } func TestPackageInitializeGorm_Rollback(t *testing.T) { type fields struct { Type Type Path string ImportPath string StructName string PackageName string IsNew bool } tests := []struct { name string fields fields wantErr bool }{ { name: "测试 &example.ExaFileUploadAndDownload{} 回滚", fields: fields{ Type: TypePackageInitializeGorm, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/model/example"`, StructName: "ExaFileUploadAndDownload", PackageName: "example", IsNew: false, }, }, { name: "测试 &example.ExaCustomer{} 回滚", fields: fields{ Type: TypePackageInitializeGorm, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/model/example"`, StructName: "ExaCustomer", PackageName: "example", IsNew: false, }, }, { name: "测试 new(example.ExaFileUploadAndDownload) 回滚", fields: fields{ Type: TypePackageInitializeGorm, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/model/example"`, StructName: "ExaFileUploadAndDownload", PackageName: "example", IsNew: true, }, }, { name: "测试 new(example.ExaCustomer) 回滚", fields: fields{ Type: TypePackageInitializeGorm, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/model/example"`, StructName: "ExaCustomer", PackageName: "example", IsNew: true, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := &PackageInitializeGorm{ Type: tt.fields.Type, Path: tt.fields.Path, ImportPath: tt.fields.ImportPath, StructName: tt.fields.StructName, PackageName: tt.fields.PackageName, IsNew: tt.fields.IsNew, } file, err := a.Parse(a.Path, nil) if err != nil { t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) } a.Rollback(file) err = a.Format(a.Path, nil, file) if (err != nil) != tt.wantErr { t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr) } }) } } ================================================ FILE: server/utils/ast/package_initialize_router.go ================================================ 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 } ================================================ FILE: server/utils/ast/package_initialize_router_test.go ================================================ package ast import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "path/filepath" "testing" ) func TestPackageInitializeRouter_Injection(t *testing.T) { type fields struct { Type Type Path string ImportPath string AppName string GroupName string ModuleName string PackageName string FunctionName string RouterGroupName string } tests := []struct { name string fields fields wantErr bool }{ { name: "测试 InitCustomerRouter 注入", fields: fields{ Type: TypePackageInitializeRouter, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "router_biz.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/router"`, AppName: "RouterGroupApp", GroupName: "Example", ModuleName: "exampleRouter", PackageName: "router", FunctionName: "InitCustomerRouter", RouterGroupName: "privateGroup", }, wantErr: false, }, { name: "测试 InitFileUploadAndDownloadRouter 注入", fields: fields{ Type: TypePackageInitializeRouter, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "router_biz.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/router"`, AppName: "RouterGroupApp", GroupName: "Example", ModuleName: "exampleRouter", PackageName: "router", FunctionName: "InitFileUploadAndDownloadRouter", RouterGroupName: "privateGroup", }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := &PackageInitializeRouter{ Type: tt.fields.Type, Path: tt.fields.Path, ImportPath: tt.fields.ImportPath, AppName: tt.fields.AppName, GroupName: tt.fields.GroupName, ModuleName: tt.fields.ModuleName, PackageName: tt.fields.PackageName, FunctionName: tt.fields.FunctionName, RouterGroupName: tt.fields.RouterGroupName, LeftRouterGroupName: "privateGroup", RightRouterGroupName: "publicGroup", } file, err := a.Parse(a.Path, nil) if err != nil { t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) } a.Injection(file) err = a.Format(a.Path, nil, file) if (err != nil) != tt.wantErr { t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr) } }) } } func TestPackageInitializeRouter_Rollback(t *testing.T) { type fields struct { Type Type Path string ImportPath string AppName string GroupName string ModuleName string PackageName string FunctionName string RouterGroupName string } tests := []struct { name string fields fields wantErr bool }{ { name: "测试 InitCustomerRouter 回滚", fields: fields{ Type: TypePackageInitializeRouter, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "router_biz.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/router"`, AppName: "RouterGroupApp", GroupName: "Example", ModuleName: "exampleRouter", PackageName: "router", FunctionName: "InitCustomerRouter", RouterGroupName: "privateGroup", }, wantErr: false, }, { name: "测试 InitFileUploadAndDownloadRouter 回滚", fields: fields{ Type: TypePackageInitializeRouter, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "router_biz.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/router"`, AppName: "RouterGroupApp", GroupName: "Example", ModuleName: "exampleRouter", PackageName: "router", FunctionName: "InitFileUploadAndDownloadRouter", RouterGroupName: "privateGroup", }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := &PackageInitializeRouter{ Type: tt.fields.Type, Path: tt.fields.Path, ImportPath: tt.fields.ImportPath, AppName: tt.fields.AppName, GroupName: tt.fields.GroupName, ModuleName: tt.fields.ModuleName, PackageName: tt.fields.PackageName, FunctionName: tt.fields.FunctionName, RouterGroupName: tt.fields.RouterGroupName, } file, err := a.Parse(a.Path, nil) if err != nil { t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) } a.Rollback(file) err = a.Format(a.Path, nil, file) if (err != nil) != tt.wantErr { t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr) } }) } } ================================================ FILE: server/utils/ast/package_module_enter.go ================================================ 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) } ================================================ FILE: server/utils/ast/package_module_enter_test.go ================================================ package ast import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "path/filepath" "testing" ) func TestPackageModuleEnter_Rollback(t *testing.T) { type fields struct { Type Type Path string ImportPath string StructName string AppName string GroupName string ModuleName string PackageName string ServiceName string } tests := []struct { name string fields fields wantErr bool }{ { name: "测试 FileUploadAndDownloadRouter 回滚", fields: fields{ Type: TypePackageRouterModuleEnter, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", "example", "enter.go"), ImportPath: `api "github.com/flipped-aurora/gin-vue-admin/server/api/v1"`, StructName: "FileUploadAndDownloadRouter", AppName: "ApiGroupApp", GroupName: "ExampleApiGroup", ModuleName: "exaFileUploadAndDownloadApi", PackageName: "api", ServiceName: "FileUploadAndDownloadApi", }, wantErr: false, }, { name: "测试 FileUploadAndDownloadApi 回滚", fields: fields{ Type: TypePackageApiModuleEnter, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", "example", "enter.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/service"`, StructName: "FileUploadAndDownloadApi", AppName: "ServiceGroupApp", GroupName: "ExampleServiceGroup", ModuleName: "fileUploadAndDownloadService", PackageName: "service", ServiceName: "FileUploadAndDownloadService", }, wantErr: false, }, { name: "测试 FileUploadAndDownloadService 回滚", fields: fields{ Type: TypePackageServiceModuleEnter, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", "example", "enter.go"), ImportPath: ``, StructName: "FileUploadAndDownloadService", AppName: "", GroupName: "", ModuleName: "", PackageName: "", ServiceName: "", }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := &PackageModuleEnter{ Type: tt.fields.Type, Path: tt.fields.Path, ImportPath: tt.fields.ImportPath, StructName: tt.fields.StructName, AppName: tt.fields.AppName, GroupName: tt.fields.GroupName, ModuleName: tt.fields.ModuleName, PackageName: tt.fields.PackageName, ServiceName: tt.fields.ServiceName, } file, err := a.Parse(a.Path, nil) if err != nil { t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) } a.Rollback(file) err = a.Format(a.Path, nil, file) if (err != nil) != tt.wantErr { t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr) } }) } } func TestPackageModuleEnter_Injection(t *testing.T) { type fields struct { Type Type Path string ImportPath string StructName string AppName string GroupName string ModuleName string PackageName string ServiceName string } tests := []struct { name string fields fields wantErr bool }{ { name: "测试 FileUploadAndDownloadRouter 注入", fields: fields{ Type: TypePackageRouterModuleEnter, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", "example", "enter.go"), ImportPath: `api "github.com/flipped-aurora/gin-vue-admin/server/api/v1"`, StructName: "FileUploadAndDownloadRouter", AppName: "ApiGroupApp", GroupName: "ExampleApiGroup", ModuleName: "exaFileUploadAndDownloadApi", PackageName: "api", ServiceName: "FileUploadAndDownloadApi", }, wantErr: false, }, { name: "测试 FileUploadAndDownloadApi 注入", fields: fields{ Type: TypePackageApiModuleEnter, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", "example", "enter.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/service"`, StructName: "FileUploadAndDownloadApi", AppName: "ServiceGroupApp", GroupName: "ExampleServiceGroup", ModuleName: "fileUploadAndDownloadService", PackageName: "service", ServiceName: "FileUploadAndDownloadService", }, wantErr: false, }, { name: "测试 FileUploadAndDownloadService 注入", fields: fields{ Type: TypePackageServiceModuleEnter, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", "example", "enter.go"), ImportPath: ``, StructName: "FileUploadAndDownloadService", AppName: "", GroupName: "", ModuleName: "", PackageName: "", ServiceName: "", }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := &PackageModuleEnter{ Type: tt.fields.Type, Path: tt.fields.Path, ImportPath: tt.fields.ImportPath, StructName: tt.fields.StructName, AppName: tt.fields.AppName, GroupName: tt.fields.GroupName, ModuleName: tt.fields.ModuleName, PackageName: tt.fields.PackageName, ServiceName: tt.fields.ServiceName, } file, err := a.Parse(a.Path, nil) if err != nil { t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) } a.Injection(file) err = a.Format(a.Path, nil, file) if (err != nil) != tt.wantErr { t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr) } }) } } ================================================ FILE: server/utils/ast/plugin_enter.go ================================================ 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) } ================================================ FILE: server/utils/ast/plugin_enter_test.go ================================================ package ast import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "path/filepath" "testing" ) func TestPluginEnter_Injection(t *testing.T) { type fields struct { Type Type Path string ImportPath string StructName string StructCamelName string ModuleName string GroupName string PackageName string ServiceName string } tests := []struct { name string fields fields wantErr bool }{ { name: "测试 Gva插件UserApi 注入", fields: fields{ Type: TypePluginApiEnter, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "api", "enter.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/service"`, StructName: "User", StructCamelName: "user", ModuleName: "serviceUser", GroupName: "Service", PackageName: "service", ServiceName: "User", }, wantErr: false, }, { name: "测试 Gva插件UserRouter 注入", fields: fields{ Type: TypePluginRouterEnter, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "router", "enter.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/api"`, StructName: "User", StructCamelName: "user", ModuleName: "userApi", GroupName: "Api", PackageName: "api", ServiceName: "User", }, wantErr: false, }, { name: "测试 Gva插件UserService 注入", fields: fields{ Type: TypePluginServiceEnter, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "service", "enter.go"), ImportPath: "", StructName: "User", StructCamelName: "user", ModuleName: "", GroupName: "", PackageName: "", ServiceName: "", }, wantErr: false, }, { name: "测试 gva的User 注入", fields: fields{ Type: TypePluginServiceEnter, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "service", "enter.go"), ImportPath: "", StructName: "User", StructCamelName: "user", ModuleName: "", GroupName: "", PackageName: "", ServiceName: "", }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := &PluginEnter{ Type: tt.fields.Type, Path: tt.fields.Path, ImportPath: tt.fields.ImportPath, StructName: tt.fields.StructName, StructCamelName: tt.fields.StructCamelName, ModuleName: tt.fields.ModuleName, GroupName: tt.fields.GroupName, PackageName: tt.fields.PackageName, ServiceName: tt.fields.ServiceName, } file, err := a.Parse(a.Path, nil) if err != nil { t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) } a.Injection(file) err = a.Format(a.Path, nil, file) if (err != nil) != tt.wantErr { t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr) } }) } } func TestPluginEnter_Rollback(t *testing.T) { type fields struct { Type Type Path string ImportPath string StructName string StructCamelName string ModuleName string GroupName string PackageName string ServiceName string } tests := []struct { name string fields fields wantErr bool }{ { name: "测试 Gva插件UserRouter 回滚", fields: fields{ Type: TypePluginRouterEnter, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "router", "enter.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/api"`, StructName: "User", StructCamelName: "user", ModuleName: "userApi", GroupName: "Api", PackageName: "api", ServiceName: "User", }, wantErr: false, }, { name: "测试 Gva插件UserApi 回滚", fields: fields{ Type: TypePluginApiEnter, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "api", "enter.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/service"`, StructName: "User", StructCamelName: "user", ModuleName: "serviceUser", GroupName: "Service", PackageName: "service", ServiceName: "User", }, wantErr: false, }, { name: "测试 Gva插件UserService 回滚", fields: fields{ Type: TypePluginServiceEnter, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "service", "enter.go"), ImportPath: "", StructName: "User", StructCamelName: "user", ModuleName: "", GroupName: "", PackageName: "", ServiceName: "", }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := &PluginEnter{ Type: tt.fields.Type, Path: tt.fields.Path, ImportPath: tt.fields.ImportPath, StructName: tt.fields.StructName, StructCamelName: tt.fields.StructCamelName, ModuleName: tt.fields.ModuleName, GroupName: tt.fields.GroupName, PackageName: tt.fields.PackageName, ServiceName: tt.fields.ServiceName, } file, err := a.Parse(a.Path, nil) if err != nil { t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) } a.Rollback(file) err = a.Format(a.Path, nil, file) if (err != nil) != tt.wantErr { t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr) } }) } } ================================================ FILE: server/utils/ast/plugin_gen.go ================================================ 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) } ================================================ FILE: server/utils/ast/plugin_gen_test.go ================================================ package ast import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "path/filepath" "testing" ) func TestPluginGenModel_Injection(t *testing.T) { type fields struct { Type Type Path string ImportPath string PackageName string StructName string IsNew bool } tests := []struct { name string fields fields wantErr bool }{ { name: "测试 GvaUser 结构体注入", fields: fields{ Type: TypePluginGen, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "gen", "main.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/model"`, PackageName: "model", StructName: "User", IsNew: false, }, }, { name: "测试 GvaUser 结构体注入", fields: fields{ Type: TypePluginGen, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "gen", "main.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/model"`, PackageName: "model", StructName: "User", IsNew: true, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := &PluginGen{ Type: tt.fields.Type, Path: tt.fields.Path, ImportPath: tt.fields.ImportPath, PackageName: tt.fields.PackageName, StructName: tt.fields.StructName, IsNew: tt.fields.IsNew, } file, err := a.Parse(a.Path, nil) if err != nil { t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) } a.Injection(file) err = a.Format(a.Path, nil, file) if (err != nil) != tt.wantErr { t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr) } }) } } func TestPluginGenModel_Rollback(t *testing.T) { type fields struct { Type Type Path string ImportPath string PackageName string StructName string IsNew bool } tests := []struct { name string fields fields wantErr bool }{ { name: "测试 GvaUser 回滚", fields: fields{ Type: TypePluginGen, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "gen", "main.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/model"`, PackageName: "model", StructName: "User", IsNew: false, }, }, { name: "测试 GvaUser 回滚", fields: fields{ Type: TypePluginGen, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "gen", "main.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/model"`, PackageName: "model", StructName: "User", IsNew: true, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := &PluginGen{ Type: tt.fields.Type, Path: tt.fields.Path, ImportPath: tt.fields.ImportPath, PackageName: tt.fields.PackageName, StructName: tt.fields.StructName, IsNew: tt.fields.IsNew, } file, err := a.Parse(a.Path, nil) if err != nil { t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) } a.Rollback(file) err = a.Format(a.Path, nil, file) if (err != nil) != tt.wantErr { t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr) } }) } } ================================================ FILE: server/utils/ast/plugin_initialize_gorm.go ================================================ 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) } ================================================ FILE: server/utils/ast/plugin_initialize_gorm_test.go ================================================ package ast import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "path/filepath" "testing" ) func TestPluginInitializeGorm_Injection(t *testing.T) { type fields struct { Type Type Path string ImportPath string StructName string PackageName string IsNew bool } tests := []struct { name string fields fields wantErr bool }{ { name: "测试 &model.User{} 注入", fields: fields{ Type: TypePluginInitializeGorm, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "gorm.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/model"`, StructName: "User", PackageName: "model", IsNew: false, }, }, { name: "测试 new(model.ExaCustomer) 注入", fields: fields{ Type: TypePluginInitializeGorm, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "gorm.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/model"`, StructName: "User", PackageName: "model", IsNew: true, }, }, { name: "测试 new(model.SysUsers) 注入", fields: fields{ Type: TypePluginInitializeGorm, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "gorm.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/model"`, StructName: "SysUser", PackageName: "model", IsNew: true, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := &PluginInitializeGorm{ Type: tt.fields.Type, Path: tt.fields.Path, ImportPath: tt.fields.ImportPath, StructName: tt.fields.StructName, PackageName: tt.fields.PackageName, IsNew: tt.fields.IsNew, } file, err := a.Parse(a.Path, nil) if err != nil { t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) } a.Injection(file) err = a.Format(a.Path, nil, file) if (err != nil) != tt.wantErr { t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr) } }) } } func TestPluginInitializeGorm_Rollback(t *testing.T) { type fields struct { Type Type Path string ImportPath string StructName string PackageName string IsNew bool } tests := []struct { name string fields fields wantErr bool }{ { name: "测试 &model.User{} 回滚", fields: fields{ Type: TypePluginInitializeGorm, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "gorm.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/model"`, StructName: "User", PackageName: "model", IsNew: false, }, }, { name: "测试 new(model.ExaCustomer) 回滚", fields: fields{ Type: TypePluginInitializeGorm, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "gorm.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/model"`, StructName: "User", PackageName: "model", IsNew: true, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := &PluginInitializeGorm{ Type: tt.fields.Type, Path: tt.fields.Path, ImportPath: tt.fields.ImportPath, StructName: tt.fields.StructName, PackageName: tt.fields.PackageName, IsNew: tt.fields.IsNew, } file, err := a.Parse(a.Path, nil) if err != nil { t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) } a.Rollback(file) err = a.Format(a.Path, nil, file) if (err != nil) != tt.wantErr { t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr) } }) } } ================================================ FILE: server/utils/ast/plugin_initialize_router.go ================================================ 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) } ================================================ FILE: server/utils/ast/plugin_initialize_router_test.go ================================================ package ast import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "path/filepath" "testing" ) func TestPluginInitializeRouter_Injection(t *testing.T) { type fields struct { Type Type Path string ImportPath string AppName string GroupName string PackageName string FunctionName string LeftRouterGroupName string RightRouterGroupName string } tests := []struct { name string fields fields wantErr bool }{ { name: "测试 Gva插件User 注入", fields: fields{ Type: TypePluginInitializeRouter, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "router.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/router"`, AppName: "Router", GroupName: "User", PackageName: "router", FunctionName: "Init", LeftRouterGroupName: "public", RightRouterGroupName: "private", }, wantErr: false, }, { name: "测试 中文 注入", fields: fields{ Type: TypePluginInitializeRouter, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "router.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/router"`, AppName: "Router", GroupName: "U中文", PackageName: "router", FunctionName: "Init", LeftRouterGroupName: "public", RightRouterGroupName: "private", }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := &PluginInitializeRouter{ Type: tt.fields.Type, Path: tt.fields.Path, ImportPath: tt.fields.ImportPath, AppName: tt.fields.AppName, GroupName: tt.fields.GroupName, PackageName: tt.fields.PackageName, FunctionName: tt.fields.FunctionName, LeftRouterGroupName: tt.fields.LeftRouterGroupName, RightRouterGroupName: tt.fields.RightRouterGroupName, } file, err := a.Parse(a.Path, nil) if err != nil { t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) } a.Injection(file) err = a.Format(a.Path, nil, file) if (err != nil) != tt.wantErr { t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr) } }) } } func TestPluginInitializeRouter_Rollback(t *testing.T) { type fields struct { Type Type Path string ImportPath string AppName string GroupName string PackageName string FunctionName string LeftRouterGroupName string RightRouterGroupName string } tests := []struct { name string fields fields wantErr bool }{ { name: "测试 Gva插件User 回滚", fields: fields{ Type: TypePluginInitializeRouter, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "router.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/router"`, AppName: "Router", GroupName: "User", PackageName: "router", FunctionName: "Init", LeftRouterGroupName: "public", RightRouterGroupName: "private", }, wantErr: false, }, { name: "测试 中文 注入", fields: fields{ Type: TypePluginInitializeRouter, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "router.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/router"`, AppName: "Router", GroupName: "U中文", PackageName: "router", FunctionName: "Init", LeftRouterGroupName: "public", RightRouterGroupName: "private", }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := &PluginInitializeRouter{ Type: tt.fields.Type, Path: tt.fields.Path, ImportPath: tt.fields.ImportPath, AppName: tt.fields.AppName, GroupName: tt.fields.GroupName, PackageName: tt.fields.PackageName, FunctionName: tt.fields.FunctionName, LeftRouterGroupName: tt.fields.LeftRouterGroupName, RightRouterGroupName: tt.fields.RightRouterGroupName, } file, err := a.Parse(a.Path, nil) if err != nil { t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) } a.Rollback(file) err = a.Format(a.Path, nil, file) if (err != nil) != tt.wantErr { t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr) } }) } } ================================================ FILE: server/utils/ast/plugin_initialize_v2.go ================================================ 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) } ================================================ FILE: server/utils/ast/plugin_initialize_v2_test.go ================================================ package ast import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "path/filepath" "testing" ) func TestPluginInitialize_Injection(t *testing.T) { type fields struct { Type Type Path string PluginPath string ImportPath string } tests := []struct { name string fields fields wantErr bool }{ { name: "测试 Gva插件 注册注入", fields: fields{ Type: TypePluginInitializeV2, Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "plugin.go"), PluginPath: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "register.go"), ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva"`, }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := PluginInitializeV2{ Type: tt.fields.Type, Path: tt.fields.Path, PluginPath: tt.fields.PluginPath, ImportPath: tt.fields.ImportPath, } file, err := a.Parse("", 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: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva"`, }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := PluginInitializeV2{ Type: tt.fields.Type, Path: tt.fields.Path, PluginPath: tt.fields.PluginPath, ImportPath: tt.fields.ImportPath, StructName: "Plugin", PackageName: "gva", } file, err := a.Parse("", 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) } }) } } ================================================ FILE: server/utils/autocode/template_funcs.go ================================================ package autocode import ( "fmt" systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "slices" "strings" "text/template" ) // 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 += 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 += ` ` result += `` return result } else if field.DictType != "" { result := fmt.Sprintf(` `, sortAttr, field.FieldDesc, field.FieldJson) result += ` ` result += `` return result } else if field.FieldType == "bool" { result := fmt.Sprintf(` `, sortAttr, field.FieldDesc, field.FieldJson) result += fmt.Sprintf(` `, field.FieldJson) result += `` return result } else if field.FieldType == "time.Time" { result := fmt.Sprintf(` `, sortAttr, field.FieldDesc, field.FieldJson) result += fmt.Sprintf(` `, field.FieldJson) result += `` return result } else if field.FieldType == "picture" { result := fmt.Sprintf(` `, field.FieldDesc, field.FieldJson) result += ` ` result += `` return result } else if field.FieldType == "pictures" { result := fmt.Sprintf(` `, field.FieldDesc, field.FieldJson) result += ` ` result += `` return result } else if field.FieldType == "video" { result := fmt.Sprintf(` `, field.FieldDesc, field.FieldJson) result += ` ` result += `` return result } else if field.FieldType == "richtext" { result := fmt.Sprintf(` `, field.FieldDesc, field.FieldJson) result += ` ` result += `` return result } else if field.FieldType == "file" { result := fmt.Sprintf(` `, field.FieldDesc, field.FieldJson) result += ` ` result += `` return result } else if field.FieldType == "json" { result := fmt.Sprintf(` `, field.FieldDesc, field.FieldJson) result += ` ` result += `` return result } else if field.FieldType == "array" { result := fmt.Sprintf(` `, field.FieldDesc, 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 += ` ` } 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 } ================================================ FILE: server/utils/breakpoint_continue.go ================================================ 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 } ================================================ FILE: server/utils/captcha/redis.go ================================================ package captcha import ( "context" "time" "github.com/flipped-aurora/gin-vue-admin/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 } ================================================ FILE: server/utils/casbin_util.go ================================================ package utils import ( "sync" "github.com/casbin/casbin/v2" "github.com/casbin/casbin/v2/model" gormadapter "github.com/casbin/gorm-adapter/v3" "github.com/flipped-aurora/gin-vue-admin/server/global" "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 } ================================================ FILE: server/utils/claims.go ================================================ package utils import ( "net" "time" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system" systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "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 } ================================================ FILE: server/utils/directory.go ================================================ package utils import ( "errors" "os" "path/filepath" "reflect" "strings" "github.com/flipped-aurora/gin-vue-admin/server/global" "go.uber.org/zap" ) //@author: [piexlmax](https://github.com/piexlmax) //@function: PathExists //@description: 文件目录是否存在 //@param: path string //@return: bool, error func PathExists(path string) (bool, error) { fi, err := os.Stat(path) if err == nil { if fi.IsDir() { return true, nil } return false, errors.New("存在同名文件") } if os.IsNotExist(err) { return false, nil } return false, err } //@author: [piexlmax](https://github.com/piexlmax) //@function: CreateDir //@description: 批量创建文件夹 //@param: dirs ...string //@return: err error func CreateDir(dirs ...string) (err error) { for _, v := range dirs { exist, err := PathExists(v) if err != nil { return err } if !exist { global.GVA_LOG.Debug("create directory" + v) if err := os.MkdirAll(v, os.ModePerm); err != nil { global.GVA_LOG.Error("create directory"+v, zap.Any(" error:", err)) return err } } } return err } //@author: [songzhibin97](https://github.com/songzhibin97) //@function: FileMove //@description: 文件移动供外部调用 //@param: src string, dst string(src: 源位置,绝对路径or相对路径, dst: 目标位置,绝对路径or相对路径,必须为文件夹) //@return: err error func FileMove(src string, dst string) (err error) { if dst == "" { return nil } src, err = filepath.Abs(src) if err != nil { return err } dst, err = filepath.Abs(dst) if err != nil { return err } revoke := false dir := filepath.Dir(dst) Redirect: _, err = os.Stat(dir) if err != nil { err = os.MkdirAll(dir, 0o755) if err != nil { return err } if !revoke { revoke = true goto Redirect } } return os.Rename(src, dst) } func DeLFile(filePath string) error { return os.RemoveAll(filePath) } //@author: [songzhibin97](https://github.com/songzhibin97) //@function: TrimSpace //@description: 去除结构体空格 //@param: target interface (target: 目标结构体,传入必须是指针类型) //@return: null func TrimSpace(target interface{}) { t := reflect.TypeOf(target) if t.Kind() != reflect.Ptr { return } t = t.Elem() v := reflect.ValueOf(target).Elem() for i := 0; i < t.NumField(); i++ { switch v.Field(i).Kind() { case reflect.String: v.Field(i).SetString(strings.TrimSpace(v.Field(i).String())) } } } // FileExist 判断文件是否存在 func FileExist(path string) bool { fi, err := os.Lstat(path) if err == nil { return !fi.IsDir() } return !os.IsNotExist(err) } ================================================ FILE: server/utils/fmt_plus.go ================================================ package utils import ( "fmt" "github.com/flipped-aurora/gin-vue-admin/server/model/common" "math/rand" "reflect" "strings" ) //@author: [piexlmax](https://github.com/piexlmax) //@function: StructToMap //@description: 利用反射将结构体转化为map //@param: obj interface{} //@return: map[string]interface{} func StructToMap(obj interface{}) map[string]interface{} { obj1 := reflect.TypeOf(obj) obj2 := reflect.ValueOf(obj) data := make(map[string]interface{}) for i := 0; i < obj1.NumField(); i++ { if obj1.Field(i).Tag.Get("mapstructure") != "" { data[obj1.Field(i).Tag.Get("mapstructure")] = obj2.Field(i).Interface() } else { data[obj1.Field(i).Name] = obj2.Field(i).Interface() } } return data } //@author: [piexlmax](https://github.com/piexlmax) //@function: ArrayToString //@description: 将数组格式化为字符串 //@param: array []interface{} //@return: string func ArrayToString(array []interface{}) string { return strings.Replace(strings.Trim(fmt.Sprint(array), "[]"), " ", ",", -1) } func Pointer[T any](in T) (out *T) { return &in } func FirstUpper(s string) string { if s == "" { return "" } return strings.ToUpper(s[:1]) + s[1:] } func FirstLower(s string) string { if s == "" { return "" } return strings.ToLower(s[:1]) + s[1:] } // MaheHump 将字符串转换为驼峰命名 func MaheHump(s string) string { words := strings.Split(s, "-") for i := 1; i < len(words); i++ { words[i] = strings.Title(words[i]) } return strings.Join(words, "") } // 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 } ================================================ FILE: server/utils/hash.go ================================================ 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)) } ================================================ FILE: server/utils/human_duration.go ================================================ 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 } ================================================ FILE: server/utils/human_duration_test.go ================================================ 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) } }) } } ================================================ FILE: server/utils/json.go ================================================ 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 } ================================================ FILE: server/utils/json_test.go ================================================ 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) } ================================================ FILE: server/utils/jwt.go ================================================ package utils import ( "context" "errors" "time" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/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 } ================================================ FILE: server/utils/plugin/plugin.go ================================================ package plugin import ( "github.com/gin-gonic/gin" ) const ( OnlyFuncName = "Plugin" ) // Plugin 插件模式接口化 type Plugin interface { // Register 注册路由 Register(group *gin.RouterGroup) // RouterPath 用户返回注册路由 RouterPath() string } ================================================ FILE: server/utils/plugin/v2/plugin.go ================================================ package plugin import ( "github.com/gin-gonic/gin" ) // Plugin 插件模式接口化v2 type Plugin interface { // Register 注册路由 Register(group *gin.Engine) } ================================================ FILE: server/utils/plugin/v2/registry.go ================================================ 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 } ================================================ FILE: server/utils/request/http.go ================================================ 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 } ================================================ FILE: server/utils/server.go ================================================ package utils import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "runtime" "time" "github.com/shirou/gopsutil/v3/cpu" "github.com/shirou/gopsutil/v3/disk" "github.com/shirou/gopsutil/v3/mem" ) const ( B = 1 KB = 1024 * B MB = 1024 * KB GB = 1024 * MB ) type Server struct { Os Os `json:"os"` Cpu Cpu `json:"cpu"` Ram Ram `json:"ram"` Disk []Disk `json:"disk"` } type Os struct { GOOS string `json:"goos"` NumCPU int `json:"numCpu"` Compiler string `json:"compiler"` GoVersion string `json:"goVersion"` NumGoroutine int `json:"numGoroutine"` } type Cpu struct { Cpus []float64 `json:"cpus"` Cores int `json:"cores"` } type Ram struct { UsedMB int `json:"usedMb"` TotalMB int `json:"totalMb"` UsedPercent int `json:"usedPercent"` } type Disk struct { MountPoint string `json:"mountPoint"` UsedMB int `json:"usedMb"` UsedGB int `json:"usedGb"` TotalMB int `json:"totalMb"` TotalGB int `json:"totalGb"` UsedPercent int `json:"usedPercent"` } //@author: [SliverHorn](https://github.com/SliverHorn) //@function: InitCPU //@description: OS信息 //@return: o Os, err error func InitOS() (o Os) { o.GOOS = runtime.GOOS o.NumCPU = runtime.NumCPU() o.Compiler = runtime.Compiler o.GoVersion = runtime.Version() o.NumGoroutine = runtime.NumGoroutine() return o } //@author: [SliverHorn](https://github.com/SliverHorn) //@function: InitCPU //@description: CPU信息 //@return: c Cpu, err error func InitCPU() (c Cpu, err error) { if cores, err := cpu.Counts(false); err != nil { return c, err } else { c.Cores = cores } if cpus, err := cpu.Percent(time.Duration(200)*time.Millisecond, true); err != nil { return c, err } else { c.Cpus = cpus } return c, nil } //@author: [SliverHorn](https://github.com/SliverHorn) //@function: InitRAM //@description: RAM信息 //@return: r Ram, err error func InitRAM() (r Ram, err error) { if u, err := mem.VirtualMemory(); err != nil { return r, err } else { r.UsedMB = int(u.Used) / MB r.TotalMB = int(u.Total) / MB r.UsedPercent = int(u.UsedPercent) } return r, nil } //@author: [SliverHorn](https://github.com/SliverHorn) //@function: InitDisk //@description: 硬盘信息 //@return: d Disk, err error func InitDisk() (d []Disk, err error) { for i := range global.GVA_CONFIG.DiskList { mp := global.GVA_CONFIG.DiskList[i].MountPoint if u, err := disk.Usage(mp); err != nil { return d, err } else { d = append(d, Disk{ MountPoint: mp, UsedMB: int(u.Used) / MB, UsedGB: int(u.Used) / GB, TotalMB: int(u.Total) / MB, TotalGB: int(u.Total) / GB, UsedPercent: int(u.UsedPercent), }) } } return d, nil } ================================================ FILE: server/utils/stacktrace/stacktrace.go ================================================ 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 } ================================================ FILE: server/utils/system_events.go ================================================ 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 } ================================================ FILE: server/utils/timer/timed_task.go ================================================ package timer import ( "github.com/robfig/cron/v3" "sync" ) type Timer interface { // 寻找所有Cron FindCronList() map[string]*taskManager // 添加Task 方法形式以秒的形式加入 AddTaskByFuncWithSecond(cronName string, spec string, fun func(), taskName string, option ...cron.Option) (cron.EntryID, error) // 添加Task Func以秒的形式加入 // 添加Task 接口形式以秒的形式加入 AddTaskByJobWithSeconds(cronName string, spec string, job interface{ Run() }, taskName string, option ...cron.Option) (cron.EntryID, error) // 通过函数的方法添加任务 AddTaskByFunc(cronName string, spec string, task func(), taskName string, option ...cron.Option) (cron.EntryID, error) // 通过接口的方法添加任务 要实现一个带有 Run方法的接口触发 AddTaskByJob(cronName string, spec string, job interface{ Run() }, taskName string, option ...cron.Option) (cron.EntryID, error) // 获取对应taskName的cron 可能会为空 FindCron(cronName string) (*taskManager, bool) // 指定cron开始执行 StartCron(cronName string) // 指定cron停止执行 StopCron(cronName string) // 查找指定cron下的指定task FindTask(cronName string, taskName string) (*task, bool) // 根据id删除指定cron下的指定task RemoveTask(cronName string, id int) // 根据taskName删除指定cron下的指定task RemoveTaskByName(cronName string, taskName string) // 清理掉指定cronName Clear(cronName string) // 停止所有的cron Close() } type task struct { EntryID cron.EntryID Spec string TaskName string } type taskManager struct { corn *cron.Cron tasks map[cron.EntryID]*task } // timer 定时任务管理 type timer struct { cronList map[string]*taskManager sync.Mutex } // AddTaskByFunc 通过函数的方法添加任务 func (t *timer) AddTaskByFunc(cronName string, spec string, fun func(), taskName string, option ...cron.Option) (cron.EntryID, error) { t.Lock() defer t.Unlock() if _, ok := t.cronList[cronName]; !ok { tasks := make(map[cron.EntryID]*task) t.cronList[cronName] = &taskManager{ corn: cron.New(option...), tasks: tasks, } } id, err := t.cronList[cronName].corn.AddFunc(spec, fun) t.cronList[cronName].corn.Start() t.cronList[cronName].tasks[id] = &task{ EntryID: id, Spec: spec, TaskName: taskName, } return id, err } // AddTaskByFuncWithSecond 通过函数的方法使用WithSeconds添加任务 func (t *timer) AddTaskByFuncWithSecond(cronName string, spec string, fun func(), taskName string, option ...cron.Option) (cron.EntryID, error) { t.Lock() defer t.Unlock() option = append(option, cron.WithSeconds()) if _, ok := t.cronList[cronName]; !ok { tasks := make(map[cron.EntryID]*task) t.cronList[cronName] = &taskManager{ corn: cron.New(option...), tasks: tasks, } } id, err := t.cronList[cronName].corn.AddFunc(spec, fun) t.cronList[cronName].corn.Start() t.cronList[cronName].tasks[id] = &task{ EntryID: id, Spec: spec, TaskName: taskName, } return id, err } // AddTaskByJob 通过接口的方法添加任务 func (t *timer) AddTaskByJob(cronName string, spec string, job interface{ Run() }, taskName string, option ...cron.Option) (cron.EntryID, error) { t.Lock() defer t.Unlock() if _, ok := t.cronList[cronName]; !ok { tasks := make(map[cron.EntryID]*task) t.cronList[cronName] = &taskManager{ corn: cron.New(option...), tasks: tasks, } } id, err := t.cronList[cronName].corn.AddJob(spec, job) t.cronList[cronName].corn.Start() t.cronList[cronName].tasks[id] = &task{ EntryID: id, Spec: spec, TaskName: taskName, } return id, err } // AddTaskByJobWithSeconds 通过接口的方法添加任务 func (t *timer) AddTaskByJobWithSeconds(cronName string, spec string, job interface{ Run() }, taskName string, option ...cron.Option) (cron.EntryID, error) { t.Lock() defer t.Unlock() option = append(option, cron.WithSeconds()) if _, ok := t.cronList[cronName]; !ok { tasks := make(map[cron.EntryID]*task) t.cronList[cronName] = &taskManager{ corn: cron.New(option...), tasks: tasks, } } id, err := t.cronList[cronName].corn.AddJob(spec, job) t.cronList[cronName].corn.Start() t.cronList[cronName].tasks[id] = &task{ EntryID: id, Spec: spec, TaskName: taskName, } return id, err } // FindCron 获取对应cronName的cron 可能会为空 func (t *timer) FindCron(cronName string) (*taskManager, bool) { t.Lock() defer t.Unlock() v, ok := t.cronList[cronName] return v, ok } // FindTask 获取对应cronName的cron 可能会为空 func (t *timer) FindTask(cronName string, taskName string) (*task, bool) { t.Lock() defer t.Unlock() v, ok := t.cronList[cronName] if !ok { return nil, ok } for _, t2 := range v.tasks { if t2.TaskName == taskName { return t2, true } } return nil, false } // FindCronList 获取所有的任务列表 func (t *timer) FindCronList() map[string]*taskManager { t.Lock() defer t.Unlock() return t.cronList } // StartCron 开始任务 func (t *timer) StartCron(cronName string) { t.Lock() defer t.Unlock() if v, ok := t.cronList[cronName]; ok { v.corn.Start() } } // StopCron 停止任务 func (t *timer) StopCron(cronName string) { t.Lock() defer t.Unlock() if v, ok := t.cronList[cronName]; ok { v.corn.Stop() } } // RemoveTask 从cronName 删除指定任务 func (t *timer) RemoveTask(cronName string, id int) { t.Lock() defer t.Unlock() if v, ok := t.cronList[cronName]; ok { v.corn.Remove(cron.EntryID(id)) delete(v.tasks, cron.EntryID(id)) } } // RemoveTaskByName 从cronName 使用taskName 删除指定任务 func (t *timer) RemoveTaskByName(cronName string, taskName string) { fTask, ok := t.FindTask(cronName, taskName) if !ok { return } t.RemoveTask(cronName, int(fTask.EntryID)) } // Clear 清除任务 func (t *timer) Clear(cronName string) { t.Lock() defer t.Unlock() if v, ok := t.cronList[cronName]; ok { v.corn.Stop() delete(t.cronList, cronName) } } // Close 释放资源 func (t *timer) Close() { t.Lock() defer t.Unlock() for _, v := range t.cronList { v.corn.Stop() } } func NewTimerTask() Timer { return &timer{cronList: make(map[string]*taskManager)} } ================================================ FILE: server/utils/timer/timed_task_test.go ================================================ 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) } } ================================================ FILE: server/utils/upload/aliyun_oss.go ================================================ package upload import ( "errors" "mime/multipart" "time" "github.com/aliyun/aliyun-oss-go-sdk/oss" "github.com/flipped-aurora/gin-vue-admin/server/global" "go.uber.org/zap" ) type AliyunOSS struct{} func (*AliyunOSS) UploadFile(file *multipart.FileHeader) (string, string, error) { bucket, err := NewBucket() if err != nil { global.GVA_LOG.Error("function AliyunOSS.NewBucket() Failed", zap.Any("err", err.Error())) return "", "", errors.New("function AliyunOSS.NewBucket() Failed, err:" + err.Error()) } // 读取本地文件。 f, openError := file.Open() if openError != nil { global.GVA_LOG.Error("function file.Open() Failed", zap.Any("err", openError.Error())) return "", "", errors.New("function file.Open() Failed, err:" + openError.Error()) } defer f.Close() // 创建文件 defer 关闭 // 上传阿里云路径 文件名格式 自己可以改 建议保证唯一性 // yunFileTmpPath := filepath.Join("uploads", time.Now().Format("2006-01-02")) + "/" + file.Filename yunFileTmpPath := global.GVA_CONFIG.AliyunOSS.BasePath + "/" + "uploads" + "/" + time.Now().Format("2006-01-02") + "/" + file.Filename // 上传文件流。 err = bucket.PutObject(yunFileTmpPath, f) if err != nil { global.GVA_LOG.Error("function formUploader.Put() Failed", zap.Any("err", err.Error())) return "", "", errors.New("function formUploader.Put() Failed, err:" + err.Error()) } return global.GVA_CONFIG.AliyunOSS.BucketUrl + "/" + yunFileTmpPath, yunFileTmpPath, nil } func (*AliyunOSS) DeleteFile(key string) error { bucket, err := NewBucket() if err != nil { global.GVA_LOG.Error("function AliyunOSS.NewBucket() Failed", zap.Any("err", err.Error())) return errors.New("function AliyunOSS.NewBucket() Failed, err:" + err.Error()) } // 删除单个文件。objectName表示删除OSS文件时需要指定包含文件后缀在内的完整路径,例如abc/efg/123.jpg。 // 如需删除文件夹,请将objectName设置为对应的文件夹名称。如果文件夹非空,则需要将文件夹下的所有object删除后才能删除该文件夹。 err = bucket.DeleteObject(key) if err != nil { global.GVA_LOG.Error("function bucketManager.Delete() failed", zap.Any("err", err.Error())) return errors.New("function bucketManager.Delete() failed, err:" + err.Error()) } return nil } func NewBucket() (*oss.Bucket, error) { // 创建OSSClient实例。 client, err := oss.New(global.GVA_CONFIG.AliyunOSS.Endpoint, global.GVA_CONFIG.AliyunOSS.AccessKeyId, global.GVA_CONFIG.AliyunOSS.AccessKeySecret) if err != nil { return nil, err } // 获取存储空间。 bucket, err := client.Bucket(global.GVA_CONFIG.AliyunOSS.BucketName) if err != nil { return nil, err } return bucket, nil } ================================================ FILE: server/utils/upload/aws_s3.go ================================================ package upload import ( "context" "errors" "fmt" "mime/multipart" "strings" "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/feature/s3/manager" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/flipped-aurora/gin-vue-admin/server/global" "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-v2. See https://docs.aws.amazon.com/sdk-for-go/v2/developer-guide/s3-example-basic-bucket-operations.html //@param: file *multipart.FileHeader //@return: string, string, error func (*AwsS3) UploadFile(file *multipart.FileHeader) (string, string, error) { client := newS3Client() uploader := manager.NewUploader(client) 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(context.TODO(), &s3.PutObjectInput{ 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-v2. See https://docs.aws.amazon.com/sdk-for-go/v2/developer-guide/s3-example-basic-bucket-operations.html //@param: key string //@return: error func (*AwsS3) DeleteFile(key string) error { client := newS3Client() filename := global.GVA_CONFIG.AwsS3.PathPrefix + "/" + key bucket := global.GVA_CONFIG.AwsS3.Bucket _, err := client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{ Bucket: aws.String(bucket), Key: aws.String(filename), }) if err != nil { global.GVA_LOG.Error("function client.DeleteObject() failed", zap.Any("err", err.Error())) return errors.New("function client.DeleteObject() failed, err:" + err.Error()) } waiter := s3.NewObjectNotExistsWaiter(client) _ = waiter.Wait(context.TODO(), &s3.HeadObjectInput{ Bucket: aws.String(bucket), Key: aws.String(filename), }, 30*time.Second) return nil } // newS3Client creates an S3 v2 client with static credentials and optional custom endpoint. // minio在这里设置Endpoint地址,可以兼容 func newS3Client() *s3.Client { cfg := global.GVA_CONFIG.AwsS3 awsCfg, _ := config.LoadDefaultConfig(context.TODO(), config.WithRegion(cfg.Region), config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider( cfg.SecretID, cfg.SecretKey, "", )), ) return s3.NewFromConfig(awsCfg, func(o *s3.Options) { if cfg.Endpoint != "" { endpoint := cfg.Endpoint if !strings.HasPrefix(endpoint, "http://") && !strings.HasPrefix(endpoint, "https://") { if cfg.DisableSSL { endpoint = "http://" + endpoint } else { endpoint = "https://" + endpoint } } o.BaseEndpoint = aws.String(endpoint) } o.UsePathStyle = cfg.S3ForcePathStyle }) } ================================================ FILE: server/utils/upload/cloudflare_r2.go ================================================ package upload import ( "context" "errors" "fmt" "mime/multipart" "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/feature/s3/manager" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/flipped-aurora/gin-vue-admin/server/global" "go.uber.org/zap" ) type CloudflareR2 struct{} func (c *CloudflareR2) UploadFile(file *multipart.FileHeader) (fileUrl string, fileName string, err error) { client := c.newR2Client() uploader := manager.NewUploader(client) 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 关闭 _, err = uploader.Upload(context.TODO(), &s3.PutObjectInput{ Bucket: aws.String(global.GVA_CONFIG.CloudflareR2.Bucket), Key: aws.String(fileName), Body: f, }) if err != nil { global.GVA_LOG.Error("function uploader.Upload() failed", zap.Any("err", err.Error())) return "", "", err } return fmt.Sprintf("%s/%s", global.GVA_CONFIG.CloudflareR2.BaseURL, fileName), fileKey, nil } func (c *CloudflareR2) DeleteFile(key string) error { client := c.newR2Client() filename := global.GVA_CONFIG.CloudflareR2.Path + "/" + key bucket := global.GVA_CONFIG.CloudflareR2.Bucket _, err := client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{ Bucket: aws.String(bucket), Key: aws.String(filename), }) if err != nil { global.GVA_LOG.Error("function client.DeleteObject() failed", zap.Any("err", err.Error())) return errors.New("function client.DeleteObject() failed, err:" + err.Error()) } waiter := s3.NewObjectNotExistsWaiter(client) _ = waiter.Wait(context.TODO(), &s3.HeadObjectInput{ Bucket: aws.String(bucket), Key: aws.String(filename), }, 30*time.Second) return nil } func (*CloudflareR2) newR2Client() *s3.Client { endpoint := fmt.Sprintf("https://%s.r2.cloudflarestorage.com", global.GVA_CONFIG.CloudflareR2.AccountID) cfg, _ := config.LoadDefaultConfig(context.TODO(), config.WithRegion("auto"), config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider( global.GVA_CONFIG.CloudflareR2.AccessKeyID, global.GVA_CONFIG.CloudflareR2.SecretAccessKey, "", )), ) return s3.NewFromConfig(cfg, func(o *s3.Options) { o.BaseEndpoint = aws.String(endpoint) }) } ================================================ FILE: server/utils/upload/local.go ================================================ package upload import ( "errors" "io" "mime/multipart" "os" "path/filepath" "strings" "sync" "time" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/utils" "go.uber.org/zap" ) var mu sync.Mutex type Local struct{} //@author: [piexlmax](https://github.com/piexlmax) //@author: [ccfish86](https://github.com/ccfish86) //@author: [SliverHorn](https://github.com/SliverHorn) //@object: *Local //@function: UploadFile //@description: 上传文件 //@param: file *multipart.FileHeader //@return: string, string, error func (*Local) UploadFile(file *multipart.FileHeader) (string, string, error) { // 读取文件后缀 ext := filepath.Ext(file.Filename) // 读取文件名并加密 name := strings.TrimSuffix(file.Filename, ext) name = utils.MD5V([]byte(name)) // 拼接新文件名 filename := name + "_" + time.Now().Format("20060102150405") + ext // 尝试创建此路径 mkdirErr := os.MkdirAll(global.GVA_CONFIG.Local.StorePath, os.ModePerm) if mkdirErr != nil { global.GVA_LOG.Error("function os.MkdirAll() failed", zap.Any("err", mkdirErr.Error())) return "", "", errors.New("function os.MkdirAll() failed, err:" + mkdirErr.Error()) } // 拼接路径和文件名 p := global.GVA_CONFIG.Local.StorePath + "/" + filename filepath := global.GVA_CONFIG.Local.Path + "/" + filename f, openError := file.Open() // 读取文件 if openError != nil { global.GVA_LOG.Error("function file.Open() failed", zap.Any("err", openError.Error())) return "", "", errors.New("function file.Open() failed, err:" + openError.Error()) } defer f.Close() // 创建文件 defer 关闭 out, createErr := os.Create(p) if createErr != nil { global.GVA_LOG.Error("function os.Create() failed", zap.Any("err", createErr.Error())) return "", "", errors.New("function os.Create() failed, err:" + createErr.Error()) } defer out.Close() // 创建文件 defer 关闭 _, copyErr := io.Copy(out, f) // 传输(拷贝)文件 if copyErr != nil { global.GVA_LOG.Error("function io.Copy() failed", zap.Any("err", copyErr.Error())) return "", "", errors.New("function io.Copy() failed, err:" + copyErr.Error()) } return filepath, filename, nil } //@author: [piexlmax](https://github.com/piexlmax) //@author: [ccfish86](https://github.com/ccfish86) //@author: [SliverHorn](https://github.com/SliverHorn) //@object: *Local //@function: DeleteFile //@description: 删除文件 //@param: key string //@return: error func (*Local) DeleteFile(key string) error { // 检查 key 是否为空 if key == "" { return errors.New("key不能为空") } // 验证 key 是否包含非法字符或尝试访问存储路径之外的文件 if strings.Contains(key, "..") || strings.ContainsAny(key, `\/:*?"<>|`) { return errors.New("非法的key") } p := filepath.Join(global.GVA_CONFIG.Local.StorePath, key) // 检查文件是否存在 if _, err := os.Stat(p); os.IsNotExist(err) { return errors.New("文件不存在") } // 使用文件锁防止并发删除 mu.Lock() defer mu.Unlock() err := os.Remove(p) if err != nil { return errors.New("文件删除失败: " + err.Error()) } return nil } ================================================ FILE: server/utils/upload/minio_oss.go ================================================ package upload import ( "bytes" "context" "errors" "io" "mime" "mime/multipart" "path/filepath" "strings" "time" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" "go.uber.org/zap" ) var MinioClient *Minio // 优化性能,但是不支持动态配置 type Minio struct { Client *minio.Client bucket string } func GetMinio(endpoint, accessKeyID, secretAccessKey, bucketName string, useSSL bool) (*Minio, error) { if MinioClient != nil { return MinioClient, nil } // Initialize minio client object. minioClient, err := minio.New(endpoint, &minio.Options{ Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""), Secure: useSSL, // Set to true if using https }) if err != nil { return nil, err } // 尝试创建bucket err = minioClient.MakeBucket(context.Background(), bucketName, minio.MakeBucketOptions{}) if err != nil { // Check to see if we already own this bucket (which happens if you run this twice) exists, errBucketExists := minioClient.BucketExists(context.Background(), bucketName) if errBucketExists == nil && exists { // log.Printf("We already own %s\n", bucketName) } else { return nil, err } } MinioClient = &Minio{Client: minioClient, bucket: bucketName} return MinioClient, nil } func (m *Minio) UploadFile(file *multipart.FileHeader) (filePathres, key string, uploadErr error) { f, openError := file.Open() // mutipart.File to os.File if openError != nil { global.GVA_LOG.Error("function file.Open() Failed", zap.Any("err", openError.Error())) return "", "", errors.New("function file.Open() Failed, err:" + openError.Error()) } filecontent := bytes.Buffer{} _, err := io.Copy(&filecontent, f) if err != nil { global.GVA_LOG.Error("读取文件失败", zap.Any("err", err.Error())) return "", "", errors.New("读取文件失败, err:" + err.Error()) } f.Close() // 创建文件 defer 关闭 // 对文件名进行加密存储 ext := filepath.Ext(file.Filename) filename := utils.MD5V([]byte(strings.TrimSuffix(file.Filename, ext))) + ext if global.GVA_CONFIG.Minio.BasePath == "" { filePathres = "uploads" + "/" + time.Now().Format("2006-01-02") + "/" + filename } else { filePathres = global.GVA_CONFIG.Minio.BasePath + "/" + time.Now().Format("2006-01-02") + "/" + filename } // 根据文件扩展名检测 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 } ================================================ FILE: server/utils/upload/obs.go ================================================ package upload import ( "mime/multipart" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/huaweicloud/huaweicloud-sdk-go-obs/obs" "github.com/pkg/errors" ) var HuaWeiObs = new(Obs) type Obs struct{} func NewHuaWeiObsClient() (client *obs.ObsClient, err error) { return obs.New(global.GVA_CONFIG.HuaWeiObs.AccessKey, global.GVA_CONFIG.HuaWeiObs.SecretKey, global.GVA_CONFIG.HuaWeiObs.Endpoint) } func (o *Obs) UploadFile(file *multipart.FileHeader) (string, string, error) { // var open multipart.File open, err := file.Open() if err != nil { return "", "", err } defer open.Close() filename := file.Filename input := &obs.PutObjectInput{ PutObjectBasicInput: obs.PutObjectBasicInput{ ObjectOperationInput: obs.ObjectOperationInput{ Bucket: global.GVA_CONFIG.HuaWeiObs.Bucket, Key: filename, }, HttpHeader: obs.HttpHeader{ ContentType: file.Header.Get("content-type"), }, }, Body: open, } var client *obs.ObsClient client, err = NewHuaWeiObsClient() if err != nil { return "", "", errors.Wrap(err, "获取华为对象存储对象失败!") } _, err = client.PutObject(input) if err != nil { return "", "", errors.Wrap(err, "文件上传失败!") } filepath := global.GVA_CONFIG.HuaWeiObs.Path + "/" + filename return filepath, filename, err } func (o *Obs) DeleteFile(key string) error { client, err := NewHuaWeiObsClient() if err != nil { return errors.Wrap(err, "获取华为对象存储对象失败!") } input := &obs.DeleteObjectInput{ Bucket: global.GVA_CONFIG.HuaWeiObs.Bucket, Key: key, } var output *obs.DeleteObjectOutput output, err = client.DeleteObject(input) if err != nil { return errors.Wrapf(err, "删除对象(%s)失败!, output: %v", key, output) } return nil } ================================================ FILE: server/utils/upload/qiniu.go ================================================ package upload import ( "context" "errors" "fmt" "mime/multipart" "time" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/qiniu/go-sdk/v7/auth/qbox" "github.com/qiniu/go-sdk/v7/storage" "go.uber.org/zap" ) type Qiniu struct{} //@author: [piexlmax](https://github.com/piexlmax) //@author: [ccfish86](https://github.com/ccfish86) //@author: [SliverHorn](https://github.com/SliverHorn) //@object: *Qiniu //@function: UploadFile //@description: 上传文件 //@param: file *multipart.FileHeader //@return: string, string, error func (*Qiniu) UploadFile(file *multipart.FileHeader) (string, string, error) { putPolicy := storage.PutPolicy{Scope: global.GVA_CONFIG.Qiniu.Bucket} mac := qbox.NewMac(global.GVA_CONFIG.Qiniu.AccessKey, global.GVA_CONFIG.Qiniu.SecretKey) upToken := putPolicy.UploadToken(mac) cfg := qiniuConfig() formUploader := storage.NewFormUploader(cfg) ret := storage.PutRet{} putExtra := storage.PutExtra{Params: map[string]string{"x:name": "github logo"}} f, openError := file.Open() if openError != nil { global.GVA_LOG.Error("function file.Open() failed", zap.Any("err", openError.Error())) return "", "", errors.New("function file.Open() failed, err:" + openError.Error()) } defer f.Close() // 创建文件 defer 关闭 fileKey := fmt.Sprintf("%d%s", time.Now().Unix(), file.Filename) // 文件名格式 自己可以改 建议保证唯一性 putErr := formUploader.Put(context.Background(), &ret, upToken, fileKey, f, file.Size, &putExtra) if putErr != nil { global.GVA_LOG.Error("function formUploader.Put() failed", zap.Any("err", putErr.Error())) return "", "", errors.New("function formUploader.Put() failed, err:" + putErr.Error()) } return global.GVA_CONFIG.Qiniu.ImgPath + "/" + ret.Key, ret.Key, nil } //@author: [piexlmax](https://github.com/piexlmax) //@author: [ccfish86](https://github.com/ccfish86) //@author: [SliverHorn](https://github.com/SliverHorn) //@object: *Qiniu //@function: DeleteFile //@description: 删除文件 //@param: key string //@return: error func (*Qiniu) DeleteFile(key string) error { mac := qbox.NewMac(global.GVA_CONFIG.Qiniu.AccessKey, global.GVA_CONFIG.Qiniu.SecretKey) cfg := qiniuConfig() bucketManager := storage.NewBucketManager(mac, cfg) if err := bucketManager.Delete(global.GVA_CONFIG.Qiniu.Bucket, key); err != nil { global.GVA_LOG.Error("function bucketManager.Delete() failed", zap.Any("err", err.Error())) return errors.New("function bucketManager.Delete() failed, err:" + err.Error()) } return nil } //@author: [SliverHorn](https://github.com/SliverHorn) //@object: *Qiniu //@function: qiniuConfig //@description: 根据配置文件进行返回七牛云的配置 //@return: *storage.Config func qiniuConfig() *storage.Config { cfg := storage.Config{ UseHTTPS: global.GVA_CONFIG.Qiniu.UseHTTPS, UseCdnDomains: global.GVA_CONFIG.Qiniu.UseCdnDomains, } switch global.GVA_CONFIG.Qiniu.Zone { // 根据配置文件进行初始化空间对应的机房 case "ZoneHuadong": cfg.Zone = &storage.ZoneHuadong case "ZoneHuabei": cfg.Zone = &storage.ZoneHuabei case "ZoneHuanan": cfg.Zone = &storage.ZoneHuanan case "ZoneBeimei": cfg.Zone = &storage.ZoneBeimei case "ZoneXinjiapo": cfg.Zone = &storage.ZoneXinjiapo } return &cfg } ================================================ FILE: server/utils/upload/tencent_cos.go ================================================ package upload import ( "context" "errors" "fmt" "mime/multipart" "net/http" "net/url" "time" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/tencentyun/cos-go-sdk-v5" "go.uber.org/zap" ) type TencentCOS struct{} // UploadFile upload file to COS func (*TencentCOS) UploadFile(file *multipart.FileHeader) (string, string, error) { client := NewClient() f, openError := file.Open() if openError != nil { global.GVA_LOG.Error("function file.Open() failed", zap.Any("err", openError.Error())) return "", "", errors.New("function file.Open() failed, err:" + openError.Error()) } defer f.Close() // 创建文件 defer 关闭 fileKey := fmt.Sprintf("%d%s", time.Now().Unix(), file.Filename) _, err := client.Object.Put(context.Background(), global.GVA_CONFIG.TencentCOS.PathPrefix+"/"+fileKey, f, nil) if err != nil { panic(err) } return global.GVA_CONFIG.TencentCOS.BaseURL + "/" + global.GVA_CONFIG.TencentCOS.PathPrefix + "/" + fileKey, fileKey, nil } // DeleteFile delete file form COS func (*TencentCOS) DeleteFile(key string) error { client := NewClient() name := global.GVA_CONFIG.TencentCOS.PathPrefix + "/" + key _, err := client.Object.Delete(context.Background(), name) if err != nil { global.GVA_LOG.Error("function bucketManager.Delete() failed", zap.Any("err", err.Error())) return errors.New("function bucketManager.Delete() failed, err:" + err.Error()) } return nil } // NewClient init COS client func NewClient() *cos.Client { urlStr, _ := url.Parse("https://" + global.GVA_CONFIG.TencentCOS.Bucket + ".cos." + global.GVA_CONFIG.TencentCOS.Region + ".myqcloud.com") baseURL := &cos.BaseURL{BucketURL: urlStr} client := cos.NewClient(baseURL, &http.Client{ Transport: &cos.AuthorizationTransport{ SecretID: global.GVA_CONFIG.TencentCOS.SecretID, SecretKey: global.GVA_CONFIG.TencentCOS.SecretKey, }, }) return client } ================================================ FILE: server/utils/upload/upload.go ================================================ package upload import ( "mime/multipart" "github.com/flipped-aurora/gin-vue-admin/server/global" ) // OSS 对象存储接口 // Author [SliverHorn](https://github.com/SliverHorn) // Author [ccfish86](https://github.com/ccfish86) type OSS interface { UploadFile(file *multipart.FileHeader) (string, string, error) DeleteFile(key string) error } // NewOss OSS的实例化方法 // Author [SliverHorn](https://github.com/SliverHorn) // Author [ccfish86](https://github.com/ccfish86) func NewOss() OSS { switch global.GVA_CONFIG.System.OssType { case "local": return &Local{} case "qiniu": return &Qiniu{} case "tencent-cos": return &TencentCOS{} case "aliyun-oss": return &AliyunOSS{} case "huawei-obs": return HuaWeiObs case "aws-s3": return &AwsS3{} case "cloudflare-r2": return &CloudflareR2{} case "minio": minioClient, err := GetMinio(global.GVA_CONFIG.Minio.Endpoint, global.GVA_CONFIG.Minio.AccessKeyId, global.GVA_CONFIG.Minio.AccessKeySecret, global.GVA_CONFIG.Minio.BucketName, global.GVA_CONFIG.Minio.UseSSL) if err != nil { global.GVA_LOG.Warn("你配置了使用minio,但是初始化失败,请检查minio可用性或安全配置: " + err.Error()) panic("minio初始化失败") // 建议这样做,用户自己配置了minio,如果报错了还要把服务开起来,使用起来也很危险 } return minioClient default: return &Local{} } } ================================================ FILE: server/utils/validator.go ================================================ 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) } ================================================ FILE: server/utils/validator_test.go ================================================ package utils import ( "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" "testing" ) type PageInfoTest struct { PageInfo request.PageInfo Name string } func TestVerify(t *testing.T) { PageInfoVerify := Rules{"Page": {NotEmpty()}, "PageSize": {NotEmpty()}, "Name": {NotEmpty()}} var testInfo PageInfoTest testInfo.Name = "test" testInfo.PageInfo.Page = 0 testInfo.PageInfo.PageSize = 0 err := Verify(testInfo, PageInfoVerify) if err == nil { t.Error("校验失败,未能捕捉0值") } testInfo.Name = "" testInfo.PageInfo.Page = 1 testInfo.PageInfo.PageSize = 10 err = Verify(testInfo, PageInfoVerify) if err == nil { t.Error("校验失败,未能正常检测name为空") } testInfo.Name = "test" testInfo.PageInfo.Page = 1 testInfo.PageInfo.PageSize = 10 err = Verify(testInfo, PageInfoVerify) if err != nil { t.Error("校验失败,未能正常通过检测") } } ================================================ FILE: server/utils/verify.go ================================================ 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()}} ) ================================================ FILE: server/utils/zip.go ================================================ 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 } ================================================ FILE: web/.docker-compose/nginx/conf.d/my.conf ================================================ server { listen 8080; server_name localhost; #charset koi8-r; #access_log logs/host.access.log main; location / { root /usr/share/nginx/html; add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0'; try_files $uri $uri/ /index.html; } location /api { proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; rewrite ^/api/(.*)$ /$1 break; #重写 proxy_pass http://177.7.0.12:8888; # 设置代理服务器的协议和地址 } location /api/swagger/index.html { proxy_pass http://127.0.0.1:8888/swagger/index.html; } } ================================================ FILE: web/.docker-compose/nginx/conf.d/nginx.conf ================================================ server { listen 80; server_name localhost; #charset koi8-r; #access_log logs/host.access.log main; location / { root /usr/share/nginx/html/dist; add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0'; try_files $uri $uri/ /index.html; } location /api { proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; rewrite ^/api/(.*)$ /$1 break; #重写 proxy_pass http://127.0.0.1:8888; # 设置代理服务器的协议和地址 } location /form-generator { proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_pass http://127.0.0.1:8888; } location /api/swagger/index.html { proxy_pass http://127.0.0.1:8888/swagger/index.html; } } ================================================ FILE: web/.dockerignore ================================================ node_modules/ ================================================ FILE: web/.gitignore ================================================ node_modules/* package-lock.json yarn.lock bun.lockb config.yaml ================================================ FILE: web/.prettierrc ================================================ { "printWidth": 80, "tabWidth": 2, "useTabs": false, "semi": false, "singleQuote": true, "trailingComma": "none", "bracketSpacing": true, "arrowParens": "always", "vueIndentScriptAndStyle": true, "endOfLine": "lf" } ================================================ FILE: web/Dockerfile ================================================ # 如果需要用 cicd ,请设置环境变量: # variables: # DOCKER_BUILDKIT: 1 FROM node:20-slim AS base ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" RUN corepack enable COPY . /app WORKDIR /app FROM base AS prod-deps RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod FROM base AS build COPY --from=prod-deps /app/node_modules /app/node_modules RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install && pnpm run build FROM nginx:alpine LABEL MAINTAINER="bypanghu@163.com" COPY --from=base /app/.docker-compose/nginx/conf.d/my.conf /etc/nginx/conf.d/my.conf COPY --from=build /app/dist /usr/share/nginx/html RUN ls -al /usr/share/nginx/html ================================================ FILE: web/README.md ================================================ # gin-vue-admin web ## Project setup ``` npm install ``` ### Compiles and hot-reloads for development ``` npm run serve ``` ### Compiles and minifies for production ``` npm run build ``` ### Run your tests ``` npm run test ``` ### Lints and fixes files ``` npm run lint ``` 整理代码结构 ```lua web ├── babel.config.js ├── Dockerfile ├── favicon.ico ├── index.html -- 主页面 ├── limit.js -- 助手代码 ├── package.json -- 包管理器代码 ├── src -- 源代码 │ ├── api -- api 组 │ ├── App.vue -- 主页面 │ ├── assets -- 静态资源 │ ├── components -- 全局组件 │ ├── core -- gva 组件包 │ │ ├── config.js -- gva网站配置文件 │ │ ├── gin-vue-admin.js -- 注册欢迎文件 │ │ └── global.js -- 统一导入文件 │ ├── directive -- v-auth 注册文件 │ ├── main.js -- 主文件 │ ├── permission.js -- 路由中间件 │ ├── pinia -- pinia 状态管理器,取代vuex │ │ ├── index.js -- 入口文件 │ │ └── modules -- modules │ │ ├── dictionary.js │ │ ├── router.js │ │ └── user.js │ ├── router -- 路由声明文件 │ │ └── index.js │ ├── style -- 全局样式 │ │ ├── base.scss │ │ ├── basics.scss │ │ ├── element_visiable.scss -- 此处可以全局覆盖 element-plus 样式 │ │ ├── iconfont.css -- 顶部几个icon的样式文件 │ │ ├── main.scss │ │ ├── mobile.scss │ │ └── newLogin.scss │ ├── utils -- 方法包库 │ │ ├── asyncRouter.js -- 动态路由相关 │ │ ├── bus.js -- 全局mitt声明文件 │ │ ├── date.js -- 日期相关 │ │ ├── dictionary.js -- 获取字典方法 │ │ ├── downloadImg.js -- 下载图片方法 │ │ ├── format.js -- 格式整理相关 │ │ ├── image.js -- 图片相关方法 │ │ ├── page.js -- 设置页面标题 │ │ ├── request.js -- 请求 │ │ └── stringFun.js -- 字符串文件 | ├── view -- 主要view代码 | | ├── about -- 关于我们 | | ├── dashboard -- 面板 | | ├── error -- 错误 | | ├── example --上传案例 | | ├── iconList -- icon列表 | | ├── init -- 初始化数据 | | | ├── index -- 新版本 | | | ├── init -- 旧版本 | | ├── layout -- layout约束页面 | | | ├── aside | | | ├── bottomInfo -- bottomInfo | | | ├── screenfull -- 全屏设置 | | | ├── setting -- 系统设置 | | | └── index.vue -- base 约束 | | ├── login --登录 | | ├── person --个人中心 | | ├── superAdmin -- 超级管理员操作 | | ├── system -- 系统检测页面 | | ├── systemTools -- 系统配置相关页面 | | └── routerHolder.vue -- page 入口页面 ├── vite.config.js -- vite 配置文件 └── yarn.lock ``` ================================================ FILE: web/babel.config.js ================================================ module.exports = { presets: [], plugins: [] } ================================================ FILE: web/eslint.config.mjs ================================================ import js from '@eslint/js' import pluginVue from 'eslint-plugin-vue' import globals from 'globals' export default [ js.configs.recommended, ...pluginVue.configs['flat/essential'], { name: 'app/files-to-lint', files: ['**/*.{js,mjs,jsx,vue}'], languageOptions: { ecmaVersion: 'latest', sourceType: 'module', globals: globals.node }, rules: { 'vue/max-attributes-per-line': 0, 'vue/no-v-model-argument': 0, 'vue/multi-word-component-names': 'off', 'no-lone-blocks': 'off', 'no-extend-native': 'off', 'no-unused-vars': ['error', { argsIgnorePattern: '^_' }] } }, { name: 'app/files-to-ignore', ignores: ['**/dist/**', '**/build/*.js', '**/src/assets/**', '**/public/**'] } ] ================================================ FILE: web/index.html ================================================
系统正在加载中,请稍候...
================================================ FILE: web/jsconfig.json ================================================ { "compilerOptions": { "baseUrl": "./", "paths": { "@/*": ["src/*"] } }, "exclude": ["node_modules", "dist"], "include": ["src/**/*"] } ================================================ FILE: web/limit.js ================================================ // 运行项目前通过node执行此脚本 (此脚本与 node_modules 目录同级) import fs from 'fs' import path from 'path' const wfPath = path.resolve(__dirname, './node_modules/.bin') fs.readdir(wfPath, (err, files) => { if (err) { console.log(err) } else { if (files.length !== 0) { files.forEach((item) => { if (item.split('.')[1] === 'cmd') { replaceStr(`${wfPath}/${item}`, /"%_prog%"/, '%_prog%') } }) } } }) // 参数:[文件路径、 需要修改的字符串、修改后的字符串] (替换对应文件内字符串的公共函数) function replaceStr(filePath, sourceRegx, targetSrt) { fs.readFile(filePath, (err, data) => { if (err) { console.log(err) } else { let str = data.toString() str = str.replace(sourceRegx, targetSrt) fs.writeFile(filePath, str, (err) => { if (err) { console.log(err) } else { console.log('\x1B[42m%s\x1B[0m', '文件修改成功') } }) } }) } ================================================ FILE: web/openDocument.js ================================================ /* 此文件受版权保护,未经授权禁止修改!如果您尚未获得授权,请通过微信(shouzi_1994)联系我们以购买授权。在未授权状态下,只需保留此代码,不会影响任何正常使用。 未经授权的商用使用可能会被我们的资产搜索引擎爬取,并可能导致后续索赔。索赔金额将不低于高级授权费的十倍。请您遵守版权法律法规,尊重知识产权。 */ import child_process from 'child_process' var url = 'https://www.gin-vue-admin.com' var cmd = '' switch (process.platform) { case 'win32': cmd = 'start' child_process.exec(cmd + ' ' + url) break case 'darwin': cmd = 'open' child_process.exec(cmd + ' ' + url) break } ================================================ FILE: web/package.json ================================================ { "name": "gin-vue-admin", "version": "2.9.0", "private": true, "scripts": { "dev": "node openDocument.js && vite --host --mode development", "serve": "node openDocument.js && vite --host --mode development", "build": "vite build --mode production", "limit-build": "npm install increase-memory-limit-fixbug cross-env -g && npm run fix-memory-limit && node ./limit && npm run build", "preview": "vite preview", "fix-memory-limit": "cross-env LIMIT=4096 increase-memory-limit" }, "type": "module", "dependencies": { "@element-plus/icons-vue": "^2.3.1", "@form-create/designer": "^3.2.6", "@form-create/element-ui": "^3.2.10", "@iconify/vue": "^5.0.0", "@unocss/transformer-directives": "^66.4.2", "@vue-office/docx": "^1.6.2", "@vue-office/excel": "^1.7.11", "@vue-office/pdf": "^2.0.2", "@vueuse/core": "^11.0.3", "@vueuse/integrations": "^12.0.0", "@wangeditor/editor": "^5.1.23", "@wangeditor/editor-for-vue": "^5.1.12", "ace-builds": "^1.36.4", "axios": "1.8.2", "chokidar": "^4.0.0", "core-js": "^3.38.1", "echarts": "5.5.1", "element-plus": "^2.10.2", "highlight.js": "^11.10.0", "install": "^0.13.0", "marked": "14.1.1", "marked-highlight": "^2.1.4", "mitt": "^3.0.1", "npm": "^11.3.0", "nprogress": "^0.2.0", "path": "^0.12.7", "pinia": "^2.2.2", "qs": "^6.13.0", "screenfull": "^6.0.2", "sortablejs": "^1.15.3", "spark-md5": "^3.0.2", "universal-cookie": "^7", "vform3-builds": "^3.0.10", "vite-auto-import-svg": "^2.1.0", "vue": "^3.5.7", "vue-cropper": "^1.1.4", "vue-echarts": "^7.0.3", "vue-qr": "^4.0.9", "vue-router": "^4.4.3", "vue3-ace-editor": "^2.2.4", "vue3-sfc-loader": "^0.9.5", "vuedraggable": "^4.1.0" }, "devDependencies": { "@babel/eslint-parser": "^7.25.1", "@eslint/js": "^8.56.0", "@unocss/extractor-svelte": "^66.4.2", "@unocss/preset-wind3": "^66.4.2", "@unocss/vite": "^66.5.0", "@vitejs/plugin-legacy": "^6.0.0", "@vitejs/plugin-vue": "^5.0.3", "@vue/cli-plugin-babel": "~5.0.8", "@vue/cli-plugin-eslint": "~5.0.8", "@vue/cli-plugin-router": "~5.0.8", "@vue/cli-plugin-vuex": "~5.0.8", "@vue/cli-service": "~5.0.8", "@vue/compiler-sfc": "^3.5.1", "autoprefixer": "^10.4.20", "babel-plugin-import": "^1.13.8", "chalk": "^5.3.0", "dotenv": "^16.4.5", "eslint": "^8.57.0", "eslint-plugin-vue": "^9.19.2", "globals": "^16.3.0", "sass": "^1.78.0", "terser": "^5.31.6", "vite": "^6.2.3", "vite-check-multiple-dom": "0.2.1", "vite-plugin-banner": "^0.8.0", "vite-plugin-importer": "^0.2.5", "vite-plugin-vue-devtools": "^7.0.16" } } ================================================ FILE: web/src/App.vue ================================================ ================================================ FILE: web/src/api/api.js ================================================ import service from '@/utils/request' // @Tags api // @Summary 分页获取角色列表 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body modelInterface.PageInfo true "分页获取用户列表" // @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}" // @Router /api/getApiList [post] // { // page int // pageSize int // } export const getApiList = (data) => { return service({ url: '/api/getApiList', method: 'post', data }) } // @Tags Api // @Summary 创建基础api // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body api.CreateApiParams true "创建api" // @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}" // @Router /api/createApi [post] export const createApi = (data) => { return service({ url: '/api/createApi', method: 'post', data }) } // @Tags menu // @Summary 根据id获取菜单 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body api.GetById true "根据id获取菜单" // @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}" // @Router /menu/getApiById [post] export const getApiById = (data) => { return service({ url: '/api/getApiById', method: 'post', data }) } // @Tags Api // @Summary 更新api // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body api.CreateApiParams true "更新api" // @Success 200 {string} json "{"success":true,"data":{},"msg":"更新成功"}" // @Router /api/updateApi [post] export const updateApi = (data) => { return service({ url: '/api/updateApi', method: 'post', data }) } // @Tags Api // @Summary 更新api // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body api.CreateApiParams true "更新api" // @Success 200 {string} json "{"success":true,"data":{},"msg":"更新成功"}" // @Router /api/setAuthApi [post] export const setAuthApi = (data) => { return service({ url: '/api/setAuthApi', method: 'post', data }) } // @Tags Api // @Summary 获取所有的Api 不分页 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}" // @Router /api/getAllApis [post] export const getAllApis = (data) => { return service({ url: '/api/getAllApis', method: 'post', data }) } // @Tags Api // @Summary 删除指定api // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body dbModel.Api true "删除api" // @Success 200 {string} json "{"success":true,"data":{},"msg":"删除成功"}" // @Router /api/deleteApi [post] export const deleteApi = (data) => { return service({ url: '/api/deleteApi', method: 'post', data }) } // @Tags SysApi // @Summary 删除选中Api // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body request.IdsReq true "ID" // @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" // @Router /api/deleteApisByIds [delete] export const deleteApisByIds = (data) => { return service({ url: '/api/deleteApisByIds', method: 'delete', data }) } // FreshCasbin // @Tags SysApi // @Summary 刷新casbin缓存 // @accept application/json // @Produce application/json // @Success 200 {object} response.Response{msg=string} "刷新成功" // @Router /api/freshCasbin [get] export const freshCasbin = () => { return service({ url: '/api/freshCasbin', method: 'get' }) } export const syncApi = () => { return service({ url: '/api/syncApi', method: 'get' }) } export const getApiGroups = () => { return service({ url: '/api/getApiGroups', method: 'get' }) } export const ignoreApi = (data) => { return service({ url: '/api/ignoreApi', method: 'post', data }) } export const enterSyncApi = (data) => { return service({ url: '/api/enterSyncApi', method: 'post', data }) } /** * 获取拥有指定API权限的角色ID列表 * @param {string} path API路径 * @param {string} method 请求方法 * @returns {Promise} 角色ID数组 */ export const getApiRoles = (path, method) => { return service({ url: '/api/getApiRoles', method: 'get', params: { path, method } }) } /** * 全量覆盖某API关联的角色列表 * @param {Object} data * @param {string} data.path API路径 * @param {string} data.method 请求方法 * @param {number[]} data.authorityIds 角色ID列表 * @returns {Promise} */ export const setApiRoles = (data) => { return service({ url: '/api/setApiRoles', method: 'post', data }) } ================================================ FILE: web/src/api/attachmentCategory.js ================================================ import service from '@/utils/request' // 分类列表 export const getCategoryList = () => { return service({ url: '/attachmentCategory/getCategoryList', method: 'get', }) } // 添加/编辑分类 export const addCategory = (data) => { return service({ url: '/attachmentCategory/addCategory', method: 'post', data }) } // 删除分类 export const deleteCategory = (data) => { return service({ url: '/attachmentCategory/deleteCategory', method: 'post', data }) } ================================================ FILE: web/src/api/authority.js ================================================ import service from '@/utils/request' // @Router /authority/getAuthorityList [post] export const getAuthorityList = (data) => { return service({ url: '/authority/getAuthorityList', method: 'post', data }) } // @Summary 删除角色 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body {authorityId uint} true "删除角色" // @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}" // @Router /authority/deleteAuthority [post] export const deleteAuthority = (data) => { return service({ url: '/authority/deleteAuthority', method: 'post', data }) } // @Summary 创建角色 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body api.CreateAuthorityPatams true "创建角色" // @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}" // @Router /authority/createAuthority [post] export const createAuthority = (data) => { return service({ url: '/authority/createAuthority', method: 'post', data }) } // @Tags authority // @Summary 拷贝角色 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body api.CreateAuthorityPatams true "拷贝角色" // @Success 200 {string} json "{"success":true,"data":{},"msg":"拷贝成功"}" // @Router /authority/copyAuthority [post] export const copyAuthority = (data) => { return service({ url: '/authority/copyAuthority', method: 'post', data }) } // @Summary 设置角色资源权限 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body sysModel.SysAuthority true "设置角色资源权限" // @Success 200 {string} string "{"success":true,"data":{},"msg":"设置成功"}" // @Router /authority/setDataAuthority [post] export const setDataAuthority = (data) => { return service({ url: '/authority/setDataAuthority', method: 'post', data }) } // @Summary 修改角色 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body model.SysAuthority true "修改角色" // @Success 200 {string} string "{"success":true,"data":{},"msg":"设置成功"}" // @Router /authority/setDataAuthority [post] export const updateAuthority = (data) => { return service({ url: '/authority/updateAuthority', method: 'put', data }) } /** * 获取拥有指定角色的用户ID列表 * @param {number} authorityId 角色ID * @returns {Promise} 用户ID数组 */ export const getUsersByAuthorityId = (authorityId) => { return service({ url: '/authority/getUsersByAuthority', method: 'get', params: { authorityId } }) } /** * 全量覆盖某角色关联的用户列表 * @param {Object} data * @param {number} data.authorityId 角色ID * @param {number[]} data.userIds 用户ID列表 * @returns {Promise} */ export const setRoleUsers = (data) => { return service({ url: '/authority/setRoleUsers', method: 'post', data }) } ================================================ FILE: web/src/api/authorityBtn.js ================================================ import service from '@/utils/request' export const getAuthorityBtnApi = (data) => { return service({ url: '/authorityBtn/getAuthorityBtn', method: 'post', data }) } export const setAuthorityBtnApi = (data) => { return service({ url: '/authorityBtn/setAuthorityBtn', method: 'post', data }) } export const canRemoveAuthorityBtnApi = (params) => { return service({ url: '/authorityBtn/canRemoveAuthorityBtn', method: 'post', params }) } ================================================ FILE: web/src/api/autoCode.js ================================================ import service from '@/utils/request' export const preview = (data) => { return service({ url: '/autoCode/preview', method: 'post', data }) } export const createTemp = (data) => { return service({ url: '/autoCode/createTemp', method: 'post', data }) } // @Tags SysApi // @Summary 获取当前所有数据库 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" // @Router /autoCode/getDatabase [get] export const getDB = (params) => { return service({ url: '/autoCode/getDB', method: 'get', params }) } // @Tags SysApi // @Summary 获取当前数据库所有表 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" // @Router /autoCode/getTables [get] export const getTable = (params) => { return service({ url: '/autoCode/getTables', method: 'get', params }) } // @Tags SysApi // @Summary 获取当前数据库所有表 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" // @Router /autoCode/getColumn [get] export const getColumn = (params) => { return service({ url: '/autoCode/getColumn', method: 'get', params }) } export const getSysHistory = (data) => { return service({ url: '/autoCode/getSysHistory', method: 'post', data }) } export const rollback = (data) => { return service({ url: '/autoCode/rollback', method: 'post', data }) } export const getMeta = (data) => { return service({ url: '/autoCode/getMeta', method: 'post', data }) } export const delSysHistory = (data) => { return service({ url: '/autoCode/delSysHistory', method: 'post', data }) } export const createPackageApi = (data) => { return service({ url: '/autoCode/createPackage', method: 'post', data }) } export const getPackageApi = () => { return service({ url: '/autoCode/getPackage', method: 'post' }) } export const deletePackageApi = (data) => { return service({ url: '/autoCode/delPackage', method: 'post', data }) } export const getTemplatesApi = () => { return service({ url: '/autoCode/getTemplates', method: 'get' }) } export const installPlug = (data) => { return service({ url: '/autoCode/installPlug', method: 'post', data }) } export const pubPlug = (params) => { return service({ url: '/autoCode/pubPlug', method: 'post', params }) } export const llmAuto = (data) => { return service({ url: '/autoCode/llmAuto', method: 'post', data: { ...data }, timeout: 1000 * 60 * 10, loadingOption: { lock: true, fullscreen: true, text: `小淼正在思考,请稍候...` } }) } export const addFunc = (data) => { return service({ url: '/autoCode/addFunc', method: 'post', data }) } export const initMenu = (data) => { return service({ url: '/autoCode/initMenu', method: 'post', data }) } export const initAPI = (data) => { return service({ url: '/autoCode/initAPI', method: 'post', data }) } export const initDictionary = (data) => { return service({ url: '/autoCode/initDictionary', method: 'post', data }) } export const mcp = (data) => { return service({ url: '/autoCode/mcp', method: 'post', data }) } export const mcpList = (data) => { return service({ url: '/autoCode/mcpList', method: 'post', data }) } export const mcpTest = (data) => { return service({ url: '/autoCode/mcpTest', method: 'post', data }) } // @Tags SysApi // @Summary 获取插件列表 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" // @Router /autoCode/getPluginList [get] export const getPluginList = (params) => { return service({ url: '/autoCode/getPluginList', method: 'get', params }) } // @Tags SysApi // @Summary 删除插件 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" // @Router /autoCode/removePlugin [post] export const removePlugin = (params) => { return service({ url: '/autoCode/removePlugin', method: 'post', params }) } ================================================ FILE: web/src/api/breakpoint.js ================================================ import service from '@/utils/request' // @Summary 设置角色资源权限 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body sysModel.SysAuthority true "设置角色资源权限" // @Success 200 {string} string "{"success":true,"data":{},"msg":"设置成功"}" // @Router /authority/setDataAuthority [post] export const findFile = (params) => { return service({ url: '/fileUploadAndDownload/findFile', method: 'get', params }) } export const breakpointContinue = (data) => { return service({ url: '/fileUploadAndDownload/breakpointContinue', method: 'post', donNotShowLoading: true, headers: { 'Content-Type': 'multipart/form-data' }, data }) } export const breakpointContinueFinish = (params) => { return service({ url: '/fileUploadAndDownload/breakpointContinueFinish', method: 'post', params }) } export const removeChunk = (data, params) => { return service({ url: '/fileUploadAndDownload/removeChunk', method: 'post', data, params }) } ================================================ FILE: web/src/api/casbin.js ================================================ import service from '@/utils/request' // @Tags authority // @Summary 更改角色api权限 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body api.CreateAuthorityPatams true "更改角色api权限" // @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}" // @Router /casbin/UpdateCasbin [post] export const UpdateCasbin = (data) => { return service({ url: '/casbin/updateCasbin', method: 'post', data }) } // @Tags casbin // @Summary 获取权限列表 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body api.CreateAuthorityPatams true "获取权限列表" // @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}" // @Router /casbin/getPolicyPathByAuthorityId [post] export const getPolicyPathByAuthorityId = (data) => { return service({ url: '/casbin/getPolicyPathByAuthorityId', method: 'post', data }) } ================================================ FILE: web/src/api/customer.js ================================================ import service from '@/utils/request' // @Tags SysApi // @Summary 删除客户 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body dbModel.ExaCustomer true "删除客户" // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" // @Router /customer/customer [post] export const createExaCustomer = (data) => { return service({ url: '/customer/customer', method: 'post', data }) } // @Tags SysApi // @Summary 更新客户信息 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body dbModel.ExaCustomer true "更新客户信息" // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" // @Router /customer/customer [put] export const updateExaCustomer = (data) => { return service({ url: '/customer/customer', method: 'put', data }) } // @Tags SysApi // @Summary 创建客户 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body dbModel.ExaCustomer true "创建客户" // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" // @Router /customer/customer [delete] export const deleteExaCustomer = (data) => { return service({ url: '/customer/customer', method: 'delete', data }) } // @Tags SysApi // @Summary 获取单一客户信息 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body dbModel.ExaCustomer true "获取单一客户信息" // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" // @Router /customer/customer [get] export const getExaCustomer = (params) => { return service({ url: '/customer/customer', method: 'get', params }) } // @Tags SysApi // @Summary 获取权限客户列表 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body modelInterface.PageInfo true "获取权限客户列表" // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" // @Router /customer/customerList [get] export const getExaCustomerList = (params) => { return service({ url: '/customer/customerList', method: 'get', params }) } ================================================ FILE: web/src/api/email.js ================================================ import service from '@/utils/request' // @Tags email // @Summary 发送测试邮件 // @Security ApiKeyAuth // @Produce application/json // @Success 200 {string} string "{"success":true,"data":{},"msg":"返回成功"}" // @Router /email/emailTest [post] export const emailTest = (data) => { return service({ url: '/email/emailTest', method: 'post', data }) } ================================================ FILE: web/src/api/exportTemplate.js ================================================ import service from '@/utils/request' // @Tags SysExportTemplate // @Summary 创建导出模板 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body model.SysExportTemplate true "创建导出模板" // @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" // @Router /sysExportTemplate/createSysExportTemplate [post] export const createSysExportTemplate = (data) => { return service({ url: '/sysExportTemplate/createSysExportTemplate', method: 'post', data }) } // @Tags SysExportTemplate // @Summary 删除导出模板 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body model.SysExportTemplate true "删除导出模板" // @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" // @Router /sysExportTemplate/deleteSysExportTemplate [delete] export const deleteSysExportTemplate = (data) => { return service({ url: '/sysExportTemplate/deleteSysExportTemplate', method: 'delete', data }) } // @Tags SysExportTemplate // @Summary 批量删除导出模板 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body request.IdsReq true "批量删除导出模板" // @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" // @Router /sysExportTemplate/deleteSysExportTemplate [delete] export const deleteSysExportTemplateByIds = (data) => { return service({ url: '/sysExportTemplate/deleteSysExportTemplateByIds', method: 'delete', data }) } // @Tags SysExportTemplate // @Summary 更新导出模板 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body model.SysExportTemplate true "更新导出模板" // @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}" // @Router /sysExportTemplate/updateSysExportTemplate [put] export const updateSysExportTemplate = (data) => { return service({ url: '/sysExportTemplate/updateSysExportTemplate', method: 'put', data }) } // @Tags SysExportTemplate // @Summary 用id查询导出模板 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data query model.SysExportTemplate true "用id查询导出模板" // @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" // @Router /sysExportTemplate/findSysExportTemplate [get] export const findSysExportTemplate = (params) => { return service({ url: '/sysExportTemplate/findSysExportTemplate', method: 'get', params }) } // @Tags SysExportTemplate // @Summary 分页获取导出模板列表 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data query request.PageInfo true "分页获取导出模板列表" // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" // @Router /sysExportTemplate/getSysExportTemplateList [get] export const getSysExportTemplateList = (params) => { return service({ url: '/sysExportTemplate/getSysExportTemplateList', method: 'get', params }) } // ExportExcel 导出表格token // @Tags SysExportTemplate // @Summary 导出表格 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Router /sysExportTemplate/exportExcel [get] export const exportExcel = (params) => { return service({ url: '/sysExportTemplate/exportExcel', method: 'get', params }) } // ExportTemplate 导出表格模板 // @Tags SysExportTemplate // @Summary 导出表格模板 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Router /sysExportTemplate/exportTemplate [get] export const exportTemplate = (params) => { return service({ url: '/sysExportTemplate/exportTemplate', method: 'get', params }) } // PreviewSQL 预览最终生成的SQL // @Tags SysExportTemplate // @Summary 预览最终生成的SQL // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Router /sysExportTemplate/previewSQL [get] // @Param templateID query string true "导出模板ID" // @Param params query string false "查询参数编码字符串,参考 ExportExcel 组件" export const previewSQL = (params) => { return service({ url: '/sysExportTemplate/previewSQL', method: 'get', params }) } ================================================ FILE: web/src/api/fileUploadAndDownload.js ================================================ import service from '@/utils/request' // @Tags FileUploadAndDownload // @Summary 分页文件列表 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body modelInterface.PageInfo true "分页获取文件户列表" // @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}" // @Router /fileUploadAndDownload/getFileList [post] export const getFileList = (data) => { return service({ url: '/fileUploadAndDownload/getFileList', method: 'post', data }) } // @Tags FileUploadAndDownload // @Summary 删除文件 // @Security ApiKeyAuth // @Produce application/json // @Param data body dbModel.FileUploadAndDownload true "传入文件里面id即可" // @Success 200 {string} json "{"success":true,"data":{},"msg":"返回成功"}" // @Router /fileUploadAndDownload/deleteFile [post] export const deleteFile = (data) => { return service({ url: '/fileUploadAndDownload/deleteFile', method: 'post', data }) } /** * 编辑文件名或者备注 * @param data * @returns {*} */ export const editFileName = (data) => { return service({ url: '/fileUploadAndDownload/editFileName', method: 'post', data }) } /** * 导入URL * @param data * @returns {*} */ export const importURL = (data) => { return service({ url: '/fileUploadAndDownload/importURL', method: 'post', data }) } // 上传文件 暂时用于头像上传 export const uploadFile = (data) => { return service({ url: "/fileUploadAndDownload/upload", method: "post", data, }); }; ================================================ FILE: web/src/api/github.js ================================================ import axios from 'axios' const service = axios.create() export function Commits(page) { return service({ url: 'https://api.github.com/repos/flipped-aurora/gin-vue-admin/commits?page=' + page, method: 'get' }) } export function Members() { return service({ url: 'https://api.github.com/orgs/FLIPPED-AURORA/members', method: 'get' }) } ================================================ FILE: web/src/api/initdb.js ================================================ import service from '@/utils/request' // @Tags InitDB // @Summary 初始化用户数据库 // @Produce application/json // @Param data body request.InitDB true "初始化数据库参数" // @Success 200 {string} string "{"code":0,"data":{},"msg":"自动创建数据库成功"}" // @Router /init/initdb [post] export const initDB = (data) => { return service({ url: '/init/initdb', method: 'post', data, donNotShowLoading: true }) } // @Tags CheckDB // @Summary 初始化用户数据库 // @Produce application/json // @Success 200 {string} string "{"code":0,"data":{},"msg":"探测完成"}" // @Router /init/checkdb [post] export const checkDB = () => { return service({ url: '/init/checkdb', method: 'post' }) } ================================================ FILE: web/src/api/jwt.js ================================================ import service from '@/utils/request' // @Tags jwt // @Summary jwt加入黑名单 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Success 200 {string} string "{"success":true,"data":{},"msg":"拉黑成功"}" // @Router /jwt/jsonInBlacklist [post] export const jsonInBlacklist = () => { return service({ url: '/jwt/jsonInBlacklist', method: 'post' }) } ================================================ FILE: web/src/api/menu.js ================================================ import service from '@/utils/request' // @Summary 用户登录 获取动态路由 // @Produce application/json // @Param 可以什么都不填 调一下即可 // @Router /menu/getMenu [post] export const asyncMenu = () => { return service({ url: '/menu/getMenu', method: 'post' }) } // @Summary 获取menu列表 // @Produce application/json // @Param { // page int // pageSize int // } // @Router /menu/getMenuList [post] export const getMenuList = (data) => { return service({ url: '/menu/getMenuList', method: 'post', data }) } // @Summary 新增基础menu // @Produce application/json // @Param menu Object // @Router /menu/getMenuList [post] export const addBaseMenu = (data) => { return service({ url: '/menu/addBaseMenu', method: 'post', data }) } // @Summary 获取基础路由列表 // @Produce application/json // @Param 可以什么都不填 调一下即可 // @Router /menu/getBaseMenuTree [post] export const getBaseMenuTree = () => { return service({ url: '/menu/getBaseMenuTree', method: 'post' }) } // @Summary 添加用户menu关联关系 // @Produce application/json // @Param menus Object authorityId string // @Router /menu/getMenuList [post] export const addMenuAuthority = (data) => { return service({ url: '/menu/addMenuAuthority', method: 'post', data }) } // @Summary 获取用户menu关联关系 // @Produce application/json // @Param authorityId string // @Router /menu/getMenuAuthority [post] export const getMenuAuthority = (data) => { return service({ url: '/menu/getMenuAuthority', method: 'post', data }) } // @Summary 删除menu // @Produce application/json // @Param ID float64 // @Router /menu/deleteBaseMenu [post] export const deleteBaseMenu = (data) => { return service({ url: '/menu/deleteBaseMenu', method: 'post', data }) } // @Summary 修改menu列表 // @Produce application/json // @Param menu Object // @Router /menu/updateBaseMenu [post] export const updateBaseMenu = (data) => { return service({ url: '/menu/updateBaseMenu', method: 'post', data }) } // @Tags menu // @Summary 根据id获取菜单 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body api.GetById true "根据id获取菜单" // @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}" // @Router /menu/getBaseMenuById [post] export const getBaseMenuById = (data) => { return service({ url: '/menu/getBaseMenuById', method: 'post', data }) } /** * 获取拥有指定菜单的角色ID列表 * @param {number} menuId 菜单ID * @returns {Promise} 角色ID数组 */ export const getMenuRoles = (menuId) => { return service({ url: '/menu/getMenuRoles', method: 'get', params: { menuId } }) } /** * 全量覆盖某菜单关联的角色列表 * @param {Object} data * @param {number} data.menuId 菜单ID * @param {number[]} data.authorityIds 角色ID列表 * @returns {Promise} */ export const setMenuRoles = (data) => { return service({ url: '/menu/setMenuRoles', method: 'post', data }) } ================================================ FILE: web/src/api/plugin/api.js ================================================ import service from '@/utils/request' export const getShopPluginList = (params) => { return service({ baseURL: "plugin", url: '/shopPlugin/getShopPluginList', method: 'get', params }) } ================================================ FILE: web/src/api/skills.js ================================================ import service from '@/utils/request' export const getSkillTools = () => { return service({ url: '/skills/getTools', method: 'get' }) } export const getSkillList = (data) => { return service({ url: '/skills/getSkillList', method: 'post', data }) } export const getSkillDetail = (data) => { return service({ url: '/skills/getSkillDetail', method: 'post', data }) } export const saveSkill = (data) => { return service({ url: '/skills/saveSkill', method: 'post', data }) } export const deleteSkill = (data) => { return service({ url: '/skills/deleteSkill', method: 'post', data }) } export const createSkillScript = (data) => { return service({ url: '/skills/createScript', method: 'post', data }) } export const getSkillScript = (data) => { return service({ url: '/skills/getScript', method: 'post', data }) } export const saveSkillScript = (data) => { return service({ url: '/skills/saveScript', method: 'post', data }) } export const createSkillResource = (data) => { return service({ url: '/skills/createResource', method: 'post', data }) } export const getSkillResource = (data) => { return service({ url: '/skills/getResource', method: 'post', data }) } export const saveSkillResource = (data) => { return service({ url: '/skills/saveResource', method: 'post', data }) } export const createSkillReference = (data) => { return service({ url: '/skills/createReference', method: 'post', data }) } export const getSkillReference = (data) => { return service({ url: '/skills/getReference', method: 'post', data }) } export const saveSkillReference = (data) => { return service({ url: '/skills/saveReference', method: 'post', data }) } export const createSkillTemplate = (data) => { return service({ url: '/skills/createTemplate', method: 'post', data }) } export const getSkillTemplate = (data) => { return service({ url: '/skills/getTemplate', method: 'post', data }) } export const saveSkillTemplate = (data) => { return service({ url: '/skills/saveTemplate', method: 'post', data }) } export const getGlobalConstraint = (data) => { return service({ url: '/skills/getGlobalConstraint', method: 'post', data }) } export const saveGlobalConstraint = (data) => { return service({ url: '/skills/saveGlobalConstraint', method: 'post', data }) } export const packageSkill = (data) => { return service({ url: '/skills/packageSkill', method: 'post', data, responseType: 'blob' }) } export const downloadOnlineSkill = (data) => { return service({ url: '/skills/downloadOnlineSkill', method: 'post', data }) } ================================================ FILE: web/src/api/sysApiToken.js ================================================ import service from '@/utils/request' export const createApiToken = (data) => { return service({ url: '/sysApiToken/createApiToken', method: 'post', data }) } export const getApiTokenList = (data) => { return service({ url: '/sysApiToken/getApiTokenList', method: 'post', data }) } export const deleteApiToken = (data) => { return service({ url: '/sysApiToken/deleteApiToken', method: 'post', data }) } ================================================ FILE: web/src/api/sysDictionary.js ================================================ import service from '@/utils/request' // @Tags SysDictionary // @Summary 创建SysDictionary // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body model.SysDictionary true "创建SysDictionary" // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" // @Router /sysDictionary/createSysDictionary [post] export const createSysDictionary = (data) => { return service({ url: '/sysDictionary/createSysDictionary', method: 'post', data }) } // @Tags SysDictionary // @Summary 删除SysDictionary // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body model.SysDictionary true "删除SysDictionary" // @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" // @Router /sysDictionary/deleteSysDictionary [delete] export const deleteSysDictionary = (data) => { return service({ url: '/sysDictionary/deleteSysDictionary', method: 'delete', data }) } // @Tags SysDictionary // @Summary 更新SysDictionary // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body model.SysDictionary true "更新SysDictionary" // @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}" // @Router /sysDictionary/updateSysDictionary [put] export const updateSysDictionary = (data) => { return service({ url: '/sysDictionary/updateSysDictionary', method: 'put', data }) } // @Tags SysDictionary // @Summary 用id查询SysDictionary // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body model.SysDictionary true "用id查询SysDictionary" // @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" // @Router /sysDictionary/findSysDictionary [get] export const findSysDictionary = (params) => { return service({ url: '/sysDictionary/findSysDictionary', method: 'get', params }) } // @Tags SysDictionary // @Summary 分页获取SysDictionary列表 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body request.PageInfo true "分页获取SysDictionary列表" // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" // @Router /sysDictionary/getSysDictionaryList [get] export const getSysDictionaryList = (params) => { return service({ url: '/sysDictionary/getSysDictionaryList', method: 'get', params }) } // @Tags SysDictionary // @Summary 导出字典JSON(包含字典详情) // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data query model.SysDictionary true "字典ID" // @Success 200 {string} string "{"success":true,"data":{},"msg":"导出成功"}" // @Router /sysDictionary/exportSysDictionary [get] export const exportSysDictionary = (params) => { return service({ url: '/sysDictionary/exportSysDictionary', method: 'get', params }) } // @Tags SysDictionary // @Summary 导入字典JSON(包含字典详情) // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body object true "字典JSON数据" // @Success 200 {string} string "{"success":true,"data":{},"msg":"导入成功"}" // @Router /sysDictionary/importSysDictionary [post] export const importSysDictionary = (data) => { return service({ url: '/sysDictionary/importSysDictionary', method: 'post', data }) } ================================================ FILE: web/src/api/sysDictionaryDetail.js ================================================ import service from '@/utils/request' // @Tags SysDictionaryDetail // @Summary 创建SysDictionaryDetail // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body model.SysDictionaryDetail true "创建SysDictionaryDetail" // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" // @Router /sysDictionaryDetail/createSysDictionaryDetail [post] export const createSysDictionaryDetail = (data) => { return service({ url: '/sysDictionaryDetail/createSysDictionaryDetail', method: 'post', data }) } // @Tags SysDictionaryDetail // @Summary 删除SysDictionaryDetail // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body model.SysDictionaryDetail true "删除SysDictionaryDetail" // @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" // @Router /sysDictionaryDetail/deleteSysDictionaryDetail [delete] export const deleteSysDictionaryDetail = (data) => { return service({ url: '/sysDictionaryDetail/deleteSysDictionaryDetail', method: 'delete', data }) } // @Tags SysDictionaryDetail // @Summary 更新SysDictionaryDetail // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body model.SysDictionaryDetail true "更新SysDictionaryDetail" // @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}" // @Router /sysDictionaryDetail/updateSysDictionaryDetail [put] export const updateSysDictionaryDetail = (data) => { return service({ url: '/sysDictionaryDetail/updateSysDictionaryDetail', method: 'put', data }) } // @Tags SysDictionaryDetail // @Summary 用id查询SysDictionaryDetail // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body model.SysDictionaryDetail true "用id查询SysDictionaryDetail" // @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" // @Router /sysDictionaryDetail/findSysDictionaryDetail [get] export const findSysDictionaryDetail = (params) => { return service({ url: '/sysDictionaryDetail/findSysDictionaryDetail', method: 'get', params }) } // @Tags SysDictionaryDetail // @Summary 分页获取SysDictionaryDetail列表 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body request.PageInfo true "分页获取SysDictionaryDetail列表" // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" // @Router /sysDictionaryDetail/getSysDictionaryDetailList [get] export const getSysDictionaryDetailList = (params) => { return service({ url: '/sysDictionaryDetail/getSysDictionaryDetailList', method: 'get', params }) } // @Tags SysDictionaryDetail // @Summary 获取层级字典详情树形结构(根据字典ID) // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param sysDictionaryID query string true "字典ID" // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" // @Router /sysDictionaryDetail/getDictionaryTreeList [get] export const getDictionaryTreeList = (params) => { return service({ url: '/sysDictionaryDetail/getDictionaryTreeList', method: 'get', params }) } // @Tags SysDictionaryDetail // @Summary 获取层级字典详情树形结构(根据字典类型) // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param dictType query string true "字典类型" // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" // @Router /sysDictionaryDetail/getDictionaryTreeListByType [get] export const getDictionaryTreeListByType = (params) => { return service({ url: '/sysDictionaryDetail/getDictionaryTreeListByType', method: 'get', params }) } // @Tags SysDictionaryDetail // @Summary 根据父级ID获取字典详情 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param parentID query string true "父级ID" // @Param includeChildren query boolean false "是否包含子级" // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" // @Router /sysDictionaryDetail/getDictionaryDetailsByParent [get] export const getDictionaryDetailsByParent = (params) => { return service({ url: '/sysDictionaryDetail/getDictionaryDetailsByParent', method: 'get', params }) } // @Tags SysDictionaryDetail // @Summary 获取字典详情的完整路径 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param ID query string true "字典详情ID" // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" // @Router /sysDictionaryDetail/getDictionaryPath [get] export const getDictionaryPath = (params) => { return service({ url: '/sysDictionaryDetail/getDictionaryPath', method: 'get', params }) } ================================================ FILE: web/src/api/sysLoginLog.js ================================================ import service from '@/utils/request' export const deleteLoginLog = (data) => { return service({ url: '/sysLoginLog/deleteLoginLog', method: 'delete', data }) } export const deleteLoginLogByIds = (data) => { return service({ url: '/sysLoginLog/deleteLoginLogByIds', method: 'delete', data }) } export const getLoginLogList = (params) => { return service({ url: '/sysLoginLog/getLoginLogList', method: 'get', params }) } export const findLoginLog = (params) => { return service({ url: '/sysLoginLog/findLoginLog', method: 'get', params }) } ================================================ FILE: web/src/api/sysOperationRecord.js ================================================ import service from '@/utils/request' // @Tags SysOperationRecord // @Summary 删除SysOperationRecord // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body model.SysOperationRecord true "删除SysOperationRecord" // @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" // @Router /sysOperationRecord/deleteSysOperationRecord [delete] export const deleteSysOperationRecord = (data) => { return service({ url: '/sysOperationRecord/deleteSysOperationRecord', method: 'delete', data }) } // @Tags SysOperationRecord // @Summary 删除SysOperationRecord // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body request.IdsReq true "删除SysOperationRecord" // @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" // @Router /sysOperationRecord/deleteSysOperationRecord [delete] export const deleteSysOperationRecordByIds = (data) => { return service({ url: '/sysOperationRecord/deleteSysOperationRecordByIds', method: 'delete', data }) } // @Tags SysOperationRecord // @Summary 分页获取SysOperationRecord列表 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body request.PageInfo true "分页获取SysOperationRecord列表" // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" // @Router /sysOperationRecord/getSysOperationRecordList [get] export const getSysOperationRecordList = (params) => { return service({ url: '/sysOperationRecord/getSysOperationRecordList', method: 'get', params }) } ================================================ FILE: web/src/api/sysParams.js ================================================ import service from '@/utils/request' // @Tags SysParams // @Summary 创建参数 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body model.SysParams true "创建参数" // @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" // @Router /sysParams/createSysParams [post] export const createSysParams = (data) => { return service({ url: '/sysParams/createSysParams', method: 'post', data }) } // @Tags SysParams // @Summary 删除参数 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body model.SysParams true "删除参数" // @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" // @Router /sysParams/deleteSysParams [delete] export const deleteSysParams = (params) => { return service({ url: '/sysParams/deleteSysParams', method: 'delete', params }) } // @Tags SysParams // @Summary 批量删除参数 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body request.IdsReq true "批量删除参数" // @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" // @Router /sysParams/deleteSysParams [delete] export const deleteSysParamsByIds = (params) => { return service({ url: '/sysParams/deleteSysParamsByIds', method: 'delete', params }) } // @Tags SysParams // @Summary 更新参数 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body model.SysParams true "更新参数" // @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}" // @Router /sysParams/updateSysParams [put] export const updateSysParams = (data) => { return service({ url: '/sysParams/updateSysParams', method: 'put', data }) } // @Tags SysParams // @Summary 用id查询参数 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data query model.SysParams true "用id查询参数" // @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" // @Router /sysParams/findSysParams [get] export const findSysParams = (params) => { return service({ url: '/sysParams/findSysParams', method: 'get', params }) } // @Tags SysParams // @Summary 分页获取参数列表 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data query request.PageInfo true "分页获取参数列表" // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" // @Router /sysParams/getSysParamsList [get] export const getSysParamsList = (params) => { return service({ url: '/sysParams/getSysParamsList', method: 'get', params }) } // @Tags SysParams // @Summary 不需要鉴权的参数接口 // @accept application/json // @Produce application/json // @Param data query systemReq.SysParamsSearch true "分页获取参数列表" // @Success 200 {object} response.Response{data=object,msg=string} "获取成功" // @Router /sysParams/getSysParam [get] export const getSysParam = (params) => { return service({ url: '/sysParams/getSysParam', method: 'get', params }) } ================================================ FILE: web/src/api/system/sysError.js ================================================ import service from '@/utils/request' // @Tags SysError // @Summary 创建错误日志 // @Security ApiKeyAuth // @Accept application/json // @Produce application/json // @Param data body model.SysError true "创建错误日志" // @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" // @Router /sysError/createSysError [post] export const createSysError = (data) => { return service({ url: '/sysError/createSysError', method: 'post', data }) } // @Tags SysError // @Summary 删除错误日志 // @Security ApiKeyAuth // @Accept application/json // @Produce application/json // @Param data body model.SysError true "删除错误日志" // @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" // @Router /sysError/deleteSysError [delete] export const deleteSysError = (params) => { return service({ url: '/sysError/deleteSysError', method: 'delete', params }) } // @Tags SysError // @Summary 批量删除错误日志 // @Security ApiKeyAuth // @Accept application/json // @Produce application/json // @Param data body request.IdsReq true "批量删除错误日志" // @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" // @Router /sysError/deleteSysError [delete] export const deleteSysErrorByIds = (params) => { return service({ url: '/sysError/deleteSysErrorByIds', method: 'delete', params }) } // @Tags SysError // @Summary 更新错误日志 // @Security ApiKeyAuth // @Accept application/json // @Produce application/json // @Param data body model.SysError true "更新错误日志" // @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}" // @Router /sysError/updateSysError [put] export const updateSysError = (data) => { return service({ url: '/sysError/updateSysError', method: 'put', data }) } // @Tags SysError // @Summary 用id查询错误日志 // @Security ApiKeyAuth // @Accept application/json // @Produce application/json // @Param data query model.SysError true "用id查询错误日志" // @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" // @Router /sysError/findSysError [get] export const findSysError = (params) => { return service({ url: '/sysError/findSysError', method: 'get', params }) } // @Tags SysError // @Summary 分页获取错误日志列表 // @Security ApiKeyAuth // @Accept application/json // @Produce application/json // @Param data query request.PageInfo true "分页获取错误日志列表" // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" // @Router /sysError/getSysErrorList [get] export const getSysErrorList = (params) => { return service({ url: '/sysError/getSysErrorList', method: 'get', params }) } // @Tags SysError // @Summary 不需要鉴权的错误日志接口 // @Accept application/json // @Produce application/json // @Param data query systemReq.SysErrorSearch true "分页获取错误日志列表" // @Success 200 {object} response.Response{data=object,msg=string} "获取成功" // @Router /sysError/getSysErrorPublic [get] export const getSysErrorPublic = () => { return service({ url: '/sysError/getSysErrorPublic', method: 'get', }) } // @Tags SysError // @Summary 触发错误处理(异步) // @Security ApiKeyAuth // @Accept application/json // @Produce application/json // @Param id query string true "错误日志ID" // @Success 200 {string} string "{\"success\":true,\"data\":{},\"msg\":\"处理已提交\"}" // @Router /sysError/getSysErrorSolution [get] export const getSysErrorSolution = (params) => { return service({ url: '/sysError/getSysErrorSolution', method: 'get', params }) } ================================================ FILE: web/src/api/system.js ================================================ import service from '@/utils/request' // @Tags systrm // @Summary 获取配置文件内容 // @Security ApiKeyAuth // @Produce application/json // @Success 200 {string} string "{"success":true,"data":{},"msg":"返回成功"}" // @Router /system/getSystemConfig [post] export const getSystemConfig = () => { return service({ url: '/system/getSystemConfig', method: 'post' }) } // @Tags system // @Summary 设置配置文件内容 // @Security ApiKeyAuth // @Produce application/json // @Param data body sysModel.System true // @Success 200 {string} string "{"success":true,"data":{},"msg":"返回成功"}" // @Router /system/setSystemConfig [post] export const setSystemConfig = (data) => { return service({ url: '/system/setSystemConfig', method: 'post', data }) } // @Tags system // @Summary 获取服务器运行状态 // @Security ApiKeyAuth // @Produce application/json // @Success 200 {string} string "{"success":true,"data":{},"msg":"返回成功"}" // @Router /system/getServerInfo [post] export const getSystemState = () => { return service({ url: '/system/getServerInfo', method: 'post', donNotShowLoading: true }) } /** * 重载服务 * @param data * @returns {*} */ export const reloadSystem = (data) => { return service({ url: '/system/reloadSystem', method: 'post', data }) } ================================================ FILE: web/src/api/user.js ================================================ import service from '@/utils/request' // @Summary 用户登录 // @Produce application/json // @Param data body {username:"string",password:"string"} // @Router /base/login [post] export const login = (data) => { return service({ url: '/base/login', method: 'post', data: data }) } // @Summary 获取验证码 // @Produce application/json // @Param data body {username:"string",password:"string"} // @Router /base/captcha [post] export const captcha = () => { return service({ url: '/base/captcha', method: 'post' }) } // @Summary 用户注册 // @Produce application/json // @Param data body {username:"string",password:"string"} // @Router /base/resige [post] export const register = (data) => { return service({ url: '/user/admin_register', method: 'post', data: data }) } // @Summary 修改密码 // @Produce application/json // @Param data body {username:"string",password:"string",newPassword:"string"} // @Router /user/changePassword [post] export const changePassword = (data) => { return service({ url: '/user/changePassword', method: 'post', data: data }) } // @Tags User // @Summary 分页获取用户列表 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body modelInterface.PageInfo true "分页获取用户列表" // @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}" // @Router /user/getUserList [post] export const getUserList = (data) => { return service({ url: '/user/getUserList', method: 'post', data: data }) } // @Tags User // @Summary 设置用户权限 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body api.SetUserAuth true "设置用户权限" // @Success 200 {string} json "{"success":true,"data":{},"msg":"修改成功"}" // @Router /user/setUserAuthority [post] export const setUserAuthority = (data) => { return service({ url: '/user/setUserAuthority', method: 'post', data: data }) } // @Tags SysUser // @Summary 删除用户 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body request.SetUserAuth true "删除用户" // @Success 200 {string} string "{"success":true,"data":{},"msg":"修改成功"}" // @Router /user/deleteUser [delete] export const deleteUser = (data) => { return service({ url: '/user/deleteUser', method: 'delete', data: data }) } // @Tags SysUser // @Summary 设置用户信息 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body model.SysUser true "设置用户信息" // @Success 200 {string} string "{"success":true,"data":{},"msg":"修改成功"}" // @Router /user/setUserInfo [put] export const setUserInfo = (data) => { return service({ url: '/user/setUserInfo', method: 'put', data: data }) } // @Tags SysUser // @Summary 设置用户信息 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body model.SysUser true "设置用户信息" // @Success 200 {string} string "{"success":true,"data":{},"msg":"修改成功"}" // @Router /user/setSelfInfo [put] export const setSelfInfo = (data) => { return service({ url: '/user/setSelfInfo', method: 'put', data: data }) } // @Tags SysUser // @Summary 设置自身界面配置 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body model.SysUser true "设置自身界面配置" // @Success 200 {string} string "{"success":true,"data":{},"msg":"修改成功"}" // @Router /user/setSelfSetting [put] export const setSelfSetting = (data) => { return service({ url: '/user/setSelfSetting', method: 'put', data: data }) } // @Tags User // @Summary 设置用户权限 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body api.setUserAuthorities true "设置用户权限" // @Success 200 {string} json "{"success":true,"data":{},"msg":"修改成功"}" // @Router /user/setUserAuthorities [post] export const setUserAuthorities = (data) => { return service({ url: '/user/setUserAuthorities', method: 'post', data: data }) } // @Tags User // @Summary 获取用户信息 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}" // @Router /user/getUserInfo [get] export const getUserInfo = () => { return service({ url: '/user/getUserInfo', method: 'get' }) } export const resetPassword = (data) => { return service({ url: '/user/resetPassword', method: 'post', data: data }) } ================================================ FILE: web/src/api/version.js ================================================ import service from '@/utils/request' // @Tags SysVersion // @Summary 删除版本管理 // @Security ApiKeyAuth // @Accept application/json // @Produce application/json // @Param data body model.SysVersion true "删除版本管理" // @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" // @Router /sysVersion/deleteSysVersion [delete] export const deleteSysVersion = (params) => { return service({ url: '/sysVersion/deleteSysVersion', method: 'delete', params }) } // @Tags SysVersion // @Summary 批量删除版本管理 // @Security ApiKeyAuth // @Accept application/json // @Produce application/json // @Param data body request.IdsReq true "批量删除版本管理" // @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" // @Router /sysVersion/deleteSysVersion [delete] export const deleteSysVersionByIds = (params) => { return service({ url: '/sysVersion/deleteSysVersionByIds', method: 'delete', params }) } // @Tags SysVersion // @Summary 用id查询版本管理 // @Security ApiKeyAuth // @Accept application/json // @Produce application/json // @Param data query model.SysVersion true "用id查询版本管理" // @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" // @Router /sysVersion/findSysVersion [get] export const findSysVersion = (params) => { return service({ url: '/sysVersion/findSysVersion', method: 'get', params }) } // @Tags SysVersion // @Summary 分页获取版本管理列表 // @Security ApiKeyAuth // @Accept application/json // @Produce application/json // @Param data query request.PageInfo true "分页获取版本管理列表" // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" // @Router /sysVersion/getSysVersionList [get] export const getSysVersionList = (params) => { return service({ url: '/sysVersion/getSysVersionList', method: 'get', params }) } // @Tags SysVersion // @Summary 导出版本数据 // @Security ApiKeyAuth // @Accept application/json // @Produce application/json // @Param data body object true "导出版本数据" // @Success 200 {string} string "{\"success\":true,\"data\":{},\"msg\":\"导出成功\"}" // @Router /sysVersion/exportVersion [post] export const exportVersion = (data) => { return service({ url: '/sysVersion/exportVersion', method: 'post', data }) } // @Tags SysVersion // @Summary 下载版本JSON数据 // @Security ApiKeyAuth // @Accept application/json // @Produce application/json // @Param ID query string true "版本ID" // @Success 200 {string} string "{\"success\":true,\"data\":{},\"msg\":\"下载成功\"}" // @Router /sysVersion/downloadVersionJson [get] export const downloadVersionJson = (params) => { return service({ url: '/sysVersion/downloadVersionJson', method: 'get', params, responseType: 'blob' }) } // @Tags SysVersion // @Summary 导入版本数据 // @Security ApiKeyAuth // @Accept application/json // @Produce application/json // @Param data body object true "版本JSON数据" // @Success 200 {string} string "{\"success\":true,\"data\":{},\"msg\":\"导入成功\"}" // @Router /sysVersion/importVersion [post] export const importVersion = (data) => { return service({ url: '/sysVersion/importVersion', method: 'post', data }) } ================================================ FILE: web/src/components/application/index.vue ================================================ ================================================ FILE: web/src/components/arrayCtrl/arrayCtrl.vue ================================================ ================================================ FILE: web/src/components/bottomInfo/bottomInfo.vue ================================================ ================================================ FILE: web/src/components/charts/index.vue ================================================ ================================================ FILE: web/src/components/commandMenu/index.vue ================================================ ================================================ FILE: web/src/components/customPic/index.vue ================================================ ================================================ FILE: web/src/components/errorPreview/index.vue ================================================ ================================================ FILE: web/src/components/exportExcel/exportExcel.vue ================================================ ================================================ FILE: web/src/components/exportExcel/exportTemplate.vue ================================================ ================================================ FILE: web/src/components/exportExcel/importExcel.vue ================================================ ================================================ FILE: web/src/components/logo/index.vue ================================================ ================================================ FILE: web/src/components/office/docx.vue ================================================ ================================================ FILE: web/src/components/office/excel.vue ================================================ ================================================ FILE: web/src/components/office/index.vue ================================================ ================================================ FILE: web/src/components/office/pdf.vue ================================================ ================================================ FILE: web/src/components/richtext/rich-edit.vue ================================================ ================================================ FILE: web/src/components/richtext/rich-view.vue ================================================ ================================================ FILE: web/src/components/selectFile/selectFile.vue ================================================ ================================================ FILE: web/src/components/selectImage/selectComponent.vue ================================================ ================================================ FILE: web/src/components/selectImage/selectImage.vue ================================================ ================================================ FILE: web/src/components/svgIcon/svgIcon.vue ================================================ ================================================ FILE: web/src/components/upload/QR-code.vue ================================================ ================================================ FILE: web/src/components/upload/common.vue ================================================ ================================================ FILE: web/src/components/upload/cropper.vue ================================================ ================================================ FILE: web/src/components/upload/image.vue ================================================ ================================================ FILE: web/src/components/warningBar/warningBar.vue ================================================ ================================================ FILE: web/src/core/config.js ================================================ /** * 网站配置文件 */ import packageInfo from '../../package.json' const greenText = (text) => `\x1b[32m${text}\x1b[0m` export const config = { appName: 'Gin-Vue-Admin', showViteLogo: true, keepAliveTabs: false, logs: [] } export const viteLogo = (env) => { if (config.showViteLogo) { console.log( greenText( `> 欢迎使用Gin-Vue-Admin,开源地址:https://github.com/flipped-aurora/gin-vue-admin` ) ) console.log(greenText(`> 当前版本:v${packageInfo.version}`)) console.log(greenText(`> 加群方式:微信:shouzi_1994 QQ群:470239250`)) console.log( greenText(`> 项目地址:https://github.com/flipped-aurora/gin-vue-admin`) ) console.log(greenText(`> 插件市场:https://plugin.gin-vue-admin.com`)) console.log( greenText(`> GVA讨论社区:https://support.qq.com/products/371961`) ) console.log( greenText( `> 默认自动化文档地址:http://127.0.0.1:${env.VITE_SERVER_PORT}/swagger/index.html` ) ) console.log( greenText(`> 默认前端文件运行地址:http://127.0.0.1:${env.VITE_CLI_PORT}`) ) console.log( greenText( `--------------------------------------版权声明--------------------------------------` ) ) console.log(greenText(`** 版权所有方:flipped-aurora开源团队 **`)) console.log(greenText(`** 版权持有公司:北京翻转极光科技有限责任公司 **`)) console.log( greenText( `** 剔除授权标识需购买商用授权:https://plugin.gin-vue-admin.com/license **` ) ) console.log('\n') } } export default config ================================================ FILE: web/src/core/error-handel.js ================================================ import { createSysError } from '@/api/system/sysError' function sendErrorTip(errorInfo) { setTimeout(() => { const errorData = { form: errorInfo.type, info: `${errorInfo.message}\nStack: ${errorInfo.stack}${errorInfo.component ? `\nComponent: ${errorInfo.component.name || 'Unknown'}` : ''}${errorInfo.vueInfo ? `\nVue Info: ${errorInfo.vueInfo}` : ''}${errorInfo.source ? `\nSource: ${errorInfo.source}:${errorInfo.lineno}:${errorInfo.colno}` : ''}`, level: 'error', solution: null } createSysError(errorData).catch(apiErr => { console.error('Failed to create error record:', apiErr) }) }, 0) } window.addEventListener('unhandledrejection', (event) => { sendErrorTip({ type: '前端', message: `错误信息: ${event.reason}`, stack: `调用栈: ${event.reason?.stack || '没有调用栈信息'}`, }); }); ================================================ FILE: web/src/core/gin-vue-admin.js ================================================ /* * gin-vue-admin web框架组 * * */ // 加载网站配置文件夹 import { register } from './global' import packageInfo from '../../package.json' export default { install: (app) => { register(app) console.log(` 欢迎使用 Gin-Vue-Admin 当前版本:v${packageInfo.version} 加群方式:微信:shouzi_1994 QQ群:622360840 项目地址:https://github.com/flipped-aurora/gin-vue-admin 插件市场:https://plugin.gin-vue-admin.com GVA讨论社区:https://support.qq.com/products/371961 默认自动化文档地址:http://127.0.0.1:${import.meta.env.VITE_SERVER_PORT}/swagger/index.html 默认前端文件运行地址:http://127.0.0.1:${import.meta.env.VITE_CLI_PORT} 如果项目让您获得了收益,希望您能请团队喝杯可乐:https://www.gin-vue-admin.com/coffee/index.html --------------------------------------版权声明-------------------------------------- ** 版权所有方:flipped-aurora开源团队 ** ** 版权持有公司:北京翻转极光科技有限责任公司 ** ** 剔除授权标识需购买商用授权:https://plugin.gin-vue-admin.com/license ** ** 感谢您对Gin-Vue-Admin的支持与关注 合法授权使用更有利于项目的长久发展** `) } } ================================================ FILE: web/src/core/global.js ================================================ import config from './config' import { h } from 'vue' // 统一导入el-icon图标 import * as ElIconModules from '@element-plus/icons-vue' import svgIcon from '@/components/svgIcon/svgIcon.vue' // 导入转换图标名称的函数 const createIconComponent = (name) => ({ name: 'SvgIcon', render() { return h(svgIcon, { localIcon: name }) } }) const registerIcons = async (app) => { const iconModules = import.meta.glob('@/assets/icons/**/*.svg') // 系统目录 svg 图标 const pluginIconModules = import.meta.glob( '@/plugin/**/assets/icons/**/*.svg' ) // 插件目录 svg 图标 const mergedIconModules = Object.assign({}, iconModules, pluginIconModules) // 合并所有 svg 图标 let allKeys = [] for (const path in mergedIconModules) { let pluginName = '' if (path.startsWith('/src/plugin/')) { pluginName = `${path.split('/')[3]}-` } const iconName = path .split('/') .pop() .replace(/\.svg$/, '') // 如果iconName带空格则不加入到图标库中并且提示名称不合法 if (iconName.indexOf(' ') !== -1) { console.error(`icon ${iconName}.svg includes whitespace in ${path}`) continue } const key = `${pluginName}${iconName}` const iconComponent = createIconComponent(key) config.logs.push({ key: key, label: key }) app.component(key, iconComponent) // 开发模式下列出所有 svg 图标,方便开发者直接查找复制使用 allKeys.push(key) } import.meta.env.MODE == 'development' && console.log(`所有可用的本地图标: ${allKeys.join(', ')}`) } export const register = (app) => { // 统一注册el-icon图标 for (const iconName in ElIconModules) { app.component(iconName, ElIconModules[iconName]) } app.component('SvgIcon', svgIcon) registerIcons(app) app.config.globalProperties.$GIN_VUE_ADMIN = config } ================================================ FILE: web/src/directive/auth.js ================================================ // 权限按钮展示指令 import { useUserStore } from '@/pinia/modules/user' export default { install: (app) => { const userStore = useUserStore() app.directive('auth', { // 当被绑定的元素插入到 DOM 中时…… mounted: function (el, binding) { const userInfo = userStore.userInfo if (!binding.value){ el.parentNode.removeChild(el) return } const waitUse = binding.value.toString().split(',') let flag = waitUse.some((item) => Number(item) === userInfo.authorityId) if (binding.modifiers.not) { flag = !flag } if (!flag) { el.parentNode.removeChild(el) } } }) } } ================================================ FILE: web/src/directive/clickOutSide.js ================================================ export default { install: (app) => { app.directive('click-outside', { mounted(el, binding) { const handler = (e) => { // 如果绑定的元素包含事件目标,或元素已经被移除,则不触发 if (!el || el.contains(e.target) || e.target === el) return // 支持函数或对象 { handler: fn, exclude: [el1, el2], capture: true } const value = binding.value if (value && typeof value === 'object') { if ( value.exclude && value.exclude.some( (ex) => ex && ex.contains && ex.contains(e.target) ) ) return if (typeof value.handler === 'function') value.handler(e) } else if (typeof value === 'function') { value(e) } } // 存到 el 上,便于解绑 el.__clickOutsideHandler__ = handler // 延迟注册,避免 mounted 时触发(比如当点击就是触发绑定动作时) setTimeout(() => { document.addEventListener('mousedown', handler) document.addEventListener('touchstart', handler) }, 0) }, unmounted(el) { const h = el.__clickOutsideHandler__ if (h) { document.removeEventListener('mousedown', h) document.removeEventListener('touchstart', h) delete el.__clickOutsideHandler__ } } }) } } ================================================ FILE: web/src/hooks/charts.js ================================================ // 本组件参考 arco-pro 的实现 // https://github.com/arco-design/arco-design-pro-vue/blob/main/arco-design-pro-vite/src/hooks/chart-option.ts import { computed } from 'vue' import { useAppStore } from '@/pinia' export default function useChartOption(sourceOption) { const appStore = useAppStore() const isDark = computed(() => { return appStore.isDark }) const chartOption = computed(() => { return sourceOption(isDark.value) }) return { chartOption } } ================================================ FILE: web/src/hooks/responsive.js ================================================ // 本组件参考 arco-pro 的实现 // https://github.com/arco-design/arco-design-pro-vue/blob/main/arco-design-pro-vite/src/hooks/responsive.ts import { onMounted, onBeforeMount, onBeforeUnmount } from 'vue' import { useDebounceFn } from '@vueuse/core' import { useAppStore } from '@/pinia' import { addEventListen, removeEventListen } from '@/utils/event' const WIDTH = 992 function queryDevice() { const rect = document.body.getBoundingClientRect() return rect.width - 1 < WIDTH } export default function useResponsive(immediate) { const appStore = useAppStore() function resizeHandler() { if (!document.hidden) { const isMobile = queryDevice() appStore.toggleDevice(isMobile ? 'mobile' : 'desktop') // appStore.toggleDevice(isMobile); } } const debounceFn = useDebounceFn(resizeHandler, 100) onMounted(() => { if (immediate) debounceFn() }) onBeforeMount(() => { addEventListen(window, 'resize', debounceFn) }) onBeforeUnmount(() => { removeEventListen(window, 'resize', debounceFn) }) } ================================================ FILE: web/src/hooks/use-windows-resize.js ================================================ // 监听 window 的 resize 事件,返回当前窗口的宽高 import { shallowRef } from 'vue' import { tryOnMounted, useEventListener } from '@vueuse/core' const width = shallowRef(0) const height = shallowRef(0) export const useWindowResize = (cb) => { const onResize = () => { width.value = window.innerWidth height.value = window.innerHeight if (cb && typeof cb === 'function') { cb(width.value, height.value) } } tryOnMounted(onResize) useEventListener('resize', onResize, { passive: true }) return { width, height } } ================================================ FILE: web/src/main.js ================================================ import './style/element_visiable.scss' import 'element-plus/theme-chalk/dark/css-vars.css' import 'uno.css' import { createApp } from 'vue' import ElementPlus from 'element-plus' import { setupVueRootValidator } from 'vite-check-multiple-dom/client'; import 'element-plus/dist/index.css' // 引入gin-vue-admin前端初始化相关内容 import './core/gin-vue-admin' // 引入封装的router import router from '@/router/index' import '@/permission' import run from '@/core/gin-vue-admin.js' import auth from '@/directive/auth' import clickOutSide from '@/directive/clickOutSide' import { store } from '@/pinia' import App from './App.vue' import '@/core/error-handel' const app = createApp(App) app.config.productionTip = false setupVueRootValidator(app, { lang: 'zh' }) app .use(run) .use(ElementPlus) .use(store) .use(auth) .use(clickOutSide) .use(router) .mount('#app') export default app ================================================ FILE: web/src/pathInfo.json ================================================ { "/src/view/about/index.vue": "About", "/src/view/dashboard/components/banner.vue": "Banner", "/src/view/dashboard/components/card.vue": "Card", "/src/view/dashboard/components/charts-content-numbers.vue": "ChartsContentNumbers", "/src/view/dashboard/components/charts-people-numbers.vue": "ChartsPeopleNumbers", "/src/view/dashboard/components/charts.vue": "Charts", "/src/view/dashboard/components/notice.vue": "Notice", "/src/view/dashboard/components/pluginTable.vue": "PluginTable", "/src/view/dashboard/components/quickLinks.vue": "QuickLinks", "/src/view/dashboard/components/table.vue": "Table", "/src/view/dashboard/components/wiki.vue": "Wiki", "/src/view/dashboard/index.vue": "Dashboard", "/src/view/error/index.vue": "Error", "/src/view/error/reload.vue": "Reload", "/src/view/example/breakpoint/breakpoint.vue": "BreakPoint", "/src/view/example/customer/customer.vue": "Customer", "/src/view/example/index.vue": "Example", "/src/view/example/upload/scanUpload.vue": "scanUpload", "/src/view/example/upload/upload.vue": "Upload", "/src/view/init/index.vue": "Init", "/src/view/layout/aside/asideComponent/asyncSubmenu.vue": "AsyncSubmenu", "/src/view/layout/aside/asideComponent/index.vue": "AsideComponent", "/src/view/layout/aside/asideComponent/menuItem.vue": "MenuItem", "/src/view/layout/aside/combinationMode.vue": "GvaAside", "/src/view/layout/aside/headMode.vue": "GvaAside", "/src/view/layout/aside/index.vue": "Index", "/src/view/layout/aside/normalMode.vue": "GvaAside", "/src/view/layout/aside/sidebarMode.vue": "SidebarMode", "/src/view/layout/header/index.vue": "Index", "/src/view/layout/header/tools.vue": "Tools", "/src/view/layout/iframe.vue": "GvaLayoutIframe", "/src/view/layout/index.vue": "GvaLayout", "/src/view/layout/screenfull/index.vue": "Screenfull", "/src/view/layout/search/search.vue": "BtnBox", "/src/view/layout/setting/components/layoutModeCard.vue": "LayoutModeCard", "/src/view/layout/setting/components/settingItem.vue": "SettingItem", "/src/view/layout/setting/components/themeColorPicker.vue": "ThemeColorPicker", "/src/view/layout/setting/components/themeModeSelector.vue": "ThemeModeSelector", "/src/view/layout/setting/index.vue": "GvaSetting", "/src/view/layout/setting/modules/appearance/index.vue": "AppearanceSettings", "/src/view/layout/setting/modules/general/index.vue": "GeneralSettings", "/src/view/layout/setting/modules/layout/index.vue": "LayoutSettings", "/src/view/layout/tabs/index.vue": "HistoryComponent", "/src/view/login/index.vue": "Login", "/src/view/person/person.vue": "Person", "/src/view/routerHolder.vue": "RouterHolder", "/src/view/superAdmin/api/api.vue": "Api", "/src/view/superAdmin/authority/authority.vue": "Authority", "/src/view/superAdmin/authority/components/apis.vue": "Apis", "/src/view/superAdmin/authority/components/datas.vue": "Datas", "/src/view/superAdmin/authority/components/menus.vue": "Menus", "/src/view/superAdmin/dictionary/sysDictionary.vue": "SysDictionary", "/src/view/superAdmin/dictionary/sysDictionaryDetail.vue": "SysDictionaryDetail", "/src/view/superAdmin/index.vue": "SuperAdmin", "/src/view/superAdmin/menu/components/components-cascader.vue": "ComponentsCascader", "/src/view/superAdmin/menu/icon.vue": "Icon", "/src/view/superAdmin/menu/menu.vue": "Menus", "/src/view/superAdmin/operation/sysOperationRecord.vue": "SysOperationRecord", "/src/view/superAdmin/params/sysParams.vue": "SysParams", "/src/view/superAdmin/user/user.vue": "User", "/src/view/system/state.vue": "State", "/src/view/systemTools/apiToken/index.vue": "Index", "/src/view/systemTools/autoCode/component/fieldDialog.vue": "FieldDialog", "/src/view/systemTools/autoCode/component/previewCodeDialog.vue": "PreviewCodeDialog", "/src/view/systemTools/autoCode/index.vue": "AutoCode", "/src/view/systemTools/autoCode/mcp.vue": "MCP", "/src/view/systemTools/autoCode/mcpTest.vue": "MCPTest", "/src/view/systemTools/autoCode/picture.vue": "Picture", "/src/view/systemTools/autoCodeAdmin/index.vue": "AutoCodeAdmin", "/src/view/systemTools/autoPkg/autoPkg.vue": "AutoPkg", "/src/view/systemTools/exportTemplate/exportTemplate.vue": "ExportTemplate", "/src/view/systemTools/formCreate/index.vue": "FormGenerator", "/src/view/systemTools/index.vue": "System", "/src/view/systemTools/installPlugin/index.vue": "Index", "/src/view/systemTools/loginLog/index.vue": "Index", "/src/view/systemTools/pubPlug/pubPlug.vue": "PubPlug", "/src/view/systemTools/skills/index.vue": "Skills", "/src/view/systemTools/sysError/sysError.vue": "SysError", "/src/view/systemTools/system/system.vue": "Config", "/src/view/systemTools/version/version.vue": "SysVersion", "/src/plugin/announcement/form/info.vue": "InfoForm", "/src/plugin/announcement/view/info.vue": "Info", "/src/plugin/email/view/index.vue": "Email" } ================================================ FILE: web/src/permission.js ================================================ import { useUserStore } from '@/pinia/modules/user' import { useRouterStore } from '@/pinia/modules/router' import getPageTitle from '@/utils/page' import router from '@/router' import Nprogress from 'nprogress' import 'nprogress/nprogress.css' // 配置 NProgress Nprogress.configure({ showSpinner: false, ease: 'ease', speed: 500 }) // 白名单路由 const WHITE_LIST = ['Login', 'Init'] function isExternalUrl(val) { return typeof val === 'string' && /^(https?:)?\/\//.test(val) } // 工具函数:统一路径归一化 function normalizeAbsolutePath(p) { const s = '/' + String(p || '') return s.replace(/\/+/g, '/') } function normalizeRelativePath(p) { return String(p || '').replace(/^\/+/, '') } // 安全注册:仅在路由名未存在时注册顶级路由 function addTopLevelIfAbsent(r) { if (!router.hasRoute(r.name)) { router.addRoute(r) } } // 将 n 级菜单扁平化为: // - 常规:一级 layout + 二级页面组件 // - 若某节点 meta.defaultMenu === true:该节点为顶级(不包裹在 layout 下),其子节点作为该顶级的二级页面组件 function addRouteByChildren(route, segments = [], parentName = null) { // 跳过外链根节点 if (isExternalUrl(route?.path) || isExternalUrl(route?.name) || isExternalUrl(route?.component)) { return } // 顶层 layout 仅用于承载,不参与路径拼接 if (route?.name === 'layout') { route.children?.forEach((child) => addRouteByChildren(child, [], null)) return } // 如果标记为 defaultMenu,则该路由应作为顶级路由(不包裹在 layout 下) if (route?.meta?.defaultMenu === true && parentName === null) { const fullPath = [...segments, route.path].filter(Boolean).join('/') const children = route.children ? [...route.children] : [] const newRoute = { ...route, path: fullPath } delete newRoute.children delete newRoute.parent // 顶级路由使用绝对路径 newRoute.path = normalizeAbsolutePath(newRoute.path) // 若已存在同名路由则整体跳过(之前应已处理过其子节点) if (router.hasRoute(newRoute.name)) return addTopLevelIfAbsent(newRoute) // 若该 defaultMenu 节点仍有子节点,继续递归处理其子节点(挂载到该顶级路由下) if (children.length) { // 重置片段,使其成为顶级下的二级相对路径 children.forEach((child) => addRouteByChildren(child, [], newRoute.name)) } return } // 还有子节点,继续向下收集路径片段(忽略外链片段) if (route?.children && route.children.length) { if(!parentName){ const firstChild = route.children[0] if (firstChild) { const fullParentPath = [...segments, route.path].filter(Boolean).join('/') const redirectPath = normalizeRelativePath( [fullParentPath, firstChild.path].filter(Boolean).join('/') ) const parentRoute = { path: normalizeRelativePath(fullParentPath), name: route.name, // 保留父级名称,以便 defaultRouter 可以指向它 meta: route.meta, redirect: "/layout/" + redirectPath, } router.addRoute('layout', parentRoute) } } const nextSegments = isExternalUrl(route.path) ? segments : [...segments, route.path] route.children.forEach((child) => addRouteByChildren(child, nextSegments, parentName)) return } // 叶子节点:注册为其父(defaultMenu 顶级或 layout)的二级子路由 const fullPath = [...segments, route.path].filter(Boolean).join('/') const newRoute = { ...route, path: fullPath } delete newRoute.children delete newRoute.parent // 子路由使用相对路径,避免 /layout/layout/... 的问题 newRoute.path = normalizeRelativePath(newRoute.path) if (parentName) { // 挂载到 defaultMenu 顶级路由下 router.addRoute(parentName, newRoute) } else { // 常规:挂载到 layout 下 router.addRoute('layout', newRoute) } } // 处理路由加载 const setupRouter = async (userStore) => { try { const routerStore = useRouterStore() await Promise.all([routerStore.SetAsyncRouter(), userStore.GetUserInfo()]) // 确保先注册父级 layout const baseRouters = routerStore.asyncRouters || [] const layoutRoute = baseRouters[0] if (layoutRoute?.name === 'layout' && !router.hasRoute('layout')) { const bareLayout = { ...layoutRoute, children: [] } router.addRoute(bareLayout) } // 扁平化:将 layout.children 与其余顶层异步路由一并作为二级子路由注册到 layout 下 const toRegister = [] if (layoutRoute?.children?.length) { toRegister.push(...layoutRoute.children) } if (baseRouters.length > 1) { baseRouters.slice(1).forEach((r) => { if (r?.name !== 'layout') toRegister.push(r) }) } toRegister.forEach((r) => addRouteByChildren(r, [], null)) return true } catch (error) { console.error('Setup router failed:', error) return false } } // 移除加载动画 const removeLoading = () => { const element = document.getElementById('gva-loading-box') element?.remove() } // 路由守卫 router.beforeEach(async (to, from) => { const userStore = useUserStore() const routerStore = useRouterStore() const token = userStore.token Nprogress.start() // 处理元数据和缓存 to.meta.matched = [...to.matched] await routerStore.handleKeepAlive(to) // 设置页面标题 document.title = getPageTitle(to.meta.title, to) if (to.meta.client) { return true } // 白名单路由处理 if (WHITE_LIST.includes(to.name)) { if (token) { if(!routerStore.asyncRouterFlag){ await setupRouter(userStore) } if(userStore.userInfo.authority.defaultRouter){ return { name: userStore.userInfo.authority.defaultRouter } } } return true } // 需要登录的路由处理 if (token) { // 处理需要跳转到首页的情况 if (sessionStorage.getItem('needToHome') === 'true') { sessionStorage.removeItem('needToHome') return { path: '/' } } // 处理异步路由 if (!routerStore.asyncRouterFlag && !WHITE_LIST.includes(from.name)) { await setupRouter(userStore) return to } return to.matched.length ? true : { path: '/layout/404' } } // 未登录跳转登录页 return { name: 'Login', query: { redirect: to.fullPath } } }) // 路由加载完成 router.afterEach(() => { document.querySelector('.main-cont.main-right')?.scrollTo(0, 0) Nprogress.done() }) // 路由错误处理 router.onError((error) => { console.error('Router error:', error) Nprogress.remove() }) // 移除初始加载动画 removeLoading() ================================================ FILE: web/src/pinia/index.js ================================================ import { createPinia } from 'pinia' import { useAppStore } from '@/pinia/modules/app' import { useUserStore } from '@/pinia/modules/user' import { useDictionaryStore } from '@/pinia/modules/dictionary' const store = createPinia() export { store, useAppStore, useUserStore, useDictionaryStore } ================================================ FILE: web/src/pinia/modules/app.js ================================================ import { defineStore } from 'pinia' import { ref, watchEffect, reactive } from 'vue' import { setBodyPrimaryColor } from '@/utils/format' import { useDark, usePreferredDark } from '@vueuse/core' export const useAppStore = defineStore('app', () => { const device = ref('') const drawerSize = ref('') const operateMinWith = ref('240') const config = reactive({ weakness: false, grey: false, primaryColor: '#3b82f6', showTabs: true, darkMode: 'auto', layout_side_width: 256, layout_side_collapsed_width: 80, layout_side_item_height: 48, show_watermark: true, side_mode: 'normal', // 页面过渡动画配置 transition_type: 'slide', global_size: 'default' }) const isDark = useDark({ selector: 'html', attribute: 'class', valueDark: 'dark', valueLight: 'light' }) const preferredDark = usePreferredDark() const toggleTheme = (darkMode) => { isDark.value = darkMode } const toggleWeakness = (e) => { config.weakness = e } const toggleGrey = (e) => { config.grey = e } const togglePrimaryColor = (e) => { config.primaryColor = e } const toggleTabs = (e) => { config.showTabs = e } const toggleDevice = (e) => { if (e === 'mobile') { drawerSize.value = '100%' operateMinWith.value = '80' } else { drawerSize.value = '800' operateMinWith.value = '240' } device.value = e } const toggleDarkMode = (e) => { config.darkMode = e } // 监听系统主题变化 watchEffect(() => { if (config.darkMode === 'auto') { isDark.value = preferredDark.value return } isDark.value = config.darkMode === 'dark' }) const toggleConfigSideWidth = (e) => { config.layout_side_width = e } const toggleConfigSideCollapsedWidth = (e) => { config.layout_side_collapsed_width = e } const toggleConfigSideItemHeight = (e) => { config.layout_side_item_height = e } const toggleConfigWatermark = (e) => { config.show_watermark = e } const toggleSideMode = (e) => { config.side_mode = e } const toggleTransition = (e) => { config.transition_type = e } const toggleGlobalSize = (e) => { config.global_size = e } const baseCoinfg = { weakness: false, grey: false, primaryColor: '#3b82f6', showTabs: true, darkMode: 'auto', layout_side_width: 256, layout_side_collapsed_width: 80, layout_side_item_height: 48, show_watermark: true, side_mode: 'normal', // 页面过渡动画配置 transition_type: 'slide', global_size: 'default' } const resetConfig = () => { for (let baseCoinfgKey in baseCoinfg) { config[baseCoinfgKey] = baseCoinfg[baseCoinfgKey] } } // 监听色弱模式和灰色模式 watchEffect(() => { document.documentElement.classList.toggle('html-weakenss', config.weakness) document.documentElement.classList.toggle('html-grey', config.grey) }) // 监听主题色 watchEffect(() => { setBodyPrimaryColor(config.primaryColor, isDark.value ? 'dark' : 'light') }) return { isDark, device, drawerSize, operateMinWith, config, toggleTheme, toggleDevice, toggleWeakness, toggleGrey, togglePrimaryColor, toggleTabs, toggleDarkMode, toggleConfigSideWidth, toggleConfigSideCollapsedWidth, toggleConfigSideItemHeight, toggleConfigWatermark, toggleSideMode, toggleTransition, resetConfig, toggleGlobalSize } }) ================================================ FILE: web/src/pinia/modules/dictionary.js ================================================ import { findSysDictionary } from '@/api/sysDictionary' import { getDictionaryTreeListByType } from '@/api/sysDictionaryDetail' import { defineStore } from 'pinia' import { ref } from 'vue' export const useDictionaryStore = defineStore('dictionary', () => { const dictionaryMap = ref({}) const setDictionaryMap = (dictionaryRes) => { dictionaryMap.value = { ...dictionaryMap.value, ...dictionaryRes } } // 过滤树形数据的深度 const filterTreeByDepth = (items, currentDepth, targetDepth) => { if (targetDepth === 0) { // depth=0 返回全部数据 return items } if (currentDepth >= targetDepth) { // 达到目标深度,移除children return items.map((item) => ({ label: item.label, value: item.value, extend: item.extend })) } // 递归处理子项 return items.map((item) => ({ label: item.label, value: item.value, extend: item.extend, children: item.children ? filterTreeByDepth(item.children, currentDepth + 1, targetDepth) : undefined })) } // 将树形结构扁平化为数组(用于兼容原有的平铺格式) const flattenTree = (items) => { const result = [] const traverse = (nodes) => { nodes.forEach((item) => { result.push({ label: item.label, value: item.value, extend: item.extend }) if (item.children && item.children.length > 0) { traverse(item.children) } }) } traverse(items) return result } // 标准化树形数据,确保每个节点都包含标准的字段格式 const normalizeTreeData = (items) => { return items.map((item) => ({ label: item.label, value: item.value, extend: item.extend, children: item.children && item.children.length > 0 ? normalizeTreeData(item.children) : undefined })) } // 根据value和depth查找指定节点并返回其children const findNodeByValue = ( items, targetValue, currentDepth = 1, maxDepth = 0 ) => { for (const item of items) { // 如果找到目标value的节点 if (item.value === targetValue) { // 如果maxDepth为0,返回所有children if (maxDepth === 0) { return item.children ? normalizeTreeData(item.children) : [] } // 否则根据depth限制返回children if (item.children && item.children.length > 0) { return filterTreeByDepth(item.children, 1, maxDepth) } return [] } // 如果当前深度小于最大深度,继续在children中查找 if ( item.children && item.children.length > 0 && (maxDepth === 0 || currentDepth < maxDepth) ) { const result = findNodeByValue( item.children, targetValue, currentDepth + 1, maxDepth ) if (result !== null) { return result } } } return null } const getDictionary = async (type, depth = 0, value = null) => { // 如果传入了value参数,则查找指定节点的children if (value !== null) { // 构建缓存key,包含value和depth信息 const cacheKey = `${type}_value_${value}_depth_${depth}` if ( dictionaryMap.value[cacheKey] && dictionaryMap.value[cacheKey].length ) { return dictionaryMap.value[cacheKey] } try { // 获取完整的树形结构数据 const treeRes = await getDictionaryTreeListByType({ type }) if ( treeRes.code === 0 && treeRes.data && treeRes.data.list && treeRes.data.list.length > 0 ) { // 查找指定value的节点并返回其children const targetNodeChildren = findNodeByValue( treeRes.data.list, value, 1, depth ) if (targetNodeChildren !== null) { let resultData if (depth === 0) { // depth=0 时返回完整的children树形结构 resultData = targetNodeChildren } else { // 其他depth值:扁平化children数据 resultData = flattenTree(targetNodeChildren) } const dictionaryRes = {} dictionaryRes[cacheKey] = resultData setDictionaryMap(dictionaryRes) return dictionaryMap.value[cacheKey] } else { // 如果没找到指定value的节点,返回空数组 return [] } } } catch (error) { console.error('根据value获取字典数据失败:', error) return [] } } // 原有的逻辑:不传value参数时的处理 // 构建缓存key,包含depth信息 const cacheKey = depth === 0 ? `${type}_tree` : `${type}_depth_${depth}` if (dictionaryMap.value[cacheKey] && dictionaryMap.value[cacheKey].length) { return dictionaryMap.value[cacheKey] } else { try { // 首先尝试获取树形结构数据 const treeRes = await getDictionaryTreeListByType({ type }) if ( treeRes.code === 0 && treeRes.data && treeRes.data.list && treeRes.data.list.length > 0 ) { // 使用树形结构数据 const treeData = treeRes.data.list let resultData if (depth === 0) { // depth=0 时返回完整的树形结构,但要确保字段格式标准化 resultData = normalizeTreeData(treeData) } else { // 其他depth值:根据depth参数过滤数据,然后扁平化 const filteredData = filterTreeByDepth(treeData, 1, depth) resultData = flattenTree(filteredData) } const dictionaryRes = {} dictionaryRes[cacheKey] = resultData setDictionaryMap(dictionaryRes) return dictionaryMap.value[cacheKey] } else { // 如果没有树形数据,回退到原有的平铺方式 const res = await findSysDictionary({ type }) if (res.code === 0) { const dictionaryRes = {} const dict = [] res.data.resysDictionary.sysDictionaryDetails && res.data.resysDictionary.sysDictionaryDetails.forEach((item) => { dict.push({ label: item.label, value: item.value, extend: item.extend }) }) dictionaryRes[cacheKey] = dict setDictionaryMap(dictionaryRes) return dictionaryMap.value[cacheKey] } } } catch (error) { console.error('获取字典数据失败:', error) // 发生错误时回退到原有方式 const res = await findSysDictionary({ type }) if (res.code === 0) { const dictionaryRes = {} const dict = [] res.data.resysDictionary.sysDictionaryDetails && res.data.resysDictionary.sysDictionaryDetails.forEach((item) => { dict.push({ label: item.label, value: item.value, extend: item.extend }) }) dictionaryRes[cacheKey] = dict setDictionaryMap(dictionaryRes) return dictionaryMap.value[cacheKey] } } } } return { dictionaryMap, setDictionaryMap, getDictionary } }) ================================================ FILE: web/src/pinia/modules/params.js ================================================ import { getSysParam } from '@/api/sysParams' import { defineStore } from 'pinia' import { ref } from 'vue' export const useParamsStore = defineStore('params', () => { const paramsMap = ref({}) const setParamsMap = (paramsRes) => { paramsMap.value = { ...paramsMap.value, ...paramsRes } } const getParams = async(key) => { if (paramsMap.value[key] && paramsMap.value[key].length) { return paramsMap.value[key] } else { const res = await getSysParam({ key }) if (res.code === 0) { const paramsRes = {} paramsRes[key] = res.data.value setParamsMap(paramsRes) return paramsMap.value[key] } } } return { paramsMap, setParamsMap, getParams } }) ================================================ FILE: web/src/pinia/modules/router.js ================================================ import { asyncRouterHandle } from '@/utils/asyncRouter' import { emitter } from '@/utils/bus.js' import { asyncMenu } from '@/api/menu' import { defineStore } from 'pinia' import { ref, watchEffect } from 'vue' import pathInfo from '@/pathInfo.json' import {useRoute} from "vue-router"; import {config} from "@/core/config.js"; const notLayoutRouterArr = [] const keepAliveRoutersArr = [] const nameMap = {} const formatRouter = (routes, routeMap, parent) => { routes && routes.forEach((item) => { item.parent = parent item.meta.btns = item.btns item.meta.hidden = item.hidden if (item.meta.defaultMenu === true) { if (!parent) { item = { ...item, path: `/${item.path}` } notLayoutRouterArr.push(item) } } routeMap[item.name] = item if (item.children && item.children.length > 0) { formatRouter(item.children, routeMap, item) } }) } const KeepAliveFilter = (routes) => { routes && routes.forEach((item) => { // 子菜单中有 keep-alive 的,父菜单也必须 keep-alive,否则无效。这里将子菜单中有 keep-alive 的父菜单也加入。 if ( (item.children && item.children.some((ch) => ch.meta.keepAlive)) || item.meta.keepAlive ) { const path = item.meta.path keepAliveRoutersArr.push(pathInfo[path]) nameMap[item.name] = pathInfo[path] } if (item.children && item.children.length > 0) { KeepAliveFilter(item.children) } }) } export const useRouterStore = defineStore('router', () => { const keepAliveRouters = ref([]) const asyncRouterFlag = ref(0) const setKeepAliveRouters = (history) => { const keepArrTemp = [] // 1. 首先添加原有的keepAlive配置 keepArrTemp.push(...keepAliveRoutersArr) if (config.keepAliveTabs) { history.forEach((item) => { // 2. 为所有history中的路由强制启用keep-alive // 通过routeMap获取路由信息,然后通过pathInfo获取组件名 const routeInfo = routeMap[item.name] if (routeInfo && routeInfo.meta && routeInfo.meta.path) { const componentName = pathInfo[routeInfo.meta.path] if (componentName) { keepArrTemp.push(componentName) } } // 3. 如果子路由在tabs中打开,父路由也需要keepAlive if (nameMap[item.name]) { keepArrTemp.push(nameMap[item.name]) } }) } keepAliveRouters.value = Array.from(new Set(keepArrTemp)) } // 处理组件缓存 const handleKeepAlive = async (to) => { if (!to.matched.some((item) => item.meta.keepAlive)) return if (to.matched?.length > 2) { for (let i = 1; i < to.matched.length; i++) { const element = to.matched[i - 1] if (element.name === 'layout') { to.matched.splice(i, 1) await handleKeepAlive(to) continue } if (typeof element.components.default === 'function') { await element.components.default() await handleKeepAlive(to) } } } } const route = useRoute() emitter.on('setKeepAlive', setKeepAliveRouters) const asyncRouters = ref([]) const topMenu = ref([]) const leftMenu = ref([]) const menuMap = {} const topActive = ref('') const setLeftMenu = (name) => { sessionStorage.setItem('topActive', name) topActive.value = name leftMenu.value = [] if (menuMap[name]?.children) { leftMenu.value = menuMap[name].children } return menuMap[name]?.children } const findTopActive = (menuMap, routeName) => { for (let topName in menuMap) { const topItem = menuMap[topName]; if (topItem.children?.some(item => item.name === routeName)) { return topName; } const foundName = findTopActive(topItem.children || {}, routeName); if (foundName) { return topName; } } return null; }; watchEffect(() => { let topActive = sessionStorage.getItem('topActive') // 初始化菜单内容,防止重复添加 topMenu.value = []; asyncRouters.value[0]?.children.forEach((item) => { if (item.hidden) return menuMap[item.name] = item topMenu.value.push({ ...item, children: [] }) }) if (!topActive || topActive === 'undefined' || topActive === 'null') { topActive = findTopActive(menuMap, route.name); } setLeftMenu(topActive) }) const routeMap = {} // 从后台获取动态路由 const SetAsyncRouter = async () => { asyncRouterFlag.value++ const baseRouter = [ { path: '/layout', name: 'layout', component: 'view/layout/index.vue', meta: { title: '底层layout' }, children: [] } ] const asyncRouterRes = await asyncMenu() const asyncRouter = asyncRouterRes.data.menus asyncRouter && asyncRouter.push({ path: 'reload', name: 'Reload', hidden: true, meta: { title: '', closeTab: true }, component: 'view/error/reload.vue' }) formatRouter(asyncRouter, routeMap) baseRouter[0].children = asyncRouter if (notLayoutRouterArr.length !== 0) { baseRouter.push(...notLayoutRouterArr) } asyncRouterHandle(baseRouter) KeepAliveFilter(asyncRouter) asyncRouters.value = baseRouter return true } return { topActive, setLeftMenu, topMenu, leftMenu, asyncRouters, keepAliveRouters, asyncRouterFlag, SetAsyncRouter, routeMap, handleKeepAlive } }) ================================================ FILE: web/src/pinia/modules/user.js ================================================ import { login, getUserInfo } from '@/api/user' import { jsonInBlacklist } from '@/api/jwt' import router from '@/router/index' import { ElLoading, ElMessage } from 'element-plus' import { defineStore } from 'pinia' import { ref, computed } from 'vue' import { useRouterStore } from './router' import { useCookies } from '@vueuse/integrations/useCookies' import { useStorage } from '@vueuse/core' import { useAppStore } from '@/pinia' export const useUserStore = defineStore('user', () => { const appStore = useAppStore() const loadingInstance = ref(null) const userInfo = ref({ uuid: '', nickName: '', headerImg: '', authority: {} }) const token = useStorage('token', '') const xToken = useCookies() const currentToken = computed(() => token.value || xToken.get('x-token') || '') const setUserInfo = (val) => { userInfo.value = val if (val.originSetting) { Object.keys(appStore.config).forEach((key) => { if (val.originSetting[key] !== undefined) { appStore.config[key] = val.originSetting[key] } }) } } const setToken = (val) => { token.value = val xToken.value = val } const NeedInit = async () => { await ClearStorage() await router.push({ name: 'Init', replace: true }) } const ResetUserInfo = (value = {}) => { userInfo.value = { ...userInfo.value, ...value } } /* 获取用户信息*/ const GetUserInfo = async () => { const res = await getUserInfo() if (res.code === 0) { setUserInfo(res.data.userInfo) } return res } /* 登录*/ const LoginIn = async (loginInfo) => { try { loadingInstance.value = ElLoading.service({ fullscreen: true, text: '登录中,请稍候...' }) const res = await login(loginInfo) if (res.code !== 0) { return false } // 登陆成功,设置用户信息和权限相关信息 setUserInfo(res.data.user) setToken(res.data.token) // 初始化路由信息 const routerStore = useRouterStore() await routerStore.SetAsyncRouter() const asyncRouters = routerStore.asyncRouters // 注册到路由表里 asyncRouters.forEach((asyncRouter) => { router.addRoute(asyncRouter) }) if(router.currentRoute.value.query.redirect) { await router.replace(router.currentRoute.value.query.redirect) return true } if (!router.hasRoute(userInfo.value.authority.defaultRouter)) { ElMessage.error('不存在可以登陆的首页,请联系管理员进行配置') } else { await router.replace({ name: userInfo.value.authority.defaultRouter }) } const isWindows = /windows/i.test(navigator.userAgent) window.localStorage.setItem('osType', isWindows ? 'WIN' : 'MAC') // 全部操作均结束,关闭loading并返回 return true } catch (error) { console.error('LoginIn error:', error) return false } finally { loadingInstance.value?.close() } } /* 登出*/ const LoginOut = async () => { const res = await jsonInBlacklist() // 登出失败 if (res.code !== 0) { return } await ClearStorage() // 把路由定向到登录页,无需等待直接reload router.push({ name: 'Login', replace: true }) window.location.reload() } /* 清理数据 */ const ClearStorage = async () => { token.value = '' // 使用remove方法正确删除cookie xToken.remove() sessionStorage.clear() // 清理所有相关的localStorage项 localStorage.removeItem('originSetting') localStorage.removeItem('token') } return { userInfo, token: currentToken, NeedInit, ResetUserInfo, GetUserInfo, LoginIn, LoginOut, setToken, loadingInstance, ClearStorage } }) ================================================ FILE: web/src/plugin/announcement/api/info.js ================================================ import service from '@/utils/request' // @Tags Info // @Summary 创建公告 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body model.Info true "创建公告" // @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" // @Router /info/createInfo [post] export const createInfo = (data) => { return service({ url: '/info/createInfo', method: 'post', data }) } // @Tags Info // @Summary 删除公告 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body model.Info true "删除公告" // @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" // @Router /info/deleteInfo [delete] export const deleteInfo = (params) => { return service({ url: '/info/deleteInfo', method: 'delete', params }) } // @Tags Info // @Summary 批量删除公告 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body request.IdsReq true "批量删除公告" // @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" // @Router /info/deleteInfo [delete] export const deleteInfoByIds = (params) => { return service({ url: '/info/deleteInfoByIds', method: 'delete', params }) } // @Tags Info // @Summary 更新公告 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body model.Info true "更新公告" // @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}" // @Router /info/updateInfo [put] export const updateInfo = (data) => { return service({ url: '/info/updateInfo', method: 'put', data }) } // @Tags Info // @Summary 用id查询公告 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data query model.Info true "用id查询公告" // @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" // @Router /info/findInfo [get] export const findInfo = (params) => { return service({ url: '/info/findInfo', method: 'get', params }) } // @Tags Info // @Summary 分页获取公告列表 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data query request.PageInfo true "分页获取公告列表" // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" // @Router /info/getInfoList [get] export const getInfoList = (params) => { return service({ url: '/info/getInfoList', method: 'get', params }) } // @Tags Info // @Summary 获取数据源 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" // @Router /info/findInfoDataSource [get] export const getInfoDataSource = () => { return service({ url: '/info/getInfoDataSource', method: 'get' }) } ================================================ FILE: web/src/plugin/announcement/form/info.vue ================================================ ================================================ FILE: web/src/plugin/announcement/view/info.vue ================================================ ================================================ FILE: web/src/plugin/email/api/email.js ================================================ import service from '@/utils/request' // @Tags System // @Summary 发送测试邮件 // @Security ApiKeyAuth // @Produce application/json // @Success 200 {string} string "{"success":true,"data":{},"msg":"发送成功"}" // @Router /email/emailTest [post] export const emailTest = (data) => { return service({ url: '/email/emailTest', method: 'post', data }) } // @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] export const sendEmail = (data) => { return service({ url: '/email/sendEmail', method: 'post', data }) } ================================================ FILE: web/src/plugin/email/view/index.vue ================================================ ================================================ FILE: web/src/router/index.js ================================================ import { createRouter, createWebHashHistory } from 'vue-router' const routes = [ { path: '/', redirect: '/login' }, { path: '/init', name: 'Init', component: () => import('@/view/init/index.vue') }, { path: '/login', name: 'Login', component: () => import('@/view/login/index.vue') }, { path: '/scanUpload', name: 'ScanUpload', meta: { title: '扫码上传', client: true }, component: () => import('@/view/example/upload/scanUpload.vue') }, { path: '/:catchAll(.*)', meta: { closeTab: true }, component: () => import('@/view/error/index.vue') }, ] const router = createRouter({ history: createWebHashHistory(), routes }) export default router ================================================ FILE: web/src/style/element/index.scss ================================================ @forward 'element-plus/theme-chalk/src/common/var.scss' with ( $colors: ( 'white': #ffffff, 'black': #000000, 'primary': ( 'base': #4d70ff ), 'success': ( 'base': #67c23a ), 'warning': ( 'base': #e6a23c ), 'danger': ( 'base': #f56c6c ), 'error': ( 'base': #f56c6c ), 'info': ( 'base': #909399 ) ) ); ================================================ FILE: web/src/style/element_visiable.scss ================================================ @use '@/style/main.scss'; @use '@/style/reset'; .el-button { font-weight: 400; border-radius: 2px; } .gva-pagination { @apply flex justify-end; .el-pagination__editor { .el-input__inner { @apply h-8; } } .is-active { @apply rounded text-white; background: var(--el-color-primary); color: #ffffff !important; } } .el-drawer__header { margin-bottom: 0 !important; padding-top: 16px !important; padding-bottom: 16px !important; @apply border-0 border-b border-solid border-gray-200; } .el-form--inline { .el-form-item { & > .el-input, .el-cascader, .el-select, .el-date-editor, .el-autocomplete { @apply w-52; } } } .el-dropdown { @apply overflow-hidden; } .el-table { tr { th { @apply dark:bg-slate-900; .cell { @apply leading-[36px] text-gray-700 dark:text-gray-200; } } } .el-table__row { td { @apply dark:bg-slate-900; .cell { @apply leading-[32px] text-gray-600 dark:text-gray-300; } } } tr { th { &.is-leaf { @apply dark:bg-slate-900; } } } } // layout // table .el-pagination { @apply mt-8; .btn-prev, .btn-next { @apply border border-solid border-gray-300 dark:border-gray-700 rounded; } .el-pager { li { @apply border border-solid border-gray-300 dark:border-gray-600 rounded text-gray-600 text-sm mx-1; } } } .el-menu { background-color: transparent !important; li { @apply my-1; } } .el-menu--vertical { .el-menu-item { border-radius: 2px; &.is-active { background-color: var(--el-color-primary) !important; color: #fff !important; } } } .el-sub-menu.el-sub-menu__hide-arrow { height: 44px; } .el-tabs__header { margin: 0 0 1px !important; } .el-sub-menu.is-active { > .el-sub-menu__title { color: var(--el-color-primary) !important; } } .el-menu-item.is-active{ color: var(--el-color-primary)!important; } .el-sub-menu__title.el-tooltip__trigger, .el-menu-item .el-menu-tooltip__trigger { justify-content: center; } .el-menu--horizontal .el-menu .el-sub-menu__title { justify-content: flex-start; } html.dark { /* 自定义深色背景颜色 */ --el-bg-color: rgb(30, 41, 59); --el-bg-color-overlay: rgb(40, 51, 69); --el-fill-color-light: rgb(15, 23, 42); --el-fill-color: rgb(15, 23, 42); } ================================================ FILE: web/src/style/iconfont.css ================================================ @font-face { font-family: 'gvaIcon'; src: url('data:font/ttf;charset=utf-8;base64,AAEAAAANAIAAAwBQRkZUTZJUyU8AAA14AAAAHEdERUYAKQARAAANWAAAAB5PUy8yPJpJTAAAAVgAAABgY21hcM0T0L4AAAHYAAABWmdhc3D//wADAAANUAAAAAhnbHlmRk3UvwAAA0wAAAbYaGVhZB/a5jgAAADcAAAANmhoZWEHngOFAAABFAAAACRobXR4DaoBrAAAAbgAAAAebG9jYQbMCGgAAAM0AAAAGG1heHABGgB+AAABOAAAACBuYW1lXoIBAgAACiQAAAKCcG9zdN15OnUAAAyoAAAAqAABAAAAAQAA+a916l8PPPUACwQAAAAAAN5YUSMAAAAA3lhRIwBL/8ADwAM1AAAACAACAAAAAAAAAAEAAAOA/4AAXAQAAAAAAAPAAAEAAAAAAAAAAAAAAAAAAAAEAAEAAAALAHIABQAAAAAAAgAAAAoACgAAAP8AAAAAAAAABAQAAZAABQAAAokCzAAAAI8CiQLMAAAB6wAyAQgAAAIABQMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUGZFZADA5mXmfQOA/4AAAAPcAIAAAAABAAAAAAAAAAAAAAAgAAEEAAAAAAAAAAQAAAAEAACLAIoAYAB1AHYASwBLAGAAAAAAAAMAAAADAAAAHAABAAAAAABUAAMAAQAAABwABAA4AAAACgAIAAIAAuZm5mrmduZ9//8AAOZl5mrmdeZ7//8ZnhmbGZEZjQABAAAAAAAAAAAAAAAAAQYAAAEAAAAAAAAAAQIAAAACAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEYAigEcAbgCUAK6AxoDbAACAIsAIANsAswAEQAjAAAlIicBJjQ3ATYeAQYHCQEeAQYhIicBJjQ3ATYeAQYHCQEeAQYDSw0J/qsLCwFVChsSAgr+xAE8CgIV/qkNCP6qCgoBVgkbEgIK/sUBOwoCFCAJATULGQsBNQoCExwI/uL+4ggbFAkBNQsZCwE1CgITHAj+4v7iCRoUAAAAAAIAigAgA2sCzAARACIAAAE0JwEmDgEWFwkBDgEWMjcBNiUBJg4BFhcJAQ4BFjI3ATY0AiAL/qsJHBECCQE8/sQJAhQZCQFVCwFA/qsKGxICCgE8/sQKAhUZCQFVCwF1DQsBNQoCExwI/uL+4gkaFAkBNQskATUKAhMcCP7i/uIJGhQJATULGQADAGD/wAOgAzUATABcAGwAAAE1NCcmJyYiBwYHBh0BDgEdARQWOwEyNj0BNCYrATU0NzY3NjIXFhcWHQEjIgYdARQWOwEGBwYHLgEjIgYUFjMyNjc2NzY3PgE9ATQmBRUUBisBIiY9ATQ2OwEyFgUUBisBIiY9ATQ2OwEyFhUDYDAvT1O+U08vMBslLB9VHi0tHiAoJkFDnENBJiggHi0tHhUPJC5SChwRHCQkHBEeCHJAMxAfKiX9kAYFVQUGBgVVBQYCVQYFVQUGBgVVBQYByQxgUlAuMDAuUFJgDAQqG6seLCweqx4tCk5DQScnJydBQ04KLR6rHiwrGiAGDxElNiUSEAc1KkUBKx6rGyhFqwQGBgSrBQYGsAQGBgSrBQYGBQAABAB1//UDjQMLABsANwBSAHEAABMyNj0BFxYyNjQvATMyNjQmKwEiBwYHBh0BFBYFIgYdAScmIgYUHwEjIgYUFjsBMjc2NzY9ATYmJQc1NCYiBh0BFBcWFxY7ATI2NCYrATc2NCYGATQ1FSYnJisBIgYUFjsBBwYUFjI/ARUUFjI2PQEnJpUNE7wJHRMKvIcMFBQM1ggCDAgCFALiDRPJCRoTCcmJDBQUDNYIAg8CAwES/gbJExkUAggKBAbWDBQUDInJCRMXAgEHCwQG2AwUFAyJvAkSHgi8ExoTAgEB9RQMibwIEhkKvBMZFAIGDAQI1gwU6hQMickJExoJyRMZFAIICgQG2AwUIsmHDBQUDNYIAg8CAxQZE8kKGRMBAcABAQIOAwMUGRO8ChkTCbyHDBQUDNYFBAAABAB2//cDjgMMABoANQBRAG0AAAEjIgYUFjsBMjc2NzY9ATQmIgYdAScmIgYUFwEzMjY0JisBIgcGBwYdARQWMjY9ARcWMjY0JyUmJyYrASIGFBY7AQcGFBYyPwEVFBYyNj0BLgE3FhcWOwEyNjQmKwE3NjQmIg8BNTQmIgYdAR4BATqJDRMTDdUJAg8CAhMaE7cKGRQKAjeJDRMTDdUJAg8CAhMaE8gJHhIK/i8HCgQH1w0TEw2JyQoTHQnIFBkTAQKoBwoEBtYNExMNibwKFBkKvBMZFAICAhoUGRMCBwoEBtYNExMNib4KExoK/iAUGRMCBwoEB9UNExMNickIEhkK8w8CAhMZFMgKGRMJyYkNExMN1QIJzQ8CAhMZFLsKGhMKvIkNExMN1QMIAAAAAAUAS//LA7UDNQAUACkAKgA3AEQAAAEiBwYHBhQXFhcWMjc2NzY0JyYnJgMiJyYnJjQ3Njc2MhcWFxYUBwYHBgMjFB4BMj4BNC4BIg4BFyIGHQEUFjI2PQE0JgIAd2ZiOzs7O2Jm7mZiOzs7O2Jmd2VXVDIzMzJUV8pXVDIzMzJUV2UrDBQWFAwMFBYUDCsNExMaExMDNTs7YmbuZmI7Ozs7YmbuZmI7O/zWMzJUV8pXVDIzMzJUV8pXVDIzAjULFAwMFBYUDAwUgBQM6w0TEw3rDBQAAQBL/+ADwAMgAD0AAAEmBg8BLgEjIgcGBwYUFxYXFjMyPgE3Ni4BBgcOAiMiJyYnJjQ3Njc2MzIeARcnJg4BFh8BMj8BNj8BNCYDpgwXAxc5yXZyY184Ojo4X2NyWaB4HgULGhcFGWaJS2FUUTAwMTBRU2FIhGQbgA0WBw4NwgUIBAwDMQ0CsQMODFhmeDk3XmHiYV43OUV9UQ0XCQsMRWo6MC9PUr9TTy8wNmNBJQMOGhYDMwMBCAu6DRYAAAAAAgBg/8YDugMiAB4AMwAABSc+ATU0JyYnJiIHBgcGFBcWFxYzMjc2NxcWMjc2JiUiJyYnJjQ3Njc2MhcWFxYUBwYHBgOxviouNDFVV8lXVTIzMzJVV2RDPzwzvgkeCAcB/hxUSEYpKiopRkioSEYpKyspRkgCvjB9RGRYVDIzNDJVWMlXVTE0GBYqvgkJChuBKylGSKhIRikqKilGSKhIRikrAAAAABIA3gABAAAAAAAAABMAKAABAAAAAAABAAgATgABAAAAAAACAAcAZwABAAAAAAADAAgAgQABAAAAAAAEAAgAnAABAAAAAAAFAAsAvQABAAAAAAAGAAgA2wABAAAAAAAKACsBPAABAAAAAAALABMBkAADAAEECQAAACYAAAADAAEECQABABAAPAADAAEECQACAA4AVwADAAEECQADABAAbwADAAEECQAEABAAigADAAEECQAFABYApQADAAEECQAGABAAyQADAAEECQAKAFYA5AADAAEECQALACYBaABDAHIAZQBhAHQAZQBkACAAYgB5ACAAaQBjAG8AbgBmAG8AbgB0AABDcmVhdGVkIGJ5IGljb25mb250AABpAGMAbwBuAGYAbwBuAHQAAGljb25mb250AABSAGUAZwB1AGwAYQByAABSZWd1bGFyAABpAGMAbwBuAGYAbwBuAHQAAGljb25mb250AABpAGMAbwBuAGYAbwBuAHQAAGljb25mb250AABWAGUAcgBzAGkAbwBuACAAMQAuADAAAFZlcnNpb24gMS4wAABpAGMAbwBuAGYAbwBuAHQAAGljb25mb250AABHAGUAbgBlAHIAYQB0AGUAZAAgAGIAeQAgAHMAdgBnADIAdAB0AGYAIABmAHIAbwBtACAARgBvAG4AdABlAGwAbABvACAAcAByAG8AagBlAGMAdAAuAABHZW5lcmF0ZWQgYnkgc3ZnMnR0ZiBmcm9tIEZvbnRlbGxvIHByb2plY3QuAABoAHQAdABwADoALwAvAGYAbwBuAHQAZQBsAGwAbwAuAGMAbwBtAABodHRwOi8vZm9udGVsbG8uY29tAAAAAAIAAAAAAAAACgAAAAAAAQAAAAAAAAAAAAAAAAAAAAAACwAAAAEAAgECAQMBBAEFAQYBBwEIAQkRYXJyb3ctZG91YmxlLWxlZnQSYXJyb3ctZG91YmxlLXJpZ2h0EGN1c3RvbWVyLXNlcnZpY2URZnVsbHNjcmVlbi1leHBhbmQRZnVsbHNjcmVlbi1zaHJpbmsGcHJvbXB0B3JlZnJlc2gGc2VhcmNoAAAAAf//AAIAAQAAAAwAAAAWAAAAAgABAAMACgABAAQAAAACAAAAAAAAAAEAAAAA1aQnCAAAAADeWFEjAAAAAN5YUSM=') format('truetype'); font-weight: 600; font-style: normal; font-display: swap; } .gvaIcon { font-family: 'gvaIcon' !important; font-size: 16px; font-style: normal; font-weight: 800; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .gvaIcon-arrow-double-left:before { content: '\e665'; } .gvaIcon-arrow-double-right:before { content: '\e666'; } .gvaIcon-fullscreen-shrink:before { content: '\e676'; } .gvaIcon-customer-service:before { content: '\e66a'; } .gvaIcon-fullscreen-expand:before { content: '\e675'; } .gvaIcon-prompt:before { content: '\e67b'; } .gvaIcon-refresh:before { content: '\e67c'; } .gvaIcon-search:before { content: '\e67d'; } ================================================ FILE: web/src/style/main.scss ================================================ @use '@/style/iconfont.css'; @use "./transition.scss"; .html-grey { filter: grayscale(100%); } .html-weakenss { filter: invert(80%); } .gva-table-box { @apply p-4 bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900 rounded my-2; .el-table { @apply border-x border-t border-b-0 rounded border-table-border border-solid -mx-[1px]; } } .gva-btn-list { @apply mb-3 flex items-center flex-wrap gap-2; .el-button+.el-button{ @apply ml-0 !important; } .el-upload{ .el-button{ @apply ml-0 !important; } } } #nprogress .bar { background: #29d !important; } .gva-customer-icon { @apply w-4 h-4; } ::-webkit-scrollbar { @apply hidden; } .gva-search-box { @apply p-4 pb-0 bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900 rounded my-2; } .gva-form-box { @apply p-4 bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900 rounded my-2; } .el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content { background: var(--el-color-primary-bg) !important; } .el-dropdown { outline: none; * { outline: none; } } ================================================ FILE: web/src/style/reset.scss ================================================ /* 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) 2. [UnoCSS]: allow to override the default border color with css var `--un-default-border-color` */ *, ::before, ::after { box-sizing: border-box; /* 1 */ border-width: 0; /* 2 */ border-style: solid; /* 2 */ border-color: var(--un-default-border-color, #e5e7eb); /* 2 */ } /* 1. Use a consistent sensible line-height in all browsers. 2. Prevent adjustments of font size after orientation changes in iOS. 3. Use a more readable tab size. 4. Use the user's configured `sans` font-family by default. */ html { line-height: 1.5; /* 1 */ -webkit-text-size-adjust: 100%; /* 2 */ -moz-tab-size: 4; /* 3 */ tab-size: 4; /* 3 */ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; /* 4 */ // TODO: 在下一个大版本更新的时候需要改回正确的16px font-size: 14px; } /* 1. Remove the margin in all browsers. 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. */ body { margin: 0; /* 1 */ line-height: inherit; /* 2 */ } /* 1. Add the correct height in Firefox. 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) 3. Ensure horizontal rules are visible by default. */ hr { height: 0; /* 1 */ color: inherit; /* 2 */ border-top-width: 1px; /* 3 */ } /* Add the correct text decoration in Chrome, Edge, and Safari. */ abbr:where([title]) { text-decoration: underline dotted; } /* Remove the default font size and weight for headings. */ h1, h2, h3, h4, h5, h6 { font-size: inherit; font-weight: inherit; } /* Reset links to optimize for opt-in styling instead of opt-out. */ a { color: inherit; text-decoration: inherit; } /* Add the correct font weight in Edge and Safari. */ b, strong { font-weight: bolder; } /* 1. Use the user's configured `mono` font family by default. 2. Correct the odd `em` font sizing in all browsers. */ code, kbd, samp, pre { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; /* 1 */ font-size: 1em; /* 2 */ } /* Add the correct font size in all browsers. */ small { font-size: 80%; } /* Prevent `sub` and `sup` elements from affecting the line height in all browsers. */ sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } sub { bottom: -0.25em; } sup { top: -0.5em; } /* 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) 3. Remove gaps between table borders by default. */ table { text-indent: 0; /* 1 */ border-color: inherit; /* 2 */ border-collapse: collapse; /* 3 */ } /* 1. Change the font styles in all browsers. 2. Remove the margin in Firefox and Safari. 3. Remove default padding in all browsers. */ button, input, optgroup, select, textarea { font-family: inherit; /* 1 */ font-feature-settings: inherit; /* 1 */ font-variation-settings: inherit; /* 1 */ font-size: 100%; /* 1 */ font-weight: inherit; /* 1 */ line-height: inherit; /* 1 */ color: inherit; /* 1 */ margin: 0; /* 2 */ padding: 0; /* 3 */ } /* Remove the inheritance of text transform in Edge and Firefox. */ button, select { text-transform: none; } /* 1. Correct the inability to style clickable types in iOS and Safari. 2. Remove default button styles. */ button, [type='button'], [type='reset'], [type='submit'] { -webkit-appearance: button; /* 1 */ /* background-color: transparent; */ background-image: none; /* 2 */ } /* Use the modern Firefox focus style for all focusable elements. */ :-moz-focusring { outline: auto; } /* Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) */ :-moz-ui-invalid { box-shadow: none; } /* Add the correct vertical alignment in Chrome and Firefox. */ progress { vertical-align: baseline; } /* Correct the cursor style of increment and decrement buttons in Safari. */ ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { height: auto; } /* 1. Correct the odd appearance in Chrome and Safari. 2. Correct the outline style in Safari. */ [type='search'] { -webkit-appearance: textfield; /* 1 */ outline-offset: -2px; /* 2 */ } /* Remove the inner padding in Chrome and Safari on macOS. */ ::-webkit-search-decoration { -webkit-appearance: none; } /* 1. Correct the inability to style clickable types in iOS and Safari. 2. Change font properties to `inherit` in Safari. */ ::-webkit-file-upload-button { -webkit-appearance: button; /* 1 */ font: inherit; /* 2 */ } /* Add the correct display in Chrome and Safari. */ summary { display: list-item; } /* Removes the default spacing and border for appropriate elements. */ blockquote, dl, dd, h1, h2, h3, h4, h5, h6, hr, figure, p, pre { margin: 0; } fieldset { margin: 0; padding: 0; } legend { padding: 0; } ol, ul, menu { list-style: none; margin: 0; padding: 0; } /* Prevent resizing textareas horizontally by default. */ textarea { resize: vertical; } /* 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) 2. Set the default placeholder color to the user's configured gray 400 color. */ input::placeholder, textarea::placeholder { opacity: 1; /* 1 */ color: #9ca3af; /* 2 */ } /* Set the default cursor for buttons. */ button, [role='button'] { cursor: pointer; } /* Make sure disabled buttons don't get the pointer cursor. */ :disabled { cursor: default; } /* 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) This can trigger a poorly considered lint error in some tools but is included by design. */ img, svg, video, canvas, audio, iframe, embed, object { display: block; /* 1 */ vertical-align: middle; /* 2 */ } /* Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) */ img, video { max-width: 100%; height: auto; } /* Make elements with the HTML hidden attribute stay hidden by default */ [hidden] { display: none; } ================================================ FILE: web/src/style/transition.scss ================================================ // 淡入淡出动画 .fade-enter-active, .fade-leave-active { transition: all 0.3s ease; } .fade-enter-from, .fade-leave-to { opacity: 0; transform: translateY(10px); } .header { border-radius: 0 0 10px 10px; } .body { height: calc(100% - 6rem); } @keyframes slideDown { from { transform: translateY(-20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } // 缩放动画 .zoom-enter-active, .zoom-leave-active { transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); } .zoom-enter-from, .zoom-leave-to { opacity: 0; transform: scale(0.95); } /* fade-slide */ .slide-leave-active, .slide-enter-active { transition: all 0.3s; } .slide-enter-from { opacity: 0; transform: translateX(-30px); } .slide-leave-to { opacity: 0; transform: translateX(30px); } ================================================ FILE: web/src/utils/asyncRouter.js ================================================ const viewModules = import.meta.glob('../view/**/*.vue') const pluginModules = import.meta.glob('../plugin/**/*.vue') export const asyncRouterHandle = (asyncRouter) => { asyncRouter.forEach((item) => { if (item.component && typeof item.component === 'string') { item.meta.path = '/src/' + item.component if (item.component.split('/')[0] === 'view') { item.component = dynamicImport(viewModules, item.component) } else if (item.component.split('/')[0] === 'plugin') { item.component = dynamicImport(pluginModules, item.component) } } if (item.children) { asyncRouterHandle(item.children) } }) } function dynamicImport(dynamicViewsModules, component) { const keys = Object.keys(dynamicViewsModules) const matchKeys = keys.filter((key) => { const k = key.replace('../', '') return k === component }) const matchKey = matchKeys[0] return dynamicViewsModules[matchKey] } ================================================ FILE: web/src/utils/btnAuth.js ================================================ import { useRoute } from 'vue-router' import { reactive } from 'vue' export const useBtnAuth = () => { const route = useRoute() return route.meta.btns || reactive({}) } ================================================ FILE: web/src/utils/bus.js ================================================ // using ES6 modules import mitt from 'mitt' export const emitter = mitt() ================================================ FILE: web/src/utils/closeThisPage.js ================================================ import { emitter } from '@/utils/bus.js' export const closeThisPage = () => { emitter.emit('closeThisPage') } ================================================ FILE: web/src/utils/date.js ================================================ // 对Date的扩展,将 Date 转化为指定格式的String // 月(M)、日(d)、小时(h)、分(m)、秒(s)、季度(q) 可以用 1-2 个占位符, // 年(y)可以用 1-4 个占位符,毫秒(S)只能用 1 个占位符(是 1-3 位的数字) // (new Date()).Format("yyyy-MM-dd hh:mm:ss.S") ==> 2006-07-02 08:09:04.423 // (new Date()).Format("yyyy-M-d h:m:s.S") ==> 2006-7-2 8:9:4.18 // eslint-disable-next-line no-extend-native Date.prototype.Format = function(fmt) { const o = { 'M+': this.getMonth() + 1, // 月份 'd+': this.getDate(), // 日 'h+': this.getHours(), // 小时 'm+': this.getMinutes(), // 分 's+': this.getSeconds(), // 秒 'q+': Math.floor((this.getMonth() + 3) / 3), // 季度 'S': this.getMilliseconds() // 毫秒 } const reg = /(y+)/ if (reg.test(fmt)) { const t = reg.exec(fmt)[1] fmt = fmt.replace( t, (this.getFullYear() + '').substring(4 - t.length) ) } for (let k in o) { const regx = new RegExp('(' + k + ')') if (regx.test(fmt)) { const t = regx.exec(fmt)[1] fmt = fmt.replace( t, t.length === 1 ? o[k] : ('00' + o[k]).substring(('' + o[k]).length) ) } } return fmt } export function formatTimeToStr(times, pattern) { let d = new Date(times).Format('yyyy-MM-dd hh:mm:ss') if (pattern) { d = new Date(times).Format(pattern) } return d.toLocaleString() } ================================================ FILE: web/src/utils/dictionary.js ================================================ import { useDictionaryStore } from '@/pinia/modules/dictionary' /** * 生成字典缓存key * @param {string} type - 字典类型 * @param {number} depth - 深度参数 * @param {string|number|null} value - 指定节点的value * @returns {string} 缓存key */ const generateCacheKey = (type, depth, value) => { if (value !== null && value !== undefined) { return `${type}_value_${value}_depth_${depth}` } return depth === 0 ? `${type}_tree` : `${type}_depth_${depth}` } /** * 获取字典数据 * @param {string} type - 字典类型,必填 * @param {Object} options - 可选参数 * @param {number} options.depth - 指定获取字典的深度,默认为0(完整树形结构) * @param {string|number|null} options.value - 指定节点的value,获取该节点的children,默认为null * @returns {Promise} 字典数据数组 * @example * // 获取完整的字典树形结构 * const dictTree = await getDict('user_status') * * // 获取指定深度的扁平化字典数据 * const dictFlat = await getDict('user_status', { * depth: 2 * }) * * // 获取指定节点的children * const children = await getDict('user_status', { * value: 'active' * }) */ export const getDict = async ( type, options = { depth: 0, value: null } ) => { // 参数验证 if (!type || typeof type !== 'string') { console.warn('getDict: type参数必须是非空字符串') return [] } if (typeof options.depth !== 'number' || options.depth < 0) { console.warn('getDict: depth参数必须是非负数') options.depth = 0 } try { const dictionaryStore = useDictionaryStore() // 调用store方法获取字典数据 await dictionaryStore.getDictionary(type, options.depth, options.value) // 生成缓存key const cacheKey = generateCacheKey(type, options.depth, options.value) // 从缓存中获取数据 const result = dictionaryStore.dictionaryMap[cacheKey] // 返回数据,确保返回数组 return Array.isArray(result) ? result : [] } catch (error) { console.error('getDict: 获取字典数据失败', { type, options, error }) return [] } } // 字典文字展示方法 export const showDictLabel = ( dict, code, keyCode = 'value', valueCode = 'label' ) => { if (!dict) { return '' } const dictMap = {} dict.forEach((item) => { if (Reflect.has(item, keyCode) && Reflect.has(item, valueCode)) { dictMap[item[keyCode]] = item[valueCode] } }) return Reflect.has(dictMap, code) ? dictMap[code] : '' } ================================================ FILE: web/src/utils/doc.js ================================================ export const toDoc = (url) => { window.open(url, '_blank') } ================================================ FILE: web/src/utils/downloadImg.js ================================================ export const downloadImage = (imgsrc, name) => { // 下载图片地址和图片名 var image = new Image() image.setAttribute('crossOrigin', 'anonymous') image.onload = function () { var canvas = document.createElement('canvas') canvas.width = image.width canvas.height = image.height var context = canvas.getContext('2d') context.drawImage(image, 0, 0, image.width, image.height) var url = canvas.toDataURL('image/png') // 得到图片的base64编码数据 var a = document.createElement('a') // 生成一个a元素 var event = new MouseEvent('click') // 创建一个单击事件 a.download = name || 'photo' // 设置图片名称 a.href = url // 将生成的URL设置为a.href属性 a.dispatchEvent(event) // 触发a的单击事件 } image.src = imgsrc } ================================================ FILE: web/src/utils/env.js ================================================ export const isDev = import.meta.env.DEV; export const isProd = import.meta.env.PROD; ================================================ FILE: web/src/utils/event.js ================================================ export function addEventListen(target, event, handler, capture = false) { if ( target.addEventListener && typeof target.addEventListener === 'function' ) { target.addEventListener(event, handler, capture) } } export function removeEventListen(target, event, handler, capture = false) { if ( target.removeEventListener && typeof target.removeEventListener === 'function' ) { target.removeEventListener(event, handler, capture) } } ================================================ FILE: web/src/utils/fmtRouterTitle.js ================================================ export const fmtTitle = (title, now) => { const reg = /\$\{(.+?)\}/ const reg_g = /\$\{(.+?)\}/g const result = title.match(reg_g) if (result) { result.forEach((item) => { const key = item.match(reg)[1] const value = now.params[key] || now.query[key] title = title.replace(item, value) }) } return title } ================================================ FILE: web/src/utils/format.js ================================================ import { formatTimeToStr } from '@/utils/date' import { getDict } from '@/utils/dictionary' import { ref } from 'vue' export const formatBoolean = (bool) => { if (bool !== null) { return bool ? '是' : '否' } else { return '' } } export const formatDate = (time) => { if (time !== null && time !== '') { var date = new Date(time) return formatTimeToStr(date, 'yyyy-MM-dd hh:mm:ss') } else { return '' } } export const filterDict = (value, options) => { // 递归查找函数 const findInOptions = (opts, targetValue) => { if (!opts || !Array.isArray(opts)) return null for (const item of opts) { if (item.value === targetValue) { return item } if (item.children && Array.isArray(item.children)) { const found = findInOptions(item.children, targetValue) if (found) return found } } return null } const rowLabel = findInOptions(options, value) return rowLabel && rowLabel.label } export const filterDataSource = (dataSource, value) => { // 递归查找函数 const findInDataSource = (data, targetValue) => { if (!data || !Array.isArray(data)) return null for (const item of data) { // 检查当前项是否匹配 if (item.value === targetValue) { return item } // 如果有children属性,递归查找 if (item.children && Array.isArray(item.children)) { const found = findInDataSource(item.children, targetValue) if (found) return found } } return null } if (Array.isArray(value)) { return value.map((item) => { const rowLabel = findInDataSource(dataSource, item) return rowLabel?.label }) } const rowLabel = findInDataSource(dataSource, value) return rowLabel?.label } export const getDictFunc = async (type) => { const dicts = await getDict(type) return dicts } const path = import.meta.env.VITE_BASE_PATH + ':' + import.meta.env.VITE_SERVER_PORT + '/' export const ReturnArrImg = (arr) => { const imgArr = [] if (arr instanceof Array) { // 如果是数组类型 for (const arrKey in arr) { if (arr[arrKey].slice(0, 4) !== 'http') { imgArr.push(path + arr[arrKey]) } else { imgArr.push(arr[arrKey]) } } } else { // 如果不是数组类型 if (arr?.slice(0, 4) !== 'http') { imgArr.push(path + arr) } else { imgArr.push(arr) } } return imgArr } export const returnArrImg = ReturnArrImg export const onDownloadFile = (url) => { window.open(path + url) } const colorToHex = (u) => { let e = u.replace('#', '').match(/../g) for (let t = 0; t < 3; t++) e[t] = parseInt(e[t], 16) return e } const hexToColor = (u, e, t) => { let a = [u.toString(16), e.toString(16), t.toString(16)] for (let n = 0; n < 3; n++) a[n].length === 1 && (a[n] = `0${a[n]}`) return `#${a.join('')}` } const generateAllColors = (u, e) => { let t = colorToHex(u) const target = [10, 10, 30] for (let a = 0; a < 3; a++) t[a] = Math.floor(t[a] * (1 - e) + target[a] * e) return hexToColor(t[0], t[1], t[2]) } const generateAllLightColors = (u, e) => { let t = colorToHex(u) const target = [240, 248, 255] // RGB for blue white color for (let a = 0; a < 3; a++) t[a] = Math.floor(t[a] * (1 - e) + target[a] * e) return hexToColor(t[0], t[1], t[2]) } function addOpacityToColor(u, opacity) { let t = colorToHex(u) return `rgba(${t[0]}, ${t[1]}, ${t[2]}, ${opacity})` } export const setBodyPrimaryColor = (primaryColor, darkMode) => { let fmtColorFunc = generateAllColors if (darkMode === 'light') { fmtColorFunc = generateAllLightColors } document.documentElement.style.setProperty('--el-color-primary', primaryColor) document.documentElement.style.setProperty( '--el-color-primary-bg', addOpacityToColor(primaryColor, 0.4) ) for (let times = 1; times <= 2; times++) { document.documentElement.style.setProperty( `--el-color-primary-dark-${times}`, fmtColorFunc(primaryColor, times / 10) ) } for (let times = 1; times <= 10; times++) { document.documentElement.style.setProperty( `--el-color-primary-light-${times}`, fmtColorFunc(primaryColor, times / 10) ) } document.documentElement.style.setProperty( `--el-menu-hover-bg-color`, addOpacityToColor(primaryColor, 0.2) ) } const baseUrl = ref(import.meta.env.VITE_BASE_API) export const getBaseUrl = () => { return baseUrl.value === '/' ? '' : baseUrl.value } export const CreateUUID = () => { let d = new Date().getTime() if (window.performance && typeof window.performance.now === 'function') { d += performance.now() } return '00000000-0000-0000-0000-000000000000'.replace(/0/g, (c) => { const r = (d + Math.random() * 16) % 16 | 0 // d是随机种子 d = Math.floor(d / 16) return (c === '0' ? r : (r & 0x3) | 0x8).toString(16) }) } ================================================ FILE: web/src/utils/image.js ================================================ export default class ImageCompress { constructor(file, fileSize, maxWH = 1920) { this.file = file this.fileSize = fileSize this.maxWH = maxWH // 最大长宽 } compress() { // 压缩 const fileType = this.file.type const fileSize = this.file.size / 1024 return new Promise((resolve) => { const reader = new FileReader() reader.readAsDataURL(this.file) reader.onload = () => { const canvas = document.createElement('canvas') const img = document.createElement('img') img.src = reader.result img.onload = () => { const ctx = canvas.getContext('2d') const _dWH = this.dWH(img.width, img.height, this.maxWH) canvas.width = _dWH.width canvas.height = _dWH.height // 清空后, 重写画布 ctx.clearRect(0, 0, canvas.width, canvas.height) ctx.drawImage(img, 0, 0, canvas.width, canvas.height) const newImgData = canvas.toDataURL(fileType, 0.9) // 压缩宽高后的图像大小 const newImgSize = this.fileSizeKB(newImgData) if (newImgSize > this.fileSize) { console.log('图片尺寸太大!' + fileSize + ' >> ' + newImgSize) } const blob = this.dataURLtoBlob(newImgData, fileType) const nfile = new File([blob], this.file.name) resolve(nfile) } } }) } /** * 长宽等比缩小 * 图像的一边(长或宽)为最大目标值 */ dWH(srcW, srcH, dMax) { const defaults = { width: srcW, height: srcH } if (Math.max(srcW, srcH) > dMax) { if (srcW > srcH) { defaults.width = dMax defaults.height = Math.round(srcH * (dMax / srcW)) return defaults } else { defaults.height = dMax defaults.width = Math.round(srcW * (dMax / srcH)) return defaults } } else { return defaults } } fileSizeKB(dataURL) { let sizeKB = 0 sizeKB = Math.round((dataURL.split(',')[1].length * 3) / 4 / 1024) return sizeKB } /** * 转为Blob */ dataURLtoBlob(dataURL, fileType) { const byteString = atob(dataURL.split(',')[1]) let mimeString = dataURL.split(',')[0].split(':')[1].split(';')[0] const ab = new ArrayBuffer(byteString.length) const ia = new Uint8Array(ab) for (let i = 0; i < byteString.length; i++) { ia[i] = byteString.charCodeAt(i) } if (fileType) { mimeString = fileType } return new Blob([ab], { type: mimeString, lastModifiedDate: new Date() }) } } const path = import.meta.env.VITE_FILE_API export const getUrl = (url) => { if (url && url.slice(0, 4) !== 'http') { if (path === '/') { return url } if (url.slice(0, 1) === '/') { return path + url } return path + '/' + url } else { return url } } const VIDEO_EXTENSIONS = ['.mp4', '.mov', '.webm', '.ogg'] const VIDEO_MIME_TYPES = ['video/mp4', 'video/webm', 'video/ogg'] const IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/svg+xml'] export const isVideoExt = (url) => { const urlLower = url?.toLowerCase() || '' return urlLower !== '' && VIDEO_EXTENSIONS.some(ext => urlLower.endsWith(ext)) } export const isVideoMime = (type) => { const typeLower = type?.toLowerCase() || '' return typeLower !== '' && VIDEO_MIME_TYPES.includes(typeLower) } export const isImageMime = (type) => { const typeLower = type?.toLowerCase() || '' return typeLower !== '' && IMAGE_MIME_TYPES.includes(typeLower) } ================================================ FILE: web/src/utils/page.js ================================================ import { fmtTitle } from '@/utils/fmtRouterTitle' import config from '@/core/config' export default function getPageTitle(pageTitle, route) { if (pageTitle) { const title = fmtTitle(pageTitle, route) return `${title} - ${config.appName}` } return `${config.appName}` } ================================================ FILE: web/src/utils/params.js ================================================ import { useParamsStore } from '@/pinia/modules/params' /* * 获取参数方法 使用示例 getParams('key').then(res) 或者 async函数下 const res = await getParams('key') * const res = ref('') * const fun = async () => { * res.value = await getParams('test') * } * fun() */ export const getParams = async(key) => { const paramsStore = useParamsStore() await paramsStore.getParams(key) return paramsStore.paramsMap[key] } ================================================ FILE: web/src/utils/request.js ================================================ import axios from 'axios' // 引入axios import { useUserStore } from '@/pinia/modules/user' import { ElLoading, ElMessage } from 'element-plus' import { emitter } from '@/utils/bus' import router from '@/router/index' const service = axios.create({ timeout: 99999 }) let activeAxios = 0 let timer let loadingInstance let isLoadingVisible = false let forceCloseTimer const showLoading = ( option = { target: null } ) => { const loadDom = document.getElementById('gva-base-load-dom') activeAxios++ // 清除之前的定时器 if (timer) { clearTimeout(timer) } // 清除强制关闭定时器 if (forceCloseTimer) { clearTimeout(forceCloseTimer) } timer = setTimeout(() => { // 再次检查activeAxios状态,防止竞态条件 if (activeAxios > 0 && !isLoadingVisible) { if (!option.target) option.target = loadDom loadingInstance = ElLoading.service(option) isLoadingVisible = true // 设置强制关闭定时器,防止loading永远不关闭(30秒超时) forceCloseTimer = setTimeout(() => { if (isLoadingVisible && loadingInstance) { console.warn('Loading强制关闭:超时30秒') loadingInstance.close() isLoadingVisible = false activeAxios = 0 // 重置计数器 } }, 30000) } }, 400) } const closeLoading = () => { activeAxios-- if (activeAxios <= 0) { activeAxios = 0 // 确保不会变成负数 clearTimeout(timer) if (forceCloseTimer) { clearTimeout(forceCloseTimer) forceCloseTimer = null } if (isLoadingVisible && loadingInstance) { loadingInstance.close() isLoadingVisible = false } loadingInstance = null } } // 全局重置loading状态的函数,用于异常情况 const resetLoading = () => { activeAxios = 0 isLoadingVisible = false if (timer) { clearTimeout(timer) timer = null } if (forceCloseTimer) { clearTimeout(forceCloseTimer) forceCloseTimer = null } if (loadingInstance) { try { loadingInstance.close() } catch (e) { console.warn('关闭loading时出错:', e) } loadingInstance = null } } // http request 拦截器 service.interceptors.request.use( (config) => { if (!config.donNotShowLoading) { showLoading(config.loadingOption) } config.baseURL = config.baseURL || import.meta.env.VITE_BASE_API const userStore = useUserStore() config.headers = { 'Content-Type': 'application/json', 'x-token': userStore.token, 'x-user-id': userStore.userInfo.ID, ...config.headers } return config }, (error) => { if (!error.config.donNotShowLoading) { closeLoading() } emitter.emit('show-error', { code: 'request', message: error.message || '请求发送失败' }) return error } ) function getErrorMessage(error) { // 优先级: 响应体中的 msg > statusText > 默认消息 return error.response?.data?.msg || error.response?.statusText || '请求失败' } // http response 拦截器 service.interceptors.response.use( (response) => { const userStore = useUserStore() if (!response.config.donNotShowLoading) { closeLoading() } if (response.headers['new-token']) { userStore.setToken(response.headers['new-token']) } if (typeof response.data.code === 'undefined') { return response } if (response.data.code === 0 || response.headers.success === 'true') { if (response.headers.msg) { response.data.msg = decodeURI(response.headers.msg) } return response.data } else { ElMessage({ showClose: true, message: response.data.msg || decodeURI(response.headers.msg), type: 'error' }) return response.data.msg ? response.data : response } }, (error) => { if (!error.config.donNotShowLoading) { closeLoading() } if (!error.response) { // 网络错误 resetLoading() emitter.emit('show-error', { code: 'network', message: getErrorMessage(error) }) return Promise.reject(error) } // HTTP 状态码错误 if (error.response.status === 401) { emitter.emit('show-error', { code: '401', message: getErrorMessage(error), fn: () => { const userStore = useUserStore() userStore.ClearStorage() router.push({ name: 'Login', replace: true }) } }) return Promise.reject(error) } emitter.emit('show-error', { code: error.response.status, message: getErrorMessage(error) }) return Promise.reject(error) } ) // 监听页面卸载事件,确保loading被正确清理 if (typeof window !== 'undefined') { window.addEventListener('beforeunload', resetLoading) window.addEventListener('unload', resetLoading) } // 导出service和resetLoading函数 export { resetLoading } export default service ================================================ FILE: web/src/utils/stringFun.js ================================================ /* eslint-disable */ export const toUpperCase = (str) => { if (str[0]) { return str.replace(str[0], str[0].toUpperCase()) } else { return '' } } export const toLowerCase = (str) => { if (str[0]) { return str.replace(str[0], str[0].toLowerCase()) } else { return '' } } // 驼峰转换下划线 export const toSQLLine = (str) => { if (str === 'ID') return 'ID' return str.replace(/([A-Z])/g, '_$1').toLowerCase() } // 下划线转换驼峰 export const toHump = (name) => { return name.replace(/\_(\w)/g, function (all, letter) { return letter.toUpperCase() }) } ================================================ FILE: web/src/view/about/index.vue ================================================ ================================================ FILE: web/src/view/dashboard/components/banner.vue ================================================ ================================================ FILE: web/src/view/dashboard/components/card.vue ================================================  ================================================ FILE: web/src/view/dashboard/components/charts-content-numbers.vue ================================================ ================================================ FILE: web/src/view/dashboard/components/charts-people-numbers.vue ================================================ ================================================ FILE: web/src/view/dashboard/components/charts.vue ================================================ ================================================ FILE: web/src/view/dashboard/components/index.js ================================================ import GvaBanner from './banner.vue' import GvaCard from './card.vue' import GvaChart from './charts.vue' import GvaTable from './table.vue' import GvaNotice from './notice.vue' import GvaQuickLink from './quickLinks.vue' import GvaWiki from './wiki.vue' import GvaPluginTable from './pluginTable.vue' export { GvaBanner, GvaCard, GvaChart, GvaTable, GvaNotice, GvaQuickLink, GvaWiki, GvaPluginTable } ================================================ FILE: web/src/view/dashboard/components/notice.vue ================================================  ================================================ FILE: web/src/view/dashboard/components/pluginTable.vue ================================================ ================================================ FILE: web/src/view/dashboard/components/quickLinks.vue ================================================  ================================================ FILE: web/src/view/dashboard/components/table.vue ================================================ ================================================ FILE: web/src/view/dashboard/components/wiki.vue ================================================ ================================================ FILE: web/src/view/dashboard/index.vue ================================================  ================================================ FILE: web/src/view/error/index.vue ================================================ ================================================ FILE: web/src/view/error/reload.vue ================================================ ================================================ FILE: web/src/view/example/breakpoint/breakpoint.vue ================================================ ================================================ FILE: web/src/view/example/customer/customer.vue ================================================ ================================================ FILE: web/src/view/example/index.vue ================================================ ================================================ FILE: web/src/view/example/upload/scanUpload.vue ================================================ ================================================ FILE: web/src/view/example/upload/upload.vue ================================================ ================================================ FILE: web/src/view/init/index.vue ================================================ ================================================ FILE: web/src/view/layout/aside/asideComponent/asyncSubmenu.vue ================================================ ================================================ FILE: web/src/view/layout/aside/asideComponent/index.vue ================================================ ================================================ FILE: web/src/view/layout/aside/asideComponent/menuItem.vue ================================================ ================================================ FILE: web/src/view/layout/aside/combinationMode.vue ================================================ ================================================ FILE: web/src/view/layout/aside/headMode.vue ================================================ ================================================ FILE: web/src/view/layout/aside/index.vue ================================================ ================================================ FILE: web/src/view/layout/aside/normalMode.vue ================================================ ================================================ FILE: web/src/view/layout/aside/sidebarMode.vue ================================================ ================================================ FILE: web/src/view/layout/header/index.vue ================================================ ================================================ FILE: web/src/view/layout/header/tools.vue ================================================ ================================================ FILE: web/src/view/layout/iframe.vue ================================================ ================================================ FILE: web/src/view/layout/index.vue ================================================ ================================================ FILE: web/src/view/layout/screenfull/index.vue ================================================ ================================================ FILE: web/src/view/layout/search/search.vue ================================================ ================================================ FILE: web/src/view/layout/setting/components/layoutModeCard.vue ================================================ ================================================ FILE: web/src/view/layout/setting/components/settingItem.vue ================================================ ================================================ FILE: web/src/view/layout/setting/components/themeColorPicker.vue ================================================ ================================================ FILE: web/src/view/layout/setting/components/themeModeSelector.vue ================================================ ================================================ FILE: web/src/view/layout/setting/index.vue ================================================ ================================================ FILE: web/src/view/layout/setting/modules/appearance/index.vue ================================================ ================================================ FILE: web/src/view/layout/setting/modules/general/index.vue ================================================ ================================================ FILE: web/src/view/layout/setting/modules/layout/index.vue ================================================ ================================================ FILE: web/src/view/layout/tabs/index.vue ================================================ ================================================ FILE: web/src/view/login/index.vue ================================================ ================================================ FILE: web/src/view/person/person.vue ================================================ ================================================ FILE: web/src/view/routerHolder.vue ================================================ ================================================ FILE: web/src/view/superAdmin/api/api.vue ================================================ ================================================ FILE: web/src/view/superAdmin/authority/authority.vue ================================================ ================================================ FILE: web/src/view/superAdmin/authority/components/apis.vue ================================================ ================================================ FILE: web/src/view/superAdmin/authority/components/datas.vue ================================================ ================================================ FILE: web/src/view/superAdmin/authority/components/menus.vue ================================================ ================================================ FILE: web/src/view/superAdmin/dictionary/sysDictionary.vue ================================================ ================================================ FILE: web/src/view/superAdmin/dictionary/sysDictionaryDetail.vue ================================================ ================================================ FILE: web/src/view/superAdmin/index.vue ================================================ ================================================ FILE: web/src/view/superAdmin/menu/components/components-cascader.vue ================================================ ================================================ FILE: web/src/view/superAdmin/menu/icon.vue ================================================ ================================================ FILE: web/src/view/superAdmin/menu/menu.vue ================================================ ================================================ FILE: web/src/view/superAdmin/operation/sysOperationRecord.vue ================================================ ================================================ FILE: web/src/view/superAdmin/params/sysParams.vue ================================================ ================================================ FILE: web/src/view/superAdmin/user/user.vue ================================================ ================================================ FILE: web/src/view/system/state.vue ================================================ ================================================ FILE: web/src/view/systemTools/apiToken/index.vue ================================================ ================================================ FILE: web/src/view/systemTools/autoCode/component/fieldDialog.vue ================================================ ================================================ FILE: web/src/view/systemTools/autoCode/component/previewCodeDialog.vue ================================================ ================================================ FILE: web/src/view/systemTools/autoCode/index.vue ================================================ ================================================ FILE: web/src/view/systemTools/autoCode/mcp.vue ================================================ ================================================ FILE: web/src/view/systemTools/autoCode/mcpTest.vue ================================================ ================================================ FILE: web/src/view/systemTools/autoCode/picture.vue ================================================ ================================================ FILE: web/src/view/systemTools/autoCodeAdmin/index.vue ================================================ ================================================ FILE: web/src/view/systemTools/autoPkg/autoPkg.vue ================================================ ================================================ FILE: web/src/view/systemTools/exportTemplate/code.js ================================================ export const getCode = (templateID) => { return ` ` } ================================================ FILE: web/src/view/systemTools/exportTemplate/exportTemplate.vue ================================================ ================================================ FILE: web/src/view/systemTools/formCreate/index.vue ================================================ ================================================ FILE: web/src/view/systemTools/index.vue ================================================ ================================================ FILE: web/src/view/systemTools/installPlugin/index.vue ================================================ ================================================ FILE: web/src/view/systemTools/loginLog/index.vue ================================================ ================================================ FILE: web/src/view/systemTools/pubPlug/pubPlug.vue ================================================ ================================================ FILE: web/src/view/systemTools/skills/index.vue ================================================  ================================================ FILE: web/src/view/systemTools/sysError/sysError.vue ================================================ ================================================ FILE: web/src/view/systemTools/system/system.vue ================================================ ================================================ FILE: web/src/view/systemTools/version/version.vue ================================================ ================================================ FILE: web/uno.config.js ================================================ import { defineConfig } from '@unocss/vite'; import presetWind3 from '@unocss/preset-wind3'; import transformerDirectives from '@unocss/transformer-directives' export default defineConfig({ theme: { backgroundColor: { main: '#F5F5F5' }, textColor: { active: 'var(--el-color-primary)' }, boxShadowColor: { active: 'var(--el-color-primary)' }, borderColor: { 'table-border': 'var(--el-border-color-lighter)' } }, presets: [ presetWind3({ dark: 'class' }) ], transformers: [ transformerDirectives(), ], }) ================================================ FILE: web/vite.config.js ================================================ import legacyPlugin from '@vitejs/plugin-legacy' import { viteLogo } from './src/core/config' import Banner from 'vite-plugin-banner' import * as path from 'path' import { loadEnv } from 'vite' import vuePlugin from '@vitejs/plugin-vue' import vueDevTools from 'vite-plugin-vue-devtools' import VueFilePathPlugin from './vitePlugin/componentName/index.js' import { svgBuilder } from 'vite-auto-import-svg' import vueRootValidator from 'vite-check-multiple-dom' import { AddSecret } from './vitePlugin/secret' import UnoCSS from '@unocss/vite' // @see https://cn.vitejs.dev/config/ export default ({ mode }) => { AddSecret('') const env = loadEnv(mode, process.cwd()) viteLogo(env) const timestamp = Date.parse(new Date()) const optimizeDeps = {} const alias = { '@': path.resolve(__dirname, './src'), vue$: 'vue/dist/vue.runtime.esm-bundler.js' } const esbuild = {} const rollupOptions = { output: { entryFileNames: 'assets/087AC4D233B64EB0[name].[hash].js', chunkFileNames: 'assets/087AC4D233B64EB0[name].[hash].js', assetFileNames: 'assets/087AC4D233B64EB0[name].[hash].[ext]' } } const base = '/' const root = './' const outDir = 'dist' const config = { base: base, // 编译后js导入的资源路径 root: root, // index.html文件所在位置 publicDir: 'public', // 静态资源文件夹 resolve: { alias }, css: { preprocessorOptions: { scss: { api: 'modern-compiler' // or "modern" } } }, server: { // 如果使用docker-compose开发模式,设置为false open: true, port: Number(env.VITE_CLI_PORT), proxy: { // 把key的路径代理到target位置 // detail: https://cli.vuejs.org/config/#devserver-proxy [env.VITE_BASE_API]: { // 需要代理的路径 例如 '/api' target: `${env.VITE_BASE_PATH}:${env.VITE_SERVER_PORT}/`, // 代理到 目标路径 changeOrigin: true, rewrite: (path) => path.replace(new RegExp('^' + env.VITE_BASE_API), '') }, '/plugin': { // 需要代理的路径 例如 '/api' target: `https://plugin.gin-vue-admin.com/api/`, // 代理到 目标路径 changeOrigin: true, rewrite: (path) => path.replace(new RegExp('^/plugin'), '') } } }, build: { minify: 'terser', // 是否进行压缩,boolean | 'terser' | 'esbuild',默认使用terser manifest: false, // 是否产出manifest.json sourcemap: false, // 是否产出sourcemap.json outDir: outDir, // 产出目录 terserOptions: { compress: { //生产环境时移除console drop_console: true, drop_debugger: true } }, rollupOptions }, esbuild, optimizeDeps, plugins: [ env.VITE_POSITION === 'open' && vueDevTools({ launchEditor: env.VITE_EDITOR }), legacyPlugin({ targets: [ 'Android > 39', 'Chrome >= 60', 'Safari >= 10.1', 'iOS >= 10.3', 'Firefox >= 54', 'Edge >= 15' ] }), vuePlugin(), svgBuilder(['./src/plugin/', './src/assets/icons/'], base, outDir, 'assets', mode), [Banner(`\n Build based on gin-vue-admin \n Time : ${timestamp}`)], VueFilePathPlugin('./src/pathInfo.json'), UnoCSS(), vueRootValidator() ] } return config } ================================================ FILE: web/vitePlugin/componentName/index.js ================================================ import fs from 'fs' import path from 'path' import chokidar from 'chokidar' const toPascalCase = (str) => { return str.replace(/(^\w|-\w)/g, clearAndUpper) } const clearAndUpper = (text) => { return text.replace(/-/, '').toUpperCase() } // 递归获取目录下所有的 .vue 文件 const getAllVueFiles = (dir, fileList = []) => { const files = fs.readdirSync(dir) files.forEach((file) => { const filePath = path.join(dir, file) if (fs.statSync(filePath).isDirectory()) { getAllVueFiles(filePath, fileList) } else if (filePath.endsWith('.vue')) { fileList.push(filePath) } }) return fileList } // 从 .vue 文件内容中提取组件名称 const extractComponentName = (fileContent) => { const regex = /defineOptions\(\s*{\s*name:\s*["']([^"']+)["']/ const match = fileContent.match(regex) return match ? match[1] : null } // Vite 插件定义 const vueFilePathPlugin = (outputFilePath) => { let root let isDev = false const generatePathNameMap = () => { const vueFiles = [ ...getAllVueFiles(path.join(root, 'src/view')), ...getAllVueFiles(path.join(root, 'src/plugin')) ] const pathNameMap = vueFiles.reduce((acc, filePath) => { const content = fs.readFileSync(filePath, 'utf-8') const componentName = extractComponentName(content) let relativePath = '/' + path.relative(root, filePath).replace(/\\/g, '/') acc[relativePath] = componentName || toPascalCase(path.basename(filePath, '.vue')) return acc }, {}) const outputContent = JSON.stringify(pathNameMap, null, 2) fs.writeFileSync(outputFilePath, outputContent) } const watchDirectoryChanges = () => { const watchDirectories = [ path.join(root, 'src/view'), path.join(root, 'src/plugin') ] const watcher = chokidar.watch(watchDirectories, { persistent: true, ignoreInitial: true }) watcher.on('all', () => { generatePathNameMap() }) } return { name: 'vue-file-path-plugin', configResolved(resolvedConfig) { root = resolvedConfig.root if (resolvedConfig.mode === 'development') { isDev = true } }, buildStart() { generatePathNameMap() }, buildEnd() { if (isDev) { watchDirectoryChanges() } } } } export default vueFilePathPlugin ================================================ FILE: web/vitePlugin/secret/index.js ================================================ export function AddSecret(secret) { if (!secret) { secret = '' } global['gva-secret'] = secret }