签入版本

This commit is contained in:
小蚂蚁云
2026-10-20 21:53:26 +08:00
parent 5cc3da657c
commit b33799aa63
427 changed files with 32547 additions and 1 deletions
+5
View File
@@ -0,0 +1,5 @@
.vitepress/cache
.vscode/
node_modules
.svn
.git
@@ -0,0 +1,51 @@
// import type { DefaultTheme, LocaleSpecificConfig } from "vitepress";
// export const META_URL = "https://www.rxthink.cn";
// export const META_TITLE = "RXThinkCMF";
// export const META_DESCRIPTION = "Enterprise Admin Framework";
// export const enConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
// description: META_DESCRIPTION,
// head: [
// ["meta", { property: "og:url", content: META_URL }],
// ["meta", { property: "og:description", content: META_DESCRIPTION }],
// ["meta", { property: "twitter:url", content: META_URL }],
// ["meta", { property: "twitter:title", content: META_TITLE }],
// ["meta", { property: "twitter:description", content: META_DESCRIPTION }],
// [
// "script",
// {},
// `
// var _hmt = _hmt || [];
// (function() {
// var hm = document.createElement("script");
// hm.src = "https://hm.baidu.com/hm.js?f2662051bbed33ef4673edf8bfe609fe";
// var s = document.getElementsByTagName("script")[0];
// s.parentNode.insertBefore(hm, s);
// })();
// `,
// ],
// ],
//
// themeConfig: {
// editLink: {
// pattern: "https://gitee.com/rxthinkcmf/RXThinkCMF_ThinkPhp8_EleVue",
// text: "Suggest changes to this page",
// },
//
// nav: [
// { text: "Guide", link: "/en/introduction.html" },
// ],
//
// sidebar: {
// "/en/": [
// {
// text: "Introduction",
// items: [
// { text: "About", link: "/en/introduction.html" },
// ],
// },
// ],
// },
// },
// };
@@ -0,0 +1,42 @@
import { defineConfig } from "vitepress";
import { enConfig } from "./en";
import { sharedConfig } from "./shared";
import { zhConfig } from "./zh";
export default defineConfig({
...sharedConfig,
ignoreDeadLinks: [
(link) => link.includes('localhost'),
],
locales: {
root: { label: "简体中文", lang: "zh-CN", link: "/", ...zhConfig },
// root: { label: "English", lang: "en-US", link: "/", ...enConfig },
// zh: { label: "简体中文", lang: "zh-CN", link: "/zh/", ...zhConfig },
// es: {
// label: "Español",
// lang: "es-ES",
// link: "https://es-pinia.vercel.app/",
// },
// ko: {
// label: "한국어",
// lang: "ko-KR",
// link: "https://pinia.vuejs.kr/",
// },
// pt: {
// label: "Português",
// lang: "pt-PT",
// link: "https://pinia-docs-pt.netlify.app/",
// },
// uk: {
// label: "Українська",
// lang: "uk-UA",
// link: "https://pinia-ua.netlify.app",
// },
// ru: {
// label: "Русский",
// lang: "ru-RU",
// link: "https://pinia-ru.netlify.app",
// },
},
});
@@ -0,0 +1,99 @@
import { defineConfig, HeadConfig } from "vitepress";
export const META_IMAGE = "/logo.png";
const rControl = /[\u0000-\u001f]/g;
const rSpecial = /[\s~`!@#$%^&*()\-_+=[\]{}|\\;:"'“”‘’<>,.?/]+/g;
const rCombining = /[\u0300-\u036F]/g;
/**
* Default slugification function
*/
export const slugify = (str: string): string =>
str
.normalize("NFKD")
// Remove accents
.replace(rCombining, "")
// Remove control characters
.replace(rControl, "")
// Replace special characters
.replace(rSpecial, "-")
// ensure it doesn't start with a number
.replace(/^(\d)/, "_$1");
export const sharedConfig = defineConfig({
title: "EasyGoAdmin",
appearance: "light",
markdown: {
theme: {
dark: "dracula-soft",
light: "vitesse-light",
},
attrs: {
leftDelimiter: "%{",
rightDelimiter: "}%",
},
anchor: {
slugify,
},
},
head: [
["link", { rel: "icon", type: "image/svg+xml", href: "/logo.svg" }],
["link", { rel: "icon", type: "image/png", href: "/logo.png" }],
[
"meta",
{
property: "og:type",
content: "website",
},
],
[
"meta",
{
property: "twitter:card",
content: "summary_large_image",
},
],
[
"meta",
{
property: "twitter:image",
content: META_IMAGE,
},
],
],
themeConfig: {
logo: "/logo.svg",
outline: [2, 3],
socialLinks: [
{
icon: "github",
link: "https://gitee.com/easygoadmin/EasyGoAdmin_GoFrame_EleVue",
},
],
footer: {
copyright:
"Copyright © 2026 www.easygoadmin.vip. Apache-2.0 Licensed.",
message: "EasyGoAdmin · GoFrame+ElementPlus 开发文档",
},
editLink: {
pattern: "https://gitee.com/easygoadmin/EasyGoAdmin_GoFrame_EleVue",
text: "对本页提出修改建议",
},
search: {
provider: "local",
},
},
});
@@ -0,0 +1,437 @@
import type { DefaultTheme, LocaleSpecificConfig } from "vitepress";
export const META_URL = "http://docs.goframe.elevue.easygoadmin.vip/";
export const META_TITLE = "EasyGoAdmin GoFrame+ElementPlus 开发文档";
export const META_DESCRIPTION =
"EasyGoAdmin 企业级多租户后台管理基座 - GoFrame v2 + Vue3 + ElementPlus";
export const zhConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
description: META_DESCRIPTION,
head: [
["meta", { property: "og:url", content: META_URL }],
["meta", { property: "og:description", content: META_DESCRIPTION }],
["meta", { property: "twitter:url", content: META_URL }],
["meta", { property: "twitter:title", content: META_TITLE }],
["meta", { property: "twitter:description", content: META_DESCRIPTION }],
],
themeConfig: {
editLink: {
pattern: "https://gitee.com/easygoadmin/EasyGoAdmin_GoFrame_EleVue",
text: "对本页提出修改建议",
},
outline: {
label: "本页内容",
},
docFooter: {
prev: "上一页",
next: "下一页",
},
nav: [
{ text: "官网", link: "https://www.easygoadmin.vip" },
{ text: "Gitee 仓库", link: "https://gitee.com/easygoadmin/EasyGoAdmin_GoFrame_EleVue" },
{ text: "在线文档", link: "http://docs.goframe.elevue.easygoadmin.vip/" },
{
text: "相关链接",
items: [
{ text: "EasyGoAdmin 官网", link: "https://www.easygoadmin.vip" },
{ text: "Gitee 仓库", link: "https://gitee.com/easygoadmin/EasyGoAdmin_GoFrame_EleVue" },
{ text: "GoFrame 官网", link: "https://goframe.org" },
{ text: "Vue3 文档", link: "https://cn.vuejs.org/" },
{ text: "ElementPlus", link: "https://element-plus.org/zh-CN/" },
],
},
],
sidebar: {
"/zh/": [
// ================================================================
// 第 1 章:了解项目
// ================================================================
{
text: "📖 1. 了解项目",
collapsed: false,
items: [
{ text: "📄 1.1 项目介绍", link: "/zh/introduction.html" },
{ text: "📄 1.2 为什么选择我们", link: "/zh/why.html" },
{ text: "📄 1.3 项目目录结构", link: "/zh/directory.html" },
{ text: "📄 1.4 新手入门指南", link: "/zh/course.html" },
{ text: "📄 1.5 技术栈说明", link: "/zh/tech-stack.html" },
{ text: "📄 1.6 更新日志", link: "/zh/changelog.html" },
],
},
// ================================================================
// 第 2 章:快速入门
// ================================================================
{
text: "🚀 2. 快速入门",
collapsed: false,
items: [
{ text: "📄 2.1 开发环境准备", link: "/zh/quickstart/environment.html" },
{ text: "📄 2.2 后端服务启动", link: "/zh/quickstart/backend-start.html" },
{ text: "📄 2.3 前端服务启动", link: "/zh/quickstart/frontend-start.html" },
{ text: "📄 2.4 数据库初始化", link: "/zh/quickstart/database-init.html" },
{ text: "📄 2.5 配置文件说明", link: "/zh/quickstart/env-vars.html" },
{ text: "📄 2.6 第一个接口调试", link: "/zh/quickstart/first-api.html" },
{ text: "📄 2.7 Docker 快速体验", link: "/zh/quickstart/docker.html" },
{ text: "📄 2.8 常见问题 FAQ", link: "/zh/quickstart/faq.html" },
],
},
// ================================================================
// 第 3 章:模块开发实战
// ================================================================
{
text: "🧪 3. 模块开发实战",
collapsed: false,
items: [
{ text: "📄 3.1 概述与目标", link: "/zh/guide/overview.html" },
{ text: "📄 3.2 数据库建表", link: "/zh/guide/database.html" },
{ text: "📄 3.3 DAO 数据层", link: "/zh/guide/model.html" },
{ text: "📄 3.4 请求与校验", link: "/zh/guide/validator.html" },
{ text: "📄 3.5 逻辑层开发", link: "/zh/guide/logic.html" },
{ text: "📄 3.6 控制器开发", link: "/zh/guide/controller.html" },
{ text: "📄 3.7 服务注册与路由", link: "/zh/guide/route.html" },
{ text: "📄 3.8 权限节点配置", link: "/zh/guide/permission.html" },
{ text: "📄 3.9 前端联调指引", link: "/zh/guide/frontend-integration.html" },
{ text: "📄 3.10 完整代码清单", link: "/zh/guide/code-list.html" },
{ text: "📄 3.11 常见问题与排错", link: "/zh/guide/faq.html" },
],
},
// ================================================================
// 第 4 章:架构设计
// ================================================================
{
text: "🏛️ 4. 架构设计",
collapsed: false,
items: [
{ text: "📄 4.1 整体架构概览", link: "/zh/architecture/overview.html" },
{ text: "📄 4.2 请求处理链路", link: "/zh/architecture/request-flow.html" },
{ text: "📄 4.3 后端分层架构", link: "/zh/architecture/backend-layer.html" },
{ text: "📄 4.4 前端分层架构", link: "/zh/architecture/frontend-layer.html" },
{ text: "📄 4.5 目录结构设计", link: "/zh/architecture/directory-design.html" },
{ text: "📄 4.6 核心基座体系", link: "/zh/architecture/base-classes.html" },
{ text: "📄 4.7 横切机制", link: "/zh/architecture/cross-cutting.html" },
{ text: "📄 4.8 缓存架构设计", link: "/zh/architecture/cache.html" },
{ text: "📄 4.9 数据库设计", link: "/zh/architecture/database-design.html" },
{ text: "📄 4.10 安全架构设计", link: "/zh/architecture/security.html" },
{ text: "📄 4.11 扩展性设计", link: "/zh/architecture/extensibility.html" },
],
},
// ================================================================
// 第 5 章:核心功能
// ================================================================
{
text: "🔐 5. 核心功能",
collapsed: false,
items: [
{
text: "📄 认证与安全",
collapsed: false,
items: [
{ text: "📄 5.1 用户认证(JWT 双令牌)", link: "/zh/core/jwt.html" },
{ text: "📄 5.2 认证流程实战", link: "/zh/core/auth-flow.html" },
{ text: "📄 5.3 验证码机制", link: "/zh/core/captcha.html" },
{ text: "📄 5.4 权限管理(RBAC)", link: "/zh/core/rbac.html" },
{ text: "📄 5.5 跨域资源共享", link: "/zh/core/cors.html" },
{ text: "📄 5.6 密码加密服务", link: "/zh/core/password.html" },
{ text: "📄 5.7 演示模式", link: "/zh/core/demo-mode.html" },
],
},
{
text: "📄 数据与配置",
collapsed: false,
items: [
{ text: "📄 5.8 多租户隔离", link: "/zh/core/tenant.html" },
{ text: "📄 5.9 数据权限", link: "/zh/core/data-scope.html" },
{ text: "📄 5.10 字典模块", link: "/zh/core/dict.html" },
{ text: "📄 5.11 字典配置与使用", link: "/zh/core/dict-guide.html" },
{ text: "📄 5.12 系统配置管理", link: "/zh/core/config.html" },
{ text: "📄 5.13 系统参数管理", link: "/zh/core/param.html" },
],
},
{
text: "📄 日志与监控",
collapsed: false,
items: [
{ text: "📄 5.14 操作日志", link: "/zh/core/operation-log.html" },
{ text: "📄 5.15 登录日志", link: "/zh/core/login-log.html" },
{ text: "📄 5.16 异常与响应处理", link: "/zh/core/exception.html" },
],
},
{
text: "📄 防护与治理",
collapsed: false,
items: [
{ text: "📄 5.17 接口限流", link: "/zh/core/idempotent.html" },
],
},
{
text: "📄 注解系统",
collapsed: false,
items: [
{ text: "📄 5.18 gmeta 注解系统总览", link: "/zh/core/attributes.html" },
],
},
],
},
// ================================================================
// 第 6 章:业务模块
// ================================================================
{
text: "📦 6. 业务模块",
collapsed: false,
items: [
{ text: "📄 6.1 用户管理", link: "/zh/modules/user.html" },
{ text: "📄 6.2 角色管理", link: "/zh/modules/role.html" },
{ text: "📄 6.3 菜单管理", link: "/zh/modules/menu.html" },
{ text: "📄 6.4 部门管理", link: "/zh/modules/dept.html" },
{ text: "📄 6.5 职级管理", link: "/zh/modules/level.html" },
{ text: "📄 6.6 岗位管理", link: "/zh/modules/position.html" },
{ text: "📄 6.7 文章管理", link: "/zh/modules/article.html" },
{ text: "📄 6.8 公告与链接管理", link: "/zh/modules/content.html" },
{ text: "📄 6.9 行政区划管理", link: "/zh/modules/city.html" },
{ text: "📄 6.10 分类管理", link: "/zh/modules/category.html" },
{ text: "📄 6.11 定时任务", link: "/zh/modules/job.html" },
{ text: "📄 6.12 参数管理", link: "/zh/modules/param.html" },
{ text: "📄 6.13 租户管理", link: "/zh/modules/tenant.html" },
{ text: "📄 6.14 文件模板管理", link: "/zh/modules/filetemplate.html" },
{ text: "📄 6.15 首页与个人中心", link: "/zh/modules/index.html" },
{ text: "📄 6.16 在线用户监控", link: "/zh/modules/online-user.html" },
{ text: "📄 6.17 服务器监控", link: "/zh/modules/server-monitor.html" },
{ text: "📄 6.18 缓存监控", link: "/zh/modules/cache-monitor.html" },
{ text: "📄 6.19 案例演示模块", link: "/zh/modules/example.html" },
],
},
// ================================================================
// 第 7 章:文件处理
// ================================================================
{
text: "🧰 7. 文件处理",
collapsed: false,
items: [
{ text: "📄 7.1 文件上传全流程", link: "/zh/tools/upload.html" },
{ text: "📄 7.2 上传配置详解", link: "/zh/tools/upload-config.html" },
{ text: "📄 7.3 富文本处理", link: "/zh/tools/rich-text.html" },
{ text: "📄 7.4 数据导入导出", link: "/zh/tools/excel.html" },
{ text: "📄 7.5 前端文件组件", link: "/zh/tools/file-components.html" },
{ text: "📄 7.6 数据库迁移工具", link: "/zh/tools/db-migrate.html" },
],
},
// ================================================================
// 第 8 章:接口规范与对接
// ================================================================
{
text: "🔗 8. 接口规范与对接",
collapsed: false,
items: [
{ text: "📄 8.1 统一响应格式", link: "/zh/api-spec/response.html" },
{ text: "📄 8.2 错误码对照表", link: "/zh/api-spec/error-code.html" },
{ text: "📄 8.3 分页查询规范", link: "/zh/api-spec/pagination.html" },
{ text: "📄 8.4 文件上传接口", link: "/zh/api-spec/upload.html" },
{ text: "📄 8.5 字典数据接口", link: "/zh/api-spec/dict-api.html" },
{ text: "📄 8.6 Token 使用说明", link: "/zh/api-spec/token.html" },
{ text: "📄 8.7 API 接口总览", link: "/zh/api-spec/overview.html" },
],
},
// ================================================================
// 第 9 章:前端开发
// ================================================================
{
text: "🖥️ 9. 前端开发",
collapsed: false,
items: [
{
text: "📄 工程基础",
collapsed: false,
items: [
{ text: "📄 9.1 前端工程概述", link: "/zh/frontend/overview.html" },
{ text: "📄 9.2 目录结构详解", link: "/zh/frontend/directory.html" },
{ text: "📄 9.3 环境与构建配置", link: "/zh/frontend/env-config.html" },
],
},
{
text: "📄 核心机制",
collapsed: false,
items: [
{ text: "📄 9.4 HTTP 请求封装", link: "/zh/frontend/http.html" },
{ text: "📄 9.5 路由与动态菜单", link: "/zh/frontend/router.html" },
{ text: "📄 9.6 状态管理(Pinia)", link: "/zh/frontend/store.html" },
{ text: "📄 9.7 权限控制", link: "/zh/frontend/permission.html" },
{ text: "📄 9.8 登录流程", link: "/zh/frontend/login.html" },
],
},
{
text: "📄 公共组件",
collapsed: false,
items: [
{ text: "📄 9.9 数据展示组件", link: "/zh/frontend/display-components.html" },
{ text: "📄 9.10 数据录入组件", link: "/zh/frontend/form-components.html" },
{ text: "📄 9.11 Excel 导入导出组件", link: "/zh/frontend/excel-components.html" },
],
},
{
text: "📄 开发实践",
collapsed: false,
items: [
{ text: "📄 9.12 新增页面开发步骤", link: "/zh/frontend/new-page.html" },
{ text: "📄 9.13 API 模块编写规范", link: "/zh/frontend/api-module.html" },
{ text: "📄 9.14 样式与主题定制", link: "/zh/frontend/style.html" },
{ text: "📄 9.15 布局系统", link: "/zh/frontend/layout.html" },
{ text: "📄 9.16 工具函数", link: "/zh/frontend/utils.html" },
{ text: "📄 9.17 枚举与全局设置", link: "/zh/frontend/i18n.html" },
],
},
],
},
// ================================================================
// 第 10 章:代码生成器
// ================================================================
{
text: "🔧 10. 代码生成器",
collapsed: false,
items: [
{ text: "📄 10.1 概述与架构", link: "/zh/generator/overview.html" },
{ text: "📄 10.2 CLI 命令行使用", link: "/zh/generator/cli.html" },
{ text: "📄 10.3 Web 管理界面", link: "/zh/generator/web-ui.html" },
{ text: "📄 10.4 表结构解析引擎", link: "/zh/generator/parser.html" },
{ text: "📄 10.5 代码生成引擎", link: "/zh/generator/code-engine.html" },
{ text: "📄 10.6 模板开发", link: "/zh/generator/templates.html" },
{ text: "📄 10.7 配置文件详解", link: "/zh/generator/config.html" },
{ text: "📄 10.8 实战案例", link: "/zh/generator/demo.html" },
{ text: "📄 10.9 常见问题", link: "/zh/generator/faq.html" },
],
},
// ================================================================
// 第 11 章:运维部署
// ================================================================
{
text: "⚙️ 11. 运维部署",
collapsed: false,
items: [
{ text: "📄 11.1 部署 Checklist", link: "/zh/deploy/checklist.html" },
{ text: "📄 11.2 环境规划", link: "/zh/deploy/env-plan.html" },
{ text: "📄 11.3 数据库迁移", link: "/zh/deploy/db-migrate.html" },
{ text: "📄 11.4 后端部署", link: "/zh/deploy/backend.html" },
{ text: "📄 11.5 前端部署", link: "/zh/deploy/frontend.html" },
{ text: "📄 11.6 Docker 生产部署", link: "/zh/deploy/docker.html" },
{ text: "📄 11.7 性能调优", link: "/zh/deploy/performance.html" },
{ text: "📄 11.8 安全加固清单", link: "/zh/deploy/security.html" },
{ text: "📄 11.9 备份与恢复", link: "/zh/deploy/backup.html" },
{ text: "📄 11.10 故障排查手册", link: "/zh/deploy/troubleshooting.html" },
{ text: "📄 11.11 CI/CD 流水线", link: "/zh/deploy/ci-cd.html" },
],
},
// ================================================================
// 第 12 章:规范标准
// ================================================================
{
text: "📋 12. 规范标准",
collapsed: false,
items: [
{
text: "📄 必须遵守",
collapsed: false,
items: [
{ text: "📄 12.1 API 响应规范", link: "/zh/standards/api-response.html" },
{ text: "📄 12.2 分页查询规范", link: "/zh/standards/pagination.html" },
{ text: "📄 12.3 Git 提交规范", link: "/zh/standards/git.html" },
],
},
{
text: "📄 建议遵守",
collapsed: false,
items: [
{ text: "📄 12.4 数据删除规范", link: "/zh/standards/deletion.html" },
{ text: "📄 12.5 代码审查清单", link: "/zh/standards/code-review.html" },
{ text: "📄 12.6 后端编码规范", link: "/zh/standards/backend-code.html" },
{ text: "📄 12.7 前端编码规范", link: "/zh/standards/frontend-code.html" },
],
},
],
},
// ================================================================
// 附录
// ================================================================
{
text: "📄 附录",
collapsed: false,
items: [
{ text: "📄 A. 常见问题汇总", link: "/zh/appendix/faq.html" },
{ text: "📄 B. 数据库表结构速查", link: "/zh/appendix/database-tables.html" },
{ text: "📄 C. 配置项速查", link: "/zh/appendix/config-reference.html" },
{ text: "📄 D. 公共函数速查", link: "/zh/appendix/functions.html" },
{ text: "📄 E. API 接口速查表", link: "/zh/appendix/api-cheatsheet.html" },
{ text: "📄 F. 中间件执行顺序", link: "/zh/appendix/middleware.html" },
{ text: "📄 G. 前端组件速查", link: "/zh/appendix/components.html" },
{ text: "📄 H. 前端 Store 速查", link: "/zh/appendix/stores.html" },
{ text: "📄 I. 前端 Enums 速查", link: "/zh/appendix/enums.html" },
{ text: "📄 J. 术语表", link: "/zh/appendix/glossary.html" },
{ text: "📄 K. 贡献指南", link: "/zh/other/contributing.html" },
{ text: "📄 L. 免责声明", link: "/zh/other/declare.html" },
{ text: "📄 M. 广告赞助", link: "/zh/other/sponsor.html" },
{ text: "📄 N. 开源协议", link: "https://gitee.com/easygoadmin/EasyGoAdmin_GoFrame_EleVue" },
],
},
],
},
},
};
export const zhSearch: DefaultTheme.AlgoliaSearchOptions["locales"] = {
zh: {
placeholder: "搜索文档",
translations: {
button: {
buttonText: "搜索文档",
buttonAriaLabel: "搜索文档",
},
modal: {
searchBox: {
resetButtonTitle: "清除查询条件",
resetButtonAriaLabel: "清除查询条件",
cancelButtonText: "取消",
cancelButtonAriaLabel: "取消",
},
startScreen: {
recentSearchesTitle: "搜索历史",
noRecentSearchesText: "没有搜索历史",
saveRecentSearchButtonTitle: "保存至搜索历史",
removeRecentSearchButtonTitle: "从搜索历史中移除",
favoriteSearchesTitle: "收藏",
removeFavoriteSearchButtonTitle: "从收藏中移除",
},
errorScreen: {
titleText: "无法获取结果",
helpText: "你可能需要检查你的网络连接",
},
footer: {
selectText: "选择",
navigateText: "切换",
closeText: "关闭",
searchByText: "搜索供应商",
},
noResultsScreen: {
noResultsText: "无法找到相关结果",
suggestedQueryText: "你可以尝试查询",
reportMissingResultsText: "你认为该查询应该有结果?",
reportMissingResultsLinkText: "点击反馈",
},
},
},
},
};
@@ -0,0 +1,151 @@
<script setup lang="ts">
import { computed } from "vue";
import { VPDocAsideSponsors } from "vitepress/theme";
import sponsors from "./sponsors.json";
// to avoid the never[] type in json
interface Sponsor {
alt: string;
href: string;
imgSrcDark: string;
imgSrcLight: string;
}
const asideSponsors = computed(() => {
return [
{
size: "mini",
items: sponsors.platinum.length
? sponsors.platinum.map((sponsor: Sponsor) => ({
name: sponsor.alt,
url: sponsor.href,
img: sponsor.imgSrcLight,
}))
: [
{
name: "Become a sponsor",
url: "https://www.rxthink.cn",
img: "/sponsor/rxthinkcmf.png",
},
],
},
{
size: "xmini",
// TODO: use gold instead once I have some
items: sponsors.silver.map((sponsor: Sponsor) => ({
name: sponsor.alt,
url: sponsor.href,
img: sponsor.imgSrcLight,
})),
},
];
});
</script>
<template>
<VPDocAsideSponsors :data="asideSponsors" />
<a class="banner mp" href="https://www.rxthink.cn" target="_blank">
<img width="22" height="22" src="/sponsor/rxthinkcmf.png" />
<span>
<p class="extra-info">赋能开发者</p>
<p class="heading">企业级中台解决方案</p>
<p class="extra-info">助力企业发展</p>
</span>
</a>
<a class="banner cert" href="https://www.rxthink.cn" target="_blank">
<img width="22" height="22" src="/sponsor/rxthinkcmf.png" />
<span>
<p class="extra-info">赋能开发者</p>
<p class="heading">企业级中台解决方案</p>
<p class="extra-info">助力企业发展</p>
</span>
</a>
</template>
<style scoped>
.VPDocAsideSponsors {
margin-top: 8px !important;
}
:deep(.vp-sponsor-grid.mini .vp-sponsor-grid-image) {
max-width: 158px;
max-height: 48px;
}
:deep(.vp-sponsor-grid.xmini .vp-sponsor-grid-image) {
max-width: 80px;
max-height: 32px;
}
.banner {
margin: 0.25rem 0;
padding: 0.4rem 0;
border-radius: 14px;
position: relative;
font-size: 0.9rem;
font-weight: 700;
line-height: 1.1rem;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
gap: 1rem;
background-color: var(--vp-c-bg-alt);
border: 2px solid var(--vp-c-bg-alt);
transition: border-color 0.5s;
}
.banner:last-of-type {
margin-bottom: 1rem;
}
.banner:hover {
border: 2px solid var(--vp-c-brand-1);
}
.banner.cert:hover {
border: 2px solid var(--vp-c-green-1);
}
.banner img {
transition: transform 0.5s;
transform: scale(1.25);
}
.banner:hover img {
transform: scale(1.75);
}
.banner .extra-info {
color: var(--vp-c-text-1);
opacity: 0;
font-size: 0.7rem;
padding-left: 0.1rem;
transition: opacity 0.5s;
}
.banner .heading {
background-image: linear-gradient(
120deg,
var(--vp-c-brand-3) 16%,
var(--vp-c-brand-2),
var(--vp-c-brand-1)
);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.banner.cert .heading {
background-image: linear-gradient(
120deg,
var(--vp-c-green-3) 16%,
var(--vp-c-green-2),
var(--vp-c-green-1)
);
}
.banner:hover .extra-info {
opacity: 0.9;
}
</style>
@@ -0,0 +1,59 @@
<script setup lang="ts">
import HomeSponsorsGroup from "./HomeSponsorsGroup.vue";
import sponsors from "./sponsors.json";
import { useData } from "vitepress";
const { site } = useData();
const translations = {
en: "Become a sponsor",
"en-US": "Become a Sponsor!",
"zh-CN": "成为赞助商!",
};
</script>
<template>
<div class="sponsors_outer">
<div>
<HomeSponsorsGroup
v-if="sponsors.platinum.length"
name="Platinum"
size="96"
/>
<HomeSponsorsGroup
v-if="sponsors.bronze.length"
name="Bronze"
size="38"
/>
<HomeSponsorsGroup
v-if="sponsors.silver.length"
name="Silver"
size="24"
/>
<div class="cta">
<a
class="become-sponsor"
href="/zh/other/sponsor.html"
target="_self"
>{{ translations[site.lang] || translations.en }}</a
>
</div>
</div>
</div>
</template>
<style scoped>
.sponsors_outer {
text-align: center;
padding: 35px 40px 45px;
background-color: var(--vp-c-bg-accent);
/* transition when toggling dark mode */
transition: background-color 300ms ease-in-out, color 300ms ease-in-out;
}
.cta {
margin-top: 2rem;
}
</style>
@@ -0,0 +1,122 @@
<template>
<h3>{{ getNameText() }} 赞助商</h3>
<p>
<a
v-for="sponsor in list"
:key="sponsor.href"
:href="sponsor.href"
:title="sponsor.alt"
target="_blank"
rel="sponsored noopener"
class="sponsor_wrapper"
:class="
isDark && sponsor.imgSrcLight === sponsor.imgSrcDark && 'apply-bg'
"
>
<img
:src="sponsor.imgSrc"
:class="
isDark &&
sponsor.imgSrcLight === sponsor.imgSrcDark &&
'invert-colors'
"
:alt="sponsor.alt"
:style="{ height: size + 'px' }"
/>
</a>
</p>
</template>
<script setup lang="ts">
import sponsors from "./sponsors.json";
import { computed } from "vue";
import { useData } from "vitepress";
const props = withDefaults(
defineProps<{
name: "Gold" | "Platinum" | "Silver" | "Bronze";
size?: number | string;
}>(),
{
size: 140,
}
);
/**
* 获取赞助商等级
*/
const getNameText = () => {
switch (props.name) {
case "Gold":
return "黄金";
break;
case "Platinum":
return "铂金";
break;
case "Silver":
return "白金";
break;
case "Bronze":
return "青铜";
break;
default:
return "白金";
break;
}
};
const { isDark } = useData();
const list = computed(() =>
sponsors[props.name.toLowerCase()].map((sponsor) => ({
...sponsor,
imgSrc: isDark.value ? sponsor.imgSrcDark : sponsor.imgSrcLight,
}))
);
</script>
<style scoped>
.sponsor_wrapper {
padding: 5px;
margin: 0 3px;
border-radius: 5px;
display: inline-block;
text-decoration: none;
vertical-align: middle;
transition: background-color 300ms ease-in-out;
}
p {
margin: 0;
}
h3 {
font-size: 1.35rem;
font-weight: 600;
margin: 0.75em 0;
}
img {
transition: all 0.3s ease;
filter: grayscale(100%);
opacity: 0.66;
}
html:not(.light) img.invert-colors {
filter: invert(1) grayscale(100%);
background-color: transparent;
}
.sponsor_wrapper.apply-bg:hover {
background-color: var(--c-text);
}
.sponsor_wrapper:hover img {
filter: none !important;
opacity: 1;
}
</style>
@@ -0,0 +1,176 @@
<script setup lang="ts">
import { useData } from "vitepress";
import { computed, ref } from "vue";
// TODO: split into 2 components
const { site } = useData();
const translations = {
videoLink: {
"en-US":
"Master this and much more with the official video course by the author of Pinia",
"zh-CN": "通过 Pinia 作者的官方视频课程掌握更多内容",
},
videoEmbed: {
"en-US": "Master this and much more with a free video from Mastering Pinia",
"zh-CN": "通过 VideoModule 的免费视频掌握更多内容",
},
watchMoreA: {
"en-US": "Watch more on ",
"zh-CN": "获取 ",
},
watchMoreB: {
"en-US": "",
"zh-CN": " 官方授权",
},
};
const props = defineProps<{ href: string; title: string; mpLink?: string }>();
const isVideo = computed(() => props.href.startsWith("/"));
const isVideoOpen = ref(false);
</script>
<template>
<div class="mp">
<template v-if="isVideo">
<div class="video-embed" v-if="isVideoOpen">
<div style="padding: 56.25% 0 0 0; position: relative">
<iframe
loading="lazy"
title="Gumlet video player"
false
:src="
href +
'?preload=false&autoplay=true&loop=false&disable_player_controls=false'
"
style="
border: none;
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
"
allow="
accelerometer;
gyroscope;
autoplay;
encrypted-media;
picture-in-picture;
fullscreen;
"
frameborder="0"
allowfullscreen
>
</iframe>
</div>
<div class="watch-more">
{{ translations.watchMoreA[site.lang] }}
<a
:href="mpLink || 'https://www.rxthink.cn'"
target="_blank"
rel="noopener"
>
小蚂蚁云
<img src="../../../public/logo.svg" alt="小蚂蚁云" />
</a>
{{ translations.watchMoreB[site.lang] }}
</div>
</div>
<button class="cta" :title v-else @click="isVideoOpen = true">
<div class="text">
<slot>{{ translations.videoEmbed[site.lang] }}</slot>
</div>
</button>
</template>
<a
v-else
:href="href"
target="_blank"
rel="noopener"
:title="title"
class="no-icon cta"
>
<div class="text">
<slot>{{ translations.videoLink[site.lang] }}</slot>
</div>
</a>
</div>
</template>
<style scoped>
.mp {
margin-top: 20px;
}
.mp:has(.cta) {
position: relative;
transition: border-color 0.5s;
padding: 1em 1.25em;
background-color: var(--vp-code-block-bg);
border-radius: 1em;
border: 2px solid var(--vp-c-bg-alt);
}
.mp:has(.cta):hover {
border: 2px solid var(--vp-c-brand-1);
}
.cta:hover {
color: var(--vp-c-brand-1);
text-decoration: underline;
}
.watch-more {
text-align: end;
font-size: 0.8em;
background-color: var(--vp-code-block-bg);
border-radius: 0 0 1em 1em;
padding-right: 1em;
}
.watch-more img {
height: 1em;
display: inline-block;
/* vertical-align: middle; */
}
.cta {
font-size: inherit;
color: var(--c-text);
position: relative;
display: flex;
gap: 1em;
grid-template-columns: 1fr auto;
align-items: center;
justify-content: space-between;
padding-left: 36px;
}
.cta .content {
flex-grow: 1;
}
.cta:before {
content: "";
position: absolute;
display: block;
width: 30px;
height: 30px;
top: calc(50% - 15px);
left: -4px;
border-radius: 50%;
background-color: var(--vp-c-brand-1);
}
html.dark .cta:after {
--play-icon-color: #000;
}
.cta:after {
content: "";
position: absolute;
display: block;
width: 0;
height: 0;
top: calc(50% - 5px);
left: 8px;
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
border-left: 9px solid var(--play-icon-color, #fff);
}
</style>
@@ -0,0 +1,38 @@
{
"platinum": [],
"gold": [],
"silver": [
{
"alt": "企业级中台解决方案",
"href": "https://www.rxthink.cn",
"imgSrcDark": "/sponsor/rxthinkcmf.png",
"imgSrcLight": "/sponsor/rxthinkcmf.png"
},
{
"alt": "企业级中台解决方案",
"href": "https://www.rxthink.cn",
"imgSrcDark": "/sponsor/rxthinkcmf.png",
"imgSrcLight": "/sponsor/rxthinkcmf.png"
}
],
"bronze": [
{
"alt": "企业级中台解决方案",
"href": "https://www.rxthink.cn",
"imgSrcDark": "/sponsor/rxthinkcmf.png",
"imgSrcLight": "/sponsor/rxthinkcmf.png"
},
{
"alt": "企业级中台解决方案",
"href": "https://www.rxthink.cn",
"imgSrcDark": "/sponsor/rxthinkcmf.png",
"imgSrcLight": "/sponsor/rxthinkcmf.png"
},
{
"alt": "企业级中台解决方案",
"href": "https://www.rxthink.cn",
"imgSrcDark": "/sponsor/rxthinkcmf.png",
"imgSrcLight": "/sponsor/rxthinkcmf.png"
}
]
}
@@ -0,0 +1,32 @@
import { h } from "vue";
import { type Theme, useRoute } from "vitepress";
import DefaultTheme from "vitepress/theme";
import "./styles/vars.css";
import imageViewer from "vitepress-plugin-image-viewer";
import vImageViewer from "vitepress-plugin-image-viewer/lib/vImageViewer.vue";
const theme: Theme = {
...DefaultTheme,
Layout() {
return h(DefaultTheme.Layout, null, {
"home-hero-image": () =>
h(
"div",
{ class: "image-src" },
h("img", {
src: "/logo.png",
alt: "EasyGoAdmin",
}),
),
});
},
setup() {
const route = useRoute();
imageViewer(route);
},
enhanceApp({ app }) {
app.component("vImageViewer", vImageViewer);
},
};
export default theme;
@@ -0,0 +1,11 @@
/* CTA link styles for the home page */
a.cta {
text-align: center;
border-radius: 8px;
}
a.cta:hover {
border-color: var(--vp-c-brand);
background-color: var(--c-bg-accent);
}
@@ -0,0 +1,204 @@
/**
* Colors
* -------------------------------------------------------------------------- */
:root {
--c-yellow-1: #165DFF;
--c-yellow-2: #165DFF;
--c-yellow-3: #165DFF;
--c-yellow-soft-1: #165DFF;
--c-yellow-soft-2: #165DFF;
--c-teal: #086367;
--c-teal-light: #33898d;
--c-white-dark: #f8f8f8;
--c-black-darker: #0d121b;
--c-black: #111827;
--c-black-light: #161f32;
--c-black-lighter: #262a44;
--c-green-1: #165DFF;
--c-green-2: #8ae99c;
--c-green-3: #165DFF;
--c-green-soft: #316334;
/* light theme is a bit different */
--vp-c-brand-1: var(--vp-c-green-1);
--vp-c-brand-2: var(--vp-c-green-2);
--vp-c-brand-3: var(--vp-c-green-3);
--vp-c-brand-soft: var(--vp-c-green-soft);
--c-text-dark-1: #d9e6eb;
--c-text-dark-2: #c4dde6;
--c-text-dark-3: #abc4cc;
--c-text-light-1: #2c3e50;
--c-text-light-2: #476582;
--c-text-light-3: #90a4b7;
--vp-c-brand-dark: var(--c-green-soft);
--vp-c-brand-darker: var(--c-green-soft);
--vp-c-brand-dimm: rgba(100, 108, 255, 0.08);
--vp-c-brand-text: var(--c-text-light-1);
--c-bg-accent: var(--c-white-dark);
--code-bg-color: var(--c-white-dark);
--code-inline-bg-color: var(--c-white-dark);
--code-font-family: 'dm', source-code-pro, Menlo, Monaco, Consolas,
'Courier New', monospace;
--code-font-size: 16px;
--vp-code-block-bg: var(--vp-c-bg-alt);
--vp-code-line-highlight-color: rgba(0, 0, 0, 0.075);
--vp-code-color: var(--vp-text-color);
}
html.dark:root {
/* --c-black: #ffffff;
--c-white: #000000; */
/* --c-divider-light: rgba(60, 60, 67, 0.12);
--c-divider-dark: rgba(84, 84, 88, 0.48); */
/* --c-brand-light: var(--c-yellow-light); */
--vp-c-brand-1: var(--c-yellow-1);
--vp-c-brand-2: var(--c-yellow-2);
--vp-c-brand-3: var(--c-yellow-3);
--vp-c-bg-alpha-with-backdrop: rgba(20, 25, 36, 0.7);
--vp-c-bg-alpha-without-backdrop: rgba(20, 25, 36, 0.9);
--vp-code-line-highlight-color: rgba(0, 0, 0, 0.5);
--vp-c-text-1: var(--c-text-dark-1);
--vp-c-brand-text: var(--c-text-light-1);
--c-text-light: var(--c-text-dark-2);
--c-text-lighter: var(--c-text-dark-3);
--c-divider: var(--c-divider-dark);
--c-bg-accent: var(--c-black-light);
/* --vp-code-inline-bg: var(--vp-c-black-light); */
--vp-c-bg: var(--c-black);
--vp-c-bg-soft: var(--c-black-light);
--vp-c-bg-soft-up: var(--c-black-lighter);
--vp-c-bg-mute: var(--c-black-light);
--vp-c-bg-soft-mute: var(--c-black-lighter);
--vp-c-bg-alt: #0d121b;
--vp-c-bg-elv: var(--vp-c-bg-soft);
--vp-c-bg-elv-mute: var(--vp-c-bg-soft-mute);
--vp-c-mute: var(--vp-c-bg-mute);
--vp-c-mute-dark: var(--c-black-lighter);
--vp-c-mute-darker: var(--c-black-darker);
--vp-home-hero-name-background: -webkit-linear-gradient(
78deg,
var(--c-yellow-2) 30%,
var(--c-green-2)
);
}
html.dark .DocSearch {
--docsearch-hit-active-color: var(--c-text-light-1);
}
/**
* Component: Button
* -------------------------------------------------------------------------- */
:root {
--vp-button-brand-border: var(--c-yellow-soft-1);
--vp-button-brand-text: var(--c-black);
--vp-button-brand-bg: var(--c-yellow-1);
--vp-button-brand-hover-border: var(--c-yellow-2);
--vp-button-brand-hover-text: var(--c-black-darker);
--vp-button-brand-hover-bg: var(--c-yellow-2);
--vp-button-brand-active-border: var(--c-yellow-soft-1);
--vp-button-brand-active-text: var(--c-black-darker);
--vp-button-brand-active-bg: var(--vp-button-brand-bg);
}
/**
* Component: Home
* -------------------------------------------------------------------------- */
:root {
--vp-home-hero-name-color: transparent;
--vp-home-hero-name-background: linear-gradient(
292deg,
var(--c-green-1) 50%,
var(--c-green-2)
);
--vp-home-hero-image-background-image: linear-gradient(
15deg,
var(--c-yellow-2) 65%,
var(--c-green-1) 30%
);
--vp-home-hero-image-filter: blur(40px);
}
.VPHero .VPImage.image-src {
max-height: 192px;
}
@media (min-width: 640px) {
:root {
--vp-home-hero-image-filter: blur(56px);
}
.VPHero .VPImage.image-src {
max-height: 256px;
}
}
@media (min-width: 960px) {
:root {
--vp-home-hero-image-filter: blur(72px);
}
.VPHero .VPImage.image-src {
max-height: 320px;
}
}
.become-sponsor {
font-size: 0.9em;
font-weight: 700;
width: auto;
text-align: center;
background-color: transparent;
padding: 0.75em 2em;
border-radius: 2em;
transition: all 0.15s ease;
box-sizing: border-box;
border: 2px solid var(--c-yellow-2);
}
.become-sponsor.become-sponsor:hover {
background-color: var(--c-yellow-1);
text-decoration: none;
border-color: var(--c-yellow-1);
color: var(--c-text-light-1);
}
.vp-doc a {
text-decoration: none;
}
.vp-doc a:hover {
text-decoration: underline;
}
.sponsors-top .become-sponsor {
font-size: 0.75em;
padding: 0.2em;
width: auto;
max-width: 150px;
}
kbd {
display: inline-block;
padding: 3px 5px;
font-size: 0.65em;
color: var(--vp-c-text-1);
vertical-align: middle;
background-color: var(--vp-c-bg-mute);
border: solid 1px var(--vp-c-bg-soft-mute);
border-radius: 6px;
box-shadow: inset 0 -1px 0 var(--vp-c-bg-soft-mute);
line-height: 0.95em;
}
@@ -0,0 +1,6 @@
{
"zh": {
"hash": "3.0.0",
"date": "2026-09-07"
}
}
+46
View File
@@ -0,0 +1,46 @@
# EasyGoAdmin GoFrame+ElementPlus 文档
基于 VitePress 构建的 EasyGoAdmin(GoFrame v2 + Vue3 + Element Plus)开发者文档站点。
+ 官网:[https://www.easygoadmin.vip](https://www.easygoadmin.vip)
+ Gitee 仓库:[https://gitee.com/easygoadmin/EasyGoAdmin_GoFrame_EleVue](https://gitee.com/easygoadmin/EasyGoAdmin_GoFrame_EleVue)
+ 在线文档:[http://docs.goframe.elevue.easygoadmin.vip/](http://docs.goframe.elevue.easygoadmin.vip/)
## 本地开发
```bash
# 安装依赖
pnpm install
# 启动开发服务器
pnpm docs
# 构建静态站点
pnpm docs:build
# 预览构建结果
pnpm docs:preview
```
## 文档结构
```
index.md # 文档首页(hero + features)
zh/
├── introduction.md # 1.1 项目介绍(及 why/directory/course/tech-stack/changelog)
├── quickstart/ # 第 2 章 快速入门
├── guide/ # 第 3 章 模块开发实战
├── architecture/ # 第 4 章 架构设计
├── core/ # 第 5 章 核心功能
├── modules/ # 第 6 章 业务模块
├── tools/ # 第 7 章 文件处理
├── api-spec/ # 第 8 章 接口规范与对接
├── frontend/ # 第 9 章 前端开发
├── generator/ # 第 10 章 代码生成器
├── deploy/ # 第 11 章 运维部署
├── standards/ # 第 12 章 规范标准
├── appendix/ # 附录 A-J
└── other/ # 附录 K-M(贡献指南、免责声明、赞助)
```
侧栏与导航配置:`.vitepress/config/zh.ts`。
+86
View File
@@ -0,0 +1,86 @@
---
layout: home
title: EasyGoAdmin 文档
titleTemplate: 前后端分离 GoFrame+AntDesign 版
hero:
name: EasyGoAdmin
text: "企业级中台解决方案 GoFrame+AntDesign 版"
tagline: "赋能开发者,助力企业发展。\n企业级多租户数据平台底座 · 让数字化触手可及。"
image:
src: /logo.svg
alt: EasyGoAdmin
actions:
- theme: brand
text: 开始使用
link: /zh/introduction
- theme: alt
text: Gitee 仓库
link: https://gitee.com/easygoadmin/EasyGoAdmin_GoFrame_AntdVue
- theme: alt
text: 在线文档
link: http://docs.goframe.antdvue.easygoadmin.vip
features:
- title: 🚀 全新技术栈
details: Go 1.27、GoFrame v2.10、Vue3、TypeScript、Vite、Pinia、AntDesign 等技术栈,采用主流前后端分离架构实现。
link: /zh/tech-stack.html
linkText: 查看更多
rel: noreferrer
- title: 🗄️ 支持多种数据库
details: 内置五方言驱动(MySQL、PostgreSQL、SQL Server、Oracle、SQLite),通过配置文件一键切换,并附带五方言整库迁移工具。
link: /zh/architecture/database-design.html
linkText: 查看更多
rel: noreferrer
- title: 📚 丰富的模块
details: 25 张业务表落地 27 个模块,集成权限架构体系、日志、字典、配置、参数、通知公告、定时任务、多租户等开箱即用功能。
link: /zh/introduction.html
linkText: 查看更多
rel: noreferrer
- title: 🧩 声明式 CRUD 基座
details: ModuleSpec 泛型基座 + 生命周期钩子,模块只需声明筛选、字典、文件字段与钩子差异点,即可获得完整增删改查能力。
link: /zh/guide/logic.html
linkText: 查看更多
rel: noreferrer
- title: 🏷️ gmeta 注解系统
details: 请求结构体上的 gmeta 扩展标签统一驱动路由注册、权限校验、操作日志与 OpenAPI 文档,注解即事实,杜绝三处不一致。
link: /zh/core/attributes.html
linkText: 查看更多
rel: noreferrer
- title: 🔧 开发工具支持
details: 内置代码生成器(HTTP + CLI 双入口),根据表结构一键生成前后端全套代码,并同步创建菜单、权限节点、字典与路由注册。
link: /zh/generator/overview.html
linkText: 查看更多
rel: noreferrer
- title: 🏢 多租户与数据权限
details: 全局钩子自动完成租户隔离与审计盖章,配合角色四档数据权限(全部/本部门及以下/本部门/仅本人),满足企业级 SaaS 场景。
link: /zh/core/tenant.html
linkText: 查看更多
rel: noreferrer
- title: 📋 操作日志与审计追踪
details: 中间件异步采集操作日志,gmeta log/logType 标签声明日志标题与类型,支持参数脱敏、IP 归属地与全链路追踪。
link: /zh/core/operation-log.html
linkText: 查看更多
rel: noreferrer
- title: ⏰ 定时任务调度
details: 数据驱动的常驻调度器,支持 Cron 表达式、HTTP/本地处理器双执行形态、三种执行策略与多实例执行锁。
link: /zh/modules/job.html
linkText: 查看更多
rel: noreferrer
- title: 📊 数据导入导出
details: 基于 excelize 的后端导入导出与 xlsx 前端组件,导出统一走 ChunkQuery 分批查询,满足大数据量场景防内存溢出。
link: /zh/tools/excel.html
linkText: 查看更多
rel: noreferrer
- title: 🛡️ 全链路安全防护
details: JWT 双令牌 + jti 黑名单、bcrypt 密码加密、固定窗口限流(认证入口独立阈值)、演示模式、SVG 防 XSS 加固。
link: /zh/architecture/security.html
linkText: 查看更多
rel: noreferrer
- title: 📡 运行监控体系
details: 在线用户、服务器监控、缓存监控三个无表监控模块,配合 Redis INFO 解析与调度器健康检查,运行状态一目了然。
link: "/zh/modules/online-user.html"
linkText: 查看更多
rel: noreferrer
---
+24
View File
@@ -0,0 +1,24 @@
{
"name": "@wiki/docs",
"version": "3.0.0",
"private": true,
"type": "module",
"scripts": {
"docs": "vitepress dev .",
"docs:build": "vitepress build .",
"docs:preview": "vitepress preview ."
},
"dependencies": {
"@mdit-vue/shared": "^2.1.3",
"@vueuse/core": "^10.11.0",
"medium-zoom": "^1.1.0",
"viewerjs": "^1.11.7",
"vitepress": "1.3.4",
"vitepress-plugin-group-icons": "^1.3.6",
"vitepress-plugin-image-viewer": "^1.1.6"
},
"devDependencies": {
"sass-embedded": "^1.85.1",
"vite": "^5.4.9"
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,35 @@
<svg width="123" height="40" viewBox="0 0 123 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_199_880)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M56.3436 14.3118L53.2454 5.56299H50.7168L55.1062 17.3316H57.486L61.8912 5.56299H59.4449L56.3467 14.3118H56.3436ZM67.9009 17.3316H70.0275L70.0117 17.1811C69.9832 16.902 69.9516 16.4724 69.9453 15.9989V9.166H67.7173V13.9136C67.7173 14.8606 67.1477 15.5222 66.1603 15.5222C65.1223 15.5222 64.6539 14.7916 64.6539 13.8791V9.166H62.426V14.3275C62.426 16.1023 63.5653 17.5479 65.559 17.5479C66.4293 17.5479 67.385 17.2155 67.8376 16.4536C67.8376 16.7859 67.8724 17.1685 67.9041 17.3347L67.9009 17.3316ZM75.7587 10.7088C74.6036 10.7088 73.9675 11.5711 73.9169 12.3175H77.6037C77.5689 11.5053 77.0341 10.7088 75.7619 10.7088H75.7587ZM77.8189 14.4435C77.5499 15.1742 76.9803 15.6884 75.9423 15.6884C74.8378 15.6884 73.9169 14.9076 73.8663 13.8289H79.762L79.7873 13.4589C79.7937 13.3617 79.7968 13.2551 79.7968 13.1484C79.7968 10.5269 78.2715 8.91514 75.7271 8.91514C73.6163 8.91514 71.6732 10.6085 71.6732 13.2143C71.6732 15.9706 73.6669 17.5793 75.9265 17.5793C77.9518 17.5793 79.2588 16.4002 79.6797 14.9891L77.8189 14.4404V14.4435ZM55.641 22.7439C58.5208 22.7439 59.6285 24.4686 59.8943 25.7982L57.8183 26.4285C57.7012 25.7825 57.1664 24.7696 55.6568 24.7696C54.5334 24.7696 53.796 25.4846 53.796 26.2623C53.796 26.9082 54.2137 27.4256 55.0872 27.5887L56.7455 27.9054C58.9069 28.3193 60.062 29.7148 60.062 31.3736C60.062 33.1829 58.5367 35.008 55.7739 35.008C52.6251 35.008 51.2358 33.0011 51.0491 31.3234L53.1947 30.759C53.296 31.9223 54.1156 32.9666 55.7897 32.9666C57.0303 32.9666 57.717 32.352 57.717 31.521C57.717 30.8405 57.198 30.3106 56.2771 30.1255L54.6188 29.7932C52.7264 29.4106 51.5016 28.2002 51.5016 26.4222C51.5016 24.3306 53.3941 22.7377 55.6378 22.7377L55.641 22.7439ZM65.4894 28.3883C66.5939 28.3883 67.1129 29.0845 67.3154 29.7649L69.3092 29.1002C68.9579 27.7235 67.6857 26.3438 65.4388 26.3438C63.0431 26.3438 61.1665 28.1531 61.1665 30.6774C61.1665 33.2017 63.0748 35.0111 65.5052 35.0111C67.6983 35.0111 68.9895 33.6157 69.3566 32.2547L67.3977 31.6088C67.2142 32.2391 66.6445 32.9854 65.5052 32.9854C64.366 32.9854 63.3944 32.1544 63.3944 30.6774C63.3944 29.2005 64.3501 28.3883 65.4894 28.3883ZM74.7682 28.3727C73.8315 28.3727 73.2776 29.0688 73.227 29.9656V34.7634H70.9991V22.7471H73.227V27.2783C73.7618 26.648 74.6669 26.3814 75.4549 26.3814C77.5499 26.3814 78.5215 27.827 78.5215 29.6176V34.7634H76.2936V30.0001C76.2936 29.0876 75.841 28.3727 74.7682 28.3727ZM84.4837 33.0042C83.3951 33.0042 82.3887 32.2077 82.3887 30.6806C82.3887 29.1535 83.3951 28.357 84.4837 28.357C85.5723 28.357 86.5787 29.1378 86.5787 30.6806C86.5787 32.2234 85.5723 33.0042 84.4837 33.0042ZM80.1608 30.6806C80.1608 28.1563 82.0216 26.3469 84.4837 26.3469C86.9458 26.3469 88.8066 28.1563 88.8066 30.6806C88.8066 33.2049 86.9458 35.0142 84.4837 35.0142C82.0216 35.0142 80.1608 33.1892 80.1608 30.6806ZM94.2657 33.0042C93.177 33.0042 92.1707 32.2077 92.1707 30.6806C92.1707 29.1535 93.177 28.357 94.2657 28.357C95.3543 28.357 96.3607 29.1378 96.3607 30.6806C96.3607 32.2234 95.3543 33.0042 94.2657 33.0042ZM89.9427 30.6806C89.9427 28.1563 91.8035 26.3469 94.2657 26.3469C96.7278 26.3469 98.5886 28.1563 98.5886 30.6806C98.5886 33.2049 96.7278 35.0142 94.2657 35.0142C91.8035 35.0142 89.9427 33.1892 89.9427 30.6806ZM102.573 34.7634V22.7471H100.345V34.7634H102.573ZM116.181 30.6806C116.181 32.2077 117.187 33.0042 118.276 33.0042C119.365 33.0042 120.371 32.2234 120.371 30.6806C120.371 29.1378 119.365 28.357 118.276 28.357C117.187 28.357 116.181 29.1378 116.181 30.6806ZM118.276 26.3469C115.814 26.3469 113.953 28.1563 113.953 30.6806C113.953 33.2049 115.814 35.0142 118.276 35.0142C120.738 35.0142 122.599 33.1892 122.599 30.6806C122.599 28.172 120.738 26.3469 118.276 26.3469ZM112.067 26.463V34.7634H109.839V26.463H112.067ZM109.731 24.2271C109.731 23.6188 110.231 23.1202 110.842 23.1202C111.453 23.1202 111.966 23.6188 111.966 24.2271C111.966 24.8355 111.466 25.3341 110.842 25.3341C110.219 25.3341 109.731 24.8355 109.731 24.2271ZM106.26 31.8565C105.421 31.8565 104.734 32.537 104.734 33.3679C104.734 34.1989 105.421 34.8794 106.26 34.8794C107.098 34.8794 107.801 34.1989 107.801 33.3679C107.801 32.537 107.114 31.8565 106.26 31.8565Z" fill="white"/>
<path d="M10.687 24.8164C7.98759 11.5395 10.5446 2.8252 10.5446 2.8252C10.5446 2.8252 9.86107 8.1278 15.2378 16.6791C15.1682 27.278 17.2664 32.8064 17.2664 32.8064L17.6968 32.4897L17.2664 32.8628C17.2664 32.8628 14.0764 30.2727 10.687 24.8164Z" fill="url(#paint0_radial_199_880)"/>
<path d="M23.3966 22.9352C24.6878 24.2271 25.8904 25.124 26.7607 25.5473C26.7543 25.5473 26.7512 25.541 26.7449 25.541C26.7449 25.541 27.7227 25.8891 27.8936 25.6571C29.0835 24.4027 29.3747 22.0666 28.5804 17.6734C24.0422 15.0958 23.4694 11.0757 23.4694 11.0757C23.4694 11.0757 22.653 16.6574 23.3966 22.9352Z" fill="url(#paint1_radial_199_880)"/>
<path d="M10.5448 2.82529C10.5448 2.82529 6.8105 15.5503 13.1303 33.6751C13.3012 34.1988 13.1303 34.7789 12.6715 35.127L6.00668 39.7742C5.37375 40.2383 4.456 39.9498 4.16801 39.1941C2.38631 34.3712 -2.49675 18.3944 2.44328 0.909333C2.6743 0.210054 3.42116 -0.194461 4.10789 0.0940304L10.5416 2.82529H10.5448Z" fill="url(#paint2_radial_199_880)"/>
<path d="M23.2983 28.3882L17.2665 32.8034C17.2665 32.8034 14.048 24.3211 15.7158 8.22831C15.8297 7.30012 16.8076 6.83289 17.6114 7.30012L23.4724 11.0756C23.4724 11.0756 22.3236 18.9182 23.9882 26.4723C24.1591 27.228 23.8743 27.9837 23.2983 28.3882Z" fill="url(#paint3_radial_199_880)"/>
<path d="M34.4438 18.5699L28.8708 14.9104C28.5258 14.6784 28.01 15.0264 28.1239 15.4341C29.4468 21.3011 29.2727 24.208 27.8929 25.6599C27.722 25.8919 26.7441 25.5438 26.7441 25.5438C27.6619 26.0079 27.8929 25.7759 27.8929 25.7759C28.8107 25.0797 32.2601 22.5805 34.4438 21.0691C35.3045 20.4294 35.3045 19.1531 34.4438 18.5699Z" fill="url(#paint4_radial_199_880)"/>
</g>
<defs>
<radialGradient id="paint0_radial_199_880" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(20.5601 29.2931) scale(22.7981 22.5901)">
<stop stop-color="#4F81E7"/>
<stop offset="1" stop-color="#2A0894"/>
</radialGradient>
<radialGradient id="paint1_radial_199_880" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(32.3611 23.7249) scale(15.4944 15.353)">
<stop stop-color="#4F81E7"/>
<stop offset="1" stop-color="#2A0894"/>
</radialGradient>
<radialGradient id="paint2_radial_199_880" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(-0.253013 -2.8388) scale(29.3389 29.0712)">
<stop stop-color="#FFDAD4"/>
<stop offset="0.92" stop-color="#FF4975"/>
</radialGradient>
<radialGradient id="paint3_radial_199_880" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(12.5184 3.38422) scale(26.8733 26.6281)">
<stop stop-color="#FFFFDC"/>
<stop offset="1" stop-color="#0BD88F"/>
</radialGradient>
<radialGradient id="paint4_radial_199_880" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(28.9706 17.0437) scale(14.8958 14.7599)">
<stop stop-color="#4BDFFF"/>
<stop offset="1" stop-color="#4F08E7"/>
</radialGradient>
<clipPath id="clip0_199_880">
<rect width="122.308" height="40" fill="white" transform="translate(0.293945)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 7.2 KiB

@@ -0,0 +1,29 @@
<svg width="35" height="41" viewBox="0 0 35 41" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.4429 25.3174C7.74345 12.0405 10.3005 3.32617 10.3005 3.32617C10.3005 3.32617 9.61693 8.62877 14.9937 17.18C14.9241 27.779 17.0222 33.3074 17.0222 33.3074L17.4526 32.9906L17.0222 33.3638C17.0222 33.3638 13.8323 30.7736 10.4429 25.3174Z" fill="url(#paint0_radial_199_1147)"/>
<path d="M23.1525 23.4362C24.4437 24.7281 25.6463 25.625 26.5165 26.0483C26.5102 26.0483 26.507 26.042 26.5007 26.042C26.5007 26.042 27.4786 26.3901 27.6495 26.158C28.8394 24.9037 29.1305 22.5676 28.3362 18.1743C23.7981 15.5967 23.2253 11.5767 23.2253 11.5767C23.2253 11.5767 22.4088 17.1583 23.1525 23.4362Z" fill="url(#paint1_radial_199_1147)"/>
<path d="M10.3007 3.32627C10.3007 3.32627 6.56636 16.0513 12.8862 34.1761C13.0571 34.6997 12.8862 35.2799 12.4273 35.6279L5.76254 40.2752C5.12961 40.7393 4.21186 40.4508 3.92387 39.695C2.14217 34.8722 -2.7409 18.8954 2.19914 1.41031C2.43016 0.711031 3.17701 0.306515 3.86375 0.595007L10.2975 3.32627H10.3007Z" fill="url(#paint2_radial_199_1147)"/>
<path d="M23.0542 28.8892L17.0223 33.3044C17.0223 33.3044 13.8039 24.8221 15.4717 8.72928C15.5856 7.80109 16.5635 7.33386 17.3673 7.80109L23.2282 11.5766C23.2282 11.5766 22.0795 19.4192 23.7441 26.9732C23.915 27.729 23.6302 28.4847 23.0542 28.8892Z" fill="url(#paint3_radial_199_1147)"/>
<path d="M34.1996 19.0708L28.6266 15.4114C28.2817 15.1793 27.7659 15.5274 27.8798 15.9351C29.2026 21.8021 29.0286 24.709 27.6488 26.1608C27.4779 26.3929 26.5 26.0448 26.5 26.0448C27.4178 26.5089 27.6488 26.2769 27.6488 26.2769C28.5665 25.5807 32.016 23.0815 34.1996 21.5701C35.0604 20.9304 35.0604 19.6541 34.1996 19.0708Z" fill="url(#paint4_radial_199_1147)"/>
<defs>
<radialGradient id="paint0_radial_199_1147" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(20.3159 29.794) scale(22.7981 22.5901)">
<stop stop-color="#4F81E7"/>
<stop offset="1" stop-color="#2A0894"/>
</radialGradient>
<radialGradient id="paint1_radial_199_1147" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(32.117 24.2259) scale(15.4944 15.353)">
<stop stop-color="#4F81E7"/>
<stop offset="1" stop-color="#2A0894"/>
</radialGradient>
<radialGradient id="paint2_radial_199_1147" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(-0.497153 -2.33783) scale(29.3389 29.0712)">
<stop stop-color="#FFDAD4"/>
<stop offset="0.92" stop-color="#FF4975"/>
</radialGradient>
<radialGradient id="paint3_radial_199_1147" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(12.2742 3.88519) scale(26.8733 26.6281)">
<stop stop-color="#FFFFDC"/>
<stop offset="1" stop-color="#0BD88F"/>
</radialGradient>
<radialGradient id="paint4_radial_199_1147" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(28.7265 17.5447) scale(14.8958 14.7599)">
<stop stop-color="#4BDFFF"/>
<stop offset="1" stop-color="#4F08E7"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 680 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 610 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 610 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 748 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 801 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 647 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 841 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 813 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 806 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 733 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 872 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 719 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 845 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 771 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 947 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 610 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 920 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 780 KiB

@@ -0,0 +1,12 @@
import { defineConfig } from "vite";
export default defineConfig({
clearScreen: false,
define: {
__DEV__: "true",
__BROWSER__: "true",
},
optimizeDeps: {
exclude: ["vue-demi", "@vueuse/shared", "@vueuse/core"],
},
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,74 @@
# 8.5 字典数据接口
:::tip 概述
字典接口分三层:管理接口(dict/dict_item 七件套,带权限)、前端消费接口(dictionary 别名 + getDictItemList,登录即可)、缓存刷新接口。机制详见 [5.10 字典模块](../core/dict)。
:::
## 管理接口(带权限)
```text
dict(/api/dict)七件套:sys:dict:{page|detail|add|update|delete|batchDelete}
(注意:无 /dict/list——列表经 page 或 /api/dictionary/list 别名)
GET /api/dict/refreshCache sys:dict:cache 刷新字典缓存
dict_item(/api/dict/item)七件套:sys:dictitem:{page|detail|add|update|delete|batchDelete}
(页面用 page;无标准 list)
```
## 前端消费接口(登录即可,无 permission)
```text
GET /api/dictionary/list 字典列表别名(前端 dictionary.ts 调用)
GET /api/dictionary/info 字典详情别名
GET /api/dict/item/getDictItemList/:code 按编码查字典项(走 Redis 缓存)
```
## 典型消费流
```text
前端下拉/单选组件初始化:
GET /api/dict/item/getDictItemList/{code}
→ 缓存命中直接返回(easygoadmin:dict:item:{code},TTL 3600s)
→ 返回 [{name:"正常", value:1}, {name:"禁用", value:2}, ...]
→ 组件 options 渲染
行内显示名({field}Text):
后端基座已填充(DictGetText),前端直接展示 statusText——
不需要再调字典接口
```
## 缓存与刷新
| 层 | TTL | 刷新方式 |
|----|-----|---------|
| Redis `dict:item:{code}` | 3600s | `GET /api/dict/refreshCache`(可全量或按 code 单刷) |
| 进程内 dictTextCache | 300s | 只能等自然过期(各实例独立) |
::: warning 刷新语义
RefreshCache **只刷 Redis 层**——{field}Text 的进程内层仍按 300s 自然过期。改字典文案"接口立即变、{field}Text 最迟 5 分钟变"是预期行为。
:::
## 缓存失效注意
```text
dict_item 增删改不主动清 Redis 缓存(最长 1 小时延迟)——
关键场景(如刚改完下拉选项要立即生效)调 GET /api/dict/refreshCache。
```
## 接口使用速查
| 需求 | 接口 |
|------|------|
| 页面展示字典项下拉 | GET /api/dict/item/getDictItemList/{code} |
| 字典管理页列表 | GET /api/dict/page |
| 字典项管理页列表 | GET /api/dict/item/page?dictId=x |
| 新建字典 | POST /api/dict/add(code 全局唯一) |
| 新建字典项 | POST /api/dict/item/add(name/value/sort) |
| 改文案后立即生效 | GET /api/dict/refreshCache(Redis 层);{field}Text 等 300s |
## 开发规约
+ 字典编码规范 `{模块snake}_{字段snake}`(user_status / job_execute_policy)
+ 列注释枚举建表 → 生成器自动建字典(幂等)
+ 前端下拉选项优先字典驱动(勿硬编码 options——example 的静态 options 是教学示例)
+ 行内显示名走后端 {field}Text(唯一真相在字典管理页)
@@ -0,0 +1,64 @@
# 8.2 错误码对照表
:::tip 概述
业务错误码定义在 internal/consts/biz_err.go(gcode.Register 语义值)。注意 Fail 映射规则:除 401 外全部降级为 code=1 返回前端——细分码主要用于日志与语义区分。
:::
## 基础错误码
| 码 | 常量 | 含义 | 触发场景 |
|----|------|------|---------|
| 1 | CodeFailed | 通用操作失败 | 未归类错误的默认出口 |
| 400 | CodeParamError | 参数校验失败 | gvalid 标签校验不过(文案取 # 后缀) |
| 401 | CodeUnauthorized | 登录已过期,请重新登录 | 令牌缺失/过期/黑名单(**唯一保留原码返回**) |
| 403 | CodeForbidden | 没有该操作权限 | HasPerm 权限判定不过 |
| 429 | CodeTooManyRequests | 请求过于频繁 | RateLimit 超限 |
| 500 | CodeServerError | 服务器内部错误 | panic 兜底 / 未预期错误 |
## 业务错误码(1000+)
| 码 | 含义 | 触发模块/场景 |
|----|------|-------------|
| 1001 | 用户不存在 | auth.Login findUser 未命中 |
| 1002 | 账号禁用 | auth.Login status!=1 |
| 1003 | 密码错误 | auth.Login bcrypt 比对失败 |
| 1004 | 验证码错误 | captcha.Verify 失败/过期 |
| 1005 | 租户已过期 | Tenant 中间件 / mustLoadTenant expire_time 已过 |
| 1006 | 租户已禁用 | Tenant 中间件 / mustLoadTenant status!=1 |
| 1007 | 租户用户数上限 | user.Add checkTenantUserMax 超 max_user_num |
| 1008 | Token 无效 | oauth2 Refresh:typ 非 refresh / 已在黑名单(防重放) |
| 1009 | 记录已存在 | 唯一预检(UniqueFields/CheckUnique)命中 |
| 1010 | 记录不存在 | 详情/更新/删除目标缺失 |
| 1011 | 存在下级或关联 | 删除守卫(dept 子级 / menu 子菜单等) |
| 1012 | 原密码错误 | index.UpdatePassword 旧密码校验失败 |
| 1013 | 租户不存在 | 租户定位失败 |
| 1014 | 记录已被引用 | 删除守卫(role 被用户引用等) |
| 1015 | 演示环境禁止操作 | Demo 中间件拦截写操作(app.demo=true) |
## 使用规约
```text
后端上抛:gerror.NewCodef(consts.CodeUserNotExist, "用户不存在")
→ Bind 包装层 response.Fail → 除 401 外降级 code=1(msg 保留)
前端识别:
code=0 成功
code=1 失败(弹 msg——"用户不存在"等细分语义在 msg 里)
code=401 未认证(跳登录)
code=403 无权限(可定制提示)
code=429 限流(可定制"请稍后再试")
```
::: tip 为什么细分码降级为 1
前端只需三条分流规则(0/1/401)即可覆盖全部场景;细分码留在服务端日志与 msg 中,避免前端维护 20+ 分支。需要按细分码做 UI 差异化时,读 msg 或扩展信封(不建议破坏现有约定)。
:::
## 排障对照
| 用户看到 | 码 | 深层原因排查 |
|---------|-----|------------|
| "登录已过期" | 401 | 令牌过期/被拉黑(登出/轮换/强退) |
| "没有该操作权限" | 403 | gmeta permission ↔ 菜单节点 ↔ 角色勾选 ↔ 身份缓存 |
| "请求过于频繁" | 429 | 限流阈值(认证入口 10/分钟更严) |
| "演示环境,禁止操作!" | 1→1015 msg | app.demo=true |
| 其他业务提示 | 1 | msg 即细分语义;服务端日志有细分码 |
@@ -0,0 +1,101 @@
# 8.7 API 接口总览
:::tip 概述
全部接口经 gmeta 注解自动生成 OpenAPI:GET /api.json(原生 JSON)+ GET /swagger(本地 Redoc UI)。本节给出接口域总览与权限码总规律,完整清单以 /api.json 为准。
:::
## 在线文档
```text
GET /swagger 本地托管 Redoc 页面(免登录;内置页依赖境外 CDN,已改本地托管)
GET /api.json 原生 OpenAPI JSON(gmeta 补注册闭包路由后全量可用)
```
gmeta 的 method/path/summary/permission 会完整体现在文档中——接口与文档天然同步。
## 接口域总览(27 模块 170+ 接口)
| 域 | 模块 | 路由前缀 | 说明 |
|----|------|---------|------|
| auth | (无表) | /api/login 等顶层 | 登录/登出/验证码/令牌(公开+顶层) |
| index | (无表) | /api/index/* | 个人中心(登录即可) |
| system | user | /api/user | 用户(含导入导出/重置密码) |
| | role | /api/role | 角色(权限分配/data_scope) |
| | menu | /api/menu | 菜单(树形) |
| | dept | /api/dept | 部门(树形) |
| | level | /api/level | 职级(导入导出) |
| | position | /api/position | 岗位 |
| | tenant | /api/tenant | 租户(开户/用户列表) |
| data | config | /api/config | 配置(+web/index、web/save) |
| | config_item | /api/config/item | 配置项 |
| | param | /api/param | 参数 |
| | dict | /api/dict | 字典(+refreshCache、dictionary 别名) |
| | dict_item | /api/dict/item | 字典项(+getDictItemList) |
| | notice | /api/notice | 公告 |
| | city | /api/city(别名 /api/area) | 行政区划(级联懒加载) |
| content | article | /api/article | 文章(富文本/多图) |
| | category | /api/category | 分类(树形) |
| | link | /api/link | 友链 |
| monitor | operation_log | /api/oper/log | 操作日志(仅查询删除) |
| | login_log | /api/login/log | 登录日志(仅查询删除) |
| | job | /api/job | 定时任务(+runOnce/pause/resume/daemonStatus) |
| | job_log | /api/job/log | 任务日志(仅查询删除) |
| | online_user | /api/onlineUser | 在线用户(无表,Redis) |
| | server_monitor | /api/server | 服务器监控(无表) |
| | cache_monitor | /api/cache | 缓存监控(无表) |
| tool | example | /api/example | 案例演示(导入导出) |
| | file_template | /api/file/template | 文件模板(+getTemplateByCode) |
| | generator | /api/generator | 代码生成(page/preview/generate/batchGenerate) |
| | upload | /api/upload | 上传(uploadFile/uploadFiles) |
基础设施(router.go 手工注册):`GET /api/health`(公开)、`GET /swagger`、`GET /api.json`、`GET /upload/**`(静态)。
## 标准接口形态(七件套 + 扩展)
```text
GET /api/{prefix}/page 分页
GET /api/{prefix}/list 列表(下拉/树形)
GET /api/{prefix}/detail/:id 详情
POST /api/{prefix}/add 新增
PUT /api/{prefix}/update 修改
DELETE /api/{prefix}/delete/:id 删除(软删)
DELETE /api/{prefix}/batchDelete 批量删除(body {ids:[]})
扩展:PUT /api/{prefix}/status(状态)
POST /api/{prefix}/import · GET /api/{prefix}/export(导入导出)
```
## 权限码总规律
```text
sys:{模块小驼峰}:{动作}
动作与方法名一致:page/list/detail/add/update/delete/batchDelete/
status/import/export/resetPwd/execute/pause/resume/
savePermission/getPermission/cache/users/save 等
特例:配置网站 sys:configWeb:data|save
生成器 sys:generator:generator|batchGenerator
上传 sys:upload:file|files
超管(ROLE_SUPER_ADMIN)绕过全部权限校验
```
## 公开路由(免登录白名单)
```text
/api/login · /api/register(白名单放行,路由未注册)· /api/captcha
/api/oauth2/token · /api/health
另放行 /api.json 与 /swagger*(文档)
三处一致:middleware/public.go 白名单 / gmeta public:"true" / router.go 手工注册
全部路由不带 /v1 版本段
```
## 通用约定速记
```text
认证:Authorization: Bearer <access_token>(登录接口返回 access_token 下划线)
响应:{code, ok, msg, data};0 成功 / 1 失败 / 401 未认证
分页:PageData[T](records/total/size/current/pages)
命名:出入参驼峰(例外仅登录/令牌接口四字段)
限流:普通 120/分钟/IP/路径;认证入口 10/分钟/IP
```
@@ -0,0 +1,90 @@
# 8.3 分页查询规范
:::tip 概述
分页契约三件套:PageReq 入参(pageNo/pageSize)+ PageData 出参(records/total/size/current/pages)+ 筛选对齐三件套(querySchemas ↔ Req 字段 ↔ spec 声明)。导出与列表同套筛选。
:::
## 分页入参(common.PageReq)
```text
pageNo 页码,默认 1
pageSize 每页条数,默认 10,上限 500(GetPageSize 截断——防恶意大 pageSize)
请求示例:GET /api/user/page?pageNo=1&pageSize=10&keyword=张
```
## 分页出参(PageData[T])
```json
{
"records": [], "total": 0, "size": 10, "current": 1, "pages": 0
}
```
records 为空输出 `[]` 而非 null(前端无需判空兼容)。
## 筛选对齐三件套(2026-10-14 定稿)
```text
前端 querySchemas.ts 每个筛选输入
↔ XxxPageReq 同名驼峰字段
↔ logic spec 声明
字符串 Input → LikeFields(逐列模糊,值取 req 同名字段,空跳过)
Select/InputNumber → EqFields(等值,零值跳过)
关键词框(可选) → KeywordFields(req.keyword 多列 OR)
——能力保留在基座,模块默认不声明,需要时再用
```
### 对齐示例(example 模块)
| 前端 querySchemas | 后端 PageReq | spec 声明 |
|-------------------|-------------|-----------|
| `field: 'name'`(Input) | `Name string \`json:"name"\`` | LikeFields: []string{"name"} |
| `field: 'type'`(Select) | `Type int \`json:"type"\`` | EqFields: []string{"type", "status"} |
| `field: 'status'`(Select) | `Status int \`json:"status"\`` | 同上 |
### 筛选链执行顺序(固定)
```text
applyFilters:LikeFields → EqFields → KeywordFields → Filter → DataScope
前置:dao.Ctx 已注入 del_flag=0 + 租户 IN(0,自身)
```
复杂条件写 Filter 钩子,**不要重复 Like/Eq 已覆盖的条件**。
## 导出=所见
```text
导出接口统一走 xxxSpec.applyFilters 与列表同套筛选:
Export 请求结构含与 PageReq 相同的筛选字段(不带 pageNo/pageSize)
→ applyFilters 同链过滤 → 全量(ChunkQuery 分批)→ Excel
保证:列表看到什么,导出就是什么(含数据权限过滤)
```
## 排序规范
```text
spec.OrderBy 默认排序(如 "id DESC"、"sort, id")
spec.ListOrderBy 列表专用排序
spec.OrderableFields 前端排序白名单(orderField/orderType 仅白名单内生效)
```
前端排序参数不可直通 SQL——白名单外的排序字段忽略(防注入)。
## 前端分页组件约定
```text
分页组件(components/pagination):
change 事件携带 pageNo/pageSize → 重新请求 /page
pageSize 切换回到第 1 页
上限保护:≤500(与后端 GetPageSize 截断一致)
```
## 开发规约
+ 分页接口 PageReq 内嵌 common.PageReq(复用默认值与截断逻辑)
+ 筛选字段必须三件套对齐——querySchemas 加了输入,Req 与 spec 都要跟上
+ 字符串筛选走 LikeFields 声明(列名显式声明,值取 req 同名驼峰字段)
+ 零值跳过是 EqFields 语义(status=0 是有效筛选值时需在 Filter 处理或调整字段设计)
+ 大表分页深翻页注意:pageNo×pageSize 过大时的 offset 成本(管理后台低频场景可接受)
@@ -0,0 +1,109 @@
# 8.1 统一响应格式
:::tip 概述
全部接口返回统一 JSON 信封:code/ok/msg/data 四字段。code=0 成功、1 通用失败、401 未认证(配合 HTTP 401);前端按 code 分流。分页用 PageData 泛型壳。
:::
## 信封结构
```json
// 成功
{
"code": 0,
"ok": true,
"msg": "操作成功",
"data": { "id": 1 }
}
// 失败
{
"code": 1,
"ok": false,
"msg": "用户不存在",
"data": {}
}
// 未认证(HTTP 401)
{
"code": 401,
"ok": false,
"msg": "登录已过期,请重新登录",
"data": {}
}
```
## 字段约定
| 字段 | 类型 | 说明 |
|------|------|------|
| code | int | 0=成功;1=通用失败;401=未认证;细分业务码见 8.2(前端主要看 0/1/401) |
| ok | bool | 与 code==0 等价的布尔便捷字段 |
| msg | string | 提示信息;失败时前端弹出 |
| data | object | 业务数据;失败时为空对象 `{}`(非 null) |
## Fail 映射规则
```text
response.Fail(err):
gerror 码 == 401 → 原样保留(配合 HTTP 401,前端跳登录)
其余 → 一律降级 code=1
设计含义:consts 中的细分错误码(1001 用户不存在……)主要用于
日志与服务端语义区分;前端统一按 1 弹 msg 即可。
```
## 分页壳(PageData[T])
```json
{
"code": 0, "ok": true, "msg": "操作成功",
"data": {
"records": [ ... ],
"total": 100,
"size": 10,
"current": 1,
"pages": 10
}
}
```
| 字段 | 说明 |
|------|------|
| records | 行数组;**为空输出 `[]` 而非 null** |
| total | 总条数 |
| size | 每页条数(pageSize,默认 10 上限 500) |
| current | 当前页码(pageNo,默认 1) |
| pages | 总页数 |
## 其他通用出参形态
```text
新增: {"id": 1} (IdRes)
修改/删除:{} (空结构体)
列表(非分页):[ ... ] (数组直接作 data)
详情: { ...单对象... }
```
## 状态页
```text
404 → {"code":1,"msg":"接口不存在: GET /api/xxx"}
500 → {"code":1,"msg":"服务器内部错误"}
非 200 一律输出统一 JSON 信封,不返回 HTML
```
## 前端处理模板
```ts
// 响应拦截器分流(语义示意)
if (code === 0) return data; // 成功解包
if (code === 401) { 跳登录/尝试refresh; }
if (code === 1) ElMessage.error(msg); // 统一弹窗
```
## 开发规约
+ 出参 json 一律驼峰;唯一例外是登录/令牌接口的下划线字段(见 8.6)
+ 业务错误用 gerror.NewCode(consts.Xxx, "消息") 上抛,由 Bind 包装层统一 Fail
+ 不要在业务代码手写 JSON 响应(统一信封在包装层完成)
+ data 为 nil 时框架输出 `{}`/`[]`,业务无需特殊处理
@@ -0,0 +1,98 @@
# 8.6 Token 使用说明
:::tip 概述
本文档面向 API 对接方:如何获取令牌、如何携带、如何刷新、如何撤销。令牌为 JWT HS256 双令牌(access 2h + refresh 7d),唯一使用下划线命名的响应。
:::
## 获取令牌(POST /api/login,公开)
```sh
curl -X POST http://127.0.0.1:8000/api/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"123456","code":"618618","key":"<captcha-key>"}'
```
```json
{
"code": 0, "ok": true, "msg": "操作成功",
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.…",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.…",
"expires_in": 7200,
"token_type": "Bearer"
}
}
```
::: warning 命名例外
登录/令牌接口返回 `access_token/refresh_token/expires_in/token_type`(OAuth2 惯例下划线命名)——这是全系统唯一例外,其余接口出入参一律驼峰。
:::
## 携带令牌调用业务接口
```sh
curl http://127.0.0.1:8000/api/user/page?pageNo=1&pageSize=10 \
-H "Authorization: Bearer <access_token>"
```
兼容细节:Bearer 前缀大小写不敏感、可无前缀直接传 token;个别场景支持查询参数 `?token=`(URL 直传)。
## 刷新令牌(POST /api/oauth2/token,公开)
access 过期(收到 code=401)时用 refresh_token 轮换新令牌对:
```sh
curl -X POST http://127.0.0.1:8000/api/oauth2/token \
-H "Content-Type: application/json" \
-d '{"grant_type":"refresh_token","refresh_token":"<refresh_token>"}'
```
```json
{ "data": { "access_token": "…", "refresh_token": "…", "expires_in": 7200, "token_type": "Bearer" } }
```
刷新语义:
```text
* 旧 refresh 入黑名单(轮换)——旧 refresh_token 立即作废,必须存新值
* 旧 access 自然过期(不主动拉黑)
* 返回同登录格式四字段
* 兼容密码模式:grant_type=password 或无 refresh_token → 复用登录逻辑
```
## 撤销与校验
```text
GET /api/logout 登出(当前 access jti 拉黑)
DELETE /api/oauth2/remove/:token 撤销指定令牌(登录可用;解析 jti 入黑名单)
GET /api/oauth2/check/token 校验令牌有效性(JWT 中间件通过即有效)
```
## 对接方实现要点
```text
1. 存储:access 与 refresh 分开存(内存/localStorage);refresh 泄露=账号风险
2. 请求:全部业务请求带 Authorization: Bearer <access>
3. 401 处理:单飞刷新(并发 401 只触发一次 refresh,其余等结果);
refresh 失败(1008/401)→ 清令牌 → 跳登录
4. 刷新成功:立即用新令牌对覆盖旧值(旧 refresh 已作废)
5. 时钟偏移:以 expires_in 倒计时提前刷新(如过期前 60s),减少 401
6. 登出:调 /api/logout 后清本地——不调的话令牌有效期内仍可用
```
## 令牌状态速查
| 状态 | 表现 | 处理 |
|------|------|------|
| 有效 | 业务正常 | — |
| access 过期 | code=401 | refresh 轮换 |
| refresh 过期/已轮换 | code=1 或 1008 | 重新登录 |
| 已拉黑(登出/强退) | code=401 | 重新登录(强退的 refresh 也拉黑,无法复活) |
| 算法/签名无效 | code=401 | 检查是否跨环境混用 jwt.secret |
## 安全建议
+ HTTPS 传输(令牌明文等价于会话)
+ refresh_token 泄露面最小化(不入 URL、不入日志——后端已脱敏)
+ 客户端不持久化 access(内存优先);persist refresh 的场景加密存储
+ 多端登录场景用在线用户模块管理(forceLogout 双令牌拉黑)
@@ -0,0 +1,95 @@
# 8.4 文件上传接口
:::tip 概述
上传接口统一 multipart:POST /api/upload/uploadFile(单文件)与 uploadFiles(多文件),字段名 file。返回可预览 URL 与入库形态 Path。两段式协议详见 [7.1 文件上传全流程](../tools/upload)。
:::
## 接口定义
```text
POST /api/upload/uploadFile 权限 sys:upload:file multipart 字段名 file
POST /api/upload/uploadFiles 权限 sys:upload:files 多文件
入参 type=image 时按图片规则校验(扩展名 + 魔数嗅探),否则按普通文件
```
multipart 标签约定:`*ghttp.UploadFile` + `p:"file"`。
## 请求示例
```sh
curl -X POST http://127.0.0.1:8000/api/upload/uploadFile \
-H "Authorization: Bearer <access_token>" \
-F "file=@avatar.png" \
-F "type=image"
```
## 响应结构
```json
{
"code": 0, "ok": true, "msg": "操作成功",
"data": {
"originalName": "avatar.png",
"fileExtension": "png",
"fileType": "image/png",
"fileSize": 102400,
"fileName": "a1b2c3....png",
"filePath": "temp/20261018/a1b2c3....png",
"fileUrl": "http://file.example.com/temp/20261018/a1b2c3....png"
}
}
```
| 字段 | 用途 |
|------|------|
| originalName | 原始文件名(前端展示/回显用) |
| fileExtension | 扩展名(小写,无点) |
| fileType | MIME 类型(按文件头内容嗅探,识别不出为 application/octet-stream) |
| fileSize | 文件大小(字节) |
| fileName | 存储文件名(= filePath 的 basename,32位随机.ext) |
| filePath | 相对 uploadDir 路径(**入库形态**;表单保存时转正) |
| fileUrl | 可直接预览的完整地址(domainUrl 拼接) |
## 校验失败场景
| 场景 | 响应 |
|------|------|
| 超过 maxSize(默认 10MB) | code=1,msg 提示大小超限 |
| 扩展名不在白名单 | code=1,msg 提示类型不允许 |
| type=image 但魔数非 image/* | code=1,拒绝(防改后缀伪造) |
| 未登录/令牌无效 | code=401 |
| 无权限 | code=403 |
| 触发限流 | code=429 |
## 两段式协议(对接要点)
```text
上传阶段:只落 temp 目录(upload/temp/日期/随机名)
前端持有 url;表单字段存 url 或 path
保存阶段:业务接口(userAdd/articleAdd/...)的 spec.saveTransform
自动 SaveFromTemp 转正到业务目录(user/日期/时间戳_随机.ext)
库存相对路径
读取阶段:Detail/List 的 readTransform 经 GetFileUrl 补域名
```
前端不要在上传后直接把 url 当最终地址持久化到业务表——转正由业务保存触发。
## 批量上传(uploadFiles)
```text
POST /api/upload/uploadFiles
multipart 多个 file 字段 → 逐个 SaveTemp → 返回结果数组
(每项同单文件结构;单项校验失败不影响其他项——对照导入的逐行语义)
```
## 安全链速查
```text
大小限制 → 扩展名白名单 → 图片魔数嗅探 → 随机文件名 → 两段式
→ svg 附件化+nosniff → temp 定期清理(tempExpireDays + 03:30 gcron)
```
公网开放上传时收紧 fileExt(当前含 js/css/sql)——配置见 [7.2 上传配置详解](../tools/upload-config)。
@@ -0,0 +1,91 @@
# E. API 接口速查表
:::tip 概述
全部接口速查(按模块)。完整权威清单以 `GET /api.json` 为准(gmeta 自动生成)。约定:无 /v1 版本段;认证 `Authorization: Bearer`;分页 pageNo/pageSize。
:::
## 公开与基础设施
```text
POST /api/login 登录(返回 access_token 等四字段)
GET /api/captcha 图形验证码
POST /api/oauth2/token 令牌刷新/密码模式
GET /api/health 健康检查
GET /swagger · /api.json API 文档
GET /api/logout 登出
GET /api/oauth2/check/token 校验令牌
DELETE /api/oauth2/remove/:token 撤销令牌
```
## index 个人中心(登录即可)
```text
GET /api/index/getUser · /getMenus · /getMenus/:id
PUT /api/index/updatePassword · /updateProfile
```
## system 域(七件套 + 扩展)
```text
/user page list detail add update status resetPwd delete batchDelete import export document
权限 sys:user:*;resetPwd=logType reset
/role 七件套 + menu/list/:roleId(getPermission)+ menu/save(savePermission)
dataScope 四档(in:1,2,3,4)
/menu 七件套(List 树形)
/dept 七件套(List 树形;删除守卫)
/level 七件套 + status + import + export
/position 七件套 + status
/tenant 七件套 + status + account(开户)+ users/:tenantId
```
## data 域
```text
/config 七件套 + web/index + web/save(sys:configWeb:*)
/config/item 七件套(sys:configitem:*)
/param 七件套
/dict 七件套(无 list)+ refreshCache;别名 /api/dictionary/list|info
/dict/item 七件套 + getDictItemList/:code
/notice 七件套 + status
/city 七件套 + getCityList/:parentCode;别名 /api/area/getParent|findByParentCode
```
## content 域
```text
/article 七件套 + status(富文本/多图)
/category 七件套(List 树形)
/link 七件套(无 status)
```
## monitor 域
```text
/oper/log page list detail delete batchDelete(sys:operLog:*,仅查询删除)
/login/log 同上五件(sys:loginLog:*)
/job/log 同上五件(sys:jobLog:*)
/job 七件套 + runOnce/:id + pause/:id + resume/:id + status + daemonStatus
/onlineUser page detail/:id forceLogout/:id forceLogoutUser/:userId(无表)
/server info(登录即可,无表)
/cache info(登录即可,无表)
```
## tool 域
```text
/example 七件套 + status + import + export
/file/template 七件套 + getTemplateByCode/:code
/generator page preview generate batchGenerate
/upload uploadFile uploadFiles(multipart 字段 file)
```
## 速记规律
```text
标准七件套:GET page/list/detail/:id · POST add · PUT update ·
DELETE delete/:id · batchDelete(body {ids:[]})
扩展:status(PUT)/ import(POST multipart)/ export(GET 同筛选)
权限:sys:{模块小驼峰}:{动作};超管绕过
日志:gmeta log "{模块中文名}-{动作}" + logType
公开:仅 login/register/captcha/oauth2/token/health(白名单三处一致)
```
@@ -0,0 +1,58 @@
# G. 前端组件速查
:::tip 概述
ui/src/components/ 全部公共组件速查(25 类),按展示/录入/布局/能力分组。详细用法见 [9.9 数据展示组件](../frontend/display-components) 与 [9.10 数据录入组件](../frontend/form-components)。
:::
## 组件清单
| 组件 | 分组 | 职责 |
|------|------|------|
| Table | 展示 | 表格封装(columns 配置驱动) |
| pagination | 展示 | 分页 |
| Page | 布局 | 页面容器(搜索区/操作区/表格区) |
| Render | 展示 | 函数式渲染(h 函数封装) |
| CountTo | 展示 | 数字滚动动画 |
| Qrcode | 展示 | 二维码生成 |
| icon | 展示 | 图标 |
| Modal | 布局 | 弹窗封装 |
| TableSelect | 录入 | 表格选择器(关联数据单选/多选) |
| Form | 录入 | 表单封装(FormSchema 配置驱动) |
| Select | 录入 | 下拉封装 |
| numberInput | 录入 | 数字输入 |
| priceInput | 录入 | 价格输入(精度处理) |
| Password | 录入 | 密码输入(强度显示) |
| ChinaArea | 录入 | 中国省市区级联 |
| Region | 录入 | 行政区划 |
| Cropper | 录入 | 图片裁剪 |
| Upload | 录入 | 上传组件族(单图/多图/文件) |
| importFile | 录入 | Excel 导入入口 |
| Excel | 能力 | Excel 处理(导入解析/导出) |
| Editor | 录入 | 富文本(TinyMCE/Quill) |
| Authority | 能力 | 权限包装容器 |
| CheckToken | 能力 | 令牌检查 |
| Lockscreen | 能力 | 锁屏 |
| Websocket | 能力 | WebSocket 封装(前端遗留能力) |
| Application | 布局 | 应用级包装 |
## 高频组合(列表页标准形态)
```text
<Page>
搜索区 Form(querySchemas 驱动)
操作区 按钮 + v-permission
表格区 Table(columns 驱动) + pagination
</Page>
弹窗 Modal + edit.vue / detail.vue
```
## 与后端对接要点
```text
Table data ← PageData.data.records(http 已解包)
Upload ← /api/upload/uploadFile(temp URL)→ 保存转正
Editor ← content 字段([IMG_URL] 契约,Detail 已还原)
ChinaArea ← /api/area 接口(级联懒加载)
Excel ← /api/{module}/import|export
Authority ← permissions(getUser,超管 *:*:*)
```
@@ -0,0 +1,99 @@
# C. 配置项速查
:::tip 概述
manifest/config/config.yaml 全键速查。环境变量覆盖(12-Factor,环境变量 > 配置文件):`GF_` 前缀 + 键路径大写下划线(如 GF_DATABASE_DEFAULT_LINK);空值视为未设置,仅覆盖本文件已有键,键名规则与 Docker 注入方式详见 [2.5 配置文件说明](../quickstart/env-vars)。
:::
## server(服务端)
| 键 | 默认 | 说明 |
|----|------|------|
| server.address | :8000 | 监听地址 |
| server.openapiPath | /api.json | OpenAPI 路径 |
| server.swaggerPath | "" | Swagger UI(本地托管 resource/swagger/) |
| server.dumpRouterMap | false | 启动打印路由表 |
| server.errorStack | true | 响应携带堆栈(生产 false) |
| server.graceful | true | 优雅重启 |
## logger / database / redis
| 键 | 默认 | 说明 |
|----|------|------|
| logger.level | all | 日志级别 |
| logger.stdout | true | 控制台输出 |
| logger.path | log | 日志目录 |
| database.default.type | mysql | 五方言:mysql/pgsql/mssql/oracle/sqlite |
| database.default.link | — | 连接串(库名含点需注意) |
| database.default.debug | false | SQL 日志(生产 false) |
| redis.default.address | 127.0.0.1:6379 | 地址 |
| redis.default.pass | — | 密码(生产强密码) |
| redis.default.db / idle / maxActive / timeout | 0/10/50/300 | 池参数 |
## jwt / captcha / rateLimit
| 键 | 默认 | 说明 |
|----|------|------|
| jwt.secret | — | 签名密钥(**生产必改**) |
| jwt.issuer | easygoadmin | 签发者 |
| jwt.accessTokenExpire | 7200 | 访问令牌秒数(2h) |
| jwt.refreshTokenExpire | 604800 | 刷新令牌秒数(7d) |
| captcha.expire | 300 | 验证码秒数 |
| captcha.bypassCode | 618618 | 万能码(**生产留空**) |
| captcha.length / chars | 6 / 排除易混淆 | 字符 |
| captcha.fontPath | resource/static/fonts/Vera.ttf | 缺失降级 SVG |
| captcha.imageWidth/Height/fontSize | 160/50/40 | 尺寸 |
| captcha.noiseLines/noiseDots | 8/500 | 干扰 |
| rateLimit.enable | true | 限流开关 |
| rateLimit.limit | 120 | 普通接口/窗口/IP/路径 |
| rateLimit.windowSeconds | 60 | 窗口 |
| rateLimit.authLimit | 10 | 认证入口严阈值 |
## operationLog / job / tenant / app
| 键 | 默认 | 说明 |
|----|------|------|
| operationLog.enable | true | 操作日志总开关 |
| operationLog.logQuery | true | 记录查询类 |
| job.logRetentionDays | 90 | 三张日志表保留天数(LogClean 消费) |
| tenant.defaultId | 1 | 超管新增的默认归属租户 |
| app.timezone | Asia/Shanghai | 时区 |
| app.demo | false | 演示模式(true 拦截全部写操作) |
| app.errorMsg | true | 详细错误信息(生产 false) |
## api / cors / file / generator / ip2region
| 键 | 默认 | 说明 |
|----|------|------|
| api.camelSnakeConvert | true | 驼峰转换(恒开) |
| api.proxyIps | 127.0.0.1,::1,CIDR… | 受信代理(XFF 信任范围) |
| cors.allowOrigin | * | 跨域来源 |
| file.uploadDir | 随环境(当前为开发机绝对路径) | 上传物理根;建议相对项目根,容器部署用 `GF_FILE_UPLOADDIR` 覆盖 |
| file.domainUrl | http://file.goframe.elevue | 文件访问域名(空=相对 /upload/ 路径) |
| file.tempDir | temp | 临时目录名 |
| file.imageExt | gif,jpg,jpeg,png,bmp,webp,svg | 图片白名单 |
| file.fileExt | (含 doc/xls/pdf/zip/js/css/sql 等) | 文件白名单(公网收紧) |
| file.maxSize | 10 | 单文件 MB |
| file.tempExpireDays | 7 | temp 保留天数(0=不清理) |
| generator.parentMenuId | 189 | 新模块菜单父级 ID |
| generator.overwrite | true | 覆盖开关(唯一入口;true=生成即覆盖,false=缺失补生成、存在跳过) |
| ip2region.path | manifest/data/ip2region.xdb | 归属地库 |
## 环境变量覆盖示例
```sh
GF_SERVER_ADDRESS=":9000"
GF_DATABASE_DEFAULT_LINK="mysql:root:secret@tcp(db:3306)/easygoadmin?charset=utf8mb4&parseTime=true"
GF_REDIS_DEFAULT_ADDRESS="redis:6379"
GF_JWT_SECRET="random"
GF_APP_DEMO="false"
GF_FILE_MAXSIZE="20"
```
## 新增配置键规范
```text
1. config.yaml 增加键 + 注释说明
2. 代码用 cfgBool/cfgInt 带默认值读取(缺失不 panic)
3. 本文档同步
4. 影响限流/日志/上传/调度的键变更后跑对应回归
```
@@ -0,0 +1,85 @@
# B. 数据库表结构速查
:::tip 概述
25 张业务表速查:表名/中文/域/租户级/关键列。定稿脚本 document/{mysql,postgresql,sqlserver,oracle,sqlite}/,MySQL 为规范基准。详细设计见 [4.9 数据库设计](../architecture/database-design)。
:::
## 全表统一审计列
```text
id(int 自增主键)create_user create_time update_user update_time del_flag
租户级表另有 tenant_id(0=平台级)
```
## 表清单
| 表 | 中文 | 域 | 租户级 | 关键列 |
|----|------|-----|--------|--------|
| goframe_user | 用户 | system | 是 | username/password/salt/mobile/email/avatar/dept_id/level_id/position_id/status/code |
| goframe_role | 角色 | system | 是 | code/data_scope(1-4) |
| goframe_role_menu | 角色菜单关联 | system | 否 | role_id/menu_id(无模块文件) |
| goframe_user_role | 用户角色关联 | system | 否 | user_id/role_id(无模块文件) |
| goframe_menu | 菜单 | system | 否 | parent_id/type(0菜单1权限)/permission/status/hide/sort |
| goframe_dept | 部门 | system | 是 | 树形 parent_id |
| goframe_level | 职级 | system | 是 | 导入导出 |
| goframe_position | 岗位 | system | 是 | |
| goframe_tenant | 租户 | system | 否 | code(T+ID)/status/expire_time/max_user_num |
| goframe_config | 系统配置 | data | 否 | code(uk) |
| goframe_config_item | 配置项 | data | 否 | config_id/value/options/type |
| goframe_param | 系统参数 | data | 否 | code(uk) |
| goframe_dict | 数据字典 | data | 否 | code(uk) |
| goframe_dict_item | 字典项 | data | 否 | dict_id/value |
| goframe_notice | 通知公告 | data | 是 | status |
| goframe_city | 行政区划 | data | 否 | pid/parent_code/area_code/level(78万+大表) |
| goframe_article | 文章 | content | 是 | category_id/cover/images/content(富文本)/status/click/sort |
| goframe_category | 文章分类 | content | 是 | 树形 parent_id |
| goframe_link | 友链 | content | 是 | 无 status |
| goframe_operation_log | 操作日志 | monitor | 否 | title/type/param/result/ip/location/consume_time/status |
| goframe_login_log | 登录日志 | monitor | 否 | log_type/ip/location/os/browser/status |
| goframe_job | 定时任务 | monitor | 否 | cron_expression/execute_policy/is_sync/url/params/status |
| goframe_job_log | 任务日志 | monitor | 否 | job_id/status/consume_time/exception_info |
| goframe_example | 案例演示 | tool | 否 | 七件套+导入导出演示 |
| goframe_file_template | 文件模板 | tool | 否 | code(uk) |
## 唯一索引(13 个)
```text
goframe_user uk_tenant_username (tenant_id, username, del_flag)
goframe_user uk_tenant_mobile (tenant_id, mobile, del_flag)
goframe_user uk_tenant_code (tenant_id, code, del_flag)
goframe_role uk_tenant_code (tenant_id, code, del_flag)
goframe_dept uk_tenant_name (tenant_id, name, del_flag)
goframe_level uk_tenant_name (tenant_id, name, del_flag)
goframe_position uk_tenant_name (tenant_id, name, del_flag)
goframe_category uk_tenant_name (tenant_id, category_name, del_flag)
goframe_config uk_code (code)
goframe_dict uk_code (code)
goframe_param uk_code (code)
goframe_file_template uk_code (code)
goframe_tenant uk_code (code)
```
要点:含 del_flag(软删行不占坑);含 tenant_id 的索引,预检范围必须等于索引范围。
## 常见枚举定稿
```text
status 1-正常 2-禁用
article.status 0-正常 1-下架
menu.type 0-菜单 1-权限节点
job.status 0-未发布 1-运行中 2-暂停
job.execute_policy 1-立即执行 2-执行一次 3-放弃执行
job.is_sync 1-同步 0-异步
role.data_scope 1-全部 2-本部门及以下 3-本部门 4-仅本人
city.level 0-省份 1-城市 2-县区 3-街道 4-社区
login_log.log_type 1-登录 2-退出
del_flag 0-正常 1-已删除
operation_log.type consts/log_type.go(0-40/99,静态映射)
```
## 租户级表(9 张,hooks.tenantTables)
```text
user / role / dept / level / position / article / category / notice / link
新增租户级表必须同步登记 internal/model/hooks/hooks.go(生成器不自动维护)
```
@@ -0,0 +1,52 @@
# I. 前端 Enums 速查
:::tip 概述
src/enums/ 枚举常量速查。前后端枚举对照原则:后端 consts 是真值(biz_err/log_type 等),前端 enums 是消费侧镜像——后端改值须同步前端(破坏性变更流程)。
:::
## 枚举文件清单
| 文件 | 常量 | 用途 |
|------|------|------|
| httpEnum.ts | RequestEnum | GET/POST/PUT/DELETE 方法 |
| | ResultEnum | SUCCESS=0 / ERROR=1 / TIMEOUT 等(信封 code 对照) |
| | ContentTypeEnum | application/json、multipart 等 |
| pageEnum.ts | PageEnum | 页面路径常量(BASE_LOGIN 等) |
| cacheEnum.ts | CacheEnum | 缓存键 |
| permissionsEnum.ts | 通配权限等 | `*:*:*` 超管通配 |
| roleEnum.ts | 角色常量 | ROLE_SUPER_ADMIN 对照 |
| avatarEnum.ts | 头像枚举 | 内置头像集 |
| breakpointEnum.ts | 断点 | 响应式断点 |
存储键另见 `store/mutation-types.ts`(ACCESS_TOKEN/FIRST_ROUTE)。
## 前后端关键对照
| 前端 | 后端真值 |
|------|---------|
| ResultEnum.SUCCESS=0 | response.Ok(code 0) |
| ResultEnum.ERROR=1 | response.Fail 降级(code 1) |
| 401 处理 | consts.CodeUnauthorized(唯一保留原码) |
| permissionsEnum 通配 `*:*:*` | index.GetUser 超管返回 |
| roleEnum ROLE_SUPER_ADMIN | consts 超管角色编码 |
| log_type 映射 | consts/log_type.go(41 值,前端按值显示中文) |
## log_type 前端映射(常用段)
```text
1=新增 2=修改 3=删除 4=查询 5=导入 6=导出
13=启用 14=禁用 15=上传 16=下载 19=代码生成 22=登录 23=登出 24=重置
99=其他
(完整 41 值见后端 consts/log_type.go——数值勿改)
```
## 使用规约
```text
import { ResultEnum } from '@/enums/httpEnum';
if (code === ResultEnum.SUCCESS) {...}
✓ 跨页复用的常量进 enums/
✓ 后端枚举映射表集中管理(勿散落)
✗ 魔法数字/魔法字符串散落页面
```
@@ -0,0 +1,89 @@
# A. 常见问题汇总
:::tip 概述
全书 FAQ 汇总(按主题分组),结论均可回溯到代码位置。分章详细版见各章 FAQ(2.8 / 3.11 / 10.9 / 11.10)。
:::
## A. 接口与路由
**Q:接口 404?**
实际路由无 /v1 版本段(api/v1 只是包名)。全部 gmeta path 直接以 /api 开头。新增接口后必须重启;新模块还需 router.go 的 Bind 注册。确认注册可开 `server.dumpRouterMap` 或查 `GET /api.json`。
**Q:返回 403?**
gmeta permission 非空且当前用户 Perms 不含该节点。处理:角色管理勾选权限节点并保存(清身份缓存)。超管不受限;permission 留空 = 登录即可。
**Q:401 但 token 刚签发?**
依次排查:① Authorization 头格式(Bearer+空格);② jwt.secret 多实例/重启后是否一致;③ 令牌是否已入黑名单(登出/轮换/强退);④ access 已过期(默认 2h)应先 refresh。
**Q:偶发 429?**
固定窗口限流:普通 120/分钟/IP/路径;认证入口 10/分钟/IP。反代后全站同一 IP → 检查 api.proxyIps 是否含代理地址。
**Q:在线调试文档在哪?**
`GET /swagger`(本地 Redoc);`GET /api.json`(含全部补注册闭包路由)。
## B. 认证与权限
**Q:验证码总是失败?**
① 一次性校验(命中即删),每次先取 /api/captcha;② Redis 降级内存后多实例不共享;③ 开发用 bypassCode,生产必须留空。
**Q:改了角色权限但用户仍被拒/仍放行?**
身份缓存 user:identity:{uid} TTL 300s。API 路径主动清;手工改库不会——等 5 分钟或 DEL 缓存键。
**Q:重置密码?**
超管 PUT /api/user/resetPwd;本人 PUT /api/index/updatePassword(需旧密码)。旧 md5 数据登录被 IsBcrypt 拒绝,需重置升级。
**Q:登录报"该用户名存在于多个租户"?**
未指定 tenantCode 时全局查 username,多租户同名优先平台租户,仍歧义则携带 tenantCode 登录(logic/auth.go findUser)。
## C. 数据与租户
**Q:Update 报 1062(uk_tenant_mobile 等)?**
唯一预检范围必须等于唯一索引范围(含 tenant_id 的索引须按"被改行现租户"检查,spec 用 Group:"tenant_id")。
**Q:租户看到别家数据?**
排查绕过 dao.Ctx 直连 g.DB();排查该表是否登记 hooks.tenantTables(新增租户级表必须手工加入)。
**Q:超管新增数据落到哪个租户?**
未显式指定时 hooks.insertHook 填 tenant.defaultId(默认 1);显式传 0 建平台共享行。业务取归属用 consts.GetTenantID。
**Q:删除了但库里还有?**
删除全是软删(del_flag=1)。物理清除仅 LogClean 三张日志表;业务表恢复需 UPDATE del_flag=0。
**Q:新加表/列代码读不到?**
gf gen dao -c hack/config.yaml 刷新 → go build → 重启。service 接口手工维护。
## D. 文件与富文本
**Q:上传成功但图片 404?**
① 展示必须经 GetFileUrl(基座 readTransform 自动处理);② file.domainUrl 与可访问地址一致性;③ 静态服务根为 resource/。
**Q:富文本路径是 [IMG_URL]/...?**
持久化契约(存库格式)。读取走 Detail/List(自动 Expand);禁止直接读 content 原值给第三方。
**Q:temp 目录越来越大?**
CleanTemp 每日 03:30 清理超过 file.tempExpireDays(默认 7 天)的日期目录;确认配置非 0 且服务在运行。
## E. 定时任务与缓存
**Q:任务不执行?**
status=1?cron 有效?daemonStatus 挂载?Scheduler.mounted vs expected 差值排查。
**Q:Redis 挂了系统还能用吗?**
能——自动降级进程内存(mode=memory)。但限流计数/任务锁/黑名单/在线用户/验证码/字典缓存全部退化单机语义。生产 Redis 必须可用。
**Q:字典改了文案没生效?**
Redis 层调 /api/dict/refreshCache;进程内 {field}Text 层等 300s 自然过期。
## F. 工程
**Q:go build 版本报错?**
go.mod 声明 go 1.27.2,升级本机 Go。
**Q:生成器不建字典?**
注释格式须 `字段:1-x 2-y`;非 MySQL 内省不到注释。
**Q:gf gen service 能跑吗?**
不能——service 接口手工维护,整体运行会覆盖。
**Q:前端 pnpm install 失败?**
Node 18+;切换 pnpm 镜像源;升级 pnpm 重装。
@@ -0,0 +1,69 @@
# D. 公共函数速查
:::tip 概述
后端共享能力速查:logic/base.go 15 个导出函数 + module_spec/data_scope 包级函数 + utility 九包。纪律:base.go 只依赖 dao/utility/consts,导出上限 15。
:::
## logic/base.go(共享能力层)
| 函数 | 职责 |
|------|------|
| SaveFileFields(ctx, data, fields, saveDir) | 单文件字段批量转正(原地更新 map) |
| SaveMultiFileFields(ctx, data, fields, saveDir) | 逗号多文件字段批量转正 |
| SaveFileField(ctx, fileUrl, saveDir) | 单值便捷版 |
| SaveMultiFileField(ctx, fileUrls, saveDir) | 逗号串便捷版 |
| LocalizeContentFields(ctx, content, saveDir) | 富文本存库本地化([IMG_URL] 占位符化) |
| ExpandContentFields(ctx, content) | 富文本读取还原(**唯一还原出口**) |
| DictGetText(ctx, code, value) | 字典值 → 显示名(进程内 300s) |
| SerializeMapOnRead(ctx, data, fieldDict) | map 行补 {field}Text |
| LogTypeText(v int) | 操作日志类型 → 中文(静态表) |
| CheckUnique(ctx, m, column, value, excludeId, label) | 唯一性快速校验(TOCTOU,真值靠 uk_*) |
| WithTrans(ctx, fn) | 事务包裹(错误自动回滚) |
| ChunkQuery(ctx, build, size, fn) | 分块遍历(导出防 OOM,默认 500/页) |
| BuildTree[T](rows, idOf, pidOf, childrenOf, less) | 泛型平铺转树 |
| CleanTemp(ctx, expireDays) | 清理上传临时日期目录 |
| fillUserCount(column, list) | 按外键统计引用数(内部,level/position 复用) |
## logic 包级共享函数
| 函数 | 位置 | 职责 |
|------|------|------|
| ResolveCityNames(ctx, codes, sep) | city.go | 区划编码 → 名称串 |
| ResolveCityCodes(ctx, cityInfo) | city.go | 名称串 → 四级编码 |
| LoadIdentity / LoadTenant | auth.go | 身份/租户加载(中间件接线) |
| ApplyDataScope(deptColumn) | data_scope.go | 行级数据权限工厂 |
| RegisterJobHandler(name, h) | job_handler.go | 本地任务处理器注册(重名 panic) |
| FillTextFields(ctx, target, maps) | module_spec.go | {field}Text 结构体填充 |
## utility 基础能力包
| 包 | 主要 API | 职责 |
|----|---------|------|
| utility/cache | Init/Set/Get/Del/Exists/SetNX/UsingMemory | 统一缓存门面(Redis 优先、内存降级) |
| utility/captcha | Generate/Verify | 验证码(一次性、bypassCode) |
| utility/crypto | GenerateSalt/HashPassword/VerifyPassword/IsBcrypt | bcrypt 双重加盐 |
| utility/excel | ReadSheet/WriteSheet | excelize 封装(FileOpener 兼容两种) |
| utility/ip2region | City/Location | xdb 归属地(懒加载+读锁) |
| utility/jwt | Sign/Parse/ExpireDuration | HS256 双令牌 |
| utility/response | Ok/Fail/Write | 统一信封写出 |
| utility/ua | ParseOS/ParseBrowser | UA 解析 |
| utility/upload | SaveTemp/SaveFromTemp/SaveFromTempMulti/GetFileUrl/BaseDir | 两段式上传 |
## consts 常量包
| 文件 | 内容 |
|------|------|
| biz_err.go | 17 个业务错误码(1/400/401/403/429/500 + 1001-1015) |
| cache_key.go | Redis 键前缀 easygoadmin: |
| context_key.go | 上下文读写(consts.GetXxx(ctx)) |
| log_type.go | 41 个日志类型常量 + LogTypeMap/LogTypeNames |
## 使用优先级建议
```text
1. spec 声明能表达 → 用声明(FileFields/SerializeMaps/UniqueFields/DataScope)
2. spec 钩子能表达 → 用钩子(Before/After)
3. 需要底层能力 → base.go / utility(组合调用)
4. 跨模块业务复用 → logic 包级函数(放具体模块文件顶部)
5. 新横切能力 → 评估进 base.go(≤15 导出)或拆 base/ 子包
```
@@ -0,0 +1,69 @@
# J. 术语表
:::tip 概述
项目核心术语速查(按字母/主题排序)。首次出现的概念均可在对应章节深入。
:::
## A-D
| 术语 | 释义 |
|------|------|
| access_token | 访问令牌(JWT HS256,2h),接口认证凭证,Header Bearer 携带 |
| ApplyDataScope | 行级数据权限共享工厂(logic/data_scope.go),spec 声明即接线 |
| BatchGenerate | 生成器批量生成(逐表独立成败+统计) |
| BodyUnmarshaler | 请求体自定义解析逃生舱接口(跳过默认 Parse) |
| BuildTree | 泛型平铺转树(base.go) |
| CheckUnique | 唯一性 UX 快速预检(TOCTOU;真值靠 uk_* 索引) |
| ChunkQuery | 分块遍历查询(导出防 OOM,默认 500/页) |
| CleanTemp | 上传临时目录清理(每日 03:30,tempExpireDays 控制) |
| del_flag | 软删除标志(0-正常 1-已删除;删除接口全部软删) |
| {field}Text | 枚举字段显示名(字典驱动填充,如 statusText) |
| DomainUrl | file.domainUrl 文件访问域名(空=相对 /upload/) |
## E-J
| 术语 | 释义 |
|------|------|
| execute_policy | 任务执行策略(1 立即执行/2 执行一次/3 放弃执行) |
| Filter | ModuleSpec 复杂条件逃生舱 |
| genDomainMap | 生成器域归属映射(user→system、默认 tool) |
| GF_ | 环境变量覆盖配置前缀 |
| golden(黄金样本) | hack/golden 接口响应回归基线 |
| gmeta | gf 框架元标签;本项目扩展解析 method/path/permission/log 等 |
| gvalid | gf 参数校验(结构体 v 标签) |
| hooks(模型钩子) | internal/model/hooks:审计盖章/软删改写/租户隔离 |
| IdentityLoader | 身份加载器(中间件 wire 接线,= logic.LoadIdentity) |
| is_sync | 任务执行方式(1 同步/0 异步) |
## K-R
| 术语 | 释义 |
|------|------|
| KeywordFields | req.keyword 多列 OR 筛选(基座保留能力,模块默认不声明) |
| LikeFields | 逐列模糊筛选声明(值取 req 同名驼峰字段) |
| LogClean | 内置日志清理任务(每日 02:30,三张日志表物理清理) |
| LogSink | 操作日志落库函数(operation_log 模块 init 注册) |
| ModuleSpec | 泛型声明式 CRUD 基座([Item, LItem, DRes] 三类型参数) |
| MergeOld | 全量覆盖辅助(把 data 缺失键用库中旧值补齐) |
| OnlinUser | 在线用户(无表;Redis zset+info;双令牌强退) |
| PageData[T] | 分页泛型壳(records/total/size/current/pages) |
| publicPaths | 免登录白名单(middleware/public.go 精确匹配) |
| refresh_token | 刷新令牌(7d;仅 /api/oauth2/token;轮换即拉黑旧值) |
## S-Z
| 术语 | 释义 |
|------|------|
| SaveTemp / SaveFromTemp | 上传两段式(临时落盘/表单保存转正) |
| serializeMaps | 模块头部字典映射声明({字段: 字典编码}) |
| skipTables | 生成器整表跳过清单(role_menu/user_role) |
| spec | ModuleSpec 声明实例(如 userSpec/jobSpec) |
| tenantTables | 租户级表清单(hooks.tenantTables,9 张,新表须手工登记) |
| ToData | spec 的 req→map 转换(新增/修改共用,全量表单语义) |
| uk_* | 唯一索引命名规范(含 del_flag;并发唯一性最终保证) |
| [IMG_URL] | 富文本持久化契约(存库占位符;ExpandContentFields 唯一还原) |
| wire.go | 中间件依赖接线(三函数变量:Identity/Tenant/LogSink) |
| 超管 | ROLE_SUPER_ADMIN 角色;绕过权限;前端通配 *:*:* |
| 数据权限 | role.data_scope 四档行级过滤(仅 Page/List/导出) |
| 两段式 | 上传协议(temp 中转 → 表单保存转正到业务目录) |
| 受信代理 | api.proxyIps(精确 IP/CIDR);信任其 XFF/X-Real-IP |
@@ -0,0 +1,65 @@
# F. 中间件执行顺序
:::tip 概述
七层中间件 + Bind 包装层的完整执行顺序与短路行为速查。注册顺序即执行顺序(router.go s.Use)。机制详解见 [4.2 请求处理链路](../architecture/request-flow)。
:::
## 执行顺序总表
| # | 中间件 | 文件 | 职责 | 短路 |
|---|--------|------|------|------|
| 1 | CORS | cors.go | 跨域头/预检 | OPTIONS 204 |
| 2 | Response | response.go | 元信息入 ctx;SVG 加固;panic 兜底 | panic→500 |
| 3 | RateLimit | ratelimit.go | 固定窗口限流(IP+路径) | 429 |
| 4 | Demo | demo.go | 演示模式拦截写操作 | 1015 |
| 5 | JWT | jwt.go | 令牌校验;身份注入(缓存 300s) | 401 |
| 6 | Tenant | tenant.go | 租户状态/过期(缓存 60s) | 1005/1006 |
| 7 | OperationLog | operation_log.go | 日志采集(不短路,异步落库) | — |
| 8 | Bind 包装层 | router/bind.go | 权限校验 → 参数解析(gvalid) → 分发 → 信封 | 403/400 |
## 请求生命周期(12 步)
```text
1 CORS 预检/头设置
2 Response 元信息入 ctx;defer panic 兜底
3 RateLimit 固定窗口计数,超限 429
4 Demo 演示模式拦截写操作
5 JWT Parse+黑名单 → 注入身份(Roles/Perms/DataScope/DeptId)
6 Tenant 租户状态/过期校验(60s 缓存)
7 OperationLog 记 start,Next()
8 Bind 包装层 元数据入 ctx → HasPerm(403) → Parse(400) → 调 controller
9 controller 转发 service 接口
10 logic ModuleSpec 流水线;dao.Ctx 查询
11 response.Write 信封写出(code/body 记入 ctx)
12 OperationLog 组装 entry → 异步通道 → 批量落库
```
## 公开路径(跳过 5/6/7 的认证依赖)
```text
publicPaths(精确匹配):
/api/login · /api/register(放行但路由未注册)· /api/captcha
/api/oauth2/token · /api/health
另放行:/api.json · /swagger*
三处一致:middleware/public.go / gmeta public:"true" / router.go 手工注册
```
## 依赖接线(wire.go 三函数变量)
```text
IdentityLoader 由 logic/auth.go init 注册(LoadIdentity)
TenantLoader 由 logic/auth.go init 注册(LoadTenant)
LogSink 由 logic/operation_log.go init 注册(SaveOperationLog)
cmd/wire.go 启动期检查非 nil(缺失告警不阻断)
```
## 新增中间件检查项
```text
□ 职责单一;读配置用 cfgBool/cfgInt;IP 用 realClientIP
□ 不 import 业务包(需要业务数据走 wire.go Loader)
□ s.Use 链按序插入(顺序即语义,评审明确插入点)
□ 确认对 IsPublicPath 的放行策略
□ 更新本速查与 4.2 文档
```
@@ -0,0 +1,46 @@
# H. 前端 Store 速查
:::tip 概述
Pinia store 模块速查(store/modules/ 七个):user/asyncRoute/tabsView/designSetting/projectSetting/lockscreen/ossConfig。详见 [9.6 状态管理(Pinia)](../frontend/store)。
:::
## 模块速查
| 模块 | 职责 | 关键状态/动作 |
|------|------|-------------|
| user.ts | 登录与身份 | token、userInfo;Login/GetInfo/LogOut |
| asyncRoute.ts | 动态路由 | 菜单树、generateRoutes、isDynamicAddedRoute |
| tabsView.ts | 标签页 | visitedTabs、cachedTabs、delCachedTab |
| designSetting.ts | 设计设置 | 主题色、水印、灰度/色弱 |
| projectSetting.ts | 项目设置 | 布局模式、标签页/面包屑开关 |
| lockscreen.ts | 锁屏 | 锁定状态、解锁 |
| ossConfig.ts | 存储配置 | OSS 配置状态 |
## 辅助文件
```text
store/index.ts createPinia(含持久化插件)
store/mutation-types.ts 存储键常量(ACCESS_TOKEN / FIRST_ROUTE)
store/types.ts 类型定义
store/plugins/ 持久化等插件
```
## 关键流转
```text
登录:Login → data.access_token → storage(ACCESS_TOKEN) + store.token
首进:守卫 → GetInfo(getUser)→ userInfo.permissions
→ generateRoutes(getMenus)→ 动态路由注入
登出/401:清 storage + store → 跳登录
持久化:token/用户设置持久化;用户信息/菜单内存态(刷新重拉)
```
## 使用模式
```ts
import { useUserStore, useUserStoreWidthOut } from '@/store/modules/user';
const userStore = useUserStore(); // setup 内
const token = userStore.token;
```
@@ -0,0 +1,98 @@
# 4.3 后端分层架构
:::tip 概述
后端五层:api / controller / service / logic / dao。分层不是形式主义——每层有明确的"允许/禁止"清单,组合起来构成依赖红线体系。本节逐层说明职责与边界。
:::
## 分层与调用关系
```text
api/v1(契约层)
▲ 依赖
controller(HTTP 薄层)
▲ 依赖接口
service(接口层,手维护)
▲ 实现
logic(业务层,ModuleSpec 基座 + 钩子 + 手写逃生舱)
▲ 唯一入口
dao(数据访问层,红线条件 + 全局钩子)
▲
model/entity · model/hooks · gdb
```
侧路能力(横向复用):`utility/{cache,captcha,crypto,excel,ip2layer,jwt,response,ua,upload}` 与 `consts`(错误码/缓存键/上下文键/日志类型)。
## 各层职责与边界
### api/v1(契约层)
+ 全部 Req/Res 结构体,**单包 v1**,每模块一文件(snake_case)
+ gmeta 注解声明 method/path/summary/permission/log/logType/public
+ gvalid 标签声明校验规则;`dc` 标签供 OpenAPI 文档
+ **禁止**:业务逻辑、数据库引用
### controller(HTTP 薄层)
+ 一行透传 `return service.Xxx().Method(ctx, req)`
+ **禁止**:业务 if/for、import dao、手写响应信封、手写参数解析
### service(接口层,手维护)
+ 每模块一文件:接口 `IXxx` + 实例变量 + 取用函数 `Xxx()` + 注册函数 `RegisterXxx()`
+ 未注册时取用 panic(启动即暴露接线错误)
+ **禁止**:整体运行 `gf gen service`(会覆盖手工重组结构)
### logic(业务层)
+ 每模块一文件:`sXxx` 实现 + `xxxSpec` 声明 + `init()` 注册 + 标准委托 + 扩展手写
+ 共享能力经 `base.go`(15 个导出上限)与 `module_spec.go` 基座
+ 跨模块复用走同包包级函数(ResolveCityNames / LoadIdentity / ApplyDataScope / DictGetText…)
+ **禁止**:反向 import task;绕过 dao;在钩子里做 HTTP/文件 IO
### dao(数据访问层)
+ 自定义模板产出:构造时挂 `hooks.Handler()`;`Ctx` 覆写注入红线条件
+ **禁止**:绕过直连 `g.DB()`;手写 del_flag/tenant_id 条件;手改 dao/internal/
## 上下文键(consts/context_key.go)
JWT 中间件写入身份,全链路 `consts.GetXxx(ctx)` 读取:
| 写入方 | 键 |
|--------|-----|
| JWT 中间件 | UserID / TenantID / Username / Realname / TokenJTI / TokenExpire / Roles / Perms / DataScope / DeptID |
| Response 中间件 | ClientIP / UserAgent / ReqMethod / ReqURL |
::: warning 两套租户读法(易错点)
`CtxTenantID(ctx)` —— 原始值(0=平台),仅供租户设施(hooks/中间件)使用
`GetTenantID(ctx)` —— 业务归属值(租户返回自身,超管返回 tenant.defaultId),**新增/导入/唯一校验统一用它**
:::
数据范围读法:`GetDataScope(ctx)` 档位(1-全部;未注入回退全部);`GetDeptID(ctx)` 部门 ID(0=未分配)。
## 响应与错误码
统一信封(utility/response):
```text
成功:{"code":0, "ok":true, "msg":"操作成功", "data":{...}}
失败:{"code":1, "ok":false, "msg":"...", "data":{}}
未认证:HTTP 401 + {"code":401,...}
```
Fail 映射规则:gerror 码 == 401 原样保留;其余一律降级 code=1(前端 ResultEnum.ERROR 才弹 msg)。consts 中的细分错误码(1001 用户不存在…)主要用于日志与语义区分。
分页契约:`PageData[T]` = `{records, total, size, current, pages}`,records 为空输出 `[]` 而非 null;pageNo 默认 1,pageSize 默认 10 上限 500。
## 横切约定速查
```text
* 主键:int 数据库自增;新增后 result.LastInsertId() 回读
* 软删:del_flag 0 正常 / 1 删除;删除接口全部软删
* 表前缀:goframe_;文件命名 snake_case 对齐 dao(前端文件仍驼峰)
* 入参/出参 json 驼峰;唯一例外:登录/令牌接口返回
access_token/refresh_token/expires_in/token_type(OAuth2 惯例,勿改)
* 缓存键统一前缀 easygoadmin:
* 权限码 sys:{module小驼峰}:{action};超管 ROLE_SUPER_ADMIN;前端通配 *:*:*
* 审计列:id/create_user/create_time/update_user/update_time/del_flag(租户表另有 tenant_id)
```
@@ -0,0 +1,111 @@
# 4.6 核心基座体系
:::tip 概述
本项目没有传统"基类继承"——用**组合**替代继承:ModuleSpec 泛型基座(module_spec.go)承担 CRUD 流水线,base.go 共享能力层提供横切函数。本节解释两者的分工与设计纪律。
:::
## 组合替代继承的架构
```text
┌────────────────────────────────────────────────────┐
│ 模块 logic 文件(example.go) │
│ var exampleSpec = ModuleSpec[...]{ 声明差异 } │──▶ 委托
│ func 方法() { exampleSpec.Page(...) } │
│ func 扩展() { 手写(合法逃生舱)} │
└───────────────────────┬────────────────────────────┘
│ 复用
┌───────────────────────▼────────────────────────────┐
│ module_spec.go:ModuleSpec 泛型基座 │
│ applyFilters / readTransform / saveTransform │
│ checkUniqueRules / FillTextFields / buildTree │
└───────────────────────┬────────────────────────────┘
│ 调用
┌───────────────────────▼────────────────────────────┐
│ base.go 共享能力层(导出上限 15 个) │
│ 文件转正 / 富文本 / 字典文本 / 唯一校验 / │
│ 事务 / 分块查询 / 泛型树 / 临时目录清理 │
└────────────────────────────────────────────────────┘
```
## base.go 能力清单(internal/logic/base.go)
| 能力 | 函数 | 一句话职责 |
|------|------|-----------|
| 文件字段 | SaveFileFields / SaveMultiFileFields | 批量转正(原地更新 map) |
| | SaveFileField / SaveMultiFileField | 单值便捷版 |
| 富文本 | LocalizeContentFields | 存库本地化([IMG_URL] 占位符化) |
| | ExpandContentFields | 读取还原(**唯一还原出口**) |
| 枚举文本 | DictGetText | 字典值 → 显示名(进程内缓存 300s) |
| | SerializeMapOnRead | map 行补 {field}Text |
| | LogTypeText | 操作日志类型 → 中文(静态表) |
| 唯一校验 | CheckUnique | UX 快速报错(TOCTOU,真值靠 uk_* 索引) |
| 事务 | WithTrans | 事务包裹(错误自动回滚) |
| 分块 | ChunkQuery | 分块遍历(导出防 OOM,默认 500/页) |
| 树 | BuildTree[T] | 泛型平铺转树 |
| 清理 | CleanTemp | 清理上传临时日期目录 |
架构纪律(文件头约束,真实注释):
```text
1) 按能力分区:文件字段 / 富文本 / 枚举文本 / 唯一校验 / 事务 / 分块查询 / 树
2) 依赖方向单向:只允许依赖 dao / utility / consts,禁止引用模块文件
3) 导出函数上限 15 个,超出按能力拆 internal/logic/base/ 子包
4) CheckUnique 仅为 UX 快速报错(TOCTOU),并发唯一性靠 uk_* 索引
5) [IMG_URL] 占位符是持久化契约,还原只能走 ExpandContentFields
```
## ModuleSpec 基座流水线(module_spec.go)
```text
Page applyFilters → Count → applyOrder.Page → structRows → readTransform
→ AfterPageList
List applyFilters → 排序 → req.limit 上限 → structRows → readTransform
→ IsTree 时 buildTree(叶子 Children 预置 [] 而非 null)
Detail Model.Where(id).One → Struct → readTransform → AfterDetail
Add ToData → BeforeAdd → saveTransform(文件/富文本转正)→ checkUniqueRules
→ Insert → LastInsertId → AfterAdd → 返回 id
Update ToData → BeforeUpdate → saveTransform → checkUniqueRules(排除自身)
→ Update(仅更新 data 中出现的键)→ AfterUpdate
Delete BeforeDelete → Delete(钩子改写软删)→ AfterDelete
BatchDelete BeforeBatchDelete → 逐条 BeforeDelete+软删 → AfterBatchDelete
```
readTransform 顺序:FileFields 补 URL → MultiFileFields 补 URL → ContentFields 展开 [IMG_URL] → FillTextFields({field}Text)。
## 设计纪律(为什么基座不会腐化)
::: warning 基座三铁律
1. **基座禁止"按模块分支"**——差异只进声明字段与钩子;写不进钩子 → 手写(合法逃生舱)。这条纪律保证基座永远不出现 `if module == "user"` 式代码。
2. **钩子用具体函数类型**,req any 经 mustAs[T] 断言(失败=编程错误,panic 暴露而非带病运行)。
3. **钩子内禁止 append、禁止 HTTP/文件 IO**——文件迁移统一在 saveTransform 完成。
:::
applyFilters 固定顺序:`LikeFields → EqFields → KeywordFields → Filter → DataScope`;租户/软删隔离由 dao.Ctx **先于本链**注入(任何模块不可绕过)。
## ModuleSpec 声明字段全景
```text
Model func(ctx) *gdb.Model 数据源(通常 dao.Xxx.Ctx)
LikeFields []string 逐列 LIKE(值取 req 同名驼峰字段,空跳过)
EqFields []string 等值(零值跳过)
KeywordFields []string req.keyword 多列 OR(基座保留能力)
Filter func(ctx, m, req) *gdb.Model 复杂条件逃生舱
OrderBy / ListOrderBy / OrderableFields 排序(白名单控制前端排序)
IsTree / ParentOf / ChildSetter 树形
SerializeMaps map[字段]字典编码 {field}Text 填充
FileFields / MultiFileFields / ContentFields / FileSaveDir 文件与富文本
UniqueFields []UniqueRule{Field, Group} 唯一性预检(Group 分组唯一)
DataScope DataScopeFunc 行级数据权限(ApplyDataScope 接线)
ToData req → map[string]any 新增/修改共用转换
钩子 Before/After × Add/Update/Delete/Batch + AfterPageList/AfterDetail
```
## 与"基类方案"的对比
| 维度 | 本项目(泛型基座+组合) | 传统基类继承 |
|------|----------------------|-------------|
| 差异表达 | 声明字段 + 钩子 | 重写虚方法 |
| 类型安全 | 泛型三类型参数编译期约束 | 常退化 any/map |
| 复用粒度 | 函数级(base.go 自由组合) | 类级(受继承链束缚) |
| 基座污染风险 | 三铁律禁止按模块分支 | 基类易堆积 if-else |
| 逃生舱 | 手写方法(完全自由) | 多重继承/桥接方法 |
@@ -0,0 +1,94 @@
# 4.8 缓存架构设计
:::tip 概述
缓存实现为 utility/cache 统一门面:Redis 优先,连接失败或运行期出错自动降级进程内存 gcache(UsingMemory() 可查)。三层缓存各司其职,Redis 键统一前缀 easygoadmin:。
:::
## 缓存分层图
```text
请求 ──▶ ① 进程内缓存(sync.Map,无 Redis 交互,重启即清)
dictTextCache(300s) · reqFieldCache/textPlanCache(反射计划,永不过期)
maskReCache(正则,永不过期)
──▶ ② Redis(跨实例共享,TTL 自动回收)
黑名单/在线/验证码/字典/租户摘要/身份/限流/任务锁
──▶ ③ 数据库(唯一真相,dao.Ctx 全量过滤条件)
设计原则:② ③ 之间的一切缓存都必须有明确的失效点或短 TTL 兜底;
① 仅缓存"计算计划/只读文本",不允许缓存业务状态。
```
## Redis 键全集
| 键模式 | TTL | 写入方 | 读取方 |
|--------|-----|--------|--------|
| `easygoadmin:token:blacklist:{jti}` | 剩余有效期+60s | Logout/Refresh 轮换/RemoveToken/ForceLogout | JWT 中间件 Exists 判定 |
| `easygoadmin:online:jti:set`(zset) | 成员随 info(score=过期 ts) | MarkOnline/MarkOffline | OnlineUser.Page(ZRangeByScore≥now) |
| `easygoadmin:online:info:{jti}` | access 剩余+60s | MarkOnline/MarkOffline | OnlineUser.Page/Detail/ForceLogout |
| `easygoadmin:captcha:{uuid}` | 300s | Captcha 生成 | Login/Verify(一次性) |
| `easygoadmin:dict:item:{code}` | 3600s | dict.setCache/RefreshCache | dict_item 快捷接口/AliasList |
| `easygoadmin:tenant:info:{id}` | 60s | Tenant 中间件/Login 路径 | Tenant 中间件/Login |
| `easygoadmin:user:identity:{uid}` | 300s | JWT 中间件(回源 Loader) | JWT 中间件(Roles/Perms/DataScope/DeptId JSON) |
| `easygoadmin:user:perms:{uid}` / `user:roles:{uid}` | 约定键(保留扩展) | — | 主路径用 identity 合并键,变更时一并清理 |
| `ratelimit:{ip}:{path}:{窗口起点}` | window+5s | RateLimit incr | RateLimit |
| `job:lock:{jobId}` | 300s 兜底 | jobTryLock(NX) | jobTryLock(执行完 defer Del) |
## 主动失效点清单
**身份缓存 `user:identity:{uid}` 删除时机**:
```text
* role.SavePermission 保存角色权限 → clearRoleIdentityCache(roleId)
→ 查该角色全部 user → 逐个 Del
* role.Update 修改角色(含 data_scope 变更)→ AfterUpdate → 同上
* user.AssignRoles 分配/调整角色 → clearIdentityCache(userId)
(user.Update / user.Add 均经 AssignRoles——改部门 dept_id 亦在此清缓存)
* 版本兼容:Identity 新增字段反序列化缺省为 0,读侧 GetDataScope 回退"全部"
——升级无需强清存量缓存
```
**其他**:租户摘要在 tenant 状态/过期变更时由 AfterUpdate clearCache 钩子清理;字典 Redis 缓存由 RefreshCache 主动重建(dict_item 变更不主动清,最长 1 小时延迟);黑名单靠 TTL 自然过期(过期即令牌本身已失效);验证码命中即删。
## 进程内缓存明细
| 缓存 | 位置 | 内容 | TTL |
|------|------|------|-----|
| dictTextCache | base.go | {code}:{value} → 显示名 | 300s |
| reqFieldCache / textPlanCache / childrenIdxCache | module_spec.go | 反射索引与填充计划 | 永不过期(类型不变即安全) |
| maskReCache | middleware/util.go | 脱敏正则 | 永不过期 |
## 内存降级的影响面
Redis 不可用时 `UsingMemory()=true`,以下能力**退化单机语义**:
```text
限流计数 各实例各自计数(阈值实际翻倍)
任务锁 job:lock 退化单机,多实例可能并发执行任务
在线用户 只看本实例登录的会话
JWT 黑名单 本实例签发的登出/强退不拦截其他实例
验证码 单实例内有效
字典缓存 各实例独立回源
```
运维可通过 `GET /api/cache/info` 的 `mode` 字段确认当前模式(redis/memory)。
## TTL 设计原则(新增缓存遵循)
```text
1) 状态判定类(黑名单/租户状态):TTL ≤ 业务有效期,且必须有主动失效点;
宁可短 TTL 多回源,不可长 TTL 陈旧放行
2) 展示文本类(字典/归属地):TTL 300s~3600s + 刷新接口,接受分钟级延迟
3) 高频读聚合类(identity):合并为单键 JSON(一次回源装多个集合),
TTL 300s,变更路径主动 Del
4) 计数/锁类:键内嵌窗口起点或场景唯一 id,TTL 只做回收不参与语义
5) 任何经 cache.Set 的值必须可 JSON 序列化;读侧 Unmarshal 失败视为未命中回源
```
## 排障速查
```text
键不存在/值陈旧 先查是否内存降级(redis-cli KEYS 'easygoadmin:*' 对比)
权限不生效 DEL easygoadmin:user:identity:{uid}
租户被禁仍可访问 等 60s 或 DEL easygoadmin:tenant:info:{id}
多实例行为不一致 确认 Redis 可用(/api/cache/info mode 应为 redis)
```
@@ -0,0 +1,116 @@
# 4.7 横切机制
:::tip 概述
审计字段盖章、软删除改写、租户隔离三类横切逻辑不在任何业务代码中出现——它们统一在 gdb 模型钩子层(internal/model/hooks)执行。配合中间件链与 Bind 包装层,构成本项目的横切机制全景。
:::
## 横切机制全景
```text
请求横切(中间件链) 数据横切(模型钩子) 契约横切(Bind 包装层)
CORS Insert 盖章 权限校验(gmeta permission)
Response(panic/元信息) Update 盖章 参数解析(gvalid)
RateLimit Delete → 软删 信封写出
Demo del_flag=0 注入 OpenAPI 补注册
JWT(身份) tenant_id 注入
Tenant(状态)
OperationLog(采集)
```
## 全局钩子(internal/model/hooks/hooks.go)
钩子经 `dao.{X}.Ctx(ctx)` 构造模型时挂载(自定义模板 dao_index.tpl):
| 钩子 | 触发 | 行为 |
|------|------|------|
| Insert | 任意 Insert | 主键交数据库自增(不填 id);补 del_flag=0、create_time、create_user(ctx 用户名,回退真实姓名);租户级表自动携带 tenant_id |
| Update | 任意 Update | 自动盖章 update_time / update_user(仅 map 数据生效) |
| Delete | 任意 Delete | 物理删除一律改写为 `UPDATE del_flag=1` |
### 租户盖章规则(Insert)
```text
租户用户插入未显式指定 tenant_id → 填自身(防逃逸)
超管未显式指定 → 填 tenant.defaultId(默认 1)
显式指定具体值 → 从显式值
显式 0 → 建平台共享行
```
### 查询条件注入(hooks.ApplyCtx)
```text
所有表: WHERE del_flag = 0
租户级表且非平台上下文:WHERE tenant_id IN (0, 当前租户)
超管(ctx tenant_id=0):不过滤,全量可见
```
## 租户级表清单(tenantTables,9 张)
```text
goframe_user / role / dept / level / position / article / category / notice / link
```
**新增租户级表必须同步修改 hooks.go 的 tenantTables**(代码生成器不自动维护此清单)。漏加的后果是租户数据互相可见。
## 两层行级过滤的叠加关系
```text
第一层(硬规则,不可配置):租户隔离
dao.Ctx / hooks.ApplyCtx 注入 tenant_id IN(0,自身)
先于一切业务过滤,任何模块不可绕过
第二层(可配置):角色数据范围(data_scope 四档)
ModuleSpec.DataScope 挂点,在 applyFilters 链末环叠加:
Like → Eq → Keyword → Filter → DataScope
共享工厂 ApplyDataScope(deptColumn),模块一行声明即生效
```
详见 [5.8 多租户隔离](../core/tenant) 与 [5.9 数据权限](../core/data-scope)。
## 请求横切:中间件与包装层分工
```text
中间件链(跨路由共性) Bind 包装层(路由级个性)
认证状态(401) 权限码校验(403,读 gmeta)
租户状态(1005/1006) 参数解析与校验(gvalid)
限流(429)/演示(1015) 信封写出
日志采集(不短路) OpenAPI 元数据
```
权限校验不设独立中间件——因为它需要每路由不同的权限码,放在读路由元数据的包装层最自然(与日志注解同源,"注解即事实")。
## 日志横切(OperationLog)
```text
gmeta log 标签 → 路由元数据 → 中间件组装 entry(脱敏 param/result)
→ 有界通道 2048(满则丢弃告警,不阻塞业务)
→ 单 worker 攒批(100 条或 1s flush)
→ LogSink(operation_log 模块 init 注册)→ INSERT goframe_operation_log
```
脱敏规则:param 中 password/oldPassword/newPassword/salt/access_token/refresh_token → `***`;result 截断 8000 字节(UTF-8 边界回退)。
## 配置横切:环境变量覆盖(utility/config)
```text
启动入口 cmd.Run 最先执行 config.RegisterEnvOverride()
→ 包装默认文件适配器为 env-first 适配器(gcfg.Adapter 官方扩展点)
→ 全部 Get/MustGet 读取点零改动获得覆盖能力(含 gdb/gredis 经整节点读连接配置)
```
```text
优先级 GF_ 环境变量 > 配置文件(12-Factor;未设环境变量时行为不变)
键名 配置路径大写下划线 + GF_ 前缀(database.default.link → GF_DATABASE_DEFAULT_LINK)
空值 环境变量空串视为未设置,不清空文件有效值
范围 仅覆盖配置文件中已存在的键(新增键先在 config.yaml 声明)
```
典型场景:容器部署注入 `GF_DATABASE_DEFAULT_LINK` / `GF_FILE_UPLOADDIR` / `GF_JWT_SECRET`,镜像内 config.yaml 保持默认不动(同一镜像可部署多环境)。键名规则与 Docker 注入方式见 [2.5 配置文件说明](../quickstart/env-vars)。
## 横切开发纪律
+ 新增横切逻辑优先问:属于请求横切(中间件)/ 数据横切(钩子)/ 契约横切(Bind)/ 配置横切(utility/config)哪一层?不要散落在业务代码。
+ 修改钩子必须全表回归——它影响 25 张表的所有写入。
+ 钩子内禁止业务分支判断(如"某模块跳过盖章")——确有需要走逃生舱(显式值优先于自动盖章)。
+ 缓存类横切(字典/身份/租户摘要)必须有明确失效点或短 TTL 兜底(见 [4.8 缓存架构设计](./cache))。
+ 业务代码读配置一律用 `g.Cfg().Get/MustGet`,不要用 `GetWithEnv`(文件优先,环境变量永不生效)。
@@ -0,0 +1,131 @@
# 4.9 数据库设计
:::tip 概述
25 张业务表、统一审计列、13 个唯一索引、9 张租户级表、注释枚举驱动字典——本节给出数据模型全景与 ER 关系。五方言定稿脚本位于 document/{mysql,postgresql,sqlserver,oracle,sqlite}/,MySQL 版为规范基准。
:::
## ER 关系图(逻辑外键,无物理外键约束)
```text
goframe_tenant ──< goframe_user >──┬──< goframe_user_role >── goframe_role
│ │
│ goframe_role_menu
│ │
├──< goframe_dept ▼
├──< goframe_level goframe_menu
├──< goframe_position ▲
│ (user.level_id/position_id/dept_id 引用)
│
goframe_config ──< goframe_config_item
goframe_dict ──< goframe_dict_item
goframe_category(树 parent_id) ──< goframe_article(tenant_id, category_id)
article: cover 单图 / images 多图 / content 富文本
goframe_notice / goframe_link(tenant_id)
goframe_city(树 pid + parent_code,全国行政区划,无租户)
goframe_job ──< goframe_job_log(job_id, job_name 冗余)
goframe_operation_log / goframe_login_log(独立日志表,无租户)
goframe_file_template(全局文件模板,code 唯一)
goframe_param(全局系统参数,code 唯一)
goframe_example(案例演示表,导入导出/状态演示用)
">" 侧为多端;如 user >-- user_role --< role 为多对多。
```
## 全表统一规范
```text
表前缀 goframe_;存储引擎 InnoDB;字符集 utf8mb4。
每张表统一审计列(25 表齐备):
id int NOT NULL AUTO_INCREMENT 主键(数据库自增,禁用雪花)
create_user varchar(50) 创建人(ctx 用户名)
create_time datetime DEFAULT (now()) 创建时间
update_user varchar(50) 更新人
update_time datetime DEFAULT (now()) 更新时间
del_flag int DEFAULT 0 0-正常 1-已删除(软删)
租户级表另有 tenant_id int DEFAULT 0(0=平台级),共 9 张。
删除一律软删(deleteHook 改写);查询一律经 dao.Ctx 过滤 del_flag=0。
```
## 25 张表清单
| 表名 | 中文注释 | 域 | 租户级 | 特征列/说明 |
|------|---------|-----|--------|------------|
| goframe_article | 文章表 | content | 是 | category_id/cover/images(多图)/content(富文本)/status/click/sort |
| goframe_category | 文章分类表 | content | 是 | 树形 parent_id、category_name/code/sort |
| goframe_city | 城市行政区划表 | data | 否 | 树形 pid+parent_code、city_code/area_code/level、lng/lat/pinyin(大数据量表) |
| goframe_config | 系统配置表 | data | 否 | name/code/sort |
| goframe_config_item | 配置项表 | data | 否 | config_id/value/options/type/status/sort |
| goframe_dept | 部门表 | system | 是 | 树形、删除有子级/用户守卫 |
| goframe_dict | 数据字典表 | data | 否 | name/code;缓存 dict:item:{code} |
| goframe_dict_item | 字典项表 | data | 否 | dict_id/name/value/sort |
| goframe_example | 案例演示表 | tool | 否 | 七件套+导入导出+状态演示 |
| goframe_file_template | 文件模板表 | tool | 否 | code 唯一;getTemplateByCode |
| goframe_job | 定时任务表 | monitor | 否 | job_name/cron_expression/execute_policy/is_sync/url/params/status(0未发布/1运行/2暂停) |
| goframe_job_log | 定时任务日志表 | monitor | 否 | job_id/status/耗时/exception_info |
| goframe_level | 职级表 | system | 是 | 导入导出启用;user_count 展示 |
| goframe_link | 友链表 | content | 是 | 无 status 列 |
| goframe_login_log | 登录日志表 | monitor | 否 | log_type(1登录/2退出)/ip/location/os/browser/status |
| goframe_menu | 菜单表 | system | 否 | 树形;type(0菜单/1权限节点)/permission/status/hide/sort |
| goframe_notice | 通知公告表 | data | 是 | status |
| goframe_operation_log | 操作日志表 | monitor | 否 | title/type(log_type枚举)/param/result/ip/location/consume_time/status/error |
| goframe_param | 系统参数表 | data | 否 | code 唯一 |
| goframe_position | 岗位表 | system | 是 | |
| goframe_role | 角色表 | system | 是 | code/data_scope(四档行级数据权限) |
| goframe_role_menu | 角色菜单关联表 | system | 否 | role_id+menu_id(无模块文件,归 role logic) |
| goframe_tenant | 租户表 | system | 否 | code(T+ID)/status/expire_time/max_user_num |
| goframe_user | 用户表 | system | 是 | username/password(bcrypt)/salt/mobile/email/avatar/dept_id/level_id/position_id/status/code |
| goframe_user_role | 用户角色关联表 | system | 否 | user_id+role_id(无模块文件,归 user logic) |
## 唯一索引清单(13 个)
| 表 | 索引名 | 列 | 语义 |
|----|--------|-----|------|
| goframe_category | uk_tenant_name | (tenant_id, category_name, del_flag) | 租户内分类名唯一 |
| goframe_config | uk_code | (code) | 全局配置编码唯一 |
| goframe_dept | uk_tenant_name | (tenant_id, name, del_flag) | 租户内部门名唯一 |
| goframe_dict | uk_code | (code) | 全局字典编码唯一 |
| goframe_file_template | uk_code | (code) | 全局模板编码唯一 |
| goframe_level | uk_tenant_name | (tenant_id, name, del_flag) | 租户内职级名唯一 |
| goframe_param | uk_code | (code) | 全局参数编码唯一 |
| goframe_position | uk_tenant_name | (tenant_id, name, del_flag) | 租户内岗位名唯一 |
| goframe_role | uk_tenant_code | (tenant_id, code, del_flag) | 租户内角色编码唯一 |
| goframe_tenant | uk_code | (code) | 租户编码全局唯一 |
| goframe_user | uk_tenant_username | (tenant_id, username, del_flag) | 租户内用户名唯一 |
| goframe_user | uk_tenant_mobile | (tenant_id, mobile, del_flag) | 租户内手机号唯一 |
| goframe_user | uk_tenant_code | (tenant_id, code, del_flag) | 租户内用户编码唯一 |
要点:索引含 del_flag(软删行不占坑,与 dao.Ctx 恒过滤语义一致);含 tenant_id 的索引,模块唯一预检范围必须等于索引范围。
## 枚举列约定(列注释驱动字典与生成器)
```text
status(账号/租户/字典项等) 1-正常 2-禁用
article.status 0-正常 1-下架
city.level 0-省份 1-城市 2-县区 3-街道 4-社区
menu.type 0-菜单 1-权限节点
job.status 0-未发布 1-运行中 2-暂停
job.execute_policy 1-立即执行 2-执行一次 3-放弃执行
job.is_sync 1-同步 0-异步
role.data_scope 1-全部 2-本部门及以下 3-本部门 4-仅本人
del_flag 0-正常 1-已删除
login_log.log_type 1-登录 2-退出
operation_log.type 见 consts/log_type.go(41 个值,静态映射不走字典)
```
## 大表与性能注意
+ **goframe_city** 全国行政区划大表(AUTO_INCREMENT 已至 78 万+):查询走 parent_code/pid 索引路径(级联懒加载 getCityList),避免全表 List;导入/迁移注意批大小。
+ **日志三表**(operation_log/login_log/job_log)只增不改,依赖 LogClean 任务按 `job.logRetentionDays`(默认 90 天)分批物理清理。
+ **导出接口**统一 ChunkQuery 分批(500/页)防 OOM。
## 建表/变更工作流
```text
1) 以 document/mysql/ 定稿为基准补 ALTER(含列注释枚举、uk_* 唯一索引、uk_/idx_ 命名规范)
2) gf gen dao -c hack/config.yaml 刷新 dao/entity/do(service 手维护)
3) 同步 document/ 其余四方言脚本(人工校对)
4) 租户级表 → 修改 hooks.tenantTables
5) go build ./... 重启;必要时 go run ./cmd/migrator 同步到其他环境库
```
@@ -0,0 +1,83 @@
# 4.5 目录结构设计
:::tip 概述
目录结构是架构决策的物理体现。本节解释"为什么这样放"——平铺单包、snake_case 命名、internal 边界、utility 无业务依赖等约定背后的原因。完整目录树见 [1.3 项目目录结构](../directory)。
:::
## 后端顶层布局与设计意图
```text
main.go 入口极薄:注册五库驱动 + Redis 适配器 → cmd.Run
internal/ 私有包边界(Go internal 语义:外部不可 import)
cmd/ 启动编排独立成包——把 main 的"做事"与"声明"分离
api/v1/ 契约层放 internal 外?——不,本项目放顶层 api/,
便于生成器与 gf 工具链定位,也表达"对外契约"语义
controller/ logic/ service/ dao/ 四层各自单包平铺
middleware/ 中间件 + wire.go 函数变量接线(不 import 业务包)
migrate/ 迁移引擎独立——gdb 裸连,必须与 dao 体系隔离
model/hooks/ 全局钩子独立于 entity/do(生成物与手写物分离)
router/ router.go(注册)+ bind.go(反射)+ meta/(元数据表)
task/ 定时任务实现——单向依赖 logic,cmd 空白导入触发
templates/ 生成器模板独立于 internal(模板演进与代码解耦)
utility/ 基础能力包——无业务依赖,可供任何层使用
manifest/ 配置与数据(ip2region.xdb)
resource/ 静态资源(SetServerRoot 提供 /upload/**、swagger 页面)
hack/ 工具链配置(gf gen dao 模板、golden 回归基线)
```
## 平铺 vs 分目录的取舍
| 维度 | 单包平铺(选择) | 按域分目录(弃用) |
|------|----------------|------------------|
| 跨模块复用 | 同包直接调用包级函数,零 import、无循环依赖风险 | 需 import,深组合易循环 |
| 新增模块 | 4 个文件各就各位,无目录推断 | 多级目录间跳转 |
| 生成器 | 目标路径唯一确定 | 需推断层级 |
| init 注册 | 同包全量加载,无需 blank import | 每包需 blank import |
| 代价 | 单包文件多(30+) | 包粒度细 |
Go 编译器对大包无压力,而"跨模块直接调用"的收益在中后台系统里极高(city 编解码、身份加载、字典文本被大量模块复用)。
## 文件命名规范
```text
后端(对齐 dao 产物):表名去 goframe_ 前缀的 snake_case
api/v1/config_item.go · internal/controller/config_item.go
internal/logic/config_item.go · internal/service/config_item.go
前端:驼峰
src/api/data/config.ts · src/views/data/configItem/
原因:后端文件名与 dao/entity/do 生成物一致,生成器与人工开发心智统一;
前端沿用 JS 社区驼峰惯例,两边各自从俗。
```
## internal 的边界价值
`internal/` 是 Go 编译器强制的私有边界——仓库外代码无法 import,防止外部项目把实现细节当公共 API 依赖。分层红线(controller 不 import dao 等)靠 code review + 生成器模板保证,internal 防的是"仓库外扩散"。
## utility 的纪律
```text
utility/* 只依赖标准库与三方库,禁止 import internal(防反向耦合)
cache 统一缓存门面(Redis 优先、内存降级)
captcha 验证码生成/校验
crypto bcrypt + salt
excel excelize 封装(ReadSheet/WriteSheet)
ip2region xdb 归属地(懒加载 + 读锁)
jwt HS256 双令牌 Sign/Parse
response 统一信封写出
ua OS/Browser 解析
upload 两段式上传(SaveTemp/SaveFromTemp/GetFileUrl)
```
## 前端目录设计
```text
views 按"域"分目录(system/data/content/monitor/tool…)与后端模块域对应;
api 同样按域分目录;
components/store/utils/hooks 不按域——全局共享层;
settings/styles/enums 全局配置面。
```
views 域归属与生成器 genDomainMap 一致(user/role→system,config→data,article→content,operation_log→monitor,example→tool,默认 tool),生成器据此决定前端产出路径。
@@ -0,0 +1,117 @@
# 4.11 扩展性设计
:::tip 概述
扩展性来自两方面:一是**标准变更的操作清单**(按变更类型给出 checklist,不漏步骤、不变异架构);二是**逃生舱挂点**(基座装不下的需求有明确的合法出口)。本节按变更类型组织。
:::
## 变更类型 Checklist
### 1. 给现有模块新增一个接口
```text
□ api/v1/{模块}.go:新增 {Mod}{Action}Req(内嵌 gmeta.Meta,path 带 /api 前缀、
permission、log、logType)+ {Mod}{Action}Res
□ controller:同名方法,签名 func (c *CXxx) Action(ctx, req) (res, err)
□ service/{模块}.go:接口追加方法声明(手维护)
□ logic:实现方法(基座可表达走 spec 钩子,复杂走手写)
□ 权限节点同步:若 permission 为新码,需在菜单管理为角色勾选
(或插入 goframe_menu type=1 节点)
□ go build ./... && gofmt;重启;/api.json 核对新条目
```
### 2. 给现有模块加字段
```text
□ MySQL ALTER(列注释按枚举规范写"字段:1-x 2-y";需要唯一性则补 uk_*)
□ gf gen dao -c hack/config.yaml(刷新 entity/do/dao internal;service 不动)
□ api/v1:Req/Res/Item 结构体补字段(json 驼峰 + dc 注释)
□ logic:ToData 补键;枚举补 SerializeMaps(字典编码 {模块}_{字段});
文件字段补 FileFields+FileSaveDir;富文本补 ContentFields
□ 前端:columns.ts / querySchemas.ts / edit.vue 同步
□ document/ 五方言脚本同步;必要时附录表清单更新
□ go build → 重启 → page/detail 黄金样本更新
```
### 3. 新增模块
```text
□ 设计表(统一审计列 + del_flag + uk_* + 注释枚举)→ DDL 落库
□ 租户级?→ hooks.tenantTables 登记
□ go run ./cmd/generator -tables "goframe_x|中文名"(或手动四件套)
□ logic 调整:spec 声明(筛选/树形/字典/文件/唯一 Group)、扩展接口
□ service 接口核对;router.go 注册行确认
□ go build ./... → 重启 → 角色勾选新权限节点
```
### 4. 新增中间件
```text
□ internal/middleware/{name}.go:func(r *ghttp.Request),职责单一;
读配置用 cfgBool/cfgInt;客户端 IP 用 realClientIP;响应用 utility/response
□ 不得 import 业务包——需要业务数据时在 wire.go 增加 Loader 函数变量,
由 logic init 注册(参照 IdentityLoader 模式)
□ router.Register 的 s.Use 链按序插入(顺序即语义,评审明确插入点)
□ public 路径行为:确认对 IsPublicPath 的放行策略
```
### 5. 新增定时任务
```text
□ internal/task/{task_name}.go + init RegisterJobHandler("TaskName", h)
□ handler 签名 (ctx, params) (string, error);长任务分批+上限;禁 HTTP 于事务内
□ 任务表配置:url="TaskName"、params JSON、cron(6 段或 # 5 段)、
execute_policy/is_sync;发布(status=1)
□ 验证:daemonStatus 挂载 + runOnce + job/log 成功记录
□ go build 重启(init 注册发生在进程加载期)
```
### 6. 自定义 / 扩展 dao
```text
□ dao/{m}.go 尾部"Add your custom methods"区追加方法(统计/联查等)
□ 修改模板本身:改 hack/tpl/dao_index.tpl 后 gf gen dao 重新生成
(overwriteDao:false 保留手工扩展;新旧模板产物差异需人工合并)
□ 红线:自定义方法也必须以 Ctx(ctx) 为查询入口;禁止在 dao 内做业务判断
```
### 7. 配置变更
```text
□ manifest/config/config.yaml 修改
□ 多环境差异用 GF_ 环境变量覆盖,不改文件
□ 新增配置键:注释说明 + 代码 cfgBool/cfgInt 带默认值读取(缺失不 panic)
□ 影响限流/日志/上传/调度的键变更后跑对应回归
```
## 逃生舱与扩展挂点
| 挂点 | 场景 | 说明 |
|------|------|------|
| BodyUnmarshaler | 请求体形态不规则(平铺 map 等) | Req 实现 `UnmarshalBody(r)` 跳过默认 Parse(自行做 gvalid) |
| ModuleSpec.Filter | 复杂查询条件 | 不与 Like/Eq 重复声明 |
| ModuleSpec.DataScope | 数据权限挂点 | 共享工厂 `ApplyDataScope(col)`,声明即生效 |
| ModuleSpec 钩子装不下 | 基座禁止按模块分支 | 整个方法手写(合法逃生舱,如 job.Detail 因嵌入结构手写) |
| logic 包级共享函数 | 跨模块复用 | 放具体模块文件顶部(同包可见);横切能力进 base.go(15 导出上限) |
| RegisterJobHandler | 本地任务处理器 | 全局注册表(重名 panic) |
| middleware wire | 业务与中间件解耦 | 三 Loader/Sink 函数变量 |
## 未启用项与前端遗留
| 项 | 现状 | 启用/处理要点 |
|----|------|-------------|
| 用户注册 | publicPaths 含 /api/register(路由未注册) | 启用时补建表逻辑 + 开放策略 + 租户归属规则 |
| 数据范围自定义档 | 现四档纯列值(role.data_scope) | 第 5 档需部门关联表 + 角色表单部门树多选 |
| 导出增强 | 当前导出返回临时文件 URL | 可加流式下载/异步导出 |
| SendSms 任务 | task/send_sms.go 仅记日志 | 接短信通道时替换实现 |
| 站内信/数据源/短信/OSS/WebSocket | 前端遗留死代码(后端桩已删除) | 不再调用即无影响 |
## 封板后的变更纪律
```text
1) 一切接口行为以 API 接口清单 + /api.json 为准,改动即更新文档
2) 表结构改动三同步:document/mysql/ 定稿 → 其余四方言脚本 → 迁移(跨环境时)
3) 破坏性变更(改名/删字段/改枚举值)必须走版本化说明 + 前端同步 +
黄金样本更新,禁止静默变更
4) 架构红线复查:绕过 dao / 手写租户条件 / 反向依赖 task / 超 15 导出的 base /
未登记 tenantTables——任何一项都应打回
```
@@ -0,0 +1,75 @@
# 4.4 前端分层架构
:::tip 概述
前端采用 Vue3 Composition API + TypeScript + Pinia + Element Plus,分层为:views(页面)→ components(公共组件)→ api(接口)→ utils/http(请求封装)→ store(状态)→ router(动态菜单)。本节给出分层视图与协作关系。
:::
## 前端分层视图
```text
┌─────────────────────────────────────────────────────────┐
│ views/{域}/{模块}/ 页面层 │
│ index.vue / edit.vue / detail.vue │
│ columns.ts(表格列) querySchemas.ts(筛选表单) │
└──────────────┬──────────────────────────────────────────┘
│ 组件/指令/组合式函数
┌──────────────▼──────────────────────────────────────────┐
│ components/ 公共组件层(25 类) │
│ Table Form Select Upload Editor Excel ChinaArea … │
│ directives/ 权限指令 v-permission 等 │
│ hooks/ core / event / setting / web │
└──────────────┬──────────────────────────────────────────┘
│ 接口调用
┌──────────────▼──────────────────────────────────────────┐
│ api/{域}/{模块}.ts 接口层(url 对齐 gmeta path) │
└──────────────┬──────────────────────────────────────────┘
│ http.request
┌──────────────▼──────────────────────────────────────────┐
│ utils/http/axios 请求封装层 │
│ baseURL=/api · JWT 注入 · 401 处理 · 信封解包 │
└──────────────┬──────────────────────────────────────────┘
│ 状态支撑
┌──────────────▼──────────────────────────────────────────┐
│ store/modules/ 状态层(Pinia) │
│ user(令牌/身份) · asyncRoute(动态菜单)… │
│ router/ 路由层 │
│ constant.ts(常量路由) · generator-routers.ts(动态) │
│ router-guards.ts(守卫) │
└─────────────────────────────────────────────────────────┘
```
## 数据流(以列表页为例)
```text
1. 路由守卫拦到未加载动态路由 → 调 /api/index/getMenus 拉菜单树 → 生成路由
2. 进入页面 index.vue:
querySchemas.ts 定义筛选表单 → 用户输入 → 拼 params
3. 调 api getExamplePage(params)
→ http 封装注入 Authorization: Bearer <access_token>
→ 后端返回统一信封,拦截器解包出 data(records/total/…)
4. columns.ts 驱动 Table 渲染;枚举列显示 {field}Text
5. 增删改走 edit.vue 表单 → exampleAdd/exampleUpdate → 成功刷新列表
```
## 关键机制索引
| 机制 | 位置 | 详见 |
|------|------|------|
| HTTP 封装(401 跳登录/令牌刷新) | utils/http/ | 9.4 |
| 动态菜单路由 | router/generator-routers.ts | 9.5 |
| 权限指令(按钮级) | directives/permission.ts | 9.7 |
| 登录多形态(账号/扫码/手机) | views/login/ | 9.8 |
| 三件套规范 | views/{域}/{模块}/ | 9.12 / 12.2 |
| 主题与设置 | settings/ + styles/ | 9.14 |
## 与后端的契约对齐
```text
api url(无 /api 前缀) ↔ gmeta path(/api 开头,无 /v1)
querySchemas field ↔ XxxPageReq json 字段(驼峰)
columns prop ↔ Item/DetailRes json 字段
权限指令参数 ↔ gmeta permission 权限码
登录读取 ↔ data.access_token(下划线例外)
```
对齐是联调问题的高发区,联调自检清单见 [3.9 前端联调指引](../guide/frontend-integration)。
@@ -0,0 +1,102 @@
# 4.1 整体架构概览
:::tip 概述
系统采用分层架构 + 声明式基座 + 全局钩子横切 + gmeta 反射注解的组合。本节给出总架构图、模块依赖方向(允许/禁止)与启动编排,是理解全部机制的地图。
:::
## 系统总架构图
```text
┌───────────────────────────── 客户端 ─────────────────────────────┐
│ ui/(Vue3 + Element Plus,baseURL=/api) 其他 API 消费方 │
└──────────────────────────────────┬─────────────────────────────────┘
│ HTTP(Bearer JWT)
╔══════════════════════════════════▼═════════════════════════════════╗
║ ghttp Server(:8000) ║
║ ┌────────────────── 中间件链(顺序执行)──────────────────┐ ║
║ │ CORS → Response → RateLimit → Demo → JWT → Tenant │ ║
║ │ → OperationLog │ ║
║ └──────────────────────────┬──────────────────────────────┘ ║
║ ┌──────────────────────────▼──────────────────────────────┐ ║
║ │ Bind 反射包装层(gmeta):权限校验 → 参数解析 → 分发 │ ║
║ │ 路由元数据表 meta(权限/日志 注解即事实,补注册 goai) │ ║
║ └──────────────────────────┬──────────────────────────────┘ ║
║ controller(薄)→ service 接口(手维护)→ logic(ModuleSpec 基座) ║
║ │ 侧路:utility/{jwt,captcha,cache,upload,excel,ip2region…} ║
║ │ 侧路:gcron 调度器(job daemon + CleanTemp) ║
║ ┌────▼─────────────────────────────────────────────────────┐ ║
║ │ dao.Xxx.Ctx(ctx) —— 唯一查询入口 │ ║
║ │ hooks.ApplyCtx:del_flag=0 + 租户级表 tenant_id IN(0,x) │ ║
║ │ hooks.Handler:Insert 盖章 / Update 盖章 / Delete→软删 │ ║
║ └────┬─────────────────────────────────────────────────────┘ ║
╚══════╪══════════════════════════════════════════════════════════════╝
│ gdb(五驱动运行时切换)
┌──────▼────────┐ ┌──────────────┐ ┌─────────────────────────────┐
│ MySQL/PgSQL/… │ │ Redis(缓存) │ │ 磁盘 resource/upload + xdb │
└───────────────┘ └──────────────┘ └─────────────────────────────┘
```
## 模块依赖方向图(允许 / 禁止)
```text
api/v1 ◀── controller ◀── logic ──▶ service(接口注册)
│
├──▶ dao ──▶ model/entity · model/hooks ──▶ gdb
├──▶ utility/*(无业务依赖)
└──▶ consts
internal/task ──▶ internal/logic ✅ 允许(任务复用 base 能力)
internal/logic ──▶ internal/task ❌ 禁止(反向依赖)
internal/middleware ──▶ 业务包 ❌ 禁止(经 wire.go 函数变量接线)
internal/migrate ──▶ dao/logic ❌ 禁止(gdb 裸连,隔离钩子)
base.go ──▶ 模块文件 ❌ 禁止(只依赖 dao/utility/consts)
```
## 启动编排(internal/cmd)
```text
main.go(注册五数据库驱动 + Redis 适配器 + blank import internal/logic)
└─ internal/cmd.Run(ctx)
├─ 1) utility/cache.Init Redis 探测,失败降级进程内存
├─ 2) wireDependencies 校验中间件依赖是否已由业务包 init 接线
├─ 3) g.Server("easygoadmin") + router.Register(s)
│ 中间件链注册 + 404/500 状态处理器 + SetServerRoot("resource")
│ + /api/health + /swagger → 逐个 Bind 全部控制器
├─ 4) go DaemonInit(延时 2s) 定时任务调度器加载 status=1 任务
├─ 5) gcron 注册 CleanTemp 每日 03:30 清理上传临时目录
└─ 6) s.Run() 阻塞运行 HTTP 服务
```
延时 2s 的原因:等 HTTP 监听就绪(任务可能是 HTTP 调自身接口)。
## 业务调用链(标准 CRUD)
```text
Bind 包装(gmeta 权限/日志元数据)
→ controller.CXxx(薄层,仅转发 service.Xxx())
→ service.IXxx 接口(internal/service/{模块}.go,手维护)
→ logic.sXxx 实现(internal/logic/{模块}.go,ModuleSpec 基座或手写)
→ dao.Xxx.Ctx(ctx)(唯一查询入口:挂全局钩子 + 注入 del_flag/租户条件)
→ gdb → 数据库
```
## 五个关键设计决策
| 决策 | 理由 |
|------|------|
| **目录平铺**:api/controller/logic/service 均单包,每模块一文件 | 跨模块直接同包调用(如 user 调 ResolveCityNames 无需 import);生成器目标路径唯一 |
| **service 接口手维护** | 接口即文档;禁止 gf gen service 整体覆盖 |
| **logic 同包 init() 注册** | 无需 blank import,包内全量加载 |
| **中间件不依赖业务层** | IdentityLoader/TenantLoader/LogSink 以函数变量接线(wire.go),cmd 启动期检查告警 |
| **dao.Ctx 唯一查询入口** | 软删/租户/审计三条红线在钩子层强制执行,业务零感知 |
## 分层职责速查
| 层 | 位置 | 职责 | 禁止 |
|----|------|------|------|
| api/v1 | api/v1/{模块}.go | 请求响应结构体 + gmeta 注解 | 业务逻辑 |
| controller | internal/controller/{模块}.go | 薄层透传 | 业务/dao/手写响应 |
| service | internal/service/{模块}.go | 接口契约(手维护) | 实现 |
| logic | internal/logic/{模块}.go | 业务实现(基座+钩子+手写) | 反向依赖 task |
| dao | internal/dao/ | 查询入口(钩子+红线条件) | 绕过直连 DB |
| hooks | internal/model/hooks/ | 审计盖章/软删/租户 | 业务判断 |
@@ -0,0 +1,110 @@
# 4.2 请求处理链路
:::tip 概述
一个受保护的写请求依次穿过七层中间件与 Bind 包装层。理解这条链路是排查 401/403/429/演示拦截/日志缺失的钥匙。注册顺序即执行顺序(router.go s.Use)。
:::
## 中间件链总览
```text
CORS → Response → RateLimit → Demo → JWT → Tenant → OperationLog → 处理器
(Bind 包装层做权限校验)
```
| # | 中间件 | 短路行为 |
|---|--------|---------|
| 1 | CORS | 预检 204;头设置 |
| 2 | Response | SVG 加固;元信息入 ctx;panic → 500 信封 |
| 3 | RateLimit | 超限 code=429 |
| 4 | Demo | `app.demo=true` 时写操作 code=1015 |
| 5 | JWT | 无效/黑名单 code=401 |
| 6 | Tenant | 租户禁用 1006 / 过期 1005 |
| 7 | OperationLog | 不短路,异步采集 |
| 8 | Bind 包装层 | 权限不足 code=403;参数校验失败 400 |
## 请求生命周期(12 步)
```text
1 CORS 预检/头设置
2 Response 元信息入 ctx;defer panic 兜底
3 RateLimit 固定窗口计数,超限 429
4 Demo 演示模式拦截写操作
5 JWT 解析校验 token → 注入身份;黑名单 401
6 Tenant 租户状态/过期校验(60s 缓存)
7 OperationLog 记 start,Next()
8 Bind 包装层 元数据入 ctx → HasPerm 权限校验(403 短路)→ Parse 参数 → 调 controller
9 controller 转发 service 接口
10 logic ModuleSpec 流水线 / 手写业务;dao.Ctx 查询(钩子已注入条件)
11 response.Write 信封写出(code/body 记入 ctx)
12 OperationLog 组装记录 → 异步通道 → 批量落库
```
## 时序图(受保护写请求 + 日志异步落库)
```text
Client 中间件链 Bind包装/业务 缓存 DB/日志
│ POST /api/user/update │ │ │
│───▶│ CORS 头/预检 │ │ │
│ │ Response: 元信息入ctx, defer recover │ │
│ │ RateLimit ──────────────────────────────────────▶ SET/GET 计数│
│ │ Demo(app.demo=false 放行) │ │ │
│ │ JWT: Parse+黑名单 ───────────────────────────────▶ Exists jti │
│ │ 注入身份; Roles/Perms/DataScope/DeptId ──────▶ 命中缓存 │
│ │ 未命中 → IdentityLoader 回源 ───────────────────────▶ user/role/menu
│ │ Tenant: status/expire ───────────────────────────▶ tenant:info │
│ │ OperationLog: start=now │ │ │
│ │ │ │ │ │
│ │ ▼ ▼ HasPerm(403?短路) │ │
│ │ Parse(req) → logic.Update │ │
│ │ spec 流水线: BeforeUpdate → │ │
│ │ saveTransform → 唯一预检 → ────────────────▶ UPDATE
│ │ (hooks 盖章 update_time/user) │ │
│ │ ▼ │ │ │
│◀───│ WriteJson{code:0,...}(code/body 记 ctx) │ │
│ │ OperationLog: 组装 entry(脱敏 param/result) │ │
│ │ select{ logChan<-entry } 非阻塞 │ │
│ │ worker: 攒批 100条/1s ──────────────────▶ INSERT operation_log
```
异常分支:
```text
* 任一中间件短路(429 / 演示拦截 / 401 / 403)→ 直接 WriteJson,后续不执行
* panic → Response recover → 500 信封(已写出响应则仅记录日志)
* logChan 满 → 丢弃并计数告警(每丢 100 条输出一次 Warning),绝不阻塞业务
```
## 各层关键实现速查
| 层 | 实现位置 | 要点 |
|----|---------|------|
| CORS | middleware/cors.go | `cors.allowOrigin` 空或 `*` 走框架默认;具体来源显式回显凭证 |
| Response | middleware/response.go | .svg 强制 attachment+nosniff(防存储型 XSS);realClientIP/UserAgent 入 ctx |
| RateLimit | middleware/ratelimit.go | 固定窗口;键 `ratelimit:{ip}:{path}:{窗口起点}`;计数经 utility/cache 多实例共享;存储异常放行 |
| Demo | middleware/demo.go | GET/HEAD/OPTIONS 与公开白名单放行;其余写操作拒绝 |
| JWT | middleware/jwt.go | Parse(HS256/过期/签发者)→ 黑名单 Exists → 注入身份(缓存 300s,回源 LoadIdentity) |
| Tenant | middleware/tenant.go | 缓存 60s;租户不存在按禁用处理;数据库基础错误放行(防存储抖动全站不可用) |
| OperationLog | middleware/operation_log.go | 有界通道 2048 + 单 worker;批量 100 条或 1s flush;参数脱敏 password/salt/token → *** |
| Bind | router/bind.go | 权限校验 + r.Parse(gvalid)+ 信封写出;BodyUnmarshaler 逃生舱 |
## 真实 IP 与受信代理
```text
util.go realClientIP:
直连来源不在 api.proxyIps(逗号分隔,精确 IP 与 CIDR)内 → 直接用 GetRemoteIp()
在列表内 → 才信任 X-Forwarded-For 首段 / X-Real-IP
```
防止伪造 XFF 绕过限流或污染日志。Nginx 反代部署务必把反代地址加入 `api.proxyIps`。
## 排障速查
| 症状 | 检查点 |
|------|--------|
| 401 | token 过期/黑名单(登出、轮换、强退都会拉黑 jti) |
| 403 | gmeta permission ↔ 菜单节点 ↔ 角色勾选 ↔ 身份缓存(300s) |
| 429 | 限流阈值;认证入口独立 authLimit=10 |
| 1015 | app.demo=true 拦截写操作 |
| 1005/1006 | 租户过期/禁用(Tenant 中间件,缓存 60s) |
| 日志缺失 | operationLog.enable / log 标签空 / logQuery 关 / 通道满告警 |
| panic 无堆栈 | server.errorStack=false;看服务端日志 |
@@ -0,0 +1,96 @@
# 4.10 安全架构设计
:::tip 概述
安全设计按威胁面组织:每项威胁对应明确控制措施与代码落点。本节给出威胁→控制总表、四条安全红线、部署基线与已知边界(诚实声明),可直接用于安全评审自查。
:::
## 威胁 → 控制总表
| 威胁面 | 控制措施 | 代码落点 |
|--------|---------|---------|
| 身份伪造 | HS256 签名校验 + iss/exp 强校验(限定 HS256 防算法混淆) | utility/jwt Parse |
| 令牌重放(登出后) | jti 黑名单,TTL=剩余有效期 | `blacklist:{jti}` |
| Refresh 重放 | 轮换:旧 refresh 入黑名单,已黑即拒(1008) | logic/auth Refresh |
| 被踢会话复活 | 强退同时拉黑 access+refresh 双令牌 | online_user ForceLogout |
| 口令窃取/泄露 | bcrypt(password+salt);日志脱敏(password/salt/token → ***) | utility/crypto; maskSecret |
| 暴力破解 | 登录/验证码/令牌兑换独立严阈值 10/min/IP | rateLimit.authLimit |
| 水平越权(跨租户) | dao.Ctx 注入 tenant_id IN(0,自身) | hooks.ApplyCtx |
| 行级越权(部门/本人) | 角色 data_scope 四档叠加过滤(仅列表/导出;写操作由 permission 兜底) | logic/data_scope.go |
| 垂直越权 | gmeta permission + HasPerm;超管放行 | Bind 包装层 |
| SQL 注入 | 全链 gdb 参数化;无方言拼接红线 | dao/Where 参数化 |
| 参数篡改 | gvalid 标签校验(Parse 统一触发) | api/v1 结构体标签 |
| 存储型 XSS | .svg 强制 attachment+nosniff;富文本外链不本地化、占位符契约 | middleware/response; base.go |
| SSRF | 富文本仅处理本站 URL,外站一律不抓取 | localizeOne 判定 |
| CSRF | Bearer 头令牌(非 Cookie)+ CORS 白名单 | JWT 中间件/CORS |
| IP 伪造 | 仅受信代理(api.proxyIps,支持 CIDR)的 XFF/X-Real-IP 生效 | realClientIP |
| 限流绕过 | 计数经 Redis 跨实例共享;窗口起点对齐 | RateLimit incrCounter |
| 越权写(演示站) | Demo 中间件拦截全部写操作 | middleware/demo |
| 审计缺失 | 操作日志异步全量记录(脱敏+响应+耗时) | OperationLog |
| 会话固定 | 无服务端 Cookie 会话,令牌全部服务端签发 | auth Login |
| 敏感信息出栈 | GetUser 永不返回 password/salt;令牌原文仅在线用户详情(需权限) | index/online_user |
## 四条租户隔离红线(安全审计重点)
::: warning 红线清单
1. 业务代码禁止 `g.DB()` 直查业务表(绕过钩子与过滤)
2. 业务代码禁止手写 del_flag / tenant_id 条件(应由 dao.Ctx 注入)
3. 新增租户级表必须登记 hooks.tenantTables(**漏登=跨租户泄漏**)
4. 唯一预检范围必须等于唯一索引范围(否则 1062 漏报/误报)
另:平台级(超管)全量可见是设计预期,管理端点须收紧 permission。
:::
## 文件上传安全链
```text
大小限制 → 扩展名白名单 → 图片魔数嗅探(前 512 字节须 image/*)
→ 随机文件名(防猜路径/覆盖)
→ 临时/正式两段式(未过校验不进正式目录)
→ 静态响应 svg 附件化 + nosniff
→ 定期清理 temp(每日 03:30,tempExpireDays 默认 7 天)
```
注意:fileExt 白名单含 js/css/sql 等——对公网用户提供上传时,建议按环境收紧 fileExt(配置层控制,无需改码)。
## 认证与授权安全细节
```text
认证:
* 令牌 HS256;密钥/签发者可配置;exp 必填
* 双令牌分工:access 2h 只作身份;refresh 7d 仅 /api/oauth2/token 可用(typ 校验)
* 验证码:一次性(命中即删)、TTL 300s、bypassCode 生产必须留空
* 密码:bcrypt DefaultCost;外部 salt 防彩虹;旧 md5 数据被 IsBcrypt 拒绝(强制重置)
授权:
* 权限点粒度到接口级(sys:module:action),按钮级由前端控制;
新增接口必须声明 permission 或显式留空(留空=登录即可,评审需明确该选择)
* 隐藏节点:menu hide=1 的权限节点不进 Perms——"禁用权限"用 status 而非 hide
* 权限缓存一致性:保存权限/分配角色/改数据范围主动清 identity 缓存,最迟 300s 过期
```
## 部署安全基线(生产)
```text
1) 修改 jwt.secret 为高强度随机值且与环境隔离
2) captcha.bypassCode=""; app.demo=false; app.errorMsg=false; errorStack=false
3) database/redis 使用独立账号与内网地址;不暴露公网
4) api.proxyIps 只列真实反代出口;未用反代时保持默认(含 127.0.0.1)
5) file.fileExt 按业务收紧;uploadDir 权限最小化;domainUrl 走 CDN/域名隔离
6) rateLimit 按压测调参(防误伤;authLimit 不建议调大)
7) HTTPS 由前置代理终止;服务本身 HTTP 内网
8) 定期核对 /api.json 与权限点清单(防新接口漏配 permission)
```
## 已知边界与取舍(诚实声明)
| 项 | 边界 |
|----|------|
| CheckUnique | 先查后插的 UX 预检,存在 TOCTOU——并发唯一性以 uk_* 索引为最终防线 |
| 数据权限 | 仅约束 Page/List/导出;Detail/Update/Delete 防线是 permission 节点 + 租户隔离 |
| 接线范围 | 数据权限当前仅 user 模块接线(dept_id 列);其他模块行级可见性由租户隔离单层兜底 |
| 部门子树 | deptSubtreeIds 每次全量拉部门表 BFS 无缓存——数百行内无感知,上万行需加短时缓存 |
| 身份缓存窗口 | 300s 内直接改库的 dept_id/data_scope 不经 API 不会即时生效(API 路径均主动清缓存) |
| 在线用户详情 | 返回令牌原文(便于运维排查),依赖 sys:onlineUser:detail 权限收口 |
| 日志截断 | param/result 截断 8KB,超长报文不完整留存 |
| 密码策略 | 无独立复杂度/定期更换强制(可按需在密码路径追加规则) |
| 防重放 | jti 黑名单依赖 TTL 自然过期,无物理防重放窗口 |
@@ -0,0 +1,52 @@
# 1.6 更新日志
:::tip 概述
本项目遵循语义化版本演进,重大架构决策与约定变更记录于此。详细提交历史见 [Gitee 仓库](https://gitee.com/easygoadmin/EasyGoAdmin_GoFrame_EleVue)。
:::
## v4.0.0(2026-10 · 当前版本)
GoFrame+ElementPlus 版本首个正式封板版本,25 张业务表 → 27 个模块完整落地。
### 架构定稿
+ **主键统一 int 数据库自增**(2026-10-13 定稿):禁用雪花算法(`utility/id` 已删除);新增后用 `result.LastInsertId()` 回读;hooks 不填 id
+ **目录平铺结构**:api/v1、controller、logic、service 均为单包,每模块一个 snake_case 文件,同包 init 自动注册
+ **中间件链定稿**:CORS → Response → RateLimit → Demo → JWT → Tenant → OperationLog,权限校验在 Bind 包装层完成
+ **筛选对齐三件套**(2026-10-14 定稿):前端 querySchemas.ts ↔ XxxPageReq 同名驼峰字段 ↔ spec 声明(LikeFields/EqFields),导出与列表同套筛选
+ **URL 约定**:全部路由统一 `/api` 前缀,无 `/v1` 版本段;公开白名单三处一致(public.go / gmeta public 标签 / router.go 手工注册)
### 功能清单
+ 认证与安全:JWT 双令牌(access 2h + refresh 7d 轮换)、jti 黑名单、图形验证码(TTF 渲染 + SVG 降级)、bcrypt 双重加盐、固定窗口限流(认证入口独立阈值)、演示模式、受信代理 IP
+ 权限体系:RBAC(user→role→menu 权限节点)、gmeta permission 标签、前端权限指令、超管通配权限 `*:*:*`
+ 多租户:9 张租户级表自动隔离(hooks.tenantTables)、租户状态/过期中间件校验(60s 缓存)、事务化开户、用户上限
+ 数据权限:role.data_scope 四档(全部/本部门及以下/本部门/仅本人),`ApplyDataScope("dept_id")` 一行声明接线
+ CRUD 基座:ModuleSpec 泛型(筛选/排序/树形/字典/文件/富文本/唯一校验/钩子/数据权限)
+ 监控体系:在线用户(双令牌拉黑强退)、服务器监控、缓存监控(Redis INFO)、操作日志(异步批量落库 + 脱敏)、登录日志(ip2region 归属地)
+ 定时任务:数据驱动调度器、HTTP/本地处理器双形态、三种执行策略、多实例执行锁(job:lock:{id})、内置 LogClean/CleanTemp
+ 代码生成器:HTTP + CLI 双入口、后端八件套 + 前端五件产出、菜单/权限/字典/路由幂等副作用、覆盖策略保护
+ 整库迁移器:五方言互迁(内省 → IR → 方言 DDL → 分批搬运 → 序列修复 → 行数校验)
+ 文件体系:两段式上传(temp → 正式目录)、富文本 [IMG_URL] 持久化契约、临时目录每日 03:30 自动清理
### 工程保障
+ 静态门禁:`go vet` + `gofmt` 检查;前端 ESLint + Prettier + Stylelint + commitlint
+ 测试:sqlite 集成测试 + hooks MySQL 集成测试
+ 回归:hack/golden 黄金样本接口响应比对(录制参数带 pageSize=3 等)
+ OpenAPI:gmeta 补注册进 gf goai,`/api.json` 原生输出 + `/swagger` 本地 Redoc 页面
## 历史版本
| 版本 | 说明 |
|------|------|
| v4.0.0-beta | 27 模块全量实现,架构决策落地(主键自增、中间件链、筛选三件套定稿) |
| v4.0.0-alpha | 项目初始化:GoFrame 骨架 + 核心模块(auth/user/role/menu)+ 前端工程搭建 |
## 破坏性变更记录
::: warning 变更须知
+ 2026-10-13:主键策略由雪花算法改为数据库自增——历史数据的 id 语义不变,但新增代码禁止依赖 `utility/id`(包已删除)。
+ URL 全部去版本段——`/api/v1/*` 形式的旧链接失效,前端 baseURL 为 `/api`。
+ 前端登录响应字段为 `access_token/refresh_token/expires_in/token_type`(下划线),与全局驼峰约定不同,这是 OAuth2 惯例,禁止"修正"。
:::
@@ -0,0 +1,83 @@
# 5.18 gmeta 注解系统总览
:::tip 概述
gmeta 注解是本项目的"声明即事实"机制:请求结构体上的一个 gmeta.Meta 标签同时驱动路由注册、权限校验、操作日志、OpenAPI 文档四件事。解析与分发在 internal/router/bind.go(反射绑定)与 meta/(路由元数据表)。
:::
## 一个标签,四份事实
```go
type UserPageReq struct {
gmeta.Meta `method:"get" path:"/api/user/page" summary:"用户分页"
permission:"sys:user:page" log:"用户管理-查询分页记录" logType:"query"`
// ...
}
```
```text
解析后同时产出:
① 路由注册 GET /api/user/page → CUser.Page
② 权限校验 HasPerm("sys:user:page")(包装层每请求判定)
③ 操作日志 标题"用户管理-查询分页记录"、类型 query
④ OpenAPI 文档 /api.json 的 path/summary/参数说明
```
不存在"代码改了忘改路由表/权限表/文档"——标签是唯一真相。
## 标签字段语义
| 标签 | 语义 |
|------|------|
| `method` | HTTP 方法(get/post/put/delete) |
| `path` | 完整路由模板(含 /api 前缀;`:id` 为路由参数,req 同名字段承接) |
| `summary` | OpenAPI 摘要 |
| `permission` | 权限节点 `sys:{module小驼峰}:{action}`;空 = 登录即可 |
| `log` | 操作日志标题 `"{模块中文名}-{动作}"`;空 = 不记录 |
| `logType` | 日志类型字符串(query/add/update/delete/import/export/reset/…),经 LogTypeMap 转 41 个枚举值 |
| `public` | `"true"` = 免登录(仍需与 middleware/public.go 白名单一致) |
## Bind 绑定流程(internal/router/bind.go)
```text
Bind(s, new(controller.CXxx)) 扫描控制器全部导出方法:
1) 校验签名:func (c *Xxx) Page(ctx, req *v1.XxxReq) (*v1.XxxRes, error)
(签名不符静默跳过——排障注意)
2) 解析请求结构体内嵌 gmeta.Meta 扩展标签
3) 按 method:path 注册路由 + meta.Register(rm) 写入路由元数据表
(键 "METHOD /path",读写锁保护;Lookup 供调试,All 供文档生成)
4) 补注册进 gf goai(闭包路由 gf 不会自动收集)→ /api.json 原生 OpenAPI
5) 生成统一包装 handler:
路由元数据写入 ctx(操作日志中间件读取)
→ 权限校验(403 短路;不设独立 Permission 中间件)
→ r.Parse(req)(路由参数+query+body,含 gvalid 校验)
→ 调用业务方法 → response.Ok/Fail 信封写出
```
## BodyUnmarshaler 逃生舱
```text
当请求体顶层形态无法用结构体标签表达(如平铺 map)时,
Req 实现 UnmarshalBody(r *ghttp.Request) error 接口:
→ 跳过默认 Parse,实现内自行完成解析与 gvalid 校验
```
## 注册验证
```sh
# OpenAPI 检查(gmeta 补注册后全量可用)
curl http://127.0.0.1:8000/api.json | grep "user/page"
# 启动路由表
# config.yaml: server.dumpRouterMap: true
```
## 开发规约
```text
* 每个接口必须有 gmeta——没有标签的方法不会注册路由
* permission 新码必须同步菜单节点 + 角色勾选(否则 403)
* public:"true" 必须同时出现在 middleware/public.go 白名单(三处一致)
* log 空 = 不记录(有意不记录才留空);logType 缺省按 HTTP 方法自动推断(0)
* path 一律 /api 开头、无 /v1 版本段
* 改 path/permission 等标签后重启生效(路由与元数据表启动期构建)
```

Some files were not shown because too many files have changed in this diff Show More